R·ex / Zeng


音游狗、安全狗、攻城狮、业余设计师、段子手、苦学日语的少年。 MUGer, hacker, developer, amateur UI designer, punster, Japanese learner.

Redirect attack - Shadowsocks 流密码的不安全因素

Shadowsocks 是一款陪伴无数玩家多年的科学上网工具,但是近年来随着墙的日益增高,一些 Shadowsocks 流量已经可以被很好的识别出来,然后就是——“同志,你梯子塌了”。

虽然“协议可以被识别”已经众所周知,但我们依旧认为,Shadowsocks 的加密做的不错,中间人应当破解不出明文信息。

然而,晚上看到的一篇 文章,稍稍动摇了一下我对 Shadowsocks 数据安全的信心,更让我重新审视了一下流密码的安全性。之前我在做安全方面的科普的时候,我总会提到“ECB 是不安全的”,但现在发现,其它的一些流密码也不一定安全,因为它们没有保证数据的完整性,因此存在数据被篡改的可能性。

Shadowsocks 如果配置得当,还是比较安全的,或者至少可以让目前的破解方法没有用武之地。如果看完本文后依旧不放心,可以使用其它的科学上网工具。

本文的一些图片引用自维基百科,可能需要科学地观看。

一些基本概念

考虑到可能有些同学没有密码学的基础,也对 Shadowsocks 的协议不是很了解,这里就简单的介绍一下。如果您已经有足够的知识,可以 跳到下一节

流密码

相信不少人都听说过 AES 之类的对称加密算法,但并没有了解的那么细致。例如什么是 IV?什么是基于数据块的加密?

事实上,AES 算法本体并不是为了处理无限长度的字符串而设计的,它一次只能处理 16 个字节(不管是 AES-128 还是 AES-256),我们管这 16 个字节叫做一个“数据块”,AES 就是一个基于数据块的加密算法。对于这一缺陷,可以用这样一种思路:先将明文切割成若干个数据块,对于每个块用 AES 做加密,再把每个加密后的块拼起来。

是不是很简单?其实这就是 ECB 模式。使用这个思路的 AES-256 算法被称为 AES-256-ECB。由于 16 字节对于一些数据来说实在是太小,很容易出现大量重复的块。例如有这样一张图片:

Original image

大家都知道这是一只小企鹅,我们用 ECB 加密一下再看看:

Encrypted using ECB mode

应该依旧能看出来图中是刚才那只小企鹅。ECB 模式无法隐藏原文的特征,请大家尽量不要使用。

Electronic Codebook (ECB) mode encryption

有一些改良的方法,例如除了密码以外,再提供一个初始向量 IV 作为第 0 个数据块,在加密第 i 个块之前,先将明文跟上一个块的加密结果异或一下,即:

Cipher[0] = IV
Cipher[i] = Encrypt(Plain[i] ^ Cipher[i - 1])

Cipher Block Chaining (CBC) mode encryption

这种方式会比刚才的 ECB 安全得多。

还有其它的一些方式,所有的这些方式(包括 ECB)统称为“流密码”。一开始的那张小企鹅,用 ECB 以外的方式加密之后是这样的,已经完全看不出是什么了:

Modes other than ECB result in pseudo-randomness

中间人攻击

中间人攻击是个比较好玩的东西,假设攻击者利用一些方法(比如用一个钓鱼热点)让受害者发出的数据包走到了自己这里,那么攻击者就可以查看或者修改数据包的内容。就好像小明在课上给小红传纸条,本来写的是“我喜欢你”,但在经过小亮的时候,小亮用自己的纸条替换了他们的,上面写着“滚犊子吧”,可见中间人攻击对我们的影响有多大。

注:在课上传纸条是不好的行为,小朋友们不要模仿。

大家在日常生活中最常经历的中间人攻击,可能是在访问某些 HTTP 网站的时候,突然弹出一个“宽带到期需要续费”的通知。这种中间人攻击是一些无良运营商干的,不过我们更习惯把它叫做“流量劫持”。

数据完整性

这一段就偷个懒,直接摘抄百度百科了(有删改):

完整性是信息安全的三个基本要点之一,指用户、进程或者硬件组件具有能力,能够验证所发送或传送的东西的准确性,并且进程或硬件组件不会被以任何方式改变。

