无论是传统的网络协议调试与分析,还是漏洞分析,一个能够对数据包进行实时的监控、拦截以及篡改的中间人位置通常是很有帮助的。对于HTTP/HTTPS等上层协议来讲,中间人位置的构造并不复杂,现有的利用http代理配合很多工具如burpsuite/mitmproxy/fildder都可以帮助我们完成这一个工作。然而,对于TCP/UDP协议来说,由于缺少工具和解决方案,构造一个中间人位置并不是那么简单明了。
问题描述
最近由于工作需求,要搭建一个TCP中间人的环境。该环境要求在电脑上可以对手机的TCP包进行实时的监控、拦截和篡改。同时,收集流量的机器和做中间人的机器最好可以分离。另外,这个中间人环境需要尽可能的对手机透明,即尽可能的避免在手机上进行额外的配置。
本以为这个问题有现成工具可以直接搞定,不料搜了一波,没有找到太靠谱的工具来做这个事情,于是决定自己动手想办法解决这个问题。
解决思路
我本来的思路是手机连入电脑发出的热点,然后在电脑上通过配置iptables将所有来自手机的流量转发到某一个端口上。之后,再写一个python脚本监听这个端口并做中间人。这个方法是可行的,但是无法直接做到对收集流量的机器和做中间人的机器进行分离。
跟yuguorui大佬讨论后,得到了一种比较有趣的解决方案。其主要结构跟我原本思路类似,但是流量不是直接转到自己写的python脚本,而是转到一个socks5客户端上。之后自己写一个socks5的服务器来接收来自socks5客户端的流量并做中间人。这样做的主要好处就是对TCP包头的修改都被socks5客户端搞定。而在socks5服务器可以轻松的对所关心的TCP包体进行监控、拦截、篡改。另外,由于socks5支持UDP协议,因此相似的思路也可以直接应用于UDP之上。
具体实施
- 安装iptables和redsocks
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大佬指出我个人关于第一种方案的一些错误理解。