搭建TCP/UDP协议的中间人环境

2022-12-22 14:37:27 浏览数 (2)

无论是传统的网络协议调试与分析,还是漏洞分析,一个能够对数据包进行实时的监控、拦截以及篡改的中间人位置通常是很有帮助的。对于HTTP/HTTPS等上层协议来讲,中间人位置的构造并不复杂,现有的利用http代理配合很多工具如burpsuite/mitmproxy/fildder都可以帮助我们完成这一个工作。然而,对于TCP/UDP协议来说,由于缺少工具和解决方案,构造一个中间人位置并不是那么简单明了。

问题描述

最近由于工作需求,要搭建一个TCP中间人的环境。该环境要求在电脑上可以对手机的TCP包进行实时的监控、拦截和篡改。同时,收集流量的机器和做中间人的机器最好可以分离。另外,这个中间人环境需要尽可能的对手机透明,即尽可能的避免在手机上进行额外的配置。

本以为这个问题有现成工具可以直接搞定,不料搜了一波,没有找到太靠谱的工具来做这个事情,于是决定自己动手想办法解决这个问题。

解决思路

我本来的思路是手机连入电脑发出的热点,然后在电脑上通过配置iptables将所有来自手机的流量转发到某一个端口上。之后,再写一个python脚本监听这个端口并做中间人。这个方法是可行的,但是无法直接做到对收集流量的机器和做中间人的机器进行分离。

跟yuguorui大佬讨论后,得到了一种比较有趣的解决方案。其主要结构跟我原本思路类似,但是流量不是直接转到自己写的python脚本,而是转到一个socks5客户端上。之后自己写一个socks5的服务器来接收来自socks5客户端的流量并做中间人。这样做的主要好处就是对TCP包头的修改都被socks5客户端搞定。而在socks5服务器可以轻松的对所关心的TCP包体进行监控、拦截、篡改。另外,由于socks5支持UDP协议,因此相似的思路也可以直接应用于UDP之上。

具体实施

  1. 安装iptables和redsocks
代码语言:javascript复制
sudo apt install iptables redsocks

2. 编写socks5的server,具体可以参照这个博客。该socks5的server部署在远程用于做中间人的主机(其ip地址为remoteaddr)。

代码语言:javascript复制
#coding=utf-8
#filename: socks5_server.py
import select
import socket
import struct
from socketserver import StreamRequestHandler, ThreadingTCPServer
SOCKS_VERSION = 5
class SocksProxy(StreamRequestHandler):
    def handle(self):
        print('Accepting connection from {}'.format(self.client_address))
        # 协商
        # 从客户端读取并解包两个字节的数据
        header = self.connection.recv(2)
        version, nmethods = struct.unpack("!BB", header)
        # 设置socks5协议,METHODS字段的数目大于0
        assert version == SOCKS_VERSION
        assert nmethods > 0
        # 接受支持的方法
        methods = self.get_available_methods(nmethods)
        # 无需认证
        if 0 not in set(methods):
            self.server.close_request(self.request)
            return
        # 发送协商响应数据包
        self.connection.sendall(struct.pack("!BB", SOCKS_VERSION, 0))
        # 请求
        version, cmd, _, address_type = struct.unpack("!BBBB", self.connection.recv(4))
        assert version == SOCKS_VERSION
        if address_type == 1:  # IPv4
            address = socket.inet_ntoa(self.connection.recv(4))
        elif address_type == 3:  # Domain name
            domain_length = self.connection.recv(1)[0]
            address = self.connection.recv(domain_length)
            #address = socket.gethostbyname(address.decode("UTF-8"))  # 将域名转化为IP,这一行可以去掉
        elif address_type == 4: # IPv6
            addr_ip = self.connection.recv(16)
            address = socket.inet_ntop(socket.AF_INET6, addr_ip)
        else:
            self.server.close_request(self.request)
            return
        port = struct.unpack('!H', self.connection.recv(2))[0]
        # 响应,只支持CONNECT请求
        try:
            if cmd == 1:  # CONNECT
                remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                remote.connect((address, port))
                bind_address = remote.getsockname()
                print('Connected to {} {}'.format(address, port))
            else:
                self.server.close_request(self.request)
            addr = struct.unpack("!I", socket.inet_aton(bind_address[0]))[0]
            port = bind_address[1]
            #reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, address_type, addr, port)
            # 注意:按照标准协议,返回的应该是对应的address_type,但是实际测试发现,当address_type=3,也就是说是域名类型时,会出现卡死情况,但是将address_type该为1,则不管是IP类型和域名类型都能正常运行
            reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, 1, addr, port)
        except Exception as err:
            logging.error(err)
            # 响应拒绝连接的错误
            reply = self.generate_failed_reply(address_type, 5)
        self.connection.sendall(reply)
        # 建立连接成功,开始交换数据
        if reply[1] == 0 and cmd == 1:
            self.exchange_loop(self.connection, remote)
        self.server.close_request(self.request)
    def get_available_methods(self, n):
        methods = []
        for i in range(n):
            methods.append(ord(self.connection.recv(1)))
        return methods
    def generate_failed_reply(self, address_type, error_number):
        return struct.pack("!BBBBIH", SOCKS_VERSION, error_number, 0, address_type, 0, 0)
    def do_mitm_send(self, data):
        print(data)
        #do something here
    def do_mitm_recv(self, data):
        print(data)
        #do something here
    def exchange_loop(self, client, remote):
        while True:
            # 等待数据,在这里做中间人
            r, w, e = select.select([client, remote], [], [])
            if client in r:
                data = client.recv(4096)
                self.do_mitm_send(data)
                if remote.send(data) <= 0:
                    break
            if remote in r:
                data = remote.recv(4096)
                self.do_mitm_recv(data)
                if client.send(data) <= 0:
                    break
if __name__ == '__main__':
    # 使用socketserver库的多线程服务器ThreadingTCPServer启动代理
    with ThreadingTCPServer(('127.0.0.1', 9011), SocksProxy) as server:
        server.serve_forever()

3. 在主机上开启一个wifi热点,并让手机连这个热点,假设手机分到的ip为10.42.0.240

4. 配置并启动redsocks, redsocks 监听在0.0.0.0:12345,socks5监听在remode_addr:9011。

代码语言:javascript复制
vim /etc/redsocks.conf
local_ip = 0.0.0.0;
local_port = 12345;

// `ip' and `port' are IP and tcp-port of proxy-server
// You can also use hostname instead of IP, only one (random)
// address of multihomed host will be used.
ip = remode_add;
port = 9011;


// known types: socks4, socks5, http-connect, http-relay
type = socks5;
systemctl restart redsocks

5. 配置iptables,使所有来自手机的流量转发到本机的12345端口。

代码语言:javascript复制
sudo iptables -t nat -A REDSOCKS -p tcp -s 10.42.0.240/32 -j REDIRECT --to-port=12345
sudo iptables -t nat -A REDSOCKS -p tcp -j RETURN
sudo iptables -t nat -A PREROUTING -p tcp -s 10.42.0.240/32 -j REDSOCKS

6. 在远程的做中间人的主机启动我们编写的socks5 server,

代码语言:javascript复制
python3 socks5_server.py

在完成这些操作之后,来自手机的所有流量都会先经由Linux主机的无线网卡转发到redsocks开放的12345端口,然后redsocks会将流量转到我们自己写的socks5服务器。我们在socks5服务器可以直接对包体的监控、拦截、篡改。

致谢

感谢riatre大佬指出我个人关于第一种方案的一些错误理解。

0 人点赞