查看原文
其他

C/C++反混淆方法

一只流浪狗 看雪学院 2019-09-18

记需要反混淆的函数为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段,将其加载到模拟器内存中。其中ctx = TritonContext(),用于获取一个Triton实例对象,用于动态分析。


phdrs = binary.segments
for phdr in phdrs:
size = phdr.physical_size
vaddr = phdr.virtual_address
debug('[+] Loading 0x%06x - 0x%06x' %(vaddr, vaddr+size))
ctx.setConcreteMemoryAreaValue(vaddr, phdr.content)


对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跟踪与输入变量相关的所有表达式,并将这些表达式提取出来。


def emulate(ctx,
pc):
global condition
global totalInstructions
global totalUniqueInstructions

count =
0
while pc:
# Fetch opcodes
opcodes = ctx.getConcreteMemoryAreaValue(pc,
16)

# Create the Triton instruction
instruction = Instruction() ##获取pc对应的指令
instruction.setOpcode(opcodes)
instruction.setAddress(pc)

# Process
if ctx.processing(instruction) == False:
debug('[-] Instruction not supported: %s' %(str(instruction)))
break

count +=
1

if pc in totalUniqueInstructions:
totalUniqueInstructions[pc] +=
1
else:
totalUniqueInstructions[pc] =
1

if instruction.getType() == OPCODE.X86.HLT: ##执行结束
break

