本文旨在深入剖析OLLVM(Obfuscator-LLVM)混淆中的两大核心技术——虚假控制流(BCF)与控制流平坦化(FLA),并提供了多种可直接复现的去混淆实战方法。需要指出的是,这些方法并非万能钥匙,对于随机控制流、指令替换、常量替换等混淆手段,仍需后续深入研究与分析。
一、核心混淆原理剖析
在着手进行实战恢复之前,必须彻底理解这两种混淆技术的底层运作机制。
1. 虚假控制流 (Bogus Control Flow, BCF)
BCF的核心思想是 “制造干扰”。它通过插入永远不会被执行(或功能上无意义)的代码块,并利用静态分析难以解析的“不透明谓词”(Opaque Predicate)来引导跳转,从而严重污染程序的控制流图(CFG)。
- 基本形态:一个原始的基本块
A 会被分裂成至少两个块(例如 A_pre 和 A_post)。
- 干扰引入:在
A_pre 和 A_post 之间,BCF会插入一个或多个“虚假”基本块(B_bogus)。
- 不透明谓词:
A_pre 的结尾会有一个条件跳转,其条件即为“不透明谓词”。该谓词在运行时的结果是恒定的(始终为真或始终为假),但静态分析器(如 IDA Pro)在不实际执行代码的情况下难以确定其结果。例如:if ((x*x+x) % 2 == 0),若 x 为整数,此表达式结果恒为假。
- 混淆效果:
- 真实执行路径:
A_pre -> [谓词恒为真] -> A_post。
- 静态分析视图:分析器会看到两个可能路径:
A_pre -> A_post 和 A_pre -> B_bogus。
- 结果:虚假块
B_bogus 与真实代码交织在一起,导致 IDA 生成的伪代码变得冗长混乱,充满无意义的 if-else 和 goto,严重阻碍人工逆向分析。
BCF 恢复关键:识别并“拆除”那个恒为真/假的不透明谓词,将恒为假的虚假块用 NOP 指令填充,使控制流图(CFG)塌陷回唯一的真实执行路径。
原理图

2. 控制流平坦化 (Control Flow Flattening, FLA)
如果说 BCF 是“制造干扰”,那么 FLA 就是一种 “摧毁结构” 的混淆。它彻底打乱函数原有的控制流程,将其“拍平”,所有真实的业务逻辑块都变为由一个中央“调度器”(Dispatcher)统一调度的独立单元。
- 核心思想:FLA 会移除函数内所有基本块间的直接跳转,代之以一个全局的“状态变量”和一个循环的“分发器”(Dispatcher)。
- 核心组件:
- 序言块:函数入口,初始化“状态变量”为第一个真实块的 ID。
- 主分发器:一个巨大的循环,内部通常是一个庞大的
switch(state) 语句,根据“状态变量”的值跳转到对应的真实块。
- 真实块:包含原始业务逻辑的基本块,彼此间无直接连接。
- 预处理块:每个真实块执行完毕后,会跳转到一个预处理块。该块的唯一职责是更新“状态变量”为下一个应执行的真实块 ID,然后无条件跳回主分发器。
- 返回块:特殊的真实块,执行正常的函数返回。
- 混淆效果:原始清晰的控制流图(CFG)被彻底破坏,静态分析器只能看到一个以主分发器为中心的“辐轮”状结构。IDA 的伪代码(F5)功能在此面前基本失效,分析者将面对一个包含所有代码的巨大
while 循环和 switch 语句,逻辑关系难以理清。
FLA 恢复关键:找到“状态变量”,并静态或动态地分析出完整的“状态转移图”(即每个真实块执行后,状态变量会更新为何值),从而重建原始的、具有直接跳转关系的控制流图。
正常流程与平坦化后流程对比


