首页 > 其他分享 >利用unicorn模拟执行去除ollvm混淆

利用unicorn模拟执行去除ollvm混淆

时间:2023-02-19 17:22:44浏览次数:56  
标签:混淆 unicorn patch file offset ollvm ins data find

去混淆思路

  • 先找到函数中所有的基本块
  • 确定状态变量是保存在宿主寄存器中还是栈中(局部变量)
  • 观察判断控制块的特点,将所有控制块剔除。剔除之后基本块中还包含真实块和虚假块
  • 确定各个真实块的执行路径,即一个真实块能跳转到哪几个块(1/2个块),通常通过模拟执行unicorn或者angr寻找路径
  • 对于只包含一个路径的真实块直接在其代码块末尾patch,跳转到对应的真实块
  • 对于包含两个路径的真实块,平坦化是通过在此真实块的末尾根据跳转条件更改状态变量的值来实现跳转到不同的真实块中,我们需要将其通过更改状态变量的跳转到不同真实块的方式修改为通过条件跳转指令的方式实现(bge,ble...)。我们需要根据实际的修改状态变量的指令来使用对应的跳转指令例如:其通过movne修改状态变量的值,那么我们就可以修改为bne指令.
  • 将控制流代码块中不是真实块的块直接nop(虚假块)

目标函数被混淆后的程序流程图如下,可以看到其进行的平坦化处理,经过分析发现还存在一些虚假块所以存在虚假控制流。

找到所有基本块

通过反汇编引擎capstone对目标函数的指令进行解析,
控制流基本块的划分是通过跳转指令ARM_GRP_JUMP以及返回指令ARM_INS_POP分割的。并且需要过滤外部函数调用指令,如果操作数在目标函数地址范围外或者操作数是一个寄存器的则说明此跳转指令是一个外部函数调用指令。

    dict_code_block_item = {}       # key: code block's item name 
                                    # import this code block's 开始地址start_address, 结束地址end_address
                                    # 基本块对应的汇编指令字符串assembly_string 

    #寻找所有的基本块,解析并保存基本块的基本信息
    is_new_block = 1
    assembly_string = ''
    for i in md.disasm(file_data[func_start_address : func_end_address], func_start_address):
        assembly_string += "0x%x:%s%s\n" %(i.address, i.mnemonic, i.op_str) 
        if  is_new_block:
            dict_code_block_item = {}
            dict_code_block_item['start_address'] = i.address
            is_new_block = 0
        #所有的条件跳转指令作为区分基本块的标准(不包含blx指令,虽然bl类的指令都可以进行函数调用,但一般都是使用blx)
        if(i.group(ARM_GRP_JUMP) or i.id == ARM_INS_POP):
            #需要过滤外部函数调用指令,这些跳转指令并不能作为基本块划分的标准。原因是因为基本块块的划分是针对属于同一个函数体的代码而言的
            if i.operands[0].type == ARM_OP_IMM:
                if (i.operands[0].value.imm < func_start_address) or (i.operands[0].value.imm > func_end_address):
                    continue
            elif i.operands[0].type == ARM_OP_REG:
                if i.id != ARM_INS_POP:
                    continue
            #保存此代码块的信息
            dict_code_block_item['end_address'] = i.address
            dict_code_block_item['assembly_string'] = assembly_string
            dict_code_blocks[dict_code_block_item['start_address']] = dict_code_block_item
            print(dict_code_block_item['assembly_string'])
            #解析下一个代码块
            is_new_block = 1
            assembly_string = ''

确定状态变量

因为后面通过模拟执行引擎寻找路径时需要主动控制状态变量的值,所以需要先找到状态变量。通过分析主分发器的代码可以确定状态变量是保存在寄存器中还是栈中(局部变量),因为每次状态变量被更改并保存在某个宿主寄存器或者栈中之后都会跳转到主分发器中继续进行路径寻找,所以很明显此函数的状态变量保存在了宿主寄存器r1

