Stack Issue ShadowSocks协议的弱点分析和改进 - fourbackup/shadowsocksr GitHub Wiki

基本的原理

原ShadowSocks协议的TCP连接部分,使用非常简单的协议,加密前是这样子的:

^ ^ ^
地址类型(1byte) 地址(不定长) 端口(2byte)

其中,地址类型有三个可能取值:1,3,4,分别对应IPv4,Remote DNS,IPv6

地址的长度就按这个地址类型取值来决定,后面这些不详细介绍了,不是关键的东西。

而加密的时候,会根据不同的加密算法,在前面添加一些随机字节,再进行加密,以使得即使发送相同的数据,加密的结果也不会相同。但是,同一个加密算法下,前面添加的随机字节的长度是固定的,而最常用的加密算法只有rc4-md5和aes系列(aes-128-cf8和aes-256-cfb等等),而非常的不巧,这些加密算法在使用的时候前面均添加16字节,于是前文所说的表示地址类型的那个字节的位置是完全固定的(在第17字节上)。

服务端为了判断数据是否有效,判断的依据就是表示地址类型的那个字节,看它是不是在那三个可能取值,如果不是,立即断开连接,如果是,就尝试解析后面的地址和端口进行连接。

主动探测

基本的原理介绍完了,以下介绍如何进行主动探测。

因为SS判断数据的合法性,仅仅依据表示地址类型的那个字节,与此同时,根据信息论,对加密后的数据解密的时候,同一个位置上的数据如果穷举全部256个编码,那么能正好一一对应解密后的全部256个原码,这与密码长度还是用什么加密算法完全无关。那么,可以通过暴力尝试全部256种可能,找出服务器是否有三种情况下没有立即关闭连接,从而成功判断出这个端口开放的是SS服务。成功判断出这是SS服务器为了什么呢?聪明的你应该明白,在这之后你的IP或者你的服务器IP其中一方会落入黑名单。

PS.因为chacha20填充的长度是8字节,和其它加密用的长度并不一样,于是你会发现用chacha20的往往比其它加密算法稳定,只是因为这个长度较少被用于探测。

关键的问题就是,只需要穷举256种可能,成本太低了,太不安全了。也许clowwindy也意识到这个问题,他在issue中提出到考虑让服务端发现连接的数据不正确的时候,把数据重定向到一个正常的http网站,让其行为看起来比较一致从而不好分辨。但是这个修改还没有完成,为什么clowwindy会被请喝茶?因为如果不阻止,那么加入这个特性后会让ss封锁起来困难很多,所以请喝茶,阻止更新,然后才方便对其实施大规模封杀(毕竟按照新的特性重新写探测代码很麻烦不是)。在我知道clowwindy被请喝茶那天,我已经明白根本不是因为ss不好封杀,而是阻止更新然后去封杀,这个弱点已经被掌握并正在被运用,大规模封杀那天会到来得很快。

但是,那个改进依然不安全,还是只要穷举256种可能,看一看是不是有253种情况是http/https协议就行了,所以我没有考虑这么做(不过我想得还是粗糙,这点其实是猜的,有误请指出)。如果是多用户服务器,就更简单了,直接端口扫描,看看开放的接口是不是大部分都是http(甚至全都指向同一地方)就行了,总之这种做法特征明显。

我感觉上,这个如果不从协议上去解决,还是很难避免被封杀,于是在原来的SS协议头,添加了一个包装,在这里复制一下这个包装的结构说明:

^ ^ ^ ^ ^ ^
标志版本号(1byte) 首包总长度(2byte) 随机填充长度(1byte) 随机填充数据 原ss首数据包 CRC32(4byte)

起关键作用的,就是末尾的CRC32,前面的数据包不管你如何去猜测,有了CRC32作为整个数据包的校验,穷举成本提升到至少256的5次方,即需要尝试10^12次,或1T次,产生的数据量将达到几十T,这样子去暴力破解还不如直接DDOS的成本低。另外,为啥CRC32放最末尾?一是为了方便校验,二是根据加密的特性,前面的数据只要改了一个,就会导致后面的数据完全不一样,这样只要前面的数据一动,后面所有的值就都错了,让CRC32校验充分发挥作用。

另外还有一个关键的地方就是随机填充不定长度的数据。现在其实除了新的Windows C#版客户端,其它平台基本没有把握手包和第一个数据包连起来发送,这样会导致SS客户端发送的第一个数据包的长度,在很多情况下是固定的,从而成为被检测的依据。就算实现了连同第一个数据包一起发送,但某些协议的连接并不会立即产生第一个数据包,而是先等待服务端的回应(比如 SSH),于是依然存在定长的问题(会精确等于16+1+4+2=23字节,只要统计发现你的IP经常产生23字节的TCP首包便可封杀)。为方便其它平台以最少的代码实现,直接在协议头部加入这个随机填充长度,以弥补ss在加密时总是产生定长数据包的问题。

这个解决方案能极大增加主动探测的难度以及首包长度检测的难度(非首包长度目前没有混淆,尽管还是可以通过非首包长度进行猜测,但毕竟需要的计算资源较大,这样调整后的SS还是能存活较长的时间的),但不想对SS协议做太大的改动从而造成各平台实现的修改困难(在Python或C#上增加支持这个协议才10行代码不到),SS之所以能实现如此多的平台,正是因为其协议简单,容易实现,所以我决定使用比较简单的协议来解决以上问题。

如果以上协议你还发现存在弱点,请留言告诉我,大家一起讨论——by Breakwa11