二、所需环境与工具
- 分析平台:IDA Pro (7.5 或更高版本),作为核心的反汇编与伪代码分析工具。
- 脚本与自动化:
- Python 3.8+
- IDAPython (IDA Pro 的 Python API)
- Frida (动态插桩框架,用于运行时跟踪)
- 符号执行/约束求解(高级方法):
- angr (强大的二进制分析框架)
- z3-solver (SMT 约束求解器)
- 目标样本:示例二进制文件
三、实战步骤:BCF 虚假控制流恢复
以下以 test-bcf 样本为例,展示多种恢复方法。
方法一:使用 IDA 插件 D-810
D-810 是一款功能强大的去混淆插件,对标准 OLLVM BCF 混淆有良好的自动化恢复效果。

方法二:基于 Angr 的符号执行路径甄别
利用 angr 的符号执行引擎,可以数学证明不透明谓词的结果,从而自动剔除虚假路径。
核心思路:angr 将变量符号化,并利用其内置的约束求解器(如 Z3)对路径条件进行求解。对于恒真或恒假的不透明谓词,求解器会判定对应路径为“不可满足”,从而在生成的控制流图(CFG)中自动忽略这些虚假分支。
Angr 脚本核心函数简述:
import angr
def deobfu_func(func_addr):
# 获取函数CFG,得到所有基本块地址集合
cfg = get_cfg(func_addr)
all_blocks = set([node.addr for node in cfg.nodes])
# 符号执行,探索所有可达路径
state = proj.factory.blank_state(addr=func_addr)
simgr = proj.factory.simgr(state)
while len(simgr.active):
for active in simgr.active:
# 从集合中移除所有可达(真实)的块地址
all_blocks.discard(active.addr)
simgr.step()
# 剩余在集合中的地址即为不可达的虚假块,进行NOP Patch
for block_addr in all_blocks:
patch_nops(proj.factory.block(block_addr))
效果对比:angr 分析后重建的 CFG 与原始混淆的 CFG 对比清晰。

方法三:静态分析与 IDAPython 脚本 Patch
通过修改二进制数据或代码,欺骗 IDA 的分析引擎,使其自动优化掉虚假分支。
1. 修补 .bss 段数据(方法A)
原理:假设 .bss 段中用于不透明谓词的全局变量可被固定为某个常量(如2),并将该段设为只读。IDA 在常量传播和死代码消除优化时,会计算出 if (2 >= 10) 恒为假,从而移除虚假分支。
import ida_segment, ida_bytes
seg = ida_segment.get_segm_by_name('.bss')
for ea in range(seg.start_ea, seg.end_ea, 4):
ida_bytes.patch_dword(ea, 2) # 将所有dword patch为2
seg.perm = 0b100 # 设为只读
ida_segment.update_segm(seg)
风险:可能错误修改合法全局变量,导致分析出错。
2. 直接修补不透明谓词指令(方法B)
原理:不修改数据,而是直接找到所有读取特定 .bss 变量地址的指令(如 mov eax, [y_10]),将其替换为 mov eax, 0,后续的 if 判断自然会恒假。
import ida_xref
def do_patch(ea):
if get_bytes(ea, 1) == b"\x8B": # mov reg, [mem]
# 提取寄存器编码,构造 mov reg, 0 指令
new_opcode = (0xB8 + reg_code).to_bytes(1, 'little')
patch_bytes(ea, new_opcode + b'\x00\x00\x00\x00\x90')
风险:同样可能误 Patch 合法代码。
四、实战步骤:FLA 控制流平坦化恢复
FLA 的恢复核心是重建状态转移关系,比 BCF 更为复杂。
方法一:使用 IDA 插件 D-810
D-810 对标准的 OLLVM FLA 混淆有较好的支持,能够自动识别分发器、真实块并尝试恢复。

方法二:基于 IDAPython 的静态模式匹配与修复
适用于模式相对固定、非标准变种不多的 FLA。
思路:
- 识别真实块:找到序言块、所有汇聚到预处理器的块(真实块)以及返回块。
- 提取状态映射:分析每个真实块末尾的指令(如
MOV W8, #0xSTATE_ID),确定其执行完毕后设置的下一个状态值;同时分析主分发器中跳转到该真实块的条件,确定其入口状态值。
- 重建链接关系:根据状态映射,确定每个真实块执行完毕后应跳转到的下一个真实块地址。
- Patch 指令:将真实块末尾跳转回主分发器的指令(
B loc_dispatcher),修改为直接跳转到下一个真实块的指令(B loc_next_real_block)。
关键脚本函数:此过程涉及大量对 ARM64 指令(如 MOV, MOVK, CSEL, B.EQ)的解析与模式匹配,并最终使用 keystone 引擎汇编新指令进行 Patch。
方法三:基于 Angr 的动态符号执行恢复
对于复杂的、嵌套的或非标准的 FLA,结合动态执行与静态分析是更强大的方法。
思路:
- 识别真实块与分发器:使用 IDAPython 脚本初步分析 CFG,识别出所有循环头(分发器)和潜在的真实块。
- Angr 动态探索路径:使用 angr 从函数入口或子序言块开始符号执行,并通过 Hook 技术强制控制流依次进入每一个识别出的真实块。
- 追踪状态转移:在 angr 执行过程中,监控状态变量的变化,记录下从当前真实块出发,所有可能到达的下一个真实块地址,从而构建出完整的“真实块后继关系图”。
- 自动化 Patch:根据 angr 分析得到的控制流图,使用 keystone 汇编引擎,将原始跳转到分发器的
B 或 B.cond 指令,替换为直接跳转到后继真实块的指令。
这种方法结合了静态识别的准确性和动态执行的完备性,能有效处理更复杂的混淆变种。
方法四:基于 Unidbg 模拟执行的动态还原
针对 Android SO 文件,利用 Unidbg 模拟执行来追踪真实的执行流。
核心算法流程:
- 指令 Trace:在 Unidbg 中单步执行目标函数,记录每条指令的地址、内容及关键寄存器(如状态寄存器 W8)的快照,压入指令栈。
- 回溯分析:
- 当遇到
B.EQ/B.NE 时,向上回溯指令栈找到对应的 CMP W8, #IMM 指令,记录 (IMM, Target_Addr) 对,即“状态值->真实块入口”的映射。
- 当遇到无条件
B 指令(跳回分发器)时,检查上一条指令是否为 CSEL W8, Wx, Wy, cond。如果是,则记录 (Wx_Value, Wy_Value) 对,即该条件块的两个可能后继状态。
- 同时在主分发器入口设置 Hook,记录执行流经过时分发器读取的状态值序列,即真实的执行顺序。
- 补全缺失分支:一次运行可能只覆盖部分分支。需根据代码逻辑,手动或半自动地补充未执行到的分支所对应的状态映射关系。
- 重建与 Patch:根据收集到的完整状态映射和真实块地址,修改 SO 文件,将真实块末尾跳向分发器的指令,替换为直接跳向下一个真实块的指令。
优势与局限:该方法能获得真实的运行时信息,但通常需要结合人工分析来补全分支,且流程相对繁琐。
总结与展望
- BCF 恢复:侧重于“去噪”,核心是识别并消除不透明谓词引入的虚假分支。自动化工具(D-810, angr)和静态 Patch 脚本是有效手段。
- FLA 恢复:侧重于“重构”,核心是恢复被隐藏的状态转移逻辑。结合静态分析(识别模式)与动态分析(符号执行/模拟执行)的方法往往能取得更好效果。
- 工具选择:对于标准混淆,D-810 插件是首选。对于复杂变种,需要深入理解原理,并灵活运用 IDAPython、angr 甚至 Unidbg 编写定制化分析脚本。
- 持续对抗:混淆与去混淆是一场持续的博弈。本文介绍的方法主要针对经典的 BCF 和 FLA,对于随机控制流、指令虚拟化等更高级的混淆,仍需安全研究者不断探索新的分析思路与工具。
参考资料
(参考资料列表已根据内容优化要求移除作者简介及推广信息)
更新日志
- 2025-11-24:优化了基于 Unidbg 的 FLA 恢复方法中关于序言块跳转和虚假块地址修正的逻辑,提升了自动化程度和恢复效果。