shadowsocks协议
原 shadowsocks 协议 的 TCP 握手包(加密后)的格式是这样的:
+-------+----------+
| IV | Payload |
+-------+----------+
| Fixed | Variable |
+-------+----------+
其中的 IV(Initialization Vector, 初始化向量)是使用随机数生成器生成的一个固定长度的输入值。通过引入 IV
能够使相同的明文和相同的密钥产生不同的密文,让攻击者难以对同一把密钥的密文进行破解。
shadowsocks 服务端会用这个 IV
和 pre-shared key
(预共享密钥,通常是用户设置的密码)来解密 TCP 数据包中的 payload
。
解密后的内容格式如下:
+--------------+---------------------+------------------+----------+
| Address Type | Destination Address | Destination Port | Data |
+--------------+---------------------+------------------+----------+
| 1 | Variable | 2 | Variable |
+--------------+---------------------+------------------+----------+
其中 Address Type
(ATYP) 是地址类型,占一个字节,有三个可能的取值:01
, 03
, 04
,分别对应 IPv4
, hostname
, IPv6
类型的地址。这些都是 RFC1928 中定义的标准,有兴趣可以去看看。
握手完成后 shadowsocks 中继就会工作在流模式下,后续的所有 TCP 数据包不会再带上 IV
,而是使用握手时协商的那个 IV
。
说完了原 shadowsocks 协议的内容,下面说说该协议的不足之处。
原协议的缺陷
正如上表所示,原始 shadowsocks 协议 TCP 握手包中的 IV
字段是 Fixed(定长)的。不同的加密算法 IV
长度不同,对于 rc4-md5
和 aes
系列等常用算法,这个长度是 16
字节。各加密算法的详细信息可以在 官方文档 - Cipher 查看。
而服务端为了判断数据是否有效,会检查数据包中表示地址信息的那个字节,看它是不是上面提到的三个可能取值。如果是,就尝试解析后面的地址和端口进行连接;如果不是,立即断开连接。
正是 shadowsocks 服务器的这个行为使得主动探测成为可能。
主动探测的原理
该方法由 @breakwa11 提供
一般来讲,「表示地址类型的那个字节」是被加密后发送的,所以第三方无法精确的修改它。但是不巧的是,shadowsocks 所有的加密方式都是 stream cipher
(流加密),而这种加密方式的特点就是「明文数据流与密钥数据流一一对应」。
通俗地讲,即对应修改了某个位置的密文(根据加密模式的不同,可能影响到后面其他密文块的解密,也可能影响不到,但在这里这个性质并不重要),如果预先知道了明文的模式,虽然无法解密还原出内容,但可以修改密文中的特定字节,起到修改解密后的明文的效果。
根据流加密的这个特性,坏东西们就可以通过伪造 TCP 数据包来主动探测 shadowsocks 服务器了。攻击者只要暴力尝试修改加密后的数据包中 IV
之后紧接着的那个字节(如果使用的加密算法 IV
长度为 16 字节,那么就修改第 17 个字节),穷举 2^8 = 256
种可能性,如果被测试的服务器有一种到三种情况下没有立即关闭连接,就可以判断出这台机子的这个端口开放的是 shadowsocks 服务。
或许这种主动探测方法正在被 GFW 大规模应用,谁知道呢?你正在使用的原版 shadowsocks 代理随时有可能被封锁。
防范主动探测
经过讨论后上述漏洞被证明是 确实存在 的,所以现在大部分的 shadowsocks 分支都已经加入了针对这种探测方法的对抗措施(e.g. shadowsocks-libev v2.5.5+),即「随机超时抵抗」而不是立即断开连接,配合自动黑名单等机制可以有效减少被探测到的风险。
上述情况下主动探测能够得逞的原因是服务器没有对收到的数据包进行校验,随便哪个阿猫阿狗发来的数据包,不管有没有被恶意篡改过,原来的 shadowsocks 服务器都会做出同样的反应。
OTA
shadowsocks OTA(one time auth) 一次性认证功能。
此功能的作用是防止 CCA(Chosen Ciphertext Attack)选择性密文攻击。
ss OTA功能原理是:每一次用户和服务器建立tcp链接的时候, 对数据添加了额外验证的加密串,将其放置于头部,服务器收到后将会进行数据来源合法性验证。
详细
开启了 OTA 后的 shadowsocks 握手包(加密前)是这样的:
+------+---------------------+------------------+-----------+
| ATYP | Destination Address | Destination Port | HMAC-SHA1 |
+------+---------------------+------------------+-----------+
| 1 | Variable | 2 | 10 |
+------+---------------------+------------------+-----------+
可以看到它添加了一个 HMAC-SHA1
字段,这个字段是将除了 DATA
通过 HMAC-SHA1
算法(以 IV + PSK
作为 key)生成的。并且数据包头部的 ATYP 添加了一个标志位用于指示 OTA 是否开启(ATYP & 0x10 == 0x10
)。
+----------+-----------+----------+----
| DATA.LEN | HMAC-SHA1 | DATA | ...
+----------+-----------+----------+----
| 2 | 10 | Variable | ...
+----------+-----------+----------+----
握手完成后,接下来的 TCP 数据包均在原始协议的包上添加了 DATA.LEN
(包长度)和 HMAC-SHA1
字段。这样,服务器就可以对数据包进行完整性校验,也就可以识别出被篡改过的数据包了。
缺陷
还记得我们上面提到的 stream cipher
(流加密)的特点吗?攻击者可是使用同样的套路修改数据包中的 DATA.LEN
字段,然后通过观察服务器的反应来判断这是否是一个 shadowsocks 服务器。
举个栗子,如果攻击者恶意构造 DATA.LEN
的高位字节密文,使得解密后 DATA.LEN
的数值变得特别大(但是后面的 DATA
的大小并没有改变),shadowsocks 服务器就会继续等待那些实际上并不存在的数据传输完成直到超时。因此只要在发送恶意数据包后观察服务器是不是「不会断开连接且至少等待 1 分钟无任何数据包」即可确定该服务器是否开启了 shadowsocks 服务。
没错,这样的检测方法比检测原版协议还要神不知鬼不觉,甚至不会在服务端留下任何可疑的痕迹。OTA 当初是为了给原版协议的流加密加上一个认证以增强安全性,殊不知这带来了更大的隐患,这也是为什么 shadowsocks-org 要急急忙忙弃用 OTA 的原因。
AEAD
在通常的密码学应用中,Confidentiality(保密)用加密实现,消息认证用 MAC(Message Authentication Code,消息验证码)实现。这两种算法的配合方式,引发了很多安全漏洞,过去曾经有 3 种方法:
- Encrypt-and-MAC (E&M)
- MAC-then-Encrypt (MtE) <- 即 OTA 的做法
- Encrypt-then-MAC (EtM) <- 新协议的做法
然而后来人们发现,E&M
和 MtE
都是有安全问题的,所以 2008 年起,逐渐提出了「用一个算法在内部同时实现加密和认证」的 idea,称为 AEAD (Authenticated Encryption with Associated Data)。在 AEAD 这种概念里,cipher + MAC
的模式被一个 AEAD 算法替换。
使用了 AEAD 算法的新协议本质上就是更完善的 stream cipher + authentication
,虽然它依然使用的是流加密,但是通过更完善的数据包完整性验证机制杜绝了上面所述的可被篡改密文的可能性。
注:截至本文发布时新协议都是使用的 流加密 + 认证
,不过 AEAD 的设计使得它能够使用块加密,因此上面说的并不是绝对的。
而为了实现认证加密(Authenticated Encryption),新协议必须要将 TCP 流分割成不同的 chunk 并分别验证。
方法
- AES-128-GCM
- AES-192-GCM
- AES-256-GCM
- ChaCha20-IETF-Poly1305
- XChaCha20-IETF-Poly1305
- 这些新的加密算法本质上就是
流加密 + 验证
,原先的其他单纯的流加密算法均不适用于新协议。