有的则保存在栈中,例如下面这个被混淆的函数就将状态变量保存在了栈中(局部变量)[sp, 8],并且其还包含了一个副本也保存在栈中(局部变量)[sp, 4]

去除所有控制块

因为控制块的目的是为了通过状态变量索引路径,本身和原程序逻辑并没有任何关系,所以需要通过其特征对其进行去除。

  • 如果状态变量保存在寄存器中则其控制块特点是:不包含pop指令,不包含外部函数调用指令,不包含任何内存操作。

  • 如果状态变量保存在栈中则其控制块特点是:不包含pop指令,不包含外部函数调用指令,不包含任何除了访问状态变量和其副本的内存操作。

根据这些特点就可以将所有的控制块过滤,剩下的基本块包含真实块和虚假块。

    #过滤出所有的真实块和虚假块
    #每一种ollvm混淆的控制块特征可能不一样,需要看具体情况分析特征
    for i in dict_code_blocks:
        flag_filter = False
        insns = dict_code_blocks[i]['assembly_string'].split('\n')
        for ins in insns:
            #真实块和虚假块一般会包含内存操作,pop指令或者bl(x)外部函数调用指令
            if  (-1 != ins.find('[') and -1 == ins.find('[sp, #' + str(status_mem[0]) + ']') and -1 == ins.find('[sp, #' + str(status_mem[1]) + ']'))   or \
                (-1 != ins.find('pop')  or \
                ((-1 != ins.find('bl') and -1 != ins.find('#0x') and \
                int(ins[ins.find('#0x') + 3: len(ins)], 16) < func_start_address and \
                int(ins[ins.find('#0x') + 3: len(ins)], 16) > func_end_address))):
                flag_filter = True
                break
        #将过滤出的真实块和虚假块的起始地址保存
        if  flag_filter:
            list_real_blocks.append(dict_code_blocks[i]['start_address'])

利用unicorn寻找真实块的路径

判断基本块存在几条路径

目标混淆函数的状态变量宿主寄存器为r1,所以一个真实块如果存在带条件后缀的movxx r1指令,并且偏移大于mov r1指令的偏移则证明此真实块可能存在两条路径,否则只存在一条路径。

判断带条件后缀的movxx r1指令偏移大于mov r1指令的偏移,如果大于就说明此基本块可能会有两条路径。对于有两条路径的真实块就需要分两次去寻找两条路径下对应的真实块,而对于没有两条路径的真实块就直接寻找一次路径即可。

        # 通过判断(mov 状态变量宿主寄存器,状态变量)和(movxx 状态变量宿主寄存器,状态变量)
        # 这两种指令在当前代码块的出现的先后顺序判断是否有可能存在两个路径
        offset = 0
        mov_offset = 0
        movxx_offset = 0
        for i in current_assembly_code:
            offset = offset + 1
            if  (i.find('movweq' + status_regs_string) != -1)  or \
                (i.find('movteq' + status_regs_string) != -1)  or \
                (i.find('moveq' + status_regs_string) != -1)   or \
                .........
                (i.find('movwne' + status_regs_string) != -1)  or \
                (i.find('movtne' + status_regs_string) != -1)  or \
                (i.find('movne' + status_regs_string) != -1) :
                movxx_offset = offset
            elif(i.find('movw' + status_regs_string) != -1)    or \
                (i.find('movt' + status_regs_string) != -1)    or \
                (i.find('mov' + status_regs_string) != -1):
                mov_offset = offset
        #如果存在movxx指令,并且movxx指令的位置大于mov指令的位置则证明可能存在两种路径
        if(movxx_offset != 0) and (movxx_offset > mov_offset):
            #寻找路径1
            is_find_two_path = True
            set_context(reg_context)
            path1_pc = find_path(pc, 1)        
            if path1_pc != None:
                exec_queue.append((path1_pc, get_context()))
                dict_all_path[pc].append(path1_pc)
            #寻找路径2
            is_find_two_path = True
            set_context(reg_context)
            path2_pc = find_path(pc, 2)
            if (path2_pc != None) and (path2_pc != path1_pc):
                exec_queue.append((path2_pc, get_context()))
                dict_all_path[pc].append(path2_pc)
        else:
            #只需要寻找一次路径
            is_find_two_path = False
            path_pc = find_path(pc)
            dict_all_path[pc].append(path_pc)
            if path_pc != None:
                exec_queue.append((path_pc, get_context()))

