OLLVM控制流平坦化混淆还原

[复制链接]
查看1141 | 回复0 | 2025-3-26 20:13:48 | 显示全部楼层 |阅读模式

立即注册,突破游客限制,与技术大牛交流切磋,畅享海量资源,让你的技术之路不再孤单!

您需要 登录 才可以下载或查看,没有账号?立即注册

×
背景
最近在研究ollvm反混淆,刚好遇到此样本,借此文章对ollvm fla控制流平坦化进行一个反混淆分析,顺便分享下idapython相关api的知识。
分析目标
娜迦加固 libgeiri.so
函数:init_proc
工具
ida7.7
pycharm:编写idapython代码,需要从ida安装目录下导入相关的库
截图202503262004296516.png
ollvm fla介绍
下面是一个标准的平坦化cfg图,经过ollvm fla混淆后几行的代码代码最后会膨胀至上百行。这里每个矩形图都叫做基本块,在这个图里我们主要关心序言块、return块和真实块。只有这三类的基本块才是我们需要的。其他的都是ollvm混淆加上的,这里统称虚假块。

截图202503262004533641.png
基本块:由多行指令组成,一般最后一条指令以跳转指令或ret结尾
主分发器:类似switch-case 结构,通过传入的状态变量,并决定跳转至目标块。
预处理器:我们可以抽象认为汇集在预处理器的基本块均为真实块
以下可以帮我们了解块与块之间的关系
  • 函数的开始地址为序言(Prologue)的地址
  • 序言的后继为主分发器(Main dispatcher)
  • 后继为主分发器的块为预处理器(Predispatcher)
  • 后继为预处理器的块为真实块(Relevant blocks)
  • 无后继的块为retn块
  • 剩下的为无用块与子分发器(Sub dispatchers)
这里我画个图概述下什么是前驱和后继,这两个名词也是用得比较多的。
注意看箭头指向,可以看到下面的执行流程是由A到B再到C,那么这里B就是中间块
因为A是B的上一个基本块那么A就是B的前驱
因为C是B的下一个基本块,那么C就是B的后继
截图202503262005193895.png
反混淆
上面简要说了下ollvm混淆的相关知识,那么我们应该如何去反混淆呢。这里只需要记住一个核心观念:找真实块(序言块、汇集到预处理器的所有块、return块)。找到所有真实块后,还需要做的是理清块与块之间的链接关系,最后需要做的工作就是根据块与块之间的关系,修改块的跳转指令patch到对应的块。这样就实现了平坦化的反混淆。
分析
ida打开so,进入到init_proc函数
截图202503262005462511.png
F5进入伪代码
截图202503262006029082.png
流程混乱,无法直观的去分析。
根据上面反混淆的理念,我们来理一理这个混淆的相关结构
序言块:0x43058

截图202503262006271355.png
主分发器:0x43120
截图202503262006433908.png
通过这个主分发器块,调用block.preds()函数,我们可以得到主分发器块的所有前驱块,也就是真实块
return块:0x43500

