自定义加密的本质:混淆而非密码
当 App 不用任何标准算法(AES/RSA/TEA 都不是),而是自己撸一套"加密"——通常是异或、加减、位移、置换的组合。从密码学角度它弱不禁风,从逆向角度它的难度在于"没有特征可循":没有常量可搜,只能硬读逻辑。
好消息是:这类算法的强度上限很低,掌握方法后还原是时间问题。
识别:先确认是自定义算法
排除法确认:
- 搜遍密码学常量(AES S 盒、TEA delta、MD5 K 表)无命中
- 加密函数里有大量
eor(ARM 异或指令)、lsl/lsr(位移)、查表操作 - 输入输出长度关系:等长(流式异或)或按块膨胀(分组+填充)
还原方法论:四步走
第一步:圈定算法边界
hook 加密函数的输入输出,确认:
- 输入:明文、key(几个?长度?)
- 输出:密文(长度和明文的关系)
- 同输入是否同输出(有无随机数/时间戳参与)
第二步:静态读逻辑,画出数据流
IDA 里读函数实现,把每个操作用伪代码记录。自定义加密的代码通常不长(几十到几百行),耐心读完:
// 典型自定义加密的伪代码
for (i = 0; i < len; i++) {
key_byte = key[i % key_len];
c[i] = p[i] ^ key_byte; // 异或
c[i] = (c[i] + i) & 0xFF; // 加位置
c[i] = (c[i] << 3) | (c[i] >> 5); // 循环位移
}第三步:写加密方向的对拍实现
先实现加密(不是解密),用 App 的输入对拍:
def custom_encrypt(p, key):
out = []
for i, b in enumerate(p):
c = b ^ key[i % len(key)]
c = (c + i) & 0xFF
c = ((c << 3) | (c >> 5)) & 0xFF
out.append(c)
return bytes(out)
# 对拍:custom_encrypt(已知明文, key) == App 输出?先写加密再写解密的原因:加密是正向模拟,不容易错;对拍通过后,逆运算推解密(异或的逆是异或,加的逆是减,循环左移的逆是循环右移)。
第四步:逆运算写解密
把每个操作反过来、顺序倒过来:
def custom_decrypt(c, key):
out = []
for i, b in enumerate(c):
p = ((b >> 3) | (b << 5)) & 0xFF # 循环右移 3
p = (p - i) & 0xFF # 减位置
p = p ^ key[i % len(key)] # 异或
out.append(p)
return bytes(out)精髓一:多轮加密的拆解
自定义加密常做 2-4 轮迭代(每轮换个 key 或位移量)。识别多轮:函数里有外层循环套内层操作。拆解技巧:hook 每轮的边界(如果轮间有函数调用边界),或对拍时先只实现第一轮,看输出是否呈现"半成品"特征。
精髓二:key 派生是常见马甲
自定义加密的 key 经常不是传入的原始 key,而是派生的:
// 原始 key "abc123" 派生成 256 字节密钥流
for (i = 0; i < 256; i++) {
key_stream[i] = raw_key[i % 6] ^ i;
}识别:hook 到的 key 长度和代码里用的不一致。对策:别还原派生逻辑,hook 派生后的密钥流直接用。
精髓三:什么时候别还原
自定义加密的还原有成本上限。这些信号出现时,放弃还原转 Frida 直调:
- 算法超过 500 行且多轮嵌套
- 密钥派生依赖大量设备状态(还原要连设备环境一起模拟)
- 算法随版本频繁更新(你还原完它换了)
记住第一原则:能调用就不还原。
总结
- 自定义加密 = 异或/位移/置换组合,无特征常量,靠排除法确认
- 还原四步:圈边界 → 读逻辑 → 对拍加密 → 逆运算解密
- 多轮拆解靠 hook 轮边界,key 派生直接 hook 派生结果
- 超 500 行或频繁更新就放弃还原,Frida 直调
交流微信:run1255