符号执行去除BR指令混淆
2024-3-26 17:50:7 Author: mp.weixin.qq.com(查看原文) 阅读量:2 收藏


开篇

    
直接进入正题,BR指令的混淆,该指令完全打断了IDA的反编译流程,导致我们无法看见看清反编译流程,一般的BR指令后续都紧跟两个跳转地址具体原理如下图所示:


正文

   

2.1 获取符号执行的后的地址

       
理清出了原理,那我们怎么去除了,这里我们选择的用miasm(东西是好东西,但是资料基本没有)符号执行去除,符号执行的优点在于它到未生成ir块和不确定的分支的时候会自己停止如下图所示,利用该特点我们只需要进行树的遍历即可探索出"基本所有分支"。
生成的下次跳转地址,就是下面这个表达式:
call_func_ret(0x1AE20, SP + 0xFFFFFFFFFFFFFF30)[0:32]?(@64[0x545B0] + 0xFFFFFFFFF775B348,@64[0x54558] + 0xFFFFFFFFF775B348)
该表达式其实是Miasm中的ExprCond表达式,因此我们只需要解析出ExprCond表达式即可,通过阅读miasm源码我们可知:
src1和src2就是我们想要的 @64[0x545B0] + 0xFFFFFFFFF775B348和@64[0x54558] + 0xFFFFFFFFF775B348,因此通过如下方式:
src1 = sb.eval_expr(symbolic_pc.src1).arg
src2 = sb.eval_expr(symbolic_pc.src2).arg
可获取到下次跳转到地址。

   

2.2 符号执行hook

        
得到地址后,我们就需要对pc值和irdst值进行hook修改,如修改成@64[0x545B0] + 0xFFFFFFFFF775B348,这样树就会向左下方进行遍历,但是需要注意的是miasm的SymbolicExecutionEngine并不具备指令hook的功能,因此我们需要阅读源码进行特定式修改,通过阅读miasm源码我们发现在SymbolicExecutionEngine中的eval_updt_irblock。
        
进行了指令的解析和执行,因此我们只需重写该方法即可。
def eval_updt_irblock(self, irb, step=False):
    """
    Symbolic execution of the @irb on the current state
    @irb: irbloc instance
    @step: display intermediate steps
    """
    for assignblk in irb:
        self.pc = assignblk.instr.offset
        self.instr = assignblk.instr
        if step:
            print(hex(assignblk.instr.offset) + ":", assignblk.instr)
            print('Assignblk:')
            print(assignblk)
            print('_' * 80)

        self.eval_updt_assignblk(assignblk)

        if assignblk.instr.offset in self.jmp_dict.keys():
            self.symbols.symbols_id[ExprId("PC", 64)] = self.eval_expr(self.jmp_dict[assignblk.instr.offset])
            self.symbols.symbols_id[ExprId("IRDst", 64)] = self.eval_expr(self.jmp_dict[assignblk.instr.offset])

    

2.3 符号执行中断问题

        
但是事情没有我们想象的那么简单,当我们把pc hook修改为特定的值的时候,符号引擎并没有像我们想象中的继续向下执行如下图所示:
        
通过阅读miasm源码发现地址不在ircfg的block中导致的(如下图所示):
解决方式也很简单,直接向asmcfg中添加即可。
def add_block(asmcfg, offset):
    block, next1 = mdis._dis_block(offset)
    asmcfg.add_block(block)

   

2.4 分支遍历不完整问题

        
在实际的分支遍历中我们遇到有些分支产生的下一个地址始终是常量(ExprInt),正常的情况应该产生ExprCond表达式才对,产生这个原因可能是需要满足某种循环后才会产生ExprCond表达式。解决方式也很简单,先通过br指令我们先遍历出所有条件设置指令如csel指令如下图所示。
def get_all_condition_addr(asm, magic="csel"):
    condition_pc = []
    for i in range(len(asm)):
        ins = asm[i]
        if ins.mnemonic == "br":
            # 开始向上寻找
            for j in range(i, 0, -1):
                magic_ins = asm[j]
                if magic_ins.mnemonic == magic.lower():
                    condition_pc.append(magic_ins.address)
                    break
                if i - j > 10:
                    raise Exception("check this br" + str(ins))
    return condition_pc
        