截图202503262006583072.png
到此,序言块,汇集到主分发器的所有块,return块这三大类的所有块我们都找到了,那我们就可以反混淆了吗。当然不是,我们只是收集到了所有块,但是还不知道块与块之间的跳转关系,所以我们还需要理清块跳转关系。
截图202503262007243337.png
我们先点击主分发器的地址,可以看到ida识别到所有引用该地址的基本块,这里提一嘴,"后继为预处理器的块为真实块"等上文6点理论都是广义上的,我们可以那样去认为,但是还是会有个别的情况与这6点相驳。
截图202503262007354545.png
比如这个基本块它的后继也是主分发器(这里的函数没有预处理器,而是真实块直接连接主分发器),难道它就是真实块吗,当然不是,这里它知识重置了状态变量w8,以供下一个switch跳转,从汇编中可以看出其中并没有任何的真实块逻辑。
截图202503262007492690.png
接着看这里的 B.NE loc_43120
w8,w9不相等走向主分发器,否则走向下面的基本块,我们也可以看到下一个基本块的最后一条指令也是直接跳转到主分发器的,所以总结出B.NE这个连接到主分发器的块不是真实块,只是一个重置switch跳转状态变量的块。所以我们只需要关注汇集到主分发的块的同时,最后一条指令还得是B指令。
好了,主分发器的前驱块分析完了,我们接着去理清跳转关系
这里我以0x43490基本块为参照物,逻辑都是一样的。
  1. loc_43490
  2. ADRP            X8, #off_97FB8@PAGE
  3. LDR             X8, [X8,#off_97FB8@PAGEOFF]
  4. LDR             W8, [X8]
  5. STR             W8, [X19,#0x20]
  6. MOV             W8, #0x39649A15
  7. B               loc_43120
复制代码
这里的MOV W8, #0x39649A15命令是重置状态变量,当我们循环到switch的时候,会匹配当前w8的值,对应上值时就跳转到相应的位置,所以可知0x39649A15是当前基本块的后继。但是这个后继我们目前只知道索引,我们是不是还得知道索引所代表的基本块是哪个啊
截图202503262008212868.png
点击0x39649A15,得到这个基本块
  1. MOV             W9, #0x39649A15
  2. CMP             W8, W9
  3. B.NE            loc_43120
复制代码
W8, W9不相等是跳转到loc_43120主分发器,这个我们不用关心,我们去看另一个走向得到如下图示
截图202503262008444715.png
这个基本块的地址是0x431D8,所以我们是不是得到了0x39649A15索引指向0x431D8基本块的地址了,得到0x43490->0x39649A15,
0x39649A15->0x431D8,换算可得0x43490->0x431D8
截图202503262008535680.png
简单画个图,以0x43490为参照,后继上面说了,这里我们看前驱。可以看到它的前驱存储的索引是0x3455F111,然后查找0x3455F111被谁引用,发现0x43314的后继索引是0x3455F111。跟着这个现象我们可以得知真实块自身存储的索引是后继,前驱存储的索引是此基本块的前驱。
接下来开始写脚本去混淆。
脚本处理获取主分发器
  1. def findDispatchers(func_start,num = 10):
  2.     func = idaapi.get_func(func_start)
  3.     blocks = idaapi.FlowChart(func)
  4.     pachers = []
  5.     for block in blocks:
  6.         preds = block.preds()
  7.         preds_list = list(preds)
  8.         if len(preds_list) > num:
  9.             pachers.append(block)
  10.     return pachers
复制代码
拿到主分发器块的地址,结果正确
截图202503262009232650.png
获取汇聚到主分发器的所有块
可以借此查看真实块的相关特征
  1. def findLoopEntryBlockAllPreds(loop_end_ea):
  2.     block = getBlockByAddress(loop_end_ea)
  3.     for pred in block.preds():
  4.         ea = idc.prev_head(pred.end_ea)
  5.         print("主分发器前驱基本块:", hex(ea), idc.GetDisasm(ea))
复制代码
存储所有真实块
截图202503262009574291.png
这里先判断真实块的最后一条指令操作符是B,并且操作数也必须是主分发器地址0x43120。然后取每个真实块B指令的上一条指令,根据MOV和MOVK的不同特征去匹配后继索引。
  1. MOV             W8, #0xA6FB
  2. STR             W0, [X19,#0x24]
  3. MOVK            W8, #0x7986,LSL#16
复制代码
简要说下这种汇编,它的意思是把0x7986左移16位,并且保持低位数据不变,这里低位是上述的w8 = 0xA6F8,1个字节2个字符,1个字节8个比特位,所以一个字符占4个比特位,左移16位后变为0x79860000,再和低位和并就是0x7986A6FB。算术运算符就是w8 =( 0x7986<<16) |0xA6FB。
截图202503262010387519.png
CSEL W8, W24, W8, EQ
注意这种的真实块,命令CSEL,这里是带分支的真实块
真实块的连接要么是顺序连接只有一个后继
要么就是分支连接,有两个后继,也就是if else。
这里样本CSEL都是有4个操作数的,EQ是操作符,当EQ条件满足时把w24赋值给w8,否则把w8赋值给w8。总之不管它的操作符如何变,条件为真时,Z标志位为1,都是取第2个操作数(w24),为假取第3个操作数(w8)
这里我就直接把相关的索引添加进去了,数量少就没去匹配特征了,数量多的话建议特征处理
截图202503262010555771.png
截图202503262011048431.png
粘贴到ida里运行,得到所有真实块的前驱索引和后继索引
截图202503262011161356.png
验证真实块跳转关系
这里有个疑问就是,怎么保证这些块的连接是不是都正确呢。针对这个我也写了个函数,验证这个对象里的连接是否都是正确
截图202503262011368042.png
截图202503262011523157.png
得到上述的关系调用链,以函数地址开头,以return块结尾,所有存储的真实块调用链执行成功无异常报错,如果其中执行失败会打印以下信息,数组的最后一个元素0x434ac里存储的0xbd9fbba索引没有找到后继块,针对这个报错可以去ida查看对应的信息,或者直接向stamp对象里补0x434ac相关的后继信息。有点类似unidbg的味道,缺啥补啥。


截图202503262012063840.png
重建块连接
最后就是调用此函数去重建块连接
  1. def rebuildControlFlow(state_map):
  2.     for block in state_map:
  3.         block_ea = int(block,16)#需要把字符串转成int
  4.         # 获取真实块保存的前驱、后继链接块索引
  5.         value = state_map[block]
  6.         # 查找块尾的跳转指令
  7.         endEa = getBlockByAddress(block_ea).end_ea
  8.         last_insn_ea = idc.prev_head(endEa)
  9.         if idc.print_insn_mnem(last_insn_ea) == "B":
  10.             # 如果是无条件跳转(B)
  11.             if len(value) == 2:
  12.                 succ_index = value[0] #当前真实块的后继块索引
  13.                 if succ_index == None: #return块没有后继,过滤掉它
  14.                     continue
  15.                 jmp_addr = getSuccBlockAddrFromMap(state_map,succ_index) #获取后继索引对应的真实块地址
  16.                 patchBranch(last_insn_ea, jmp_addr)
  17.             # 如果是条件跳转(CSEL)
  18.             elif len(value) == 3:
  19.                 succ_0 = value[0] #后继块的索引值
  20.                 jmp_addr_0 = getSuccBlockAddrFromMap(state_map, succ_0) #后继块的地址
  21.                 patchBranch(last_insn_ea, jmp_addr_0,1)
  22.                 succ_1 = value[1]   #后继块的索引值
  23.                 jmp_addr_1 = getSuccBlockAddrFromMap(state_map, succ_1)
  24.                 patchBranch(last_insn_ea, jmp_addr_1)
  25.         if idc.print_insn_mnem(last_insn_ea) == "MOV":
  26.             succ_index = value[0]  # 当前真实块的后继块索引
  27.             # if succ_index == None:  # return块没有后继,过滤掉它
  28.             #     continue
  29.             jmp_addr = getSuccBlockAddrFromMap(state_map, succ_index)  # 获取后继索引对应的真实块地址
  30.             patchBranch(last_insn_ea, jmp_addr)
复制代码
所有流程走完了,现在去ida执行脚本
截图202503262012383467.png
可以看到反混淆后的代码已经能直观看到运行整个代码的运行逻辑了。





样本so和去混淆脚本.zip

732.64 KB, 下载次数: 6

售价: 5 金钱  [记录]  [购买]

这个人还没有签名
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

图文推荐
大神求
大神求
6 天前
热门排行