31、加密算法逆向(四):自定义加密算法分析与还原
说实话,做逆向这么多年,我最怕遇到的就是自定义加密算法。为什么?因为标准算法你查查文档、翻翻开源库,基本就能对上号。但自定义算法——那是开发者自己拍脑袋想出来的,没有标准可循,没有资料可查。你只能硬着头皮,从汇编代码里一点一点把逻辑抠出来。
不过话说回来,自定义算法也有它的软肋。它们往往实现得比较粗糙,甚至会有明显的逻辑漏洞。今天我就带你走一遍我分析自定义加密算法的完整流程。
自定义加密的常见特征
在开始分析之前,我们先聊聊怎么识别一个算法是「自定义」的。我在项目中遇到过好几次,一开始以为是标准算法,结果越看越不对劲。
| 特征 | 说明 | 我的判断依据 |
|---|---|---|
| 魔数/常量异常 | 没有标准算法的固定常量(如AES的S盒、MD5的初始向量) | 看到一堆奇怪的数字,比如0x5A827999这种,基本就是自定义 |
| 运算组合奇怪 | 异或、加法、移位混着用,但看不出标准模式 | 比如先左移3位,再异或0x7F,再加一个变量——这明显不是标准套路 |
| 密钥使用方式 | 密钥直接参与运算,没有密钥扩展或轮函数 | 我见过直接把密钥当异或掩码用的,简单粗暴 |
| 轮数不固定 | 循环次数取决于输入长度或其他变量 | 标准算法轮数是固定的,自定义的经常随心所欲 |
分析流程:从黑盒到白盒
我个人习惯把自定义加密分析分成四个阶段。你想想看,这其实跟拆解一个机械手表差不多——先看外观,再拆外壳,然后研究齿轮,最后画图纸。
实战案例:一个简单的自定义加密
好,我们来看一个真实的例子。这是我之前逆向一个Android App时遇到的。它的加密函数大概长这样——当然,我做了简化处理。
// 这是反编译后的伪代码
int custom_encrypt(unsigned char *input, int len, unsigned char *key, int key_len) {
unsigned char state = 0;
for (int i = 0; i < len; i++) {
state = (state + key[i % key_len]) & 0xFF;
input[i] = input[i] ^ state;
state = (state + input[i]) & 0xFF;
input[i] = input[i] ^ (state >> 3);
}
return len;
}
你看,这个算法其实很简单。它用了一个状态变量 state,每次迭代都更新它。但就是这种「简单」,反而让很多新手栽跟头——他们以为加密一定很复杂,结果被这种「小把戏」绕晕了。
还原步骤详解
我是怎么一步步把这个算法还原出来的?嗯,这里我详细说说。
第一步:定位加密函数
在IDA里搜索字符串,比如"encrypt"、"cipher"、"encode"之类的。如果找不到,就找那些频繁调用异或、移位操作的函数。我一般会重点关注循环体——加密算法几乎都有循环。
第二步:提取运算逻辑
把反编译出来的伪代码,一句一句翻译成高级语言。注意,这里不要急着优化,先原样照搬。我曾经犯过一个错误——觉得某个变量没用就跳过了,结果发现它是关键的状态种子。
// 原始反编译代码
v3 = 0;
do {
v4 = key[v3 % key_len];
state = (state + v4) & 0xFF;
input[v3] ^= state;
state = (state + input[v3]) & 0xFF;
input[v3] ^= (state >> 3);
++v3;
} while (v3 < len);
第三步:动态验证
这一步很关键。我会写一个测试程序,用同样的输入和密钥,跑一遍我还原的算法。如果输出和原始程序一致,那就对了。如果不一致——嗯,那就回去看汇编,看看是不是漏了什么细节。
__ROR__ 循环右移,但IDA把它显示成了普通右移。结果我验证了三天都没通过。后来用调试器单步跟踪才发现,实际执行的是循环右移,不是算术右移。所以,永远不要100%相信反编译器的输出。
第四步:写出解密函数
加密算法还原了,解密自然就出来了。对于对称加密,解密就是加密的逆过程。拿我们刚才的例子来说:
// 解密函数
int custom_decrypt(unsigned char *input, int len, unsigned char *key, int key_len) {
unsigned char state = 0;
unsigned char temp;
for (int i = 0; i < len; i++) {
state = (state + key[i % key_len]) & 0xFF;
temp = input[i] ^ (state >> 3); // 先还原第二步异或
state = (state + temp) & 0xFF; // 注意:这里要用还原后的值
input[i] = temp ^ state; // 再还原第一步异或
}
return len;
}
注意看,解密时我引入了一个临时变量 temp。为什么?因为加密时 state 的更新依赖于 input[i] 的原始值,而解密时我们需要先恢复出这个原始值,才能正确更新 state。这种「顺序依赖」是自定义加密里最常见的坑。
常见陷阱与应对策略
- 陷阱一:反编译器优化过度。有些编译器会把多个操作合并成一个,比如
a = a ^ (b + c)可能被优化成a ^= b + c。你还原时要注意拆开。 - 陷阱二:状态变量隐藏。有些算法会把状态变量藏在全局变量里,或者通过指针传递。你需要在IDA里追踪数据流。
- 陷阱三:多轮迭代。有些自定义算法会循环多次,但每次的密钥或参数不同。这时候要特别注意循环变量的变化。
总结
自定义加密算法的逆向,说白了就是一场「猜谜游戏」。开发者设计了一个谜题,你要做的就是找到解开谜题的钥匙。没有标准答案,没有捷径可走。但只要你掌握了方法——黑盒观察、静态分析、动态调试、算法还原——再复杂的自定义加密也能被拆解。
我记得有一次,一个朋友问我:「你花了三天时间逆向一个自定义算法,值得吗?」我说:「值得。因为下次再遇到类似的,我可能只需要三小时。」这就是经验的价值。
公众号:蓝海资料掘金营,微信deep3321