模拟执行hook

在利用unicorn进行模拟执行的时候设置hook回调函数,每次指令待执行时都会先调用此回调函数

pass无关代码

在利用unicorn模拟执行代码寻找路径的时候只关心真实块能够寻找到的路径,如果当前待执行指令是无关的指令可以直接pass跳过避免产生错误。例如外部函数跳转指令,不在自己映射范围内的内存访问指令。

        #对bl带链接跳转指令进行判断,如果跳转超过范围或者通过寄存器间接跳转则pass不执行
        if -1 != ins.mnemonic.find('bl'):
            #如果操作数是立即数则计算器跳转范围,超过函数范围pass不执行
            if ins.operands[0].type == ARM_OP_IMM:
                if (ins.operands[0].value.imm < func_start_address) or (ins.operands[0].value.imm > func_end_address):
                    flag_pass = True
            elif ins.operands[0].type == ARM_OP_REG:
                flag_pass = True
        #对于不在自己映射范围的内存操作,直接pass不执行
        elif ins.op_str.find('[') != -1:
            if ins.op_str.find('[sp') == -1:    
                flag_pass = True             
                for op in ins.operands:
                    if op.type == ARM_OP_MEM:
                        addr = 0
                        if op.value.mem.base != 0:
                            addr += mu.reg_read(reg_ctou(ins.reg_name(op.value.mem.base)))
                        elif op.value.index != 0:
                            addr += mu.reg_read(reg_ctou(ins.reg_name(op.value.mem.index)))
                        elif op.value.disp != 0:
                            addr += op.value.disp
                        if (addr >=0 and addr <= 4 * 1024 * 1024) or (addr >= 0x80000000 and addr <= 0x80000000 + (4 * 1024 * 128)):
                            flag_pass = False       #如果是我们自己的映射范围内就不pass

获取待变更状态变量的值

如果当前待执行指令为movxx带条件后缀的指令并且第一个操作数为状态变量宿主寄存器r1,那么就获取第二个操作数的值,此值就是状态变量待变更的值。并且需要pass这条修改状态变量的指令,后面我们自己主动控制状态变量的值

        if (ins.mnemonic == 'movweq'    or \
            ins.mnemonic == 'movteq'    or \
            ins.mnemonic == 'moveq'     or \
            ........
            ins.mnemonic == 'movwne'    or \
            ins.mnemonic == 'movtne'    or \
            ins.mnemonic == 'movne'):
            if is_find_two_path == True:
                #如果第一个操作数是状态变量宿主寄存器
                if ins.operands[0].type == ARM_OP_REG and ins.operands[0].value.reg == status_regs:
                    #对于控制状态变量的mov指令,直接pass,我们主动控制状态变量的值
                    #先获取控制变量的值
                    flag_pass = True            
                    #对于包含w,t的mov指令而言,其对应的第二个操作数就是立即数
                    if ins.mnemonic.find('w') != -1:
                        if ins.operands[1].type == ARM_OP_IMM:
                            status_num = ins.operands[1].value.imm
                    elif ins.mnemonic.find('t') != -1:
                        if ins.operands[1].type == ARM_OP_IMM:
                            status_num = status_num + (ins.operands[1].value.imm << 16)
                    #对于不包含w, t的mov指令而言,其对应的第二个操作数可能是立即数,或者是寄存器
                    else:
                        if ins.operands[1].type == ARM_OP_IMM:
                            status_num = ins.operands[1].value.imm
                        elif ins.operands[1].type == ARM_OP_REG:
                            status_num = uc.reg_read(ins.operands[1].value.reg)

主动控制状态变量的值