翻译成白话就是:能保证数据在传输过程中不被篡改,小红收到的纸条内容跟小明传出去的一模一样。

  • 大家听说过的 ECC 内存纠错算法就能保证数据完整性,它可以发现内存数据被篡改(例如遭受了高能粒子冲击或硬件的部分损坏),并通过一些算法尽量恢复被损坏的部分;
  • HTTPS 中的数字签名也是一种保证数据完整性的方法,它可以发现传输的数据被篡改(例如中间人攻击),并且立即停止数据传输,以防止用户或服务器因为接收到虚假数据而遭受损失。

Shadowsocks 协议基础

虽然 Shadowsocks 使用的底层协议是 SOCKS5,但对于本文而言,底层的 SOCKS5 并不是重点,我们只需要关注 Shadowsocks 的客户端与服务器之间是如何传输数据的。

根据官方文档所说,客户端向服务器发送的数据,一开始是流密码的 IV(也就是说,IV 由客户端生成,并直接扔进数据包中),之后就是一段加密数据,它的明文格式是这样的:

[目标地址][数据]

其中数据可以是任意长度;至于目标地址,Shadowsocks 用的是 SOCKS5 的表示法:

[1 字节类型][主机名][2 字节端口]

其中,类型是 1 字节的枚举值:

  • 0x01:主机名是 IPv4 地址;
  • 0x03:主机名是变长字符串,首字节表示长度(最大 255),后面是数据;
  • 0x04:主机名是 IPv6 地址。

一次代理的过程如下:

  1. 客户端将这些数据加密后发到服务器;
  2. 服务器收到后将其解密,会得到 [1 字节类型][主机名][2 字节端口][数据]
  3. 服务器会将数据部分直接发送给 主机名:端口
  4. 服务器将主机返回的数据直接使用同样的算法加密(如果加密算法用了流密码,则会生成并使用一个新的 IV,并将其放在包的最前面),发送给客户端;
  5. 客户端解密后即可得到主机返回的数据。

Shadowsocks + 流密码的不安全因素

回到一开始说的那篇文章上,作者的发现是:如果攻击者抓到了一个 Shadowsocks 服务器返回的包,并且已知数据部分的开头七个字节,那么有可能在不知道密码的情况下,利用那个 Shadowsocks 服务器来解出包的绝大部分内容(最多损失 16 字节)。

作者的思路是这样的:

假设有一台 Shadowsocks 服务器,攻击者通过嗅探或其它方式抓到了这个 Shadowsocks 服务器返回的一个包。

为了知道明文内容,攻击者要么暴力破解密码(随着大家安全意识的提升,这已经几乎不可行了),要么想办法利用这台 Shadowsocks 服务器帮忙解密。

作者选择了后者,即想办法把这个包变成客户端发的包,让服务器解密后代理到自己指定的服务器,这被称为 Redirect attack

上一节说到,Shadowsocks 客户端发的包格式(明文状态下)是 [1 字节类型][主机名][2 字节端口][数据]。如果攻击者可以利用加密算法的缺陷来篡改明文数据,就可以把主机名改成攻击者的服务器地址,Shadowsocks 服务器就会以为客户端想访问攻击者的服务器,于是就把解密后的包中的数据部分发了过去。

先考虑如何篡改数据。假设这台 Shadowsocks 服务器的加密算法使用的是 AES-256-CFB,那么解密的方式如维基百科所述是这样的:

Cipher Feedback (CFB) mode decryption

其中 IV、每一块 Ciphertext 和 Plaintext 长度都是 16 字节。

作者发现,key 是不变的,IV 也可以重用服务器返回包中的那个,那么如果只修改第一块 Ciphertext,那么只有前两块 Plaintext 会改变,更重要的是,由于第一块 Plaintext 就是第一块 Ciphertext 跟某个串 A 的异或值,那么攻击者完全可以通过修改第一块 Ciphertext 的值来控制第一块 Plaintext!具体方法如下:

假设当前的第一块 Ciphertext 是 c1,第一块 Plaintext 是 p1IV 做了一系列 whatever 的运算得到的结果是 a,那么:

已知 a ^ c1 == p1
那么 a ^ c1 ^ X == p1 ^ X
根据异或的结合律
可得 a ^ (c1 ^ X) == (p1 ^ X)
也就是说,攻击者对 c1 做的异或操作,会完完全全反映在 p1 上

假设最终让 p1 变成了 q1,我们可以认为
已有 p1 ^ X == q1,将两边同时异或 p1
可得 p1 ^ X ^ p1 == q1 ^ p1
由于异或的性质,左边的两个 p1 抵消
可得 X == q1 ^ p1

