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 只是把它搬到了软件层面。

我记得有一次分析一个签名算法,里面全是这种替换后的表达式。乍一看以为是某种自定义加密,仔细一算才发现就是普通的加法。嗯,这里要注意——不要被复杂的表达式吓到,先尝试化简。

我的经验:遇到指令替换,先别急着手动化简。用符号执行工具自动做代数化简,往往能直接还原出原始表达式。我曾经用 angr 的 VSA(值集分析)模块,几分钟就搞定了一个手工分析需要半天的混淆代码。

基于符号执行的还原:以子之矛攻子之盾

既然 OLLVM 把控制流变成了数据流,那我们就用数据流分析来对付它。符号执行(Symbolic Execution)就是干这个的。

符号执行的核心思想很简单:不跑具体的值,而是用符号变量代替输入,然后跟踪这些符号在程序中的传播路径。对于平坦化后的代码,我们可以通过符号执行来恢复原始的控制流图。

具体做法是这样的:

  1. 识别状态变量:找到那个控制 switch-case 的 state 变量。通常它会在 while 循环开始处被读取,在 case 块末尾被赋值。
  2. 符号化状态变量:把 state 当作符号变量,跟踪它的所有可能取值。
  3. 路径约束求解:对每个 case 块,记录它被执行的条件(即 state 等于某个值),以及它如何修改 state。
  4. 重建控制流:根据 state 的传递关系,把分散的 case 块重新连接成原始的控制流图。

我常用的工具是 angr 的 CFG 恢复功能。它内置了对 OLLVM 平坦化的处理逻辑,可以自动识别并还原。不过要注意,如果 App 还加了其他保护(比如反调试、自修改代码),符号执行可能会跑偏。

避坑指南:我曾经在分析一个加了 OLLVM + 反调试的样本时,angr 直接卡死了。后来发现是反调试代码在符号执行环境下触发了无限循环。解决办法是先 patch 掉反调试逻辑,再跑符号执行。记住——工具不是万能的,预处理很重要。

知识体系总览

下面这张图总结了 OLLVM 混淆与符号执行还原的完整流程:

OLLVM 混淆与符号执行还原流程 原始代码 清晰的 if-else / 循环 OLLVM 编译 混淆后代码 while + switch 状态机 控制流平坦化 基本块拉平 + 状态变量 指令替换 算术表达式等价替换 符号执行还原 angr / Triton / 自定义符号执行引擎 还原后的控制流图 接近原始逻辑 混淆阶段 还原阶段

实战中的还原策略

在实际项目中,我一般按这个优先级来:

难度等级 混淆程度 推荐方法 工具选择
简单 仅指令替换 手动化简 + 模式匹配 IDA Pro + 脚本
中等 控制流平坦化 符号执行自动还原 angr / deflat.py
困难 平坦化 + 指令替换 + 反调试 混合分析(静态 + 动态) Unicorn + angr 联合

对于中等难度的样本,我推荐先用 deflat.py(一个专门针对 OLLVM 平坦化的脚本)做预处理。它基于符号执行,能自动识别状态变量并重建控制流。我试过很多次,成功率在 80% 以上。

如果 deflat.py 搞不定,那就上 angr 的 CFG 恢复。不过要注意,angr 对 ARM64 的支持不如 x86 好,分析 iOS App 时可能需要额外配置。

一个小技巧:在分析 OLLVM 混淆的代码时,先看看有没有明显的「分发器」模式。通常平坦化后的代码会有一个 while(1) 循环,里面是一个 switch 语句,switch 的变量就是状态变量。找到它,你就找到了还原的入口。

最后说一句——OLLVM 混淆不是不可战胜的。它本质上是一种「确定性混淆」,只要掌握了符号执行这个武器,大部分情况下都能还原。我见过最头疼的是 OLLVM + 花指令 + 反调试的组合拳,那才叫真正的硬骨头。不过那是另一个话题了,咱们下次再聊。


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