首页
社区
课程
招聘
[原创]写一个简单的ollvm混淆还原
发表于: 2025-10-3 23:14 7783

[原创]写一个简单的ollvm混淆还原

2025-10-3 23:14
7783

大家好,我是[逆天而行]。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

虚假块:剩下的都是。

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

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

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

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

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

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

模拟执行 (Emulation) - 如 Unicorn Engine

思路:简单粗暴,效果拔群。用 Unicorn 把代码跑起来,当遇到分支判断时,我们手动 patch 寄存器或内存,强制它走某一条路,记录下这条路通往的下一个真实块。重复此过程,遍历所有分支。

优点:实现直观,精准控制。

缺点:需要手动处理分支点,自动化程度相对较低。

符号执行 (Symbolic Execution) - 如 angr

思路:更“优雅”的学院派方法。angr 会将输入和变量视为符号,而不是具体的值。当遇到分支时,它能自动探索所有可能的路径,并解出每条路径的约束条件。

优点:自动化程度极高,能自动构建出完整的控制流图(CFG)。

缺点:对于复杂的函数,路径爆炸问题可能导致资源和时间消耗巨大,有点“烧电脑”。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

解决思路无非两条路:

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

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

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

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

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

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

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

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 的混淆还原 我觉得就两步:

定位到真实块

还原真实块的关系

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

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

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

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

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

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

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

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

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

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

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

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

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

样本:

F5只能看到一个

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

得到结果 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上面了

570K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6z5K9g2c8A6j5h3&6q4M7W2S2A6L8X3M7$3y4U0k6Q4x3V1k6Q4x3X3c8m8L8X3c8J5L8$3W2V1f1X3g2$3k6i4u0K6k6g2)9J5c8Y4c8J5k6h3g2Q4x3V1k6E0j5h3W2F1i4K6u0r3e0@1I4x3g2V1@1`.

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

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

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

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

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

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

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

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

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

; 伪代码示例
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   ; 跳转到预分发块
; 伪代码示例
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   ; 跳转到预分发块
arm
; 简单的 if-else
cmp w8, #0
b.ne loc_then   ; 如果 w8 != 0, 跳转到 then 代码块
b loc_else      ; 否则,跳转到 else 代码块
arm
; 简单的 if-else
cmp w8, #0
b.ne loc_then   ; 如果 w8 != 0, 跳转到 then 代码块
b loc_else      ; 否则,跳转到 else 代码块
arm
; 状态选择
cmp  w8, #0
csel w9, #0xDEADBEEF, #0xCAFEF00D, ne  ; 条件成立选一个“魔法数”,否则选另一个
str  w9, [sp, #0x10]                   ; 写入状态变量(可在栈/全局/寄存器)
 
b    dispatcher                        ; 去“中央车站”报到
 
; -------------------------
dispatcher:
ldr  w9, [sp, #0x10]                   ; 读取状态
; ...一堆真假块的计算...
adr  x10, jumptable                    ; 寻址跳表
ldr  x11, [x10, w9, lsl #3]            ; 根据状态计算,取出真实目标地址
br   x11                               ; 间接跳转,起飞!
arm
; 状态选择
cmp  w8, #0
csel w9, #0xDEADBEEF, #0xCAFEF00D, ne  ; 条件成立选一个“魔法数”,否则选另一个
str  w9, [sp, #0x10]                   ; 写入状态变量(可在栈/全局/寄存器)
 
b    dispatcher                        ; 去“中央车站”报到
 
; -------------------------
dispatcher:
ldr  w9, [sp, #0x10]                   ; 读取状态
; ...一堆真假块的计算...
adr  x10, jumptable                    ; 寻址跳表
ldr  x11, [x10, w9, lsl #3]            ; 根据状态计算,取出真实目标地址
br   x11                               ; 间接跳转,起飞!
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 之后没有发现分支,可能混淆逻辑更复杂。")
python
运行

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 155
支持
分享
最新回复 (82)
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2025-10-3 23:22
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
感谢分享
2025-10-3 23:22
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2025-10-3 23:23
0
雪    币: 294
活跃值: (665)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
666
2025-10-3 23:25
0
雪    币: 3512
活跃值: (5040)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
感谢分享,还是热的
2025-10-3 23:29
0
雪    币: 0
活跃值: (1175)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
支持下
2025-10-4 00:19
0
雪    币: 7538
活跃值: (7643)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
8
前排热乎的,最近也在学ollvm,学成总结一波混淆原理和去混淆方案
2025-10-4 00:38
1
雪    币: 4596
活跃值: (5884)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
9
东方玻璃 前排热乎的,最近也在学ollvm,学成总结一波混淆原理和去混淆方案[em_056]
一起交流学习
2025-10-4 00:46
1
雪    币: 8237
活跃值: (4778)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
感谢分享
2025-10-4 01:18
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
感谢分享
2025-10-4 02:02
0
雪    币: 374
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
学习学习
2025-10-4 09:31
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
1111
2025-10-5 22:23
0
雪    币: 268
活跃值: (1323)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
666
2025-10-6 10:43
0
雪    币: 537
活跃值: (1649)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
666
2025-10-6 10:50
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
2025-10-6 13:41
0
雪    币: 96
活跃值: (212)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
6666
2025-10-7 11:46
0
雪    币: 224
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18
学习大佬思路
2025-10-7 17:09
0
雪    币: 8645
活跃值: (6402)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
感谢分享
2025-10-7 18:09
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20
6666
2025-10-7 18:10
0
雪    币: 6701
活跃值: (5661)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
谢谢分享  
2025-10-7 19:08
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
22
11
2025-10-7 22:36
0
雪    币: 1762
活跃值: (1255)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
23
谢谢
2025-10-8 03:58
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24
谢谢
2025-10-9 10:59
0
雪    币: 32
活跃值: (635)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
谢谢分享
2025-10-9 16:10
0
游客
登录 | 注册 方可回帖
返回