攻击者需要做的就是将 c1 ^= (q1 ^ p1)。但这里有一个问题,我们并不能知道具体的 p1 是什么!不过还好,它是明文数据的一部分,在上网的过程中,总有些协议的头几个字节是固定的,例如 HTTP 协议。

在 21 世纪的第三个十年,大家应该早就切换成 HTTP 1.1 了,因此返回的数据包一开始一定是 8 个字节 HTTP/1.1。攻击者能否将 TA 的主机地址压缩到这么小呢?毕竟除掉 1 字节类型和 2 字节端口以外,可用空间只有 5 字节了。

对于绝大部分攻击者来说,不可能拿到不超过 5 字节的域名,因此只能考虑 IPv4 了,而且如果用 IPv4 的话,甚至只需要总共 7 个字节!举个例子:

01 c0 a8 01 03 12 12
-- ----------- -----

划线的三部分分别代表了:使用 IPv4 协议、地址是 192.168.1.3,端口是 4626。

那么我们完全可以令:

p1 = 'HTTP/1.'
q1 = '\x01\xc0\xa8\x01\x03\x12\x12'
new_c_part = c1[0:7] ^ p1 ^ q1

用这个 7 字节的 new_c_part 替换掉之前 c1 的前 7 个字节,然后直接将替换后的整个包发送到刚才的 Shadowsocks 服务器。

Shadowsocks 服务器尝试解密,解密后发现明文是这样的:

01 c0 a8 01 03 12 12 XX XX XX XX XX XX XX XX XX

服务器会认为这是一个合法的客户端请求,因此将后面的一串 XX(明文数据)按照前 7 个字节的要求,转发到了 192.168.1.3:4626

攻击者早就在这儿坐等了,方法非常简单,只需要用 nc 启动一个端口监听即可:

$ nc -l -p 4626

由于攻击者修改了 c1,而 c1 在 CFB 模式中又用来解密 p2,因此收到的 p2 这 16 个字节应该是乱码。攻击者最终可以还原出 p2 以外的所有数据。论文中的命令行截图也说明了这点,获取到的数据的第一个字节是之前包的明文的第 8 个字节(前 7 个是 HTTP/1.),然后有 9 个字节是正确的,之后 16 个字节是乱码,再之后是完全正确的:

1 304 Not???????????????? Sat, 26 Jan 2019 07:15:21 GMT
Connection: close
Via: 1.1 varnish
Cache-Control: max-age=600
ETag: W/"5c45d22a-127"
Expires: Sat, 26 Jan 2019 06:59:41 GMT
Age: 0
....

作者给出的防御措施是:

  • 禁用 shadowsocks-py、shadowsocks-go、go-shadowsocks2、shadowsocks-nodejs
  • 只用 shadowsocks-libev,并且只使用 AEAD 加密

原因如下:shadowsocks-libev 的实现很久之前就已经禁止了 IV 重用,可以在一定程度上防止这种攻击;只要加密算法带有 AEAD 特性,那么数据就无法被篡改,本文的攻击方式也是无效的。

对大众的影响

虽然文章中只列举了 HTTP 协议和 CFB 模式的例子,但理论上来说,所有头部 7 个字节已知的协议和所有类似流密码的组合,都可以被这种方法攻击。你不能保证你科学上的网总是 HTTPS,即使是 HTTPS,如果是国内网站,当某些不可抗力获取了其证书之后,你的 TLS 流量总是会被解密的。

不过有一些值得欣慰的地方:

由于墙的逐渐升高,大家已经逐渐意识到“只有加密是不行的”了,因此纷纷改用带有混淆功能的科学上网工具。由于攻击者无法得知混淆的参数(甚至不知道哪个流量是梯子的流量),因此这个方法不再起作用了。

目前大部分科学上网工具已经禁用了旧的加密算法,甚至强制只让使用带有 GCM 或者 Poly1305 的加密算法,这些算法有严格的 AEAD 特性,可以极大保证数据安全。TLS 1.3 强制使用 AEAD 也在某些程度上为它的安全性做了担保。

如果你还在用 Shadowsocks 或其衍生工具,并且依旧使用普通的流密码来加密,那么请立即听从作者给出的防御措施,为了你的服务器,也为了你自己。

参考资料

Update 2020-02-15

添加了对 IV 重用相关的补充。

版权声明:除文章开头有特殊声明的情况外,所有文章均可在遵从 CC BY 4.0 协议的情况下转载。
上一篇: TypeScript 类型系统与函数式编程
下一篇: Go module 踩坑备忘

这是我们共同度过的

第 1591 天