c/c++反混淆方法

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

    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的函数。

    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

    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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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指令重新编译生成可执行文件。

1

2

3

4

5

6

7

8

9

10

11

12

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

流程如下:

1

2

3

4

5

generateSymbolicExpressions(0)

# Generate llvm of the first path

M = generateLLVMExpressions(ctx, 0)

# Recompile the LLVM-IL

recompile(M)

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

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

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

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] 反混淆后程序控制流程图

 

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

https://bbs.pediy.com/thread-249210.htm

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值