发现shadowsocks的通信协议出现了重大的安全问题,比较好奇,学习一下。
0x1 复习一下socks5 协议
socks5代理协议是一个非常轻量级,简单却实用的代理协议。整个协议其实就是在建立TCP连接之后,在真正的内容传输之前,加一点内容。
通讯中各部分的定义如下:
1 | /-> | Firewall(防火墙) | ->\ |
第一步,Client与Server建立连接
建立TCP连接之后,Client发送如下数据:
1 | +----+----------+----------+ |
- VER 是指协议版本,因为是 socks5,所以值是 0x05,一个字节
- NMETHODS 是指有多少个可以使用的方法,也就是客户端支持的认证方法,一个字节,有以下值:
- 0x00 NO AUTHENTICATION REQUIRED 不需要认证
- 0x01 GSSAPI 参考:https://en.wikipedia.org/wiki/Generic_Security_Services_Application_Program_Interface
- 0x02 USERNAME/PASSWORD 用户名密码认证
- 0x03 to 0x7f IANA ASSIGNED 一般不用。INNA保留。
- 0x80 to 0xfe RESERVED FOR PRIVATE METHODS 保留作私有用处。
- 0xFF NO ACCEPTABLE METHODS 不接受任何方法/没有合适的方法
- METHODS 就是方法值,1-255个字节,有多少个方法就有多少个byte
第二步,Server返回可以使用的方法
收到Client的请求之后,Server选择一个自己也支持的认证方案,然后返回:
1 | +----+--------+ |
VER 和 METHOD 的取值与上一节相同。
第三步,client 向 server 发送 Dst 的地址
1 | +----+-----+-------+------+----------+----------+ |
- VER 还是版本,取值是 0x05
- CMD 是指要做啥,取值如下:
- CONNECT 0x01 连接
- BIND 0x02 端口监听(也就是在Server上监听一个端口)
- UDP ASSOCIATE 0x03 使用UDP
- RSV 是保留位,值是 0x00
- ATYP 是目标地址类型,有如下取值:
- 0x01 IPv4
- 0x03 域名
- 0x04 IPv6
- DST.ADDR 就是目标地址的值了,如果是IPv4,那么就是4 bytes,如果是IPv6那么就是16 bytes,如果是域名,那么第一个字节代表接下来有多少个字节是表示目标地址
- DST.PORT 两个字节代表端口号
第四步,服务端回复
1 | +----+-----+-------+------+----------+----------+ |
- VER 还是版本,值是 0x05
- REP 是状态码,取值如下:
- 0x00 succeeded
- 0x01 general SOCKS server failure
- 0x02 connection not allowed by ruleset
- 0x03 Network unreachable
- 0x04 Host unreachable
- 0x05 Connection refused
- 0x06 TTL expired
- 0x07 Command not supported
- 0x08 Address type not supported
- 0x09 to 0xff unassigned
- RSV 保留位,取值为 0x00
- ATYP 是目标地址类型,有如下取值:
- 0x01 IPv4
- 0x03 域名
- 0x04 IPv6
- DST.ADDR 就是目标地址的值了,如果是IPv4,那么就是4 bytes,如果是IPv6那么就是16 bytes,如果是域名,那么第一个字节代表接下来有多少个字节是表示目标地址
- DST.PORT 两个字节代表端口号
第五步,开始传输流量
接下来就是流量传输了,clinet端将需要发送给dst的流量直接发送给server就可以了。
server端简单的代码实现
1 |
|
0x2 shadowsocks协议
阅读了一下shadowsocks的部分源码并抓包分析了一下通信过程。通过分析发现我对shadowsocks通信是基于socks5协议的这种说法的理解是完全不对的的,下面画一个shadowsocks通信的原理图。
shadowsocks由sslocal和ssserver两部分组成,而真正利用socks5协议进行通信的只有sslocal,sslocal和ssserver之间的通信用的是非常简陋的通信协议,或者说根本就没有用协议。
为了分析协议,shadowsocks的ssserver和sslocal的配置文件分别如下:
1 | { |
1 | { |
下面结合wireshark抓流量进行分析。
sslocal与clinet端的通信
sslocal可以分为两个部分,第一个部分是socks5服务端,它负责监听本地的请求。另外一个部分是信息发送端,它负责向远程的ssserver发送数据包。这个节我们只分析 sslocal 作为 socks5 服务器的这一部分。
用tcp.port==1090
过滤一下数据包,就会看到 socks5 协议的整个通信过程。
- 首先是
client
端发送请求建立连接的请求,发送的数据是05 02 00 01
- sslocal的socks5服务器回复
05 00
,表示不需要认证。
- clinet发送通信目标的ip和port
sslocal的socks5服务器回复
05 00 00 01 00 00 00 00 10 10
,对比上面的socks5通信协议会知道这里返回的ip是00 00 00 00
,port 是10 10
,这俩都是假的值,因为 sslocal 并没有真实的和client要求的目标地址通信,而是向ssserver发起了请求。接下来就是socks5数据传输过程。client段发送自己的请求,我这里是个http请求。
sslocal与ssserver的通信
sslocal发送给ssserver的数据
通过设置filter tcp.port==7878
获取 sslocal 发送给 ssserver的第一条数据如下:
因为不知道通信协议的格式,所以并不知道发送了什么数据,不过我们可以先看一下shadowsocks中的数据解密函数:
1 | def decrypt(self, buf): |
通过这个函数,知道发送的数据前 decipher_iv_len
是加密所用的初始iv的长度,我这里用的加密算法是 aes-256-cfb
,跟一下代码知道这里 decipher_iv_len
是16。
所以 4222143a3190ce92e4aa8609a7036aeb
是iv,db55f138d80414873c3e792896935691dc3769f1ada0c0122c58e8e825298fc2b2a5a9eef3dd6ca2c4204b30c6814b28c0644744272b21d2b9b4a9b0ddfb35e082d82629cea42c87437ca1fabdde735f6c72bc95
是数据,对数据进行解密,得到解密后的数据是01b7e8e7ac0050474554202f20485454502f312e310d0a486f73743a207777772e62616964752e636f6d0d0a557365722d4167656e743a206375726c2f372e36342e310d0a4163636570743a202a2f2a0d0a0d0a
转为ascii为:
前面是一段乱码吗,推测可能是通信的某些控制字段,后面跟着就是发送的http请求。
通过阅读shadowsocks的源代码,知道这条数据的格式如下:
1 | +-----+-------+-------+------------------+ |
类型
- 0x1 目标部分是 IPV4 地址
- 0x03 目标部分是域名,是变长字符串,第一个字节表示后面数据的长度。
- 0x04 目标部分是一个 16 字节的 IPV6 地址
数据部分就是用户原始的请求(TCP或UDP数据包部分)
ssserver发送给sslocal的数据
ssserver发送给sslocal的数据如下:
根据上面的经验解密之。
发现直接是目标返回的内容,ssserver没有添加任何额外的头部,直接把原始数据返回。
看完这个 shadowsocks 的通信过程,我真是给它跪了,这个通信设计的也太简单粗暴点了吧,数据完整性校验,压缩,签名一概都没有。只把ssloca和sserver之间的通信数据进行加密,通信过程做了iv的随机化,每次发送的数据都会带上本次加密的iv。这一点数据伪装都没做,哎,怪不得被gfw干趴下(可以自己稍微改造改造,加点数据伪装等,尝试过一下…)。
0x3 针对shadowsocks的通信的攻击
360公开了对shadowsocks流加密通信过程的攻击文章,https://github.com/edwardz246003/shadowsocks,比较感兴趣,就学习一下。
文中提到的重定向攻击原理也十分简单,但是设计却十分巧妙,感觉其实算是一种重放攻击,下面详细介绍一下这个攻击的原理。
复习分组密码CFB模式
CFB模式的全称是 Cipher FeedBack模式(密文反馈模式),在CFB模式中前一密文分组会被送到密码算法的输入端,进行下一分组的加密。
加密的流程如下图所示:
相反的解密流程如下所示:
现在只看解密流程,如果我们知道了 明文分组1
和 密文分组1
,接下来就可以通过构造一个fake_密文分组1
,让ss解密来伪造一个任意的fake_明文分组1
,原因如下:
首先初始化向量iv加密之后的值,这里记它为 enc_iv
,那么有如下的等式:
1 | `明文分组1` xor `密文分组1` = `enc_iv` |
通过这样的方式控制 fake_密文分组1
就可以构造任意的fake_明文分组1
了。
漏洞利用过程
通过上面协议的分析,可以得出 sslocal 发送给 ssserver 的数据格式为:
1 | 随机IV + encrypt([ 1-byte type][variable-length host][2-byte port][payload]) |
ssserver 发送给 sslocal 的数据格式为:
1 | 随机IV + encrypt([payload]) |
如果我们拿到了 ssserver 发送给 sslocal 的数据,使用常规的非暴力手段是无法解密的,但是如果我们知道了此数据的前7个字节,那么就可以利用上面介绍的CFB明文伪造攻击将 fake_明文分组1
的前7个字节伪造为 [ 1-byte type][variable-length host][2-byte port]
,然后把此数据包做为 sslocal 发送给 ssserver 的数据,发给 ssserver。
因为数据 [ 1-byte type][variable-length host][2-byte port]
的内容可以完全被我们控制,所以将目标地址修改为我们自己的服务器,然后 ssserver 就会把解密完的数据发送到我们自己的服务器上,工作过程如下所示:
1 | ss-local(fake one) <--[encrypted]--> ss-remote <---> target(controlled) |
那关键问题是怎么知道加密数据的前7个字节的明文呢?论文中提供了一种方法,如果用户使用 shadowsocks 进行 http 通信,那么响应的前7个字节是HTTP/1.
,我们可以利用这7个字节来解密整个数据包。
具体的代码实现如下:
1 | c=c.decode('hex') |
因为修改了第一个密文分组,所以解密出来的第二个明文分组是不正确的,如下图所示,有16个字节的错误数据:
因为不知道到底哪个数据包传输的内容是 http 协议,所以需要多试几次,直到解密成功一个为止。 一旦解密成功,就可以知道一段密文分组经过key加密之后的值,就可以反解出key,进而破解所有数据包。
参考文章
https://tools.ietf.org/html/rfc1928
https://jiajunhuang.com/articles/2019_06_06-socks5.md.html
https://blog.gfkui.com/2018/04/29/shadowsocks%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/index.html
https://loggerhead.me/posts/shadowsocks-yuan-ma-fen-xi-tcp-dai-li.html
https://github.com/shadowsocks/shadowsocks/tree/master/shadowsocks
https://github.com/edwardz246003/shadowsocks