31、加密算法逆向(四):自定义加密算法分析与还原

说实话,做逆向这么多年,我最怕遇到的就是自定义加密算法。为什么?因为标准算法你查查文档、翻翻开源库,基本就能对上号。但自定义算法——那是开发者自己拍脑袋想出来的,没有标准可循,没有资料可查。你只能硬着头皮,从汇编代码里一点一点把逻辑抠出来。

不过话说回来,自定义算法也有它的软肋。它们往往实现得比较粗糙,甚至会有明显的逻辑漏洞。今天我就带你走一遍我分析自定义加密算法的完整流程。

自定义加密的常见特征

在开始分析之前,我们先聊聊怎么识别一个算法是「自定义」的。我在项目中遇到过好几次,一开始以为是标准算法,结果越看越不对劲。

特征 说明 我的判断依据
魔数/常量异常 没有标准算法的固定常量(如AES的S盒、MD5的初始向量) 看到一堆奇怪的数字,比如0x5A827999这种,基本就是自定义
运算组合奇怪 异或、加法、移位混着用,但看不出标准模式 比如先左移3位,再异或0x7F,再加一个变量——这明显不是标准套路
密钥使用方式 密钥直接参与运算,没有密钥扩展或轮函数 我见过直接把密钥当异或掩码用的,简单粗暴
轮数不固定 循环次数取决于输入长度或其他变量 标准算法轮数是固定的,自定义的经常随心所欲
我的小技巧: 在IDA里搜索立即数时,如果发现0xFFFFFFFF、0x55555555这类「整整齐齐」的数字,大概率是自定义掩码或轮换因子。标准算法很少用这种「强迫症」数字。

分析流程:从黑盒到白盒

我个人习惯把自定义加密分析分成四个阶段。你想想看,这其实跟拆解一个机械手表差不多——先看外观,再拆外壳,然后研究齿轮,最后画图纸。

阶段一 黑盒观察 输入输出对比 阶段二 静态分析 反汇编/反编译 阶段三 动态调试 断点/内存/寄存器 阶段四 算法还原 伪代码/验证 每个阶段都可能需要回退到上一阶段 —— 这不是线性流程,而是迭代过程 迭代回退

实战案例:一个简单的自定义加密

好,我们来看一个真实的例子。这是我之前逆向一个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里追踪数据流。
  • 陷阱三:多轮迭代。有些自定义算法会循环多次,但每次的密钥或参数不同。这时候要特别注意循环变量的变化。
我的习惯: 每次还原完一个算法,我都会用Python写一个测试脚本。输入几组不同的数据,对比原始程序和我的还原版本。如果全部通过,我才会说「这个算法我搞定了」。否则,继续调试。

总结

自定义加密算法的逆向,说白了就是一场「猜谜游戏」。开发者设计了一个谜题,你要做的就是找到解开谜题的钥匙。没有标准答案,没有捷径可走。但只要你掌握了方法——黑盒观察、静态分析、动态调试、算法还原——再复杂的自定义加密也能被拆解。

我记得有一次,一个朋友问我:「你花了三天时间逆向一个自定义算法,值得吗?」我说:「值得。因为下次再遇到类似的,我可能只需要三小时。」这就是经验的价值。


公众号:蓝海资料掘金营,微信deep3321