长亭百川云 - 文章详情

使用BinaryNinja去除libtprt.so的混淆

0xEEEE

78

2024-08-14

插件代码https://github.com/EEEEhex/detx


版本: speedmobile_1.45.0.53757.apk中的libtprt.so


文章将分享去除[寄存器间接跳转]与[魔改控制流平坦化]混淆的思路, 并编写去混淆插件代码。


0. 混淆类型


libtprt.so中的混淆大体分为三种类型:

◆魔改的控制流平坦化

◆寄存器间接跳转

◆无效循环


以及这三种的穿插混合, 这些混淆要么是获取信息麻烦, 要么是Patch起来麻烦, 总之就是很麻烦。本文将先分享去除[寄存器间接跳转]混淆的思路, 主要是Patch思路。



12
3寄存器间接跳转混淆


1.1 原理


其实就是跳转地址是计算出来的, 如下图所示:



这种混淆就是把原先的逻辑跳转改为了jmp(var2)。


其中var2 = mem[var1 (<< num)] + const 这些值其实都是可以确定的, 即:


1//-----------------
2if (Cond)
3  jmp(true_addr)
4else
5  jmp(false_addr)
6//-----------------
7变为了->
8//-----------------
9if (Cond)
10  var1 = 0;
11else 
12  var1 = 1;
13var2 = data_1fd630[var1];
14var3 = var2 - 0x7218df2;
15jump(var3); 
16//-----------------

通过cond设置偏移var1, 然后从跳转表data_1fd630中拿出var1偏移处的值, 然后+/-一个常量就得到真正的跳转地址了。


1.2 获取跳转地址思路


我的思路是静态分析+模拟执行:


1.从BinaryNinja的mlil ssa层面, 可以获取到jump变量var的指令。


2.然后层层向上找, 找到所有涉及到的mlil指令(就如上图中所有红框中的指令)。


3.然后拿到这些mlil指令对应的汇编指令去模拟执行就可以得到跳转寄存器的值。

