首页
论坛
专栏
课程

[原创]c/c++反混淆方法

2019-1-29 22:56 2741

[原创]c/c++反混淆方法

2019-1-29 22:56
2741

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段,将其加载到模拟器内存中。

    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)
    

    其中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跟踪与输入变量相关的所有表达式,并将这些表达式提取出来。
  • 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 编译生成反混淆程序

###a 这里假设程序只有一条执行路径
即程序不会根据输入值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打开看到的控制流程图。虽然反混淆后程序的控制流图跟原程序有所不同,但执行结果是一致的。
原程序CFG
[图1] 原程序控制流程图

 

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

 

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

 

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



[推荐]看雪企服平台,提供安全分析、定制项目开发、APP等级保护、渗透测试等安全服务!

上一主题 下一主题
打赏 + 2.00
打赏次数 2 金额 + 2.00
收起 
赞赏  kanxue   +1.00 2019/02/01 春节快乐
赞赏  kanxue   +1.00 2019/02/01 感谢分享~
最新回复 (15)
endlif 1 2019-1-30 00:05
2
0
感谢分享
vasthao 5 2019-1-30 00:49
3
1
Taint Analysis(污点分析)、Symbolic Execution(符号执行),学术界研究较多,比较注重理论,在实践上有很多限制,类似代码混淆反混淆、虚拟机保护还原等还是要基于编译器或反编译器的中端IR进行控制流/数据流分析
邓dg 2019-1-30 07:52
4
0
不懂
Editor 2019-1-30 09:39
5
0
感谢分享!
MsScotch 2019-1-30 10:00
6
0
不谈应用场景的还原都是耍(刷)流氓(量)。
Zkeleven 2019-1-30 10:35
7
0
用来搞搞简单的crackeme还行
LowRebSwrd 1 2019-1-30 10:39
8
0
感谢分享!
一只流浪狗 2019-1-30 11:51
9
0
vasthao Taint Analysis(污点分析)、Symbolic Execution(符号执行),学术界研究较多,比较注重理论,在实践上有很多限制,类似代码混淆反混淆、虚拟机保护还原等还是要基于编译器或反编 ...
这里只是抛砖引玉,希望业界能有更完善的反混淆方案被提出来。
frozenrain 2019-1-30 12:18
10
0
Triton不支持arm。
Vn小帆 2019-1-30 14:57
11
0
感谢分享啊
不知世事 1 2019-1-30 15:32
12
0
Tigress这种源码保护做的很强
roysue 3 2019-1-30 15:36
13
0
不谈应用场景的还原都是耍(刷)流氓(量)。
一只流浪狗 2019-1-30 22:53
14
0
Zkeleven 用来搞搞简单的crackeme还行
其实不只能用来搞简单的crackme,其他相对复杂点的程序也是可以的,只是需要人工参与并且需要一些工作量。它这个思路对于反控制流混淆确实说得通。
葫芦娃 1 2019-1-30 23:52
15
0
Triton不支持arm+1
许文伟爱工作 2019-1-31 10:34
16
0
看不懂,但依然感谢分享。
游客
登录 | 注册 方可回帖
返回