26、混淆算法还原:Ollvm混淆(控制流平坦化、指令替换)、基于符号执行的还原
聊到混淆算法还原,OLLVM 是绕不过去的一座山。我最早接触它是在分析某款金融类 App 时,整个核心逻辑被搅成一锅粥,静态看代码完全找不到北。后来我才意识到,这就是 OLLVM 的「控制流平坦化」在作祟。
说白了,OLLVM 不是加密算法,而是一套代码混淆工具链。它把你的正常控制流打碎、重组、塞进一个巨大的 switch-case 结构里。你想想看,原本清晰的 if-else 和循环,全变成了一个状态机——每次执行都先跳到一个分发器,再根据当前状态决定下一步去哪。嗯,这就是它的核心思路。
控制流平坦化:把代码变成迷宫
控制流平坦化(Control Flow Flattening)是 OLLVM 最常用的手段。它会把函数里的基本块全部拉平,然后通过一个「状态变量」来控制执行顺序。
我举个例子,原始代码可能是这样的:
int foo(int x) {
int result = 0;
if (x > 0) {
result = x * 2;
} else {
result = x + 1;
}
return result;
}
经过平坦化后,它会变成:
int foo(int x) {
int result = 0;
int state = 0;
while (1) {
switch (state) {
case 0:
if (x > 0) state = 1;
else state = 2;
break;
case 1:
result = x * 2;
state = 3;
break;
case 2:
result = x + 1;
state = 3;
break;
case 3:
return result;
}
}
}
你看,原本清晰的两条路径,现在变成了一个 while+switch 的循环结构。每个基本块都变成了一个 case,状态变量 state 控制着执行流。我在项目中遇到过最夸张的情况——一个函数被展开了 200 多个基本块,手动分析基本不可能。
指令替换:偷梁换柱的算术混淆
除了控制流,OLLVM 还会对指令本身动手脚。指令替换(Instruction Substitution)会把简单的运算替换成等价的复杂表达式。
比如一个简单的加法:
c = a + b;
可能被替换成:
c = (a ^ b) + 2 * (a & b);
或者更复杂的:
c = (a | b) + (a & b);
为什么会这样?因为 (a + b) 在二进制层面等价于 (a ^ b) + 2*(a & b)。这是加法器的硬件实现原理,OLLVM 只是把它搬到了软件层面。
我记得有一次分析一个签名算法,里面全是这种替换后的表达式。乍一看以为是某种自定义加密,仔细一算才发现就是普通的加法。嗯,这里要注意——不要被复杂的表达式吓到,先尝试化简。
基于符号执行的还原:以子之矛攻子之盾
既然 OLLVM 把控制流变成了数据流,那我们就用数据流分析来对付它。符号执行(Symbolic Execution)就是干这个的。
符号执行的核心思想很简单:不跑具体的值,而是用符号变量代替输入,然后跟踪这些符号在程序中的传播路径。对于平坦化后的代码,我们可以通过符号执行来恢复原始的控制流图。
具体做法是这样的:
- 识别状态变量:找到那个控制 switch-case 的 state 变量。通常它会在 while 循环开始处被读取,在 case 块末尾被赋值。
- 符号化状态变量:把 state 当作符号变量,跟踪它的所有可能取值。
- 路径约束求解:对每个 case 块,记录它被执行的条件(即 state 等于某个值),以及它如何修改 state。
- 重建控制流:根据 state 的传递关系,把分散的 case 块重新连接成原始的控制流图。
我常用的工具是 angr 的 CFG 恢复功能。它内置了对 OLLVM 平坦化的处理逻辑,可以自动识别并还原。不过要注意,如果 App 还加了其他保护(比如反调试、自修改代码),符号执行可能会跑偏。
知识体系总览
下面这张图总结了 OLLVM 混淆与符号执行还原的完整流程:
实战中的还原策略
在实际项目中,我一般按这个优先级来:
| 难度等级 | 混淆程度 | 推荐方法 | 工具选择 |
|---|---|---|---|
| 简单 | 仅指令替换 | 手动化简 + 模式匹配 | IDA Pro + 脚本 |
| 中等 | 控制流平坦化 | 符号执行自动还原 | angr / deflat.py |
| 困难 | 平坦化 + 指令替换 + 反调试 | 混合分析(静态 + 动态) | Unicorn + angr 联合 |
对于中等难度的样本,我推荐先用 deflat.py(一个专门针对 OLLVM 平坦化的脚本)做预处理。它基于符号执行,能自动识别状态变量并重建控制流。我试过很多次,成功率在 80% 以上。
如果 deflat.py 搞不定,那就上 angr 的 CFG 恢复。不过要注意,angr 对 ARM64 的支持不如 x86 好,分析 iOS App 时可能需要额外配置。
最后说一句——OLLVM 混淆不是不可战胜的。它本质上是一种「确定性混淆」,只要掌握了符号执行这个武器,大部分情况下都能还原。我见过最头疼的是 OLLVM + 花指令 + 反调试的组合拳,那才叫真正的硬骨头。不过那是另一个话题了,咱们下次再聊。
公众号:蓝海资料掘金营,微信deep3321