看雪学院 11月04日 08:21
OLLVM 控制流平坦化与BR混淆的解析与对抗
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入剖析了OLLVM中的控制流平坦化(FLA)和BR混淆技术,并提供了实用的逆向分析与对抗方法。文章详细拆解了FLA的各个组件,如序言块、预分发块、主分发器、真实块、虚假块等,并介绍了识别这些组件的技巧。针对BR混淆,文章阐述了其原理是将条件跳转转换为状态变量和间接跳转,并通过模拟执行和符号执行(如angr)提供了解析方案。此外,文章还探讨了魔改FLA的思路,如插入中间块、制造多返回块等,并强调了利用IDA、JEB等工具以及LLVM IR进行代码优化的重要性。最后,文章提供了一个IDA脚本辅助FLA还原,并给出了BR混淆的符号执行Python代码示例。

🔑 **FLA核心组件识别与结构化分析**:文章详细分解了控制流平坦化(FLA)的构成,包括序言块、预分发块(通常具有最高入度)、主分发器、承载业务逻辑的真实块、以及用于扰乱分析的虚假块。通过识别这些组件的关键特征,特别是预分发块及其前驱(真实块),可以系统地拆解复杂的FLA函数,为后续的控制流重建奠定基础。

🚀 **BR混淆原理与符号执行对抗**:BR混淆通过将明晰的条件跳转转化为间接跳转(如 `br xN` 或跳表),使得静态分析工具难以绘制清晰的控制流图。文章指出,符号执行工具(如angr)是破解BR混淆的有效手段,能够通过模拟执行探索所有可能的分支路径,并直接给出跳转目标地址,极大地简化了分析过程。

💡 **魔改FLA与多级调度以增强混淆**:为了对抗现有的自动化去混淆工具,文章提出了一些“魔改”FLA的思路,例如在真实块和预分发块之间插入无意义的中间块,破坏自动化脚本依赖的强特征;或者设计多级/嵌套分发器,将复杂的跳转逻辑拆分成多层状态机,指数级提升分析难度。

🛠️ **现代化工具与IR化在反混淆中的应用**:文章强调了利用现代逆向工程工具(如IDA Pro 9.2+、JEB)的反编译器和代数化简能力来处理SUB混淆。对于更复杂的混淆,建议将机器码提升到中间表示(IR),如LLVM IR,然后利用编译器优化器进行代数化简和代码优化,再反编译回C代码,实现“以其人之道还治其人之身”。

🔧 **实战还原工具与脚本分享**:文章提供了实用的实战经验,包括为FLA混淆开发IDA插件以辅助还原真实块和探测块,以及使用Python脚本结合angr进行BR混淆的符号执行和约束求解,旨在帮助读者快速掌握OLLVM混淆的分析与对抗方法。

逆天而行 2025-11-01 18:00 上海

看雪论坛作者ID:逆天而行

羽扇纶巾,谈笑间,小小混淆樯橹灰飞烟灭。

相信每个逆向萌新在刚接触加固App时,都曾有过下面这个“痛的领悟”:兴致勃勃地把样本拖进 IDA,准备大展拳脚,结果……

F5一按,满屏的while乱舞,控制流图(CFG)扭曲得像一碗打翻的意大利面。脑海里只剩下一句话:“卧槽,这还分析个毛线!”

没错,这就是我们今天的主角——控制流平坦化(Control Flow Flattening, FLA)

市面上不乏 d8、jeb 等强大的反混淆工具,它们能一键还原大部分标准 OLLVM 混淆。但用别人的轮子,爽则爽矣,总觉得隔靴搔痒。正如上篇文章所言:不造一次轮子,怎能体会轮子里的精髓?

所以,本着“我学会 = 大家学会”的开源精神,我决定写下这篇“施工笔记”。让我们一起,亲手揭开 OLLVM-FLA 的神秘面纱,看透它背后的设计哲学。

FLA 混淆

一、FLA 的核心组件解剖

经过一番资料翻阅和实战分析,我为大家总结了一套“傻瓜式”的 FLA 结构图鉴。任何复杂的 FLA 函数,都可以看作是以下几种“零件”的组合:

