無論是傳統的網路協議除錯與分析,還是漏洞分析,一個能夠對資料包進行實時的監控、攔截以及篡改的中間人位置通常是很有幫助的。對於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)。

#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。

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埠。

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,

python3 socks5_server。py

在完成這些操作之後,來自手機的所有流量都會先經由Linux主機的無線網絡卡轉發到redsocks開放的12345埠,然後redsocks會將流量轉到我們自己寫的socks5伺服器。我們在socks5伺服器可以直接對包體的監控、攔截、篡改。

致謝

感謝riatre大佬指出我個人關於第一種方案的一些錯誤理解。