搭建TCPUDP協議的中間人環境
無論是傳統的網路協議除錯與分析,還是漏洞分析,一個能夠對資料包進行實時的監控、攔截以及篡改的中間人位置通常是很有幫助的。對於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大佬指出我個人關於第一種方案的一些錯誤理解。