1. RC4 加密简介
RC4 (Rivest Cipher 4) 加密算法是由 Ron Rivest 于 1987 年设计的一种对称密钥流密码 (symmetric key stream cipher),广泛应用于 SSL/TLS 协议和 WEP/WPA 协议。
在对称密码中,加密者和解密者使用相同的密钥。数据发送方利用密钥将明文经过特殊加密算法处理后,向数据接受方发送复杂的加密密文;接受方收到密文后,需要使用加密所用的密钥和相同加密算法的逆算法对密文进行解密,才能得到可读明文。
RC4 面向字节,算法简单而高效。作为流密码,RC4 的密钥流长度与明文的长度相同。密钥流派生自一个较短的初始密钥,派生算法通常为一个伪随机数生成算法。初始密钥长度是可变的,变化范围为 1-256 bytes (8-2048 bits)。
在现代技术的支持下,当初始密钥长度达到 128 bits 时,用暴力法搜索密钥已经不太可行,因此很长一段时间内,RC4 被应用于 SSL、WPA 等流行的互联网加密协议中。
然而,随着时间的推移,RC4 的弱点也不断暴露出来。各大机构已经不再推荐使用 RC4 算法。[1][2]
2. RC4 加密流程分析
完整的 RC4 加密分为三步:初始化算法 (Key Scheduling Algorithm, KSA)、伪随机数生成算法 (Pseudo-random generation Algorithm, PRGA) 和异或加密。前两个步骤生成 RC4 需要的密钥流。
2.1 初始化算法 (KSA)
-
初始化一个包含 256 字节的状态向量 S,作为生成密钥流的种子 1。按照升序顺序,依次给每个字节赋值,即 ,.
1
2for i = 0 to 255 do
S[i] = i
这个向量 S 也有一个专有名词 “S 盒”,表示密码加密过程中的混淆过程。
-
创建一个包含 256 字节的向量 T。由用户输入初始密钥 k,如果 k 的长度等于 256 字节,则直接将 k 赋值给 T;如果小于,则将 k 轮转输入直到填满 T(例如,如果密钥 k 为 “1,2,3,4”,那么 T 应为 “1,2,3,4,1,2,3,4,1,2,3,4,…”)。填充后的 T 作为生成密钥流的种子 2.
1
2for i = 0 to 255 do
T[i] = k[i mod keylen] -
置换向量 S。这一步会引入向量 T.
1
2
3
4j = 0
for i = 0 to 255 do
j = (j + S[i] + T[i]) mod 256
swap (S[i], S[j])
2.2 伪随机数生成算法(PRGA)
使用 S 生成密钥流 K。
1 | i = j = 0 |
2.3 异或运算
将密钥流 K 与明文 P 一个字节一个字节对应,进行异或算法,生成密文 C。
1 | C[i] = P[i] ⊕ K[i] |
2.4 C 代码演示
1 |
|
2.5 RC4 解密
解密通过使用相同的密钥生成相同的密钥流,并将其与密文进行异或运算以恢复明文:
1 | P[i] = C[i] ⊕ K[i] |
换句话说,解密时,把密文当明文走一遍加密流程,那么输出的密文就是明文。这就是 “对称密码” 的性质。
3. RC4 特征分析
RC4 通常是使用加密过程的特征去识别的。
- 次数为 256 的循环,以及
% 256
的运算。 - 最后处理输入数据的是异或运算。
4. RC4 加密的处理
RC4 加密的处理思路为:enc_data = RC4(flag); flag = RC4(enc_data)
对于一般的 RC4 加密,通过逆向找到密文和初始密钥,然后重走一遍加密流程就好了。这个加密流程既可以自己重写,也可以通过修改内存,让目标程序处理密文。
众所周知,RC4 是出了名的不统一,各个语言的 RC4 结果基本上都不一样,所以碰到 RC4 的逆向题一般只能写代码去解密。
RC4 属于互逆密码 (Reciprocal cipher),可以将密文输入到系统中的相同位置以获得明文。
接下来,我们介绍一些魔改的 RC4 加密算法:
- 魔改初始化算法。可以随意指定 S 的初始值(不一定为 ,也不一定在 0-255);也可以在 S 的置换过程中加入可逆的运算。
- 魔改异或算法。密钥流不和原文进行异或。
- 数据重加密。在异或加密后添加其他可逆运算。
5. 习题示例
拖进 DIE 看看信息
64 位,没有壳。
拖进 IDA,找 main
函数
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
通过阅读伪代码,可知 flag 长度应为 45,其密文存储在 v8
中。用户输入存储在 s
中。
我们感兴趣的是 58
、59
这两行。58
行将一个字符串存入 v5
中,59
行的 sub_400874
将 v5
和 s
进行某种处理,然后存入 v7
.
查看 sub_400874
的伪代码
1 | __int64 __fastcall sub_400874(__int64 a1, __int64 a2, __int64 a3) |
又发现两个未知函数 sub_40067A
和 sub_400753
。
先查看 sub_40067A
1 | __int64 __fastcall sub_40067A(const char *a1, __int64 a2) |
发现几个次数 256
的循环以及 % 256
操作。很明显的 RC4 特征。
再看 sub_400753
1 | __int64 __fastcall sub_400753(__int64 a1, const char *a2, __int64 a3) |
% 256
操作和异或运算,也是很浓的 RC4 味道。
因此该程序使用的加密算法大概率为 RC4。s
是明文,v7
是生成的密文,那么 s
就是密钥。
现在我们要获取 flag 的密文。在静态调试时,我们点击 v8
,会发现所有的元素都是问号:这些数据本来是存储在 main
函数的栈段的。所以我们使用远程调试。
有毅力的话也可以试试手动输入……
远程调试时,在汇编窗口框选所需要的数据,使用 Shift+E 或者 LazyIDA 输出数据。
脚本:
1 |
|
得到:
1 | Flag: n1book{us1nG_f3atur3s_7o_de7erm1n3_4lg0ri7hm} |