最早期版本 ss 的加解密方式详解

0x00 引言

对于这款玩鸡必备的工具 Shadowsocks ,我相信很多人对于它使用的加密方式非常感兴趣,通过观察源码才有了这篇博客,并不是凭空捏造,另附这次研究的源码地址: shadowsocks/shadowsocks/7d80058 。这是作者第四次提交的代码,到我写博客的时刻提交次数已经高达 819 ,最新版本已经支持非常多的加密方式,本次研究的加密方式也已经去除。既然研究的是源码,那就非常有必要去看作者早期的代码,然后慢慢向后拓展,所以此博客的后篇会根据我的研究进展同步。

0x01 这是何种加密方式?

这次提交的源码是最简单的,只有两个文件,加起来 200 多行代码,有兴趣的朋友可以自己研究。代码写的如此简单,那么加密方式肯定也是非常 low 的,仔细分析之后,采用了 古典密码 中的 单表代换 原则进行加解密的。
单表代换密码 常用的有两种 移位变换 仿射变换 。作者用的是后者 仿射变换 仿射变换 是一种 线性变化 , 即 f(x) = g(x)x 表示对应位置, fx 位置上的内容对应变换为 gx 位置上的内容,简单说就两个字 按位映射

0x02 加 / 解密步骤

对于 ss ,我总结出加密需四步:

  1. 变量 a 取密钥 keymd5 值的前 8 字节,即 a, _ = struct.unpack('<QQ', s)
  2. 生成 ASCII 码 的映射表 table ,即 table = list(map(chr, range(256)))
  3. 根据公式函数 lambda x, y: int(a % (ord(x) + i) - a % (ord(y) + i)),对 table 进行多次排序,彻底打乱 table 表的顺序,以此就得到了 加密参照表 encrypt_table ,可以把 encrypt_table 想象成 ASCII 码索引表 table 的一个 映射表
  4. 最后,将需要加密的 明文字节串 ,参照 encrypt_table 映射成 密文字节串

根据上面的方式加密,用户只需要提供一个长度随意的字符串作为key 就可以了。

为了方便理解,这里列出各变量的长度:

变量 长度(单位:byte) 备注
key 的 md5 16
a 8 取 md5 值的前 8 位
table 256 ASCII 码的索引,共 256,例如 table[97]=’a’
encrypt_table 256 只是将 table 次序彻底打乱,称 encrypt_tabletable 映射表 , 此时 table[97] 不一定是 ‘a’

解密需五步:

  1. 变量 a 取密钥 keymd5 值的前 8 位,即 a = md5bytes(key) >> 8
  2. 生成 ASCII 码 的映射表 table ,即 table = list(map(chr, range(256)))
  3. 根据公式函数 lambda x, y: int(a % (ord(x) + i) - a % (ord(y) + i)),对 table 进行多次排序,彻底打乱 table 表的顺序,以此得到了 加密参照表 encrypt_table
  4. 先生成 加密参照表 encrypt_tableASCII 码索引表 table 映射关系表 ,即为 解密参照表 decrypt_table
  5. 再将 解密参照表 decrypt_table 作为 密文字节串 的 映射表进行映射得到 明文字节串

到这里,你可能疑惑 映射表 映射关系表 的区别,这里举个例子,例如 有两个表 T1T2 :
T1 :

索引
0 a
1 b
2 c
3 d

T2 :

索引
0 e
1 f
2 g
3 h

这两个表的 长度一致 ,并且表中 值不重复,当 T2 作为 T1 的映射表时,可得映射关系 a->e、b->f、c->g、d->h ,这个映射关系用表格表示如下:

索引 映射关系
0 a->e
1 b->f
2 c->g
3 d->h

该映射关系表能够清楚地反映出了 T1T2 两表之间的映射关系,参照该映射表可将T1 表中的 abcd 四个字母组成的明文进行映射得到密文,例如 ‘ddaa’->’hhee’、’abab’->’efef’。

0x03 Python 中映射关系表的生成

在 Python2 中生成映射关系表的函数为 string.maketrans(),Python3 中为 str.maketrans() 。由于 Python3 中 maketrans 变为了 内置函数,它的源码就跟解释器挂钩了,例如 cpython 就是 c 实现的内置函数,所以这里给出的是 Python2 的相关源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
l = map(chr, xrange(256))
_idmap = str('').join(l)
del l

_idmapL = None

def maketrans(fromstr, tostr):
if len(fromstr) != len(tostr):
raise ValueError, "maketrans arguments must have same length"
global _idmapL
if not _idmapL:
_idmapL = list(_idmap)
L = _idmapL[:]
fromstr = map(ord, fromstr)
for i in range(len(fromstr)):
L[fromstr[i]] = tostr[i]
return ''.join(L)

注:对应的 python3cpython 源码见 bytes_methods.c 第 406 行

此函数的参数 fromstrtostr 必须长度一样,并且值不重复,这样才能使映射关系唯一。方便理解这段程序,这里举例说明:

em: 函数 string.maketrans('abyz', 'zyba') 的执行过程
fromstr 初始化内容:

索引
0 a
1 b
2 y
3 z

tostr 初始化内容:

索引
0 z
1 y
2 b
3 a

还要确定 L 初始化内容是什么。默认情况下 _idmapL 空值,所以 L 的值其实为 _idmap 的副本,可以见到代码:

1
2
3
l = map(chr, xrange(256))
_idmap = str('').join(l)
del l

所以,chr 函数将 ASCII 码 转化为对应的 字符 , 故L 的内容如下:

索引
97 a
98 b
121 y
122 z

注: 由于表长 256 ,此处仅列出本例中需要用到的值,下面类似。

然后 fromstr = map(ord, fromstr)ord 函数将 字符 转化为 ASCII 码 ,转化为 fromstr 内容如下:

索引
0 97
1 98
2 121
3 122

之后的 for 循环操作 L[fromstr[i]] = tostr[i] ,将 L 中对应的字符 转化为 映射表 tostr 中对应值,最后得到新的 L :

索引
97 z
98 y
121 b
122 a

最后这个 L 就是 映射关系表 , tostr 就是 映射表。通过观察表格的变化,每个人都能对 Python 中映射关系表的实现有了更深入的了解。

0x04 ss 的这种加解密方式的优缺点

由于是一对一的转化映射,所以明文和密文长度一致,并且密钥 key 固定生成的密文也是固定的,所以会存在爆破风险,此时建议密钥越复杂越好。当然,无论如何,如果采集加密过的流量足够多的话,也能直接找到映射关系,此时也就用不着密钥便能破解加密流量了。

还有一点就是,由于是每个字节都变了,失去了协议特征,在网关处检测到的这种流量定性为 未知流量 , 如果网关屏蔽此类流量,都不用特征识别,这将会是一招秒的结局。

所以适合出入流量不多,而且使用时间比较分散的人群,这种加密方式加密速度快,但安全性低,与当今社会已经完全格格不入了。





root@kali ~# cat 重要声明
本博客所有原创文章,作者皆保留权利。转载必须包含本声明,保持文本完整,并以超链接形式注明出处【Techliu】。查看和编写文章评论都需翻墙,为了更方便地获取文章信息,可订阅RSS,如果您还没有一款喜爱的阅读器,不妨试试Inoreader.
root@kali ~# Thankyou!