因为所有的真实块最后都会执行b指令跳转到预处理器,或者主分发器中。在这之前状态变量的值可能会根据程序原有的条件判断进行修改,上面已经将修改状态变量的指令pass不执行了,所以我们需要自己主动控制状态变量的值进而去寻找两条目标路径。当寻找第一条路径时不对状态变量的值进行修改,当寻找第二天指令的时候强制对状态变量的值进行修改。

        # 所有的真实块最后都会执行b指令跳转到预处理器,获取主分发器中
        if ins.mnemonic == 'b':
            if is_find_two_path == True:
                #如果是路径1
                if branch_control == 1:
                    print('first')
                #如果是路径2
                elif branch_control == 2:
                    #如果状态变量保存在栈中
                    if status_mem[0] != 0:
                        print(status_num.to_bytes(4, 'little'))
                        print(uc.reg_read(UC_ARM_REG_SP))
                        print(uc.reg_read(UC_ARM_REG_SP) + status_mem[0])
                        uc.mem_write(uc.reg_read(UC_ARM_REG_SP) + status_mem[0], status_num.to_bytes(4, 'little'))
                    #如果状态变量保存在寄存器中
                    else:    
                        uc.reg_write(status_regs, status_num)
                is_find_two_path = False

判断是否寻找到新路径

通过判断当前待指令的地址是否等于之前过滤出来的所有真实块的起始地址中的任意一个,如果是就说明找到了新路径,暂停模拟执行并返回。

        #如果待执行的指令是一个真实块的首地址则说明找到了新路径 且不是一次新的模拟执行过程
        if address in list_real_blocks and flag_new_exec != True:
            is_find_path_success = True
            path_address = address
            uc.emu_stop()
            return

patch程序重建控制流

  • 对于不包含条件判断的真实块(只有一条路径),直接在真实块尾部构建b指令跳转到对应的唯一路径的真实块
  • 对于包含了条件判断的真实块(包含两条路径),利用跳转指令跳转到第一条路径,然后利用b指令跳转到第二条路径。需要注意条件跳转指令的选择,要看其在修改状态变量对应的条件判断指令来选择对应的跳转指令,例如在修改状态变量时使用moveq那么就需要patchbeq指令。
  • 其他虚拟块也可以都nop掉
