deobfuscation
记需要反混淆的函数为output=obf-function(input)。
反混淆的思路,首先标记输入的变量记为input-symbol,通过Taint跟踪改变量的流向,并提取与该输入变量input-symbol和输出变量output-symbol有关的所有表达式exprs。再将exprs转化为Arybo表达式,然后再将其转化为LLVM-IR指令的形式。最后使用llvm编译器-o3选项对llvm-ir指令做编译优化,生成可执行文件。
1 使用LIEF解析binary文件
-
提取
binary文件中PT_LOAD段,将其加载到模拟器内存中。1
2
3
4
5
6
phdrs=binary.segmentsforphdrinphdrs:size=phdr.physical_sizevaddr=phdr.virtual_addressdebug('[+] Loading 0x%06x - 0x%06x'%(vaddr, vaddr+size))ctx.setConcreteMemoryAreaValue(vaddr, phdr.content)其中
ctx = TritonContext(),用于获取一个Triton实例对象,用于动态分析。 -
对
binary文件中的导入符号做hook处理(具体可以看makeRelocation()),确保模拟执行时能够正常执行所需的API函数,如:printf,memcpy,rand,strtoul等,即使用python语法来实现这些API函数。用于后面模拟器执行。
2 模拟执行并提取相关指令。
- 模拟
binary程序的执行过程,首先使用LIEF获取pc入口地址(binary.entrypoint). - 在x86指令长度为16,从入口地址中获取16比特长度的数据,即获取一条指令(
opcodes = ctx.getConcreteMemoryAreaValue(pc, 16))。 - 指令执行,调用
ctx.processing执行指令。 - 使用Taint跟踪与输入变量相关的所有表达式,并将这些表达式提取出来。
-
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
defemulate(ctx, pc):globalconditionglobaltotalInstructionsglobaltotalUniqueInstructionscount=0whilepc:# Fetch opcodesopcodes=ctx.getConcreteMemoryAreaValue(pc,16)# Create the Triton instructioninstruction=Instruction()##获取pc对应的指令instruction.setOpcode(opcodes)instruction.setAddress(pc)# Processifctx.processing(instruction)==False:debug('[-] Instruction not supported: %s'%(str(instruction)))breakcount+=1ifpcintotalUniqueInstructions:totalUniqueInstructions[pc]+=1else:totalUniqueInstructions[pc]=1ifinstruction.getType()==OPCODE.X86.HLT:##执行结束breakifctx.isRegisterSymbolized(ctx.registers.rip)andlen(condition)==0:#ctx.isRegisterSymbolized(ctx.registers.rip),判断ip是否跟symbol有关联,如果是,则表示改instruction中ip跟变量symbol有关,即变量symbol的值会改变此处指令中ip的跳转,关于symbol变量的初始化看下面的hookingHandler()。exprs=ctx.sliceExpressions(ctx.getSymbolicRegister(ctx.registers.rip))#获取与ip有关的所有表达式,并将它保存到全局变量comdition中。condition.append((instruction.isConditionTaken(), exprs))##isConditionTaken()判断跳转条件是否成立。##sliceExpressions(),用于获取与`expr`相关的所有`expressions`。# Simulate routineshookingHandler(ctx)##处理前面hook的函数,并引入输入变量symbol,再对该变量做Taint跟踪,通过Taint跟踪来得到与之相关的所有表达式。# Nextpc=ctx.getConcreteRegisterValue(ctx.registers.rip)##获取ip值。debug('[+] Instruction executed: %d'%(count))debug('[+] Unique instruction executed: %d'%(len(totalUniqueInstructions)))debug('[+] PC len: %d'%(len(condition)))# Used for metrictotalInstructions+=countreturnhookingHandler,从名字可以知道,这个函数用于处理之前被hook的函数。
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
52
53
54
55
56
57
58
59
60
61
defhookingHandler(ctx):globalconditionglobalpathsglobaltotalFunctionspc=ctx.getConcreteRegisterValue(ctx.registers.rip)forrelincustomRelocation:ifrel[2]==pc:# Emulate the routine and the return valueret_value=rel[1](ctx)##executing handlerifret_valueisnotNone:ctx.concretizeRegister(ctx.registers.rax)ctx.setConcreteRegisterValue(ctx.registers.rax, ret_value)# Used for metrictotalFunctions+=1# tigress user inputifrel[0]=='strtoul':debug('[+] Symbolizing the strtoul return')##rax为strtoul返回值,使用convertRegisterToSymbolicVariable()将该返回值转化为用于被Taint跟踪的变量symbol。var1=ctx.convertRegisterToSymbolicVariable(ctx.registers.rax)#rax 为strtoul的返回值,将返回值转化为变量的形式,ref_156 = SymVar_0。var0=ctx.getSymbolicVariableFromId(0)ctx.setConcreteVariableValue(var0, ctx.getConcreteVariableValue(var1))rax=ctx.getSymbolicRegister(ctx.registers.rax)ast=ctx.getAstContext()rax.setAst(ast.variable(var0))# tigress user end-pointifrel[0]=='printf':debug('[+] Slicing end-point user expression')## rsi中保存要被printf()打印出来的output值(以sample/sample4.c为例),getSymbolicRegister(rsi)来判断用于Taint跟踪的输入值symbol是否与rsi有关。# 如果是,则使用sliceExpressions(rsi)把从symbol过来的,并且与rsi相关的所有表达式expres保存在全局变量paths中。# 如果有兴趣可以把expres打印出来看看,就知道什么样子了。ifctx.getSymbolicRegister(ctx.registers.rsi):exprs=ctx.sliceExpressions(ctx.getSymbolicRegister(ctx.registers.rsi))paths.append(exprs)#else:# ast = ctx.getAstContext()# n = ctx.newSymbolicExpression(ast.bv(ctx.getConcreteRegisterValue(ctx.registers.rsi), 64))# exprs = {n.getId() : n}# paths.append(exprs)else:debug('[+] -------------------------------------------------------------- ')debug('[+] /!\ /!\ /!\ /!\ /!\ /!\ Symbolic lost! /!\ /!\ /!\ /!\ /!\ /!\ ')debug('[+] -------------------------------------------------------------- ')sys.exit(-1)# Get the return addressret_addr=ctx.getConcreteMemoryValue(MemoryAccess(ctx.getConcreteRegisterValue(ctx.registers.rsp), CPUSIZE.QWORD))##获取函数调用的返回地址,并赋值给rip。# Hijack RIP to skip the callctx.concretizeRegister(ctx.registers.rip)ctx.setConcreteRegisterValue(ctx.registers.rip, ret_addr)# Restore RSP (simulate the ret)ctx.concretizeRegister(ctx.registers.rsp)ctx.setConcreteRegisterValue(ctx.registers.rsp, ctx.getConcreteRegisterValue(ctx.registers.rsp)+CPUSIZE.QWORD)return
3 编译生成反混淆程序
###a 这里假设程序只有一条执行路径
即程序不会根据输入值input-symbol的不同而执行不同的分支。
则此时,全局变量paths[0]中得到与之相关的所有表达式。
使用下面代码,将所有表达式保存到文件中,其中pathNumber=0。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
使用Arybo开源项目中tritonexprs2arybo()方法,将exprs中所所有表达式转化为arybo表达式;以及使用tritonast2arybo()获取输入变量input-symbol。
最后使用to_llvm_function将其转化为llvm-ir指令,再recompile将这些ir指令重新编译生成可执行文件。
| 1 2 3 4 5 6 7 8 9 10 11 12 |
|
流程如下:
| 1 2 3 4 5 |
|
假设程序执行路径存在分支
流程如下,直接在代码里面解释,这代码里面假设程序只有两条可能的路径会走。
| 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
|
4 实验结果对比
结果如图1,图2,图3所示。其中图1为原程序编译后使用ida打开看到的控制流程图;图2为使用相关混淆方法对源程序做混淆处理后编译生成的可执行文件,再使用ida打开看到的控制流程图;图3为使用本文方法对混淆后可执行文件做反混淆处理后得到的新的可执行文件,再使用ida打开看到的控制流程图。虽然反混淆后程序的控制流图跟原程序有所不同,但执行结果是一致的。
[图1] 原程序控制流程图

[图2] 混淆后程序控制流程图

[图3] 反混淆后程序控制流程图
我只是解读搬运工,有兴趣读者可以直接下载该开源项目,点击链接。
当然,该方法仍存在一定程度上的局限性,还无法做到通用。有什么问题可留言。

14万+

被折叠的 条评论
为什么被折叠?