3.1. 其中在汇编层面是通过条件选择指令(csel, cset, cinc等)来改变值导致最终跳转地址的变化(就是上面红框中x11#1=8和x11#2=0x30)

3.2. 因此可将条件选择指令改为mov等指令直接赋值, 模拟执行两次来分别获取if和else的真实跳转地址


例如一次混淆涉及到的如下指令:



具体来说就是:


1.首先要识别出一次混淆涉及到的所有汇编指令, 就是上图中所有红框的汇编指令。


2.识别出其中可以改变跳转地址的那条指令(cset/csinc等), 本次混淆就是csel x11, x28, x27。


3.将csel x11, x28, x27分别改为"mov x11, x28"或"mov x11, x27"然后模拟执行从而获取两个跳转地址。


问: 可以直接模拟执行br之前的全部指令, 不去识别一次混淆涉及到的指令吗?
答: 可以是可以, 但这样会涉及到非混淆的真实指令, 我感觉处理起这种情况来不比去识别混淆指令简单。


1.3 Patch思路 \*


假设现在已经知道了两个跳转地址是多少, 怎么去Patch呢?


我们的Patch不能改变了原始的逻辑, 比如说:

问: 可以把"csel x11, x28, x27"改为"b.lt t_addr", 把"br x12"改为"b f_addr"这样patch吗?
答: 不可以, 因为原始逻辑是在csel之后还执行了"0x9cc6c 0x9cc70 0x9cc78"这些指令, 如果从0x9cc64处就改成"b.lt"跳转, 那逻辑就不对了, 原逻辑中br之前执行的指令就少执行了一部分。
.
问: 可以上移指令(因为混淆指令是无效的可以随便覆盖), 然后在末尾插入"b.lt + b"指令吗?
答: 不可以, 比如说0x9cc74处的指令属于混淆指令, 是无效的, 将其改为:


![](https://mmbiz.qpic.cn/sz_mmbiz_png/1UG7KPNHN8FSoGhzo0HbRdKE846iagMxBibASC4L32qaNBXxgxN6x9YCmuG1TaOx8amJ2S9GR9tiantAzjb9iaUu3Q/640?wx_fmt=png&from=appmsg)

就是把csel ... br中间的指令全部上移覆盖上一个指令, 在末尾多出一个指令的空间, 但这样会出现一个问题, 原逻辑中是:


10x9cc60 cmp w12, w23  
2....... 改变跳转寄存器x12
30x9cc7c br x12

这样Patch之后就变成了:


10x9cc60 cmp w12, w23
2....... ............
30x9cc70 cmp w12, w15
4....... ............
50x9cc78 b.lt 满足条件地址
60x9cc7c b 不满足条件地址

条件判断被覆盖了, 原本逻辑是判断的"cmp w12, w23"这样一改变成判断"cmp w12, w15"了。
.
那要怎么Patch?我的思路如下:


11. 一次混淆!至少!涉及以下7个指令(中间穿插着其他逻辑的指令):  
2  mov     w10, #0x60  
3  ...  
4  mov     w11, #0x58  
5  ...  
6  cmp     w7, w22  
7  ...  
8  csel    x23, x11, x10, lt  
9  ...  
10  ldr     x25, [x12, x23]  
11  ...  
12  add     x7, x25, x13  
13  ...  
14  br      x7  
152. 改为如下:
16  mov     w10, #0x60      <- 可以nop掉 不nop也不影响结果  
17  ...  
18  mov     w11, #0x58       
19  ...
20  nop                     <-  cmp     w7, w22 [cmp语句要最后统一nop 因为会可能有多个逻辑共用同一个cmp]  
21  ...  
22  nop                     <-  csel    x23, x11, x10, lt  
23  ...  
24  nop                     <-  其他涉及到的指令  
25  ...  
26  cmp     w7, w22         <-  ldr     x25, [x12, x23]  
27  b.lt    ...             <-  add     x7, x25, x13  
28  b       ...             <-  br      x7  
29  大多只有第一次混淆的时候这些混淆指令会穿插在一起, 之后基本都是ldr+add+br一个整体了

就是 cmp下沉 , 将"cmp + b.cc + b"放到一起, 这样就不会因为其他指令的cmp导致条件被覆盖了。


问: 这样下沉如果cmp w7, w22中的w7和w22被之前的指令改变了怎么办?
答: 事实证明是不会的, 我一开始的思路是不移动cmp而是在cmp之后保存nzcv标志位到例如w10中, 然后b.cc之前再恢复标志位, 结果发现有没有保存nzcv都一样。


其实这个so中的函数都是在控制流平坦化之上又加了一层寄存器间接跳转, 所以这些cmp指令其实是控制流平坦化的分发指令, 这些值(w7,w22之类的)在进入分发逻辑之前就确定好了, 是不会被改变的。



12
3编写插件代码

代码逻辑分为: ①模拟执行 ②信息获取 ③Patch逻辑 三部分


2.1 模拟执行代码


采用unicorn框架, 具体请查看emulate.py中的"Emulator" "FuncEmulate" "DeJmpRegEmulate"三个类, 其实就是给unicorn封装了一层。


修改 条件选择指令 时要根据不同的类型进行修改:


1#如果是csinc指令, 不满足条件应该改为add x24, x1, #1 | csinc是条件不满足则xd=xm+1, cinc是条件满足则xd=xn+1
2if ((insn_token[0] == 'csinc' ) and (index == 1)) or ((insn_token[0] == 'cinc') and (index == 0)): 
3    if value == 'xzr':#如果是xzr寄存器就不能用add, 相当于赋值为了1
4        mov_opcode = bv.arch.assemble(f"mov {cond_set_reg}, #1", condition_insn_addr) 
5    else:
6        mov_opcode = bv.arch.assemble(f"add {cond_set_reg}, {value}, #1", condition_insn_addr) 
7elif (insn_token[0] == 'csinv') and (index == 1): 
8    mov_opcode = bv.arch.assemble(f"mvn {cond_set_reg}, {value}", condition_insn_addr) #按位取反
9elif (insn_token[0] == 'sneg') and (index == 1):
10    mov_opcode = bv.arch.assemble(f"neg {cond_set_reg}, {value}", condition_insn_addr) #取负值
11else:
12    mov_opcode = bv.arch.assemble(f"mov {cond_set_reg}, {value}", condition_insn_addr) #汇编mov x4, x9


2.2 信息获取代码


问: 怎么通过代码拿到一次混淆涉及到的全部指令?
答: 我是通过从mlil ssa层面, 因为用ssa的话, 可以很方便的查找一个变量的被写入的语句, 代码中是通过def_site。


比如从jump(x9_2#5)开始, 先拿到x9_2#5的def_site, 比如说是"x9_2#5 = x9_1#4 + 0x3872d170", 然后取出这条语句的等号右边涉及到的变量, 这里是x9_1#4, 然后拿到x9_1#4的def_site, 比如是x9_1#4 = [&data_1dd4c0 + x9#3].q @ mem#1, 然后拿x9#3的def_site, 比如是x9#3 = ϕ(x9#1, x9#2), 最后得到x9#1 = 0, x9#2 = 0x58. 其实用一个递归就解决了:


1def get_involve_insns(jmp_insn: MediumLevelILJump):
2    def get_right_ssa_var(expr, vars: list):
3        if isinstance(expr, SSAVariable):
4            vars.append(expr)
5            return
6        elif isinstance(expr, list):
7            for ope in expr:
8                if isinstance(ope, SSAVariable):
9                    vars.append(ope)
10            return
11
12        if hasattr(expr, 'operands'):
13            for ope in expr.operands:
14                get_right_ssa_var(ope, vars)
15        return
16
17    involve_insns = [] #涉及到的指令
18    jmp_var = jmp_insn.dest.var
19    var_stack = []
20    var_stack.append(jmp_var)
21    while len(var_stack) != 0: #拿到一次寄存器间接跳转混淆涉及到的所有指令
22        cur_ssa_var = var_stack.pop()
23        insn_ = cur_ssa_var.def_site #一条指令 应该是MediumLevelILSetVarSsa或MediumLevelILVarPhi
24        if insn_ == None:
25            break
26
27        if insn_ in involve_insns:
28            break #如果拿到的指令已经在之前获取到的指令中了, 说明遇到循环了
29        else:
30            involve_insns.append(insn_) #添加涉及到的指令
31
32        if 'cond' in insn_.dest.name:#遇到cond:20#1 = x8#2 == 0x586b6221这种就不再继续了 要不然有可能遇到phi节点导致死循环
33            break
34
35        insn_right = insn_.src #这条指令=右边的表达式
36        get_right_ssa_var(insn_right, var_stack) #拿到表达式中的变量             
37    
38    return involve_insns

然后通过mlssa_insn.llils拿到一条mlil指令涉及到的llil指令, llil和汇编指令的地址是基本一一对应的:


1involve_asm_addrs = [] #涉及到的汇编指令的地址 可能少csx赋值指令 后面补上
2for mlssa_insn in involve_insns:
3    llil_insns = mlssa_insn.llils
4    for insn_ in llil_insns:
5        if insn_.address not in involve_asm_addrs:
6            involve_asm_addrs.append(insn_.address)

实际这样下来可能会缺少指令, 就是那两个设置跳转表偏移量的指令, 比如"mov w27, #0x30"和"mov w28, #0x8"。


那么就通过从当前块开始, 向前继块从后往前搜索指令, 先拿到csel/cinc指令的操作寄存器, 然后搜索类型是"mov", 第一个寄存器是条件选择指令操作寄存器的指令。


具体逻辑请查看dejmpreg.py。


2.3 Patch代码


首先要拿到 从csel到br之间所有 指令, 当然可以分段获取然后移动构造, 而且分段获取的话还可以应对从后往前跳转的情况(当前混淆中是没有这种情况的, 只是我懒得写了)。


1#0. 拿到所有要操作的指令
2obf_insns_index = [] #指在csx2br_insns_text中的index
3csx2br_insns_text = [] #从csx到br中的所有指令文本 (包含csx不包含br)
4#1. 将混淆指令全转为nop, 并删除最后两个nop(一个nop改bcc, 一个nop改cmp)
5for i in obf_insns_index:
6    csx2br_insns_text[i] = 'nop'
7csx2br_insns_text.pop(obf_insns_index[-1])
8csx2br_insns_text.pop(obf_insns_index[-2]) #index本身就是从小到大排序的, 所以直接pop不影响
9
10#2. 下沉cmp
11cmp_txt = bv.get_disassembly(cmp_addr)
12csx2br_insns_text.append(cmp_txt)
13
14#3. 获取select指令的寄存器 并添加跳转
15csx_tokens = (bv.get_disassembly(cond_addr)).split() #获取csel/cset/csinc等的token
16csx_cond = csx_tokens[-1] #条件eq/lt等
17bcc_cond = 'b.' + csx_cond
18bcc_txt = f"{bcc_cond} {hex(tbr_addr)}"
19csx2br_insns_text.append(bcc_txt)
20b_txt = f"b {hex(fbr_addr)}"
21csx2br_insns_text.append(b_txt)
22logger.log_info(f"csx2br_insns_text: {csx2br_insns_text}")



12
3效果





看雪ID:0xEEEE

https://bbs.kanxue.com/user-home-901761.htm

\*本文为看雪论坛优秀文章,由 0xEEEE 原创,转载请注明来自看雪社区




# 往期推荐

1、 移植 Youpk 到 Aosp10

2、 第十七届CISCN总决赛-AWDP-PWN部分题解

3、 aarch64架构的某so模拟执行和加密算法分析

4、 Android系统启动源码分析

5、 Linux 内核重大安全漏洞曝光!indler 漏洞威胁数亿计算机系统

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2