然后我们hook该条件位置,强行进行符号设置即可。
def eval_updt_irblock(self, irb, step=False):
    """
    Symbolic execution of the @irb on the current state
    @irb: irbloc instance
    @step: display intermediate steps
    """
    for assignblk in irb:
        self.pc = assignblk.instr.offset
        self.instr = assignblk.instr
        if step:
            print(hex(assignblk.instr.offset) + ":", assignblk.instr)
            print('Assignblk:')
            print(assignblk)
            print('_' * 80)

        if assignblk.instr.offset in self.condition_pc:
            # handle cesl
            assigns_key = next(iter(assignblk._assigns.keys()))
            assigns_value = assignblk._assigns[assigns_key]
            assignblk._assigns[assigns_key] = replace_exprcond2(assigns_value)

        self.eval_updt_assignblk(assignblk)

        if assignblk.instr.offset in self.jmp_dict.keys():
            self.symbols.symbols_id[ExprId("PC", 64)] = self.eval_expr(self.jmp_dict[assignblk.instr.offset])
            self.symbols.symbols_id[ExprId("IRDst", 64)] = self.eval_expr(self.jmp_dict[assignblk.instr.offset])

def replace_exprcond2(expr):
    if isinstance(expr, ExprCond):
        # 对条件表达式的每个分支进行递归替换
        src1 = replace_exprcond2(expr.src1)
        src2 = replace_exprcond2(expr.src2)
        # 生成两个新的表达式,分别代表条件成立或不成立的情况
        return ExprCond(ExprId("SuperMan", 64), src1, src2)

    elif isinstance(expr, ExprOp):
        # 对ExprOp的每个参数进行递归替换
        parts = [replace_exprcond2(arg) for arg in expr.args]
        ret = ExprOp(expr.op, *parts)
        return ret

    else:
        # 对于其他类型的表达式(如ExprId, ExprInt等),直接返回
        return expr

2.5 递归进行分支树的遍历

        
这个应该是最简单的一块了直接展示代码。
def handle(blocks, condition, jmp_dict):
    print("[*]*****************************************************************")
    sb, ircfg = init_machine(blocks, condition, jmp_dict)
    try:
        symbolic_pc = sb.run_at(ircfg, start_addr, step=True)
    except Exception as e:
        print("--------Exception---------")
        print(e)
        return
    if symbolic_pc is not None and str(symbolic_pc) != "LR":
        if not symbolic_pc.is_cond():
            pc = sb.pc
            if type(symbolic_pc.arg) == int:
                next_pc = symbolic_pc.arg
                # fp.write(f"{hex(pc)},{hex(next_pc)}\n")
                if next_pc not in blocks and next_pc < end_addr:
                    blocks.append(next_pc)
                    handle(blocks, condition, jmp_dict)
            else:
                expr_list = replace_exprcond(symbolic_pc)
                next_pc_list = []
                for expr in expr_list:
                    simp_expr = sb.eval_expr(sb.expr_simp(expr))
                    if simp_expr not in next_pc_list:
                        next_pc_list.append(simp_expr)

                if len(next_pc_list) == 2:
                    src1 = next_pc_list[0].arg
                    src2 = next_pc_list[1].arg
                    if src1 not in blocks and src1 < end_addr:
                        blocks.append(src1)
                        jmp_dict[pc] = next_pc_list[0]
                        handle(blocks, condition, jmp_dict)
                    if src2 not in blocks and src2 < end_addr:
                        blocks.append(src2)
                        jmp_dict[pc] = next_pc_list[1]
                        handle(blocks, condition, jmp_dict)
                elif len(next_pc_list) == 1:
                    next_pc = next_pc_list[0].arg
                    if next_pc not in blocks and next_pc < end_addr:
                        blocks.append(next_pc)
                        handle(blocks, condition, jmp_dict)
                else:
                    raise Exception("you need check your code")
        else:
            pc = sb.pc
            src1 = sb.eval_expr(symbolic_pc.src1).arg
            src2 = sb.eval_expr(symbolic_pc.src2).arg
            # fp.write(f"{hex(pc)},{hex(src1)},{hex(src2)}\n")
            if src1 not in blocks and src1 < end_addr:
                blocks.append(src1)
                jmp_dict[pc] = symbolic_pc.src1
                handle(blocks, condition, jmp_dict)
            if src2 not in blocks and src2 < end_addr:
                blocks.append(src2)
                jmp_dict[pc] = symbolic_pc.src2
                handle(blocks, condition, jmp_dict)