if ctx.isRegisterSymbolized(ctx.registers.rip)
and len(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 routines
hookingHandler(ctx) ##处理前面hook的函数,并引入输入变量symbol,再对该变量做Taint跟踪,通过Taint跟踪来得到与之相关的所有表达式。

#
Next
pc = 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 metric
totalInstructions +=
count
return

hookingHandler,从名字可以知道,这个函数用于处理之前被hook的函数。

def hookingHandler(ctx):
global condition
global paths
global totalFunctions

pc = ctx.getConcreteRegisterValue(ctx.registers.rip)
for rel in customRelocation:
if rel[2] ==
pc:
# Emulate the routine
and the
return value
ret_value = rel[1](ctx) ##executing handler
if ret_value
is not None:
ctx.concretizeRegister(ctx.registers.rax)
ctx.setConcreteRegisterValue(ctx.registers.rax, ret_value)

# Used
for metric
totalFunctions +=
1

# tigress user
input
if rel[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-point
if rel[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打印出来看看,就知道什么样子了。
if ctx.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 address
ret_addr = ctx.getConcreteMemoryValue(MemoryAccess(ctx.getConcreteRegisterValue(ctx.registers.rsp), CPUSIZE.QWORD))
##获取函数调用的返回地址,并赋值给rip。
# Hijack RIP
to skip the
call
ctx.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编译生成反混淆程序


这里假设程序只有一条执行路径


即程序不会根据输入值input-symbol的不同而执行不同的分支。则此时,全局变量paths[0]中得到与之相关的所有表达式。使用下面代码,将所有表达式保存到文件中,其中pathNumber=0。


def generateSymbolicExpressions(pathNumber):
global paths
exprs = paths[pathNumber]

ssa = str()
last =
0
for k, v in sorted(exprs.items()):
ssa += str(v) +
'\n'
last =
k

name =
'symbolic_expressions/%s.py' %(sys.argv[1].split('/')[-1])
debug('[+] Generating %s' %(name))
fd =
open(name,
'w')
fd.write(TEMPLATE_GENERATE_HASH_SSA % (ssa,
last))
fd.close()
return


使用Arybo开源项目中tritonexprs2arybo()方法,将exprs中所所有表达式转化为arybo表达式;以及使用tritonast2arybo()获取输入变量input-symbol。


最后使用to_llvm_function将其转化为llvm-ir指令,再recompile将这些ir指令重新编译生成可执行文件。


def generateLLVMExpressions(ctx, pathNumber):
global paths
exprs = paths[pathNumber]

debug('[+] Converting symbolic expressions to an LLVM module...')
e = tritonexprs2arybo(exprs)
var = tritonast2arybo(ctx.getAstContext().variable(ctx.getSymbolicVariableFromId(0)))

debug('[+] Converting arybo to an LLVM function...')
M = to_llvm_function(e,[var.v])

return M

流程如下:


generateSymbolicExpressions(0)
# Generate llvm of the first path
M = generateLLVMExpressions(ctx, 0)
# Recompile the LLVM-IL
recompile(M)


假设程序执行路径存在分支


流程如下,直接在代码里面解释,这代码里面假设程序只有两条可能的路径会走。


ssa_pc = str()
exprs_pc = condition[0][1] ##condition为一个全局变量,在前面emulate中condition.append((instruction.isConditionTaken(), exprs)),保存的条件跳转表达式。
####exprs_pc保存这与ip跳转相关的所有表达式。
last_pc = None
for k, v in sorted(exprs_pc.items()):
# print 'v', str(v)
ssa_pc += str(v) + '\n'
last_pc = v

ssa_b1 = str()
exprs_b1 = paths[0] ###第一条执行路径的所有表达式。
# print 'exprs_b1', exprs_b1
last_b1 = 0
for k, v in sorted(exprs_b1.items()):
ssa_b1 += ' ' + str(v) + '\n'
last_b1 = k
# print 'path v', str(v)

ssa_b1 += ' endb = ref_%d\n' %(last_b1)

debug('[+] Asking for a new input...')
pcAst = ctx.getPathConstraintsAst()
print 'pcAst, ', pcAst
ast = ctx.getAstContext()
model = ctx.getModel(ast.lnot(pcAst)) #lnot:Creates a lnot node (logical NOT).
if model:
VM_INPUT = str(model[0].getValue()) ###获取能够触发另外一条执行路径的输入值。
else:
debug('[+] No model found! Opaque predicate?')
# Generate symbolic epxressions of the first path
generateSymbolicExpressions(0)

# Generate llvm of the first path
M = generateLLVMExpressions(ctx, 0)

# Recompile the LLVM-IL
recompile(M)
return 0

# Re-simulate an execution to take another path
run(ctx, binary) #######重新模拟执行一次binary程序。

ssa_b2 = str()
exprs_b2 = paths[1] ####另一条执行路径的所有表达式。
last_b2 = 0
for k, v in sorted(exprs_b2.items()):
ssa_b2 += ' ' + str(v) + '\n'
last_b2 = k
ssa_b2 += ' endb = ref_%d\n' %(last_b2)

name = 'symbolic_expressions/%s.py' %(sys.argv[1].split('/')[-1])
debug('[+] Generating %s' %(name))
fd = open(name, 'w')
##ssa_b1以及ssa_b2分别表示两条执行路径。分别对应condition判断条件true以及false对应的两个跳转分支。ssa_pc则表示与该条件判断有关的所有表达式,用于计算该condition是true或false
if condition[0][0]: 
fd.write(TEMPLATE_GENERATE_HASH_SSA_PC1 % (ssa_pc, '%s' %(str(last_pc.getAst().getChildren()[0])), ssa_b1, ssa_b2))
else:
fd.write(TEMPLATE_GENERATE_HASH_SSA_PC1 % (ssa_pc, '%s' %(str(last_pc.getAst().getChildren()[0])), ssa_b2, ssa_b1))
fd.close()

debug('[+] Converting symbolic expressions to an LLVM module...')
last_pc_expr = None
last_pc_id = 0
exprs_pc = condition[0][1]
for k, v in sorted(exprs_pc.items()):
last_pc_expr = v
last_pc_id = k
del condition[0][1][last_pc_id]

ast = ctx.getAstContext()
nc = ast.ite(last_pc_expr.getAst().getChildren()[0], ast.bvtrue(), ast.bvfalse())
expr = ctx.newSymbolicExpression(nc)
condition[0][1][expr.getId()] = expr

c = tritonexprs2arybo(condition[0][1])
e1 = tritonexprs2arybo(paths[0])
e2 = tritonexprs2arybo(paths[1])
ast = ctx.getAstContext()
var = tritonast2arybo(ast.variable(ctx.getSymbolicVariableFromId(0)))
if condition[0][0]:
M = to_llvm_function(ExprCond(c, e1, e2), [var.v])
else:
M = to_llvm_function(ExprCond(c, e2, e1), [var.v])

# Recompile the LLVM-IL
recompile(M)


4 实验结果对比


结果如图1,图2,图3所示。其中图1为原程序编译后使用ida打开看到的控制流程图;图2为使用相关混淆方法对源程序做混淆处理后编译生成的可执行文件,再使用ida打开看到的控制流程图;图3为使用本文方法对混淆后可执行文件做反混淆处理后得到的新的可执行文件,再使用ida打开看到的控制流程图。虽然反混淆后程序的控制流图跟原程序有所不同,但执行结果是一致的。



[图1] 原程序控制流程图

 


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

 


[图3] 反混淆后程序控制流程图

 

我只是解读搬运工,有兴趣读者可以直接下载该开源项目,点击链接。


当然,该方法仍存在一定程度上的局限性,还无法做到通用。有什么问题可留言。




- End -




看雪ID:一只流浪狗 

https://bbs.pediy.com/user-787403.htm  



本文由看雪论坛 一只流浪狗 原创

转载请注明来自看雪社区




往期热门回顾

1、APICloud解密本地资源到逆向APP算法到通用资源解密

2、D-Link DIR 645 远程命令执行漏洞

3、如何实现 Https拦截进行 非常规“抓包”

4、峰会预告 | RDP: 从补丁到远程代码执行







京华结交尽奇士,意气相期矜豪纵。今夏与君相约看雪,把酒言欢,共话安全技术江湖梦。


10大议题正式公布!第三届看雪安全开发者峰会重磅来袭!




↙点击下方“阅读原文”,查看更多干货

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存