任何像意面一样打结的 FLA 函数,都能拆成这几类部件(AArch64 口径):

序言块(Prologue):入口,初始化状态或栈框架。

预分发块(Pre-Dispatcher):真实块的汇合点入度通常最高

主分发器(Master Dispatcher):预分发块的唯一后继;按state分发。

真实块(Real Blocks):承载业务语义;执行完多半回到预分发块形成循环。

返回块(Return Block):正常收束的终点,可视为特殊真实块。

子分发器(Sub-Dispatcher):多级/嵌套调度时的次级分发节点。

虚假块(Bogus Blocks):假计算 + 无条件跳转,用来搅局。

连接块(Connector Blocks):相邻真实块之间的过渡边,工程上单列方便重连。

一句话:先锚“预分发”,再锁“主分发”,真实块就是它的前驱们。其他多半是陪跑。

快速识别技巧

掌握了组件定义,识别它们就跟给小动物分类一样简单,抓住关键特征即可:

1.序言块:函数的第一个块,无可争议的Entry

2.返回块:没有后继(Successors)的块,执行到这就结束了。

3.预分发块:拥有**最多前驱(Predecessors)**的块,因为几乎所有真实块都指向它。

4.主分发块:通常是预分发块的唯一后继,负责做switch跳转。

5.真实块:所有指向预分发块的那些前驱块。

6.虚假块:剩下的都是。

一句话总结:以“预分发块”为突破口,先抓大佬(分发器),再揪小弟(真实块),剩下的杂鱼(虚假块)直接忽略。

二、拨乱反正:如何重建原始控制流?

识别出所有真实块后,下一个问题是:它们原本的邻里关系是怎样的?

思路其实非常朴素:“骗”程序自己跑一遍,我们在一旁做记录。唯一的难点在于如何处理分支。

我们来看一个典型的真实块结尾:

; 伪代码示例
LDRB            W8, [X19,W24,SXTW]  ; 从某处加载一个值
MOV             W9, #0x1F4E8494
CMP             W8, #0               ; 比较
MOV             W8, #0x4FA089C5
CSEL            W9, W9, W8, EQ       ; 如果相等(EQ),W9 = W8,否则 W9 = W9
B               loc_pre_dispatcher   ; 跳转到预分发块

这个块的state变量(可能存储在W9)有两个可能的值,对应了if-else的两条路径。我们只要能控制这个选择,就能探索所有可能的执行路径。

目前主流的两种自动化方案:

模拟执行 (Emulation) - 如 Unicorn Engine

符号执行 (Symbolic Execution) - 如 angr

我个人更偏爱用 angr 做快速实验,因为它能把我们从繁琐的分支处理中解放出来,专注于逻辑本身。

三、魔改 FLA 的思路

说实话,原版 OLLVM-FLA 在当今各种商业级加固面前,已经有些不够看了。d8、jeb 等工具的去混淆脚本早已对它了如指掌。

如果我们是防御方,就要思考如何让它变得更“毒辣”。以下是我的一些“魔改”思路,部分已实践:

1.破坏“真实块 -> 预分发块”的固定模式

骚操作:在真实块和预分发块之间插入一个或多个无意义的“中间块”,形成真实块 -> 中间块 -> ... -> 预分发块的跳转链。

杀伤力:直接破坏了“预分发块的前驱都是真实块”这一强特征,让依赖此特征的自动化脚本当场懵逼。

实践效果:我测试过这个思路,d8 和 jeb 的通用脚本确实都失效了,效果拔群!

2.制造多个返回块

骚操作:正常函数只有一个逻辑终点。如果我通过不同的state值,让函数可以从五六个不同的代码块return呢?

杀伤力:许多分析工具都基于“单入口,单出口”的假设。多出口会让CFG分析和路径探索变得异常复杂。

3.序言块迷宫

骚操作:在真正的序言块之后,不直接进入主分发器,而是先经过一连串由虚假块组成的“迷宫”,兜兜转转再回到主循环。

杀伤力:干扰分析者对函数主体的快速定位。

4.多级/嵌套分发器