遍历途中我们需要记录br指令跳转的符号,这个简单,我们只需要eval_updt_irblock中增加一个判断处理逻辑即可。
def eval_updt_irblock(self, irb, step=False):
    """
    Symbolic execution of the @irb on the current state
    @irb: irbloc instance
    @step: display intermediate steps
    """
    for assignblk in irb:
        self.pc = assignblk.instr.offset
        self.instr = assignblk.instr
        if step:
            print(hex(assignblk.instr.offset) + ":", assignblk.instr)
            print('Assignblk:')
            print(assignblk)
            print('_' * 80)

        if assignblk.instr.offset in self.condition_pc:
            # handle cesl
            assigns_key = next(iter(assignblk._assigns.keys()))
            assigns_value = assignblk._assigns[assigns_key]
            assignblk._assigns[assigns_key] = replace_exprcond2(assigns_value)

        self.eval_updt_assignblk(assignblk)

        if assignblk.instr.offset in self.jmp_dict.keys():
            self.symbols.symbols_id[ExprId("PC", 64)] = self.eval_expr(self.jmp_dict[assignblk.instr.offset])
            self.symbols.symbols_id[ExprId("IRDst", 64)] = self.eval_expr(self.jmp_dict[assignblk.instr.offset])

        if step:
            self.dump(mems=False)
            '''
            内存打印太多了
            '''
            # if assignblk.instr.offset == 0xca768:
            #     self.dump(ids=False)
            print('_' * 80)

        if assignblk.instr.name == "BR":
            symbolic_pc = self.symbols.symbols_id[assignblk.instr.args[0]]
            if not symbolic_pc.is_cond():
                pc = self.pc
                if type(symbolic_pc.arg) == int:
                    next_pc = symbolic_pc.arg
                    self.write(f"{hex(pc)},{hex(next_pc)}\n")
                else:
                    expr_list = replace_exprcond(symbolic_pc)
                    next_pc_list = []
                    for expr in expr_list:
                        simp_expr = self.eval_expr(self.expr_simp(expr))
                        if simp_expr not in next_pc_list:
                            next_pc_list.append(simp_expr)

                    if len(next_pc_list) == 2:
                        src1 = next_pc_list[0].arg
                        src2 = next_pc_list[1].arg
                        self.write(f"{hex(pc)},{hex(src1)},{hex(src2)}\n")
                    elif len(next_pc_list) == 1:
                        next_pc = next_pc_list[0].arg
                        self.write(f"{hex(pc)},{hex(next_pc)}\n")
                    else:
                        raise Exception("you need check your code")
            else:
                pc = self.pc
                src1 = int(str(self.eval_expr(symbolic_pc.src1)), 16)
                src2 = int(str(self.eval_expr(symbolic_pc.src2)), 16)
                self.write(f"{hex(pc)},{hex(src1)},{hex(src2)}\n")

    dst = self.eval_expr(self.lifter.IRDst)

    return dst

    

2.6 patch文件

        
后续我们根据生成指令的映射关系文件进行处理去重patch即可。
import keystone
import capstone
import ida_bytes
import idc
from module.utils import MAGIC

