最近看了一篇关于 Shadowsocks 重定向攻击 的论文,觉得很有趣。但是仓库里的 poc 代码没法直接用,自己重新抓了包,结果出了点问题,做了一通宵实验,才折腾出来,写篇博客记录一下

browser -> ss-local <======encrypted data======> ss-server -> outside internet

ss 的数据流如上图,ss-local 对浏览器而言,就是一个普通的 socks5 代理。为了简化实现,shadowsocks 协议和 socks5 协议的地址表示方式是一致的,浏览器发给 ss-local 的请求,可以去掉 socks5 协议里的 VER CMD RSV 头三个字节,就直接拿来用。ss-local 收到请求后,直接发起 tcp 链接到 ss-server,包的内容是 [IV][remote_host_addr][payload],这里 [IV] 是 16 个字节长度的随机字符串。接下来的 [remote_host_addr][payload] 是经过加密的,用的是用户自己设定的加密算法和加密密钥。ss-server 收到后,会连接 [remote_host_addr],发送 [payload] 内容。ss-server 的回包结构则是 [IV][payload]

本次 POC 针对的,就是特定的 stream cipher 加密方法,比如 aes-256-cfb 之类的,基本上非 AEAD 系的都会中招。理想状态下,加密后的 [remote_host_addr][payload] 应该极难解密。但是,对于特定的流式加密和特定的数据包,可以通过精心构造的包体,让 ss-server 连接到恶意 [remote_host_addr],发送解密后的 [payload],主动给攻击者解密。流式加密的加解密流程:

stream_encrypt_func(key, IV, plaintext) => ciphertext
stream_decrypt_func(key, IV, ciphertext) => plaintext

Ciphertext0 = IV
Ciphertext1 = Encrypt(K,C0) XOR plaintext1
Ciphertext2 = Encrypt(K,C1) XOR plaintext2
...

假设我们知道明文 p1 是什么,我们就能在不知道 key 的情况下,将 p1 对应的密文 c1,变换成 c1’,同时不破坏后续的解密。形如:

p1 XOR r = attacker_p1
r = p1 XOR attacker_p1
c1 XOR r = attacker_c1

因为 ss 用来上网浏览比较多,我们可以假设抓到的是一个 http 的流。那么服务器的回包内容多半是 HTTP/1.1 开头的。利用这一点,我们可以将服务器的回包 resp 抓下来,稍加修改构造成客户端的请求包 req,发给 ss-server 给我们解密。resp 的结构是 [IV][payload],req 的结构是 [IV][remote_host_addr][payload],我们只要将 resp[16:16+7] 的内容,替换成我们控制的服务器 s1 的 ip 地址,ss-server 就会发起 tcp 链接到 s1,并将解密后的内容发过去

自己做实验的时候,需要将原来仓库里的 attack_with_http_pocket.py 里的 c,换成自己抓的服务器包返回值,然后发给同一个服务器 ( 或者相同密码的服务器 ) 解密,只有知道密码的服务器才能解开包。另外,tcpdump -i lo src port 8080 -X 抓下来的是 ip 包的 hex,还不能直接用,最后用 wireshark 转换了一下才搞定