def patch():
    global dict_all_path
    global dict_code_blocks
    global file_data
    for k in dict_all_path:
        if dict_all_path[k] == [None]:  continue
        patch_offset = 0
        #被patch的这些块都属于控制块
        for i in dict_code_blocks:
            if dict_code_blocks[i]['start_address'] == k: 
                dict_code_block_item = dict_code_blocks[i]
                insns = dict_code_block_item['assembly_string'].split('\n')

                #如果此代码块有分支
                if len(dict_all_path[k]) == 2:
                    branch1 = dict_all_path[k][0]
                    branch2 = dict_all_path[k][1]

                    patch_offset = dict_code_blocks[i]['end_address'] - 4
                    print('patch_offset: %x' % patch_offset)
                    for ins in insns:
                        if ins.find('eq' + status_regs_string) != -1:
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("beq #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:]
                        elif ins.find('ne' + status_regs_string) != -1:    
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("bne #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:]                         
                        elif ins.find('cs' + status_regs_string) != -1: 
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("bcs #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:] 
                        elif ins.find('hs' + status_regs_string) != -1: 
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("bhs #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:] 
                        elif ins.find('ls' + status_regs_string) != -1: 
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("bls #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:] 
                        elif ins.find('ge' + status_regs_string) != -1: 
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("bge #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:] 
                        elif ins.find('hi' + status_regs_string) != -1: 
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("bhi #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:] 
                        elif ins.find('lt' + status_regs_string) != -1: 
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("blt #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:] 
                        elif ins.find('gt' + status_regs_string) != -1: 
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("bgt #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:] 
                        elif ins.find('cc' + status_regs_string) != -1: 
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("bcc #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:] 
                        elif ins.find('lo' + status_regs_string) != -1: 
                            file_data = file_data[:patch_offset] + bytes(_ks_assemble(("blo #0x%x" % branch2),patch_offset)) + bytes(_ks_assemble(("b #0x%x" % branch1),patch_offset+ 4)) + file_data[patch_offset+8:] 
                #如果此代码块无分支
                elif len(dict_all_path[k]) == 1:
                    branch = dict_all_path[k][0]
                    patch_offset = dict_code_blocks[i]['end_address']
                    print('patch_offset: %x' % patch_offset)
                    file_data = file_data[:patch_offset] + bytes(_ks_assemble(("b #0x%x" % branch),patch_offset)) + file_data[patch_offset+4:]

去混淆的效果

去混淆后的控制流程图

与ollvm反混淆插件obpo的去混淆结果对比基本一致。另外发现好像obpo是不支持thumb指令模式的。

以上均为个人研究观点,仅供参考。
参考链接:
https://bbs.pediy.com/thread-252321.htm
https://github.com/obpo-project/obpo-plugin

标签:混淆,unicorn,patch,file,offset,ollvm,ins,data,find
From: https://www.cnblogs.com/revercc/p/17135120.html

相关文章

  • 探索JS混淆加解密技术
    JavaScript混淆是保护您的代码免受未经授权的访问的有效方法。然而,当您需要修改或扩展混淆代码时,混淆可以使代码难以理解和修改。解决这个问题的方法是使用JS混淆解密技术。......
  • Unicorn 初探
    前言笔者由于对IOT比较感兴趣,故经常需要模拟一些IOT的固件,之前我习惯直接直接使用qemu对固件进行模拟。这几天由于任务需要接触了一下unicorn,一个轻量级,多平台,多架构......
  • 安卓混淆加固重签名工具1.7.7更新(附下载地址)
    安卓APK混淆加密重签名软件,可以对安卓APP进行混淆加固加密处理,对代码和资源文件进行混淆,进行重新签名等.  可以保护APK,增加逆向分析难度,防止误报毒等.   安卓......
  • OLLVM代码混淆
    OLLVM代码混淆理论上这个时候看这个有点早,但是它的功能好nm强大啊!!!原理嘛......理论部分看懂了,代码实现部分反正是没怎么看懂,但我只想玩它的功能~诶嘿(≧∇≦)/Li......
  • 基于AST的babel库实现js反混淆还原基础案例荟萃
    基本概念AST简介AST全称AbstractSyntaxTree,即抽象语法树,简称语法树(Syntaxtree),树上的每个节点都表示源代码中的一种结构。JavaScript领域常用的AST解析库有babel、esp......
  • JS混淆解密案例③
    今天收到一个特别大的js解密...部分代码如下functionQ0ooO(Q0oqq,Q0oqO){varoooQ0o=O0Q000o;try{if(Q0oqO&&(oooQ0o(0x293,'Qoqq')==typeofQ......
  • 在VSCode中的markdown里插入混淆矩阵HTML源码
    最近在看论文的时候习惯用markdown记录笔记,就有了如题的需求。由于原生的markdown不能合并表格的单元格(或者我不知道,OS:真菜),但是markdown支持HTML,直接写一段代码扔进去就......
  • 记录一次容易混淆的指针正确打开方式
    //c#中对应c/c++的定长数组定义publicfixedfloatmp_osi[4];//表示float数组,大小4个限制:只能在结构体中进行定义,作为结构体中的字段使用//c#中使用指针fixed(float*......
  • Unicorn快速入门
    看James给的SPIR-V看麻了,转过来快速入门一下unicorn。早该学学了。官网:https://www.unicorn-engine.org/(搞完这个继续看SPIR-V(确信))简介与安装Unicorn是一个基于QEMU的......
  • Unicorn备忘录
    摘自https://bbs.kanxue.com/thread-224330.htm#msg_header_h3_7_,个人存档用fromunicornimport*-加载Unicorn库。包含一些函数和基本的常量。fromunicorn.x86_cons......