KS = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
CS = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)

NOP_BYTES = b'\x1f\x20\x03\xd5'
def handle_txt(path):
    addr_dict = {}
    f = open(path)
    line = f.readline().replace("\n", "")
    while line:
        info = line.split(",")
        if len(info) == 2:
            patch_addr = info[0]
            b_addr = info[1]
            if patch_addr not in addr_dict.keys():
                addr_dict[patch_addr] = [b_addr]
            else:
                addr_list = addr_dict[patch_addr]
                if b_addr not in addr_list:
                    addr_dict[patch_addr].append(b_addr)
        elif len(info) == 3:
            patch_addr = info[0]
            b_addr1 = info[1]
            b_addr2 = info[2]
            if patch_addr not in addr_dict.keys():
                addr_dict[patch_addr] = [b_addr1, b_addr2]
            else:
                addr_list = addr_dict[patch_addr]
                if b_addr1 not in addr_list:
                    addr_dict[patch_addr].append(b_addr1)
                if b_addr2 not in addr_list:
                    addr_dict[patch_addr].append(b_addr1)
        else:
            raise Exception("check your code")
        line = f.readline().replace("\n", "")
    f.close()
    return addr_dict

def get_b_const_bytes(ea, const):
    '''
    返回例如 ea: b 0x12334的指令
    :param ea:
    :param const:
    :return:
    '''
    ea = int(ea, 16)
    CODE = f"b {const[0]}"
    encoding, count = KS.asm(CODE, ea)
    return ea, bytes(encoding)

def find_magic(ea):
    for i in range(10):
        asm = idc.GetDisasm(ea)
        if asm.startswith(MAGIC):
            info = asm.split(",")
            cond = info[-1].replace(" ","").lower()
            return ea, cond
        ea = idc.prev_head(ea)
    raise Exception("check your code:" + hex(ea))

def get_bxx_const_bytes(ea, const_list):
    ea = int(ea, 16)
    patch_ea, cond = find_magic(ea)
    code = f"b{cond} {const_list[0]}"
    encoding, count = KS.asm(code, patch_ea)
    ret = bytes(encoding)
    code = f"b {const_list[1]}"
    encoding, count = KS.asm(code, patch_ea + 4)
    ret += bytes(encoding)
    return patch_ea, ret

addr_dict = handle_txt("./addr.txt")

for key in addr_dict.keys():
    value = addr_dict[key]
    if len(value) == 1:
        ea, patch_bytes = get_b_const_bytes(key, value)
        ida_bytes.patch_bytes(ea, patch_bytes)
    else:
        ea, patch_bytes = get_bxx_const_bytes(key, value)
        ida_bytes.patch_bytes(ea, patch_bytes)

效果对比,恢复流程前:
恢复流程后:


总结

    
优点:相较于纯模拟执行如unicorn等,开发相较于简便一些,如:在CSEL处设置一个符号即可得到两个地址。
    
缺点:有些指令不支持需要patch掉。
    
需要注意的事,有些可能存在死循环,执行时间很长,请耐心等待!
    
工程文件均在https://github.com/0xjacklove/xflower

看雪ID:大帅锅

https://bbs.kanxue.com/user-home-759174.htm

*本文为看雪论坛优秀文章,由 大帅锅 原创,转载请注明来自看雪社区

# 往期推荐

1、Frida 实战 KGB Messenger

2、打造柚子(yuzu)模拟器的金手指工具

3、APP sign签名参数分析

4、Chrome v8漏洞 CVE-2021-30632浅析

5、CVE-2022-2588 Dirty Cred漏洞分析与复现

6、为无源码的数据批量处理软件添加功能

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458548918&idx=1&sn=55ee111155a67c220eb89e6368db3e0b&chksm=b18d4a3c86fac32a882b1fe8d24b8ab35bb6546a1d78bf7ad24407913c030660db7ad46c52c3&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh