使用BinaryNinja去除libtprt.so的混淆 (二)
文章中的思路只是个人想法, 并不是最优解, 如有错误还望斧正.
插件代码github: detx https://github.com/EEEEhex/detx
版本: speedmobile_1.45.0.53757.apk中的libtprt.so
本文将分享去除[魔改的控制流平坦化]混淆的思路
1. 魔改的控制流平坦化
1.1 原理
我们知道标准的控制流平坦化就是把各个basicblock放到了一个switch中, 然后通过改变switch(var)中判断的这个变量var来分发到其他基本块中,
那么当一个case基本块结束, 一定会往var中写入一个新值, 来让下一轮分发运行到另一个case基本块中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | uint32_t var = 0x1234 ;
while ( 1 )
{
switch(var)
{
case 0x1234 :
/ / 逻辑 1. ...
var = 0x2345 ;
break ;
case 0x2345 :
/ / 逻辑 2. ..
if (v1 > 7 )
var = 0x3456 ;
var = 0x4567 ;
break ;
case 0x3456 :
/ / 逻辑N...
var = 0x4567 ;
break ;
/ / ...
case 0x4567 :
exit( 0 );
break ;
}
}
|
这个逻辑可以抽象为:
标准的一定包括: entry块(进行分发逻辑之前的第一个块), loopEntry块(分发循环开始的块), 分发块, 真实块(源代码逻辑), Ret块(跳出函数的块), loopEnd块(分发循环结束再次进入loopEntry的块)
.
因为ollvm源代码pass里就是这么写的, 但libtprt修改了细节
.
libtprt里面的平坦化去除了loopEnd块, 且存在很多编译优化的情况, 比如分发块和真实块共用, 真实块共用同一个swtich变量赋值指令, 判断提前等等, 而且在一个函数内有多个控制流平坦化(平行或嵌套):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | / / 平行的控制流平坦化 [ 1 ]
int32_t x9 = 0x4ba39ac1 ;
while (true)
{
if (x9 = = 0xde219aba )
break ;
if (x9 = = 0x4ba39ac1 )
x9 = 0x2e3a7efe ;
if (x9 = = 0x2e3a7efe )
{
s_2 = s_1;
x9 = - 0x21de6546 ;
}
}
int32_t x9_1;
/ / 判断提前
if ((arg2 & 1 ) ! = 0 )
x9_1 = - 0x25a2a29c ;
else
x9_1 = 0x64c05daa ;
/ / 第二个控制流平坦化 [ 2 ]
while (true)
{
int32_t x9_2 = 0x412e838c ;
while (true)
{
if (x9_2 < 0x172ae48c )
{
/ / 真实块逻辑....
}
if (x9_2 = = ...)
{
x9_2 = x9_1; / / 判断提前
}
if (x9_2 > = ...)
{
int32_t x9_3 = ...;
while (true)
{
switch (x9_3)
... / / 嵌套的控制流平坦化 [ 3 ]
}
}
}
}
|
魔改的控制流平坦化不再是一个完全的switch结构体了, 其可能是以下CFG图:
1.2 去混淆思路
但无论怎么改, 怎么编译优化, 平坦化本质上就是通过设置一个值, 然后分发这个值, 然后看这个值会到达哪个真实块:
真实块里会重新设置一个值, 然后重新进入分发逻辑, 到达后继的真实块.
.
那么我的思路是:
- 一定会有一个分发变量, 所有往这个分发变量里写入值的, 都是真实块
- 所有if中判断了分发变量的, 都是分发块
- 没有后继的就是Ret块
然后就是正常的去平坦化的思路:
- 获取所有真实块, 并拿到真实块的后继值
- 通过设置分发变量为后继值, 然后模拟执行分发逻辑, 看会到达哪个真实块
- 那么此块就是真正的后继块, 最后进行Patch
关键点在于: ①怎么获取这分发变量 ②怎么拿到所有写入了分发变量的块 ③怎么拿到真实块的后继值(要设置的分发变量的值)
幸运的是, 这些问题通过BinaryNinja的mlil ssa层面都可以很轻松的解决
1.2.1 获取分发变量
这个需要自己获取, 我是通过获取鼠标当前行(鼠标要点击loopEntry块)的if语句的条件中的变量, 比如下面:
用户需要找到分发开始块, 然后获取到x8#2, 同时也能获取到loopEntry
1.2.2 获取赋值块
我将写入了分发变量的块称为'赋值块' (注意: 赋值块不一定包含了全部的真实块):
怎么获取呢, 可以发现, 在loopEntry块中的if语句上有一个'x8#2 = ϕ(x8#1, x8#2, x8#5, x8#6, x8#9, x8#14)'.
.
这里面的x8#1, x8#5... x8#14都是被写入的分发变量, 可以通过def_site拿到赋值语句, 就是图上的"x8#1 = 0x703c1e1e","x8#6 = 0x6110cf13"等, 赋值语句所在的块就是'赋值块', 用il_basicblock.source_block获取.
1.2.3 获取真实块的后继值
怎么拿到一个真实块设置的分发变量的值呢?
如果是没有条件的话, 就赋一个值的那种, 其实通过1.2.2就获取到了, 但如果是有条件的话:
1 2 3 4 | if (arg3 = = arg2)
x8 = 0x2de2ab44 ;
else
x8 = - 0x26983ee ;
|
在mlil ssa层面是:
也就是1.2.2获取到的是'x8#9 = ϕ(x8#7, x8#8)'这条语句, 然后通过x8#7/x8#8的def_site一样可以获取到两个后继值, 然后通过'il_basic_block.incoming_edges[0].type == BranchType.TrueBranch '来拿到哪个后继值是满足条件时设置的, 哪个后继值是不满足条件时设置的.
1.2.4 模拟执行分发逻辑
首先通过1.2.1拿到了loopEntry块的地址, 但在从loopEntry开始执行分发逻辑之前, 需要进行分发比较值的初始化, 因为可以看到下图中, cmp语句的寄存器其实在进入分发逻辑之前就赋值好了, 所以在模拟执行分发逻辑前需要进行分发比较值的初始化:
我的做法是拿到分发逻辑之前的所有块, 都当作init块, 然后模拟执行.
当然也可以从llil层面, 拿到if条件中的寄存器被写入的语句, 然后该语句所在的块就是init块(其实应该是这种写法比较合理)
1.3 嵌套平坦化
从1.1节里的代码可以看到, 其实在一个函数中是存在多个平坦化的, 可能两个平坦化是平行的, 也可能在一个平坦化的if中又有一个平坦化.
无论是平行还是嵌套, 一样可以通过1.2节的思路去除, 但是会少一个真实块的地址:
如上图中所示, 如果仅通过1.2.2把分发变量里写入值的当作真实块, 就是图上黄框的块.
那么模拟执行的时候, 当执行到'if (x8_19 == 0x70d4e113) break;'时, 就暂停不了了, 因为没有遇到黄框块(因为实际上要遇到蓝框块), 实际上当执行到这里逻辑时, 是要从内层平坦化跳出来, 所以需要把图上蓝框的块(这个块可能是任何块也可能是外层平坦化的分发块)也当作真实块, 这样当模拟执行时遇到此块就会暂停返回.
.
我在代码中并没有自动去搜索出口块, 需要用户输入, 当然也是很好分辨的, 内层平坦化毕竟是一个循环, 所有的出口后继都是loopEntry块, 唯有一个后继不是loopEntry块的, 那就是出口块.
1.4 编译优化
实际上编译优化的情况比较多, 需要特殊分析
1.4.1 判断提前
当分支判断并不是在真实块中进行的, 而且提前到了循环外怎么办?
可能有人会想, 在mlil ssa层面无非就是多了一层赋值呗, 一层一层往上找一样能找到后继值.
确实是这样没错, 但问题是拿到该块对应的后继块后, 怎么Patch?:
如果想把他下沉放到真实块里, 那"cmp + b.cc + b"放哪里?, 况且逻辑上也不能放到真实块里, 比如这个条件判断的是"cmp x1, #0", 如果在对应的真实块执行之前, x1被改变了怎么办, 那逻辑就完成不正确了.
我的思路是:
- 判断改为cset
- 条件传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | mov w20, w1
...
tst w20,
...
csel w9, w20, w12, ne { 0xda5d5d64 } { 0x64c05daa }
...
str w9, [sp,
...
cmp wX, w10 / / 分发逻辑
...
/ / - - - - - - 真实块开始 - - - -
ldr w9, [sp,
b 0xafcdc
/ / - - - - - - 真实块结束 - - - -
|
改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | mov w20, w1
...
tst w20,
...
cset w9, ne { 1 } { 0 } / / csel改为cset
...
str w9, [sp,
...
cmp wX, w10
...
ldr w9, [sp,
b 随机找一个分发块(因为分发快是无用块 可以随便Patch)
|
- > cmp w9,
b.ne 满足条件地址
b 不满足条件地址
(如果分发块放不下三条指令就接着找或拆分)
|
什么意思呢, 用伪代码表示就是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | / / - - - 原逻辑 - - - - - - - - - - - - -
if (arg2 = = 0 )
x28 = 0x3208a470 ;
else
x28 = - 0x3059f83a ;
/ / ...
x8 = x28;
/ / - - - - - - - - - - - - - - - - - - - - -
改为 = = >
/ / - - - - - - - - - - - - - - - - - - - - -
if (arg2 = = 0 )
x28 = 1 ;
else
x28 = 0 ;
if (x28 = = 1 )
{
/ / 0x3208a470 对应的真实块
}
else
{
/ / 0x3059f83a 对应的真实块
}
|
原先的判断逻辑不变, 只是把x28用cset设置成了0或1, 不再是后继值了, 然后在真实块里判断x28是0还是1.
问题是这么改真实块还是需要额外容纳三条指令(因为这样改真实块的原指令就不能动了), 那还是放不下, 所以就需要拿分发块(无用块)去patch:
1.4.2 共用分发变量赋值语句
就是两个真实块的后继值是一样的, 比如:
1 2 3 4 5 | / / 真实块 1. ..
x8 = 0x124897684
/ / 真实块 2
x8 = 0x124897684
|
此时在汇编层面会把这个'x8 = 0x124897684'单独拆出来:
此时通过拿往分发变量里写入值的块当真实块就会少那俩块, 所以1.2.2节说赋值块不一定包含了全部的真实块,此时就需要判断当一个赋值块有多个直接前继时, 它的前继也是真实块.
.
当然还有其他情况, 比如共用cmp, 分发块和真实块合并为一个, 但这些都无伤大雅不影响整体逻辑.
2. 编写插件代码
具体逻辑请查看deflat2.py与emulate.py
2.1 模拟执行逻辑
具体请查看emulate.py中的"Emulator" "FuncEmulate" "DeflatEmulate"三个类, 其实就是给unicorn封装了一层.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | for info in assign_bb_infos:
var_value = info.var_value
real_sucs_ = []
if isinstance (var_value, AssignBBInfo.VarValueInfoU):
deflat_emu.set_switch_var_value(var_value.u_value)
suc_addr = deflat_emu.start_until_stop()
real_sucs_.append(suc_addr)
elif isinstance (var_value, AssignBBInfo.VarValueInfoTF):
deflat_emu.set_switch_var_value(var_value.t_value)
suc_addr = deflat_emu.start_until_stop()
real_sucs_.append(suc_addr)
deflat_emu.set_switch_var_value(var_value.f_value)
suc_addr = deflat_emu.start_until_stop()
real_sucs_.append(suc_addr)
info.real_suc = real_sucs_
|
2.2 获取信息逻辑
具体逻辑都在'def deflat2(bv: BinaryView, func: Function, switch_var_ssa: SSAVariable, extra_real_addr = None, manual_value = None, witch_check = False)'函数中, 比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | switch_vars = loop_phi_var_insn.src
for svar in switch_vars:
assign_insn = svar.def_site
if assign_insn = = loop_phi_var_insn:
continue
cur_ssa_bb = assign_insn.il_basic_block
disasm_bb = cur_ssa_bb.source_block
real_sbb.append(disasm_bb)
if ( len (disasm_bb.incoming_edges) = = 2 ):
pre_edge1 = disasm_bb.incoming_edges[ 0 ]
pre_edge2 = disasm_bb.incoming_edges[ 1 ]
if (pre_edge1. type = = BranchType.UnconditionalBranch) and (pre_edge2. type = = BranchType.UnconditionalBranch):
pre_bb1 = pre_edge1.source
pre_bb2 = pre_edge2.source
if (pre_bb1 not in dispatch_sbb) and (pre_bb2 not in dispatch_sbb):
real_sbb.append(pre_bb1)
real_sbb.append(pre_bb2)
cur_bb_info = AssignBBInfo()
cur_bb_info.bb_start = cur_ssa_bb.source_block.start
cur_bb_info.set_var_addr = assign_insn.address
if isinstance (assign_insn, MediumLevelILSetVarSsa):
elif isinstance (assign_insn, MediumLevelILVarPhi) and ( len (assign_insn.src) = = 2 ):
assign_bb_infos.append(cur_bb_info)
|
3. 效果
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2024-8-15 15:57
被0xEEEE编辑
,原因: 修改细节