逆向工程实战手册 第 40 章

第 40 章 混淆技术原理深解(OLLVM / MBA / VM)

第 8 章讲"怎么绕过混淆",本章讲"混淆是怎么实现的"。懂实现才能高效反混淆——知道每个混淆在汇编层的特征,反混淆工具的选择和手动还原就有依据。

📍 知识点地图 | 主题:混淆原理 | 前置:第8章 | 后续:— | 核心概念:CFF、MBA、字符串加密、VM混淆

40.1 混淆的四大目标(分类学)

1. 控制流混淆: 让程序"怎么走"看不懂 → 平坦化/不透明谓词/花指令
2. 数据混淆: 让数据"是什么"看不懂 → 字符串加密/常量编码/MBA
3. 语义混淆: 让逻辑"在干什么"看不懂 → VM 化/代码虚拟化
4. 结构混淆: 让程序"长什么样"看不懂 → 命名混淆/合并拆分

对应反混淆: 去平坦化 → 字符串解密/化简 → 还原 VM → 符号恢复

40.2 控制流平坦化(CFF)—— 最经典的混淆

原理(OLLVM 实现)

原始控制流:
  A → B → C
  A → D → C

平坦化后:
  A: 设置 state=1
  B: 设置 state=2
  C: 设置 state=3
  ...
  dispatcher: switch(state) { case 1: 跳 A; case 2: 跳 B; ... }
  → 所有基本块都被压平到一个循环 + switch 分发器

汇编特征:
  main 函数巨大化(所有逻辑并进一个函数)
  一个"分发块"反复出现(取 state → 跳表跳转)
  大量 jmp [table + offset]

为什么难读

原始逻辑顺序被打散,每个基本块的"前后关系"藏在 state 里
静态阅读需要: 还原 state 图 → 重建控制流

反平坦化原理(工具怎么做到的)

1. 识别分发器(找循环里的跳表)
2. 解析每个基本块的 state 值(常量传播)
3. 建立 state → 基本块映射
4. 还原真实边(A 的出口 state 指向谁)
5. 重建控制流图 → 输出正常函数

工具: D-810(IDA)、GOOMBA(Ghidra)、obpo、ollvm-unflattener

40.3 不透明谓词(Opaque Predicates)

原理

问题: 普通分支一眼看穿
方案: 插入"看似是分支、实际恒定"的判断

if (x == 0) { /* 死代码 */ }    ; x 恒为 0 → 分支恒真
等价形式: x^2 + x + 41 恒为奇数(欧拉多项式)

汇编特征:
  分支前有一个复杂计算
  跳转两边都有代码,但一边永远不执行
  编译器优化难以消除(值分析不出恒定)

识别与去除

识别: 统计分支实际方向(动态运行一次全记录)→ 恒定向的分支 = 候选
去除: 把恒定分支替换为直接跳转(D-810 的 opaque predicate removal)

40.4 MBA(混合布尔算术)—— 数据混淆

原理

目标: 把简单的算术/逻辑表达式替换成"看起来很复杂的等价式"

核心恒等式(记忆这 5 条就能认):
  x + y            == (x ^ y) + 2 * (x & y)
  x + y            == (x | y) + (x & y)
  x ^ y            == (x | y) - (x & y)
  x | y            == x + y - (x & y)
  x - y            == x + (~y) + 1

多层嵌套: MBA 可以套 MBA(每层倍增复杂度)

识别特征

反编译出现大量"无意义"的 and/or/xor/add 组合
同样的值被用多种方式重复计算
常量被展开成复杂表达式

化简工具

D-810 的 MBA simplification(IDA)
SiMBA(pip install simba-simplifier)—— 自动识别替换
angr/z3 符号化简: 约束重写为最简形式
手工: 背核心恒等式,逐式替换

40.5 字符串加密

原理

目标: 让 strings/静态搜索失效
实现: 字符串加密存储 → 运行时解密到栈上使用
常见: XOR key / 单字节 / 自定义算法

反混淆

方法 1: 动态观察(最快)—— 断在字符串使用点,dump 已解密的栈内容
方法 2: 静态还原 —— 找到解密函数 → 批量解密 .rodata
方法 3: Frida hook 字符串函数(打印解密后的实参)
工具: FLOSS(自动识别+解密恶意软件字符串)

40.6 代码虚拟化(VM 混淆)—— 最高级对抗

原理

把原始指令翻译成自定义 VM 的字节码
运行时: VM 解释器(handler)循环执行字节码

结构:
  字节码(加密/编码的原始操作)
  handler 表(每种 opcode 一个处理函数)
  分发循环(取 opcode → 找 handler → 执行)

识别特征:
  入口处有一个"读取-分派-执行"循环
  大量 handler 函数(每个很小)
  原函数逻辑全部消失,只剩 VM 循环
  代表: VMProtect、Themida、自研 VM

反 VM 的四个层次

层次 方法 成本
1. 行为级 动态观察"它做了什么"(Hook 输出/API)
2. 指令级 跟踪 VM 执行流(Hook 分发点,记录 opcode+操作数)
3. 语义级 把字节码提升到 IR(Miasm/自定义)→ 优化还原
4. 完整还原 逆向整个 VM 架构,写反编译器 很高

实战建议:CTF/日常分析中,层次 1-2 通常够用(拿到行为+数据即可),除非目标是复用算法才做 3-4。

40.7 混合对抗(现实世界)

真实样本通常是多层组合:
  字符串加密 + CFF + MBA + 反调试 + 自修改
应对策略:
  1. 分层剥离(每层一种混淆 → 逐个击破)
  2. 动态优先(先动态观察,再静态还原)
  3. 工具链配合(D-810 → SiMBA → 动态 dump → 符号恢复)
  4. 判断 ROI(虚拟化 → 行为级理解往往足够)

40.8 反混淆工作流(综合)

1. 侦察: 识别混淆类型(特征表 40.1-40.6)
2. 先动态: 运行观察/Hook/内存 dump(绕过静态混淆)
3. 静态工具: 按类型选(CFF→D-810;MBA→SiMBA;字符串→FLOSS)
4. 手动兜底: 高价值函数人工还原(配合 LLM 第 29 章)
5. 验证: 还原结果与动态行为一致

动手练习

  1. 用 ollvm 编译一个小程序(启用 CFF + MBA + 字符串加密),对比混淆前后的反编译输出。
  2. 用 D-810 对平坦化函数去平坦化,对比还原前后控制流。
  3. 手写 5 个 MBA 恒等式的替换练习(40.4 表),训练识别能力。
  4. 分析一个 VMProtect 样本:只做"行为级"理解(观察它调用了什么 API、处理什么数据),不尝试完整还原。

深入阅读

  • 仓库:skills/reverse-engineering/anti-analysis.md(94 种反分析技术全集)
  • 仓库:skills/reverse-engineering/tools-advanced.md(D-810/obpo/Miasm/GOOMBA)
  • 仓库:skills/reverse-engineering/references/ollvm-deobfuscation.md(OLLVM 去混淆专项)
  • 仓库:skills/reverse-engineering/patterns.md(自定义 VM、Nanomites、自修改代码)
  • 仓库:skills/reverse-engineering/tools-advanced.md(VMProtect/Themida 分析)