骚操作:将一个大的switch拆成多个小的、嵌套的switch。例如,主分发器决定跳转到A或B,而A本身又是一个子分发器,内部维护自己的一套state和代码块。

杀伤力:这会形成多层状态机,简单的单层state跟踪脚本会直接“跑飞”,分析难度指数级上升。

BR混淆

BR混淆原理

这招堪称OLLVM的“当家花旦”,杀伤力极大,专治各种不服。

通俗的讲,它把原本“明明白白的条件跳转”(b.eq/ne/...)给拆了,改写成先把分支结果编码到一个状态变量,再通过一个像“中央车站”一样的统一调度器(Dispatcher)+ 间接跳转(br xN/跳表)去落到对应的基本块。你看,所有人都得先去车站报到,然后再决定去哪,这下IDA可就懵了,因为它画不出清晰的路线图了。

混淆前(邻家小妹,清纯可人):

arm
; 简单的 if-else
cmp w8, #0
b.ne loc_then   ; 如果 w8 != 0, 跳转到 then 代码块
b loc_else      ; 否则,跳转到 else 代码块

BR 混淆后(浓妆艳抹,六亲不认):

arm
; 状态选择
cmpw8#0
cselw9#0xDEADBEEF#0xCAFEF00Dne  ; 条件成立选一个“魔法数”,否则选另一个
strw9[sp, #0x10]                   ; 写入状态变量(可在栈/全局/寄存器)
bdispatcher                        ; 去“中央车站”报到
-------------------------
dispatcher:
ldrw9[sp, #0x10]                   ; 读取状态
; ...一堆真假块的计算...
adrx10jumptable                    ; 寻址跳表
ldrx11[x10, w9, lsl #3]            ; 根据状态计算,取出真实目标地址
brx11                               ; 间接跳转,起飞!

x86/x64上也是一个道理,常见把jcc换成cmovcc/ 计算索引,再jmp [table+idx*8]。原理很简单,就是在CPU执行的那一刻,我们这些静态分析的凡人才知道它到底要去哪。

破局之法:符号执行的降维打击

解决思路无非两条路:

1.模拟执行:用Unicorn或者DBI框架(如Frida)跑起来,在dispatcher那里下断点,打印出寄存器的值,再手动修回去。这叫“体力活”,适合硬汉,但我们是儒将,讲究一个“雅”字。

2.符号执行:让机器自己去算!我们只需要告诉它:“老铁,从这儿开始跑,把所有可能的分支都给我算出来!” 这就是约束求解的魅力。

我个人是angr的铁粉,这种场景简直是为它量身定做的。 talk is cheap,直接上代码。

假设我们有这么一段被混淆的函数,我们想知道在dispatcher之后,它到底会跳到哪两个地方去。

python
运行
import angr
import logging
logging.getLogger('angr').setLevel('ERROR')
elf_path = "path/to/your/binary"
fun_addr = 0x401000  # 混淆函数的起始地址
dispatcher_addr = 0x401080 # dispatcher 的地址
proj = angr.Project(elf_path, load_options={'auto_load_libs': False})
state = proj.factory.entry_state(addr=fun_addr)
simgr = proj.factory.simulation_manager(state)
print(f"[*] 开始探索函数 0x{fun_addr:x}...")
# 让 angr 跑到 dispatcher 的位置
simgr.explore(find=dispatcher_addr)
if not simgr.found:
print(f"[!] 没能跑到 dispatcher at 0x{dispatcher_addr:x},检查下地址?")
else:
    found_state = simgr.found[0]
print(f"[*] 成功抵达 dispatcher!当前状态:{found_state}")
# 从 dispatcher 开始,继续单步执行,直到发生分支
    successors = found_state.step()
# 再次步进,穿过复杂的计算,直到 br 指令
while len(successors.successors) == 1:
        successors = successors.successors[0].step()
if len(successors.successors) > 1:
print("
[+] 抓到你了!发现分支:")
for i, succ_state in enumerate(successors.successors):
            branch_target = succ_state.addr
# 我们可以让 angr 帮我们求解出触发这个分支的条件
            condition = succ_state.solver.constraints
print(f"    - 分支 {i+1}: 跳转到 -> 0x{branch_target:x}")
# print(f"      触发条件: {condition}") # 如果需要,可以打印具体约束
print("
[*] 搞定!这两个地址就是你要找的真实块。现在可以去写patch脚本了。")
else:
print("[!] 奇怪,dispatcher 之后没有发现分支,可能混淆逻辑更复杂。")

看到没?我们全程没去看那些乱七八糟的计算,angr就像一个开了上帝视角的玩家,直接告诉我们最终的两个落点。拿到了这两个地址,无论是用KeyStone写个patch脚本把b dispatcher改成b.ne loc_real_A; b loc_real_B,还是在IDA里手动修复,那都是手到擒来。

指令替换(SUB)

SUB混淆原理

这招就更“文”一点了,它不搞结构,专搞你的脑子。它会把简单的算术运算,用一堆看起来复杂但结果等价的指令序列来替换。

代数等价的“七十二变”:

a - b ≡ a + (-b)(这太基础了)

-b ≡ ~b + 1(补码的基本原理,但写成汇编就有点唬人)

a - b ≡ (a + c) - (b + c)(c可以是任意常量,甚至可以是从某个不透明谓词算出来的)

a - (b + k) ≡ (a - b) - k

a - (b - k) ≡ (a - b) + k

a + b ≡ a - (~b + 1)(加法变减法)

这还只是冰山一角。它就像一个魔术师,在你眼前一通操作,你以为他变了个大象出来,其实他只是把手里的兔子换了个颜色。

破解之道:让“专业工具”做专业的事

我个人觉得,跟这玩意儿斗智斗勇,性价比不高。为什么?因为现代化的逆向工具已经越来越“卷”了。

1. 相信IDA,相信JEB现在的IDA(比如9.2之后的版本)和JEB,它们的反编译器已经集成了相当强大的代数化简引擎。很多时候,你看着汇编是一坨屎山,切到反编译窗口一看:return a - b;。 那一刻,你会感动得流下泪来,想给Hex-Rays的工程师们磕一个。

[图片:展示一段混淆的SUB汇编,和旁边IDA F5后清爽的C代码的对比图]

2. 终极武器:代码IR化 + 优化器当然,如果真遇到连IDA都“消化不良”的硬骨头,那我也有一点小小的思路,咱们可以上“核武器”。

这个思路就是:将目标机器码提升(lift)到中间表示(IR),然后调用强大的编译器优化器去“盘”它

LLVM本身就是靠优化器吃饭的,那我们为什么不“以其人之道,还治其人之身”呢?

流程大概是这样:二进制指令 -> VEX IR / LLVM IR -> 运行一系列优化Pass(如常量折叠、代数化简) -> 优化后的IR -> 反编译回C代码

angr本身就干了第一步,它把机器码转成了VEX IR。我们可以写脚本遍历VEX IR,做一些模式匹配和替换。更高级的玩法是使用rev.ngmcsema这类工具,它们能把二进制完整地翻译成LLVM IR,然后你就可以用法力无边的LLVM优化Pass去处理了。这就好比请了编译器大神来帮你做逆向,专业对口了属于是。

实战测试

FLA篇

FLA 的混淆还原 我觉得就两步:

1.定位到真实块

2.还原真实块的关系

就这两步,基本就完成了。 所以我编写了一个 ida 脚本 辅助还原

这个是arm64架构下的ollvm fla混淆,如图,很多标准的结构已经不对了。

先用插件 上个色 对任意基本块 鼠标右键 选择插件,设置该函数。

点击插件的寻找结构  可以辅助你找一些真实块,当然这个不一定准确。

可以看见 大部分结构还是对应出来了,但是被llvm 优化后 有部分 真实块是没有被上色的,这时候就可以人工的灵活添加上,把一些不是真实块的删除。

当然这些操作 你也可以直接 对 脚本的编辑框进行操作。

经过设置插件编辑框内容:

真实块:[0x2390C0, 0x2390F4, 0x239148]

探测块:[0x2390C0, 0x2390F4, 0x239124, 0x239134, 0x239148, 0x239158]

这里为啥有个探测块?  考虑到灵活性 有的真实块 是连接在一起。

这两个块都是 真实块,又刚好是 真实块的后继。所以这里的填写的真实块就是需要修改后继的真实块,(不需要添加序言),探测块就是真正的真实块,还要把返回块加进去,到时候才能匹配上。

然后点击处理 fla 等一会儿,就再目标文件下生成了一个patch文件了 ,就很简单完美还原了。

BR混淆篇

思路就是:对指定的汇编 约束求解 看又啥值? 一般两个 又或者 一个 然后aptch 修改就搞定!

样本:

F5只能看到一个

这里利用约束求解 到指定br的值

def symbolic_execution(project, Br_addr, start_addr, hook_addrs=None, modify_value=None, inspect=False):
def retn_procedure(state):
"""hook 用:遇到 call/bl 直接‘ret’,防止路径跑飞到库/外部"""
        ip = state.solver.eval(state.regs.ip)
        project.unhook(ip)
return
def statement_inspect(state):
        expressions = list(
            state.scratch.irsb.statements[state.inspect.statement].expressions)
if len(expressions) != 0 and isinstance(expressions[0], pyvex.expr.ITE):
            state.scratch.temps[expressions[0].cond.tmp] = modify_value
            state.inspect._breakpoints['statement'] = []
if hook_addrs is not None:
        skip_length = 4      # ARM/ARM64 指令 4 字节
for hook_addr in hook_addrs:
            project.hook(hook_addr, retn_procedure, length=skip_length)
    state = project.factory.blank_state(addr=start_addr, remove_options={
                                        angr.sim_options.LAZY_SOLVES})
if inspect:
        state.inspect.b(
'statement', when=angr.state_plugins.inspect.BP_BEFORE, action=statement_inspect)
    sm = project.factory.simulation_manager(state)
    sm.explore(find=Br_addr)
if len(sm.found) > 0:
        found = sm.found[0]
return found.solver.eval(found.regs.x8)
return None  # 未命中

得到结果 patch 即可

sub 的还原没啥可说的,用d8,ida 9.2 等等 基本可以还原。 想要实践可以试试 llvm 优化ir 的方式。

作者的话

代码思路参考过很多项目:

https://bbs.kanxue.com/thread-285968.htm

https://bbs.kanxue.com/thread-288598.htm

https://bbs.kanxue.com/thread-286256.htm

https://bbs.kanxue.com/thread-287262.htm

https://bbs.kanxue.com/thread-286549.htm

等等 很多道友的文章。

我将fla 的还原制作为了ida的插件,br的还原只是脚本(不想写ida插件了 哈哈哈) ,当然都上传到我的github上面了

https://github.com/NiTianErXing666/-AndroidReverse/tree/main/OLLVM

代码写的很烂,思路可行,愿君斟酌。

总结

总结成四句话,送给读者当作一页小抄:

锚定预分发:入度最高的块,找到它就找到核心。

收集真实块:它的前驱就是业务逻辑的碎片。

重建邻接:骗程序自己跑,把真实路径记下来。

抓住 BR:盯住br xN,看清它要跳向何处。

如此一来,曾经乱舞的while、缠成意面的 CFG,就会在羽扇轻摇、谈笑间化作规整的直线。

樯橹灰飞烟灭,不过是多看一眼预分发,多盯一刻跳表的事。
愿你下次再遇见 OLLVM,能心里暗笑一句:“小小混淆,终究不过烟火。”

本文仅用于技术研究、教学与自有样本的安全分析。请勿用于未授权的目标或任何非法用途。涉及商业加固样本时,请遵守当地法律与授权协议。

*本文为看雪论坛优秀文章,由 逆天而行原创,转载请注明来自看雪社区

球分享

球点赞

球在看

点击阅读原文查看更多

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

OLLVM 控制流平坦化 FLA BR混淆 逆向工程 符号执行 angr IDA Pro 代码混淆 软件安全 Control Flow Flattening Branch Obfuscation Reverse Engineering Symbolic Execution Obfuscation Software Security
相关文章