首页
社区
课程
招聘
[原创]符号执行去除BR指令混淆
2024-3-1 16:44 10699

[原创]符号执行去除BR指令混淆

2024-3-1 16:44
10699

一.开篇

    直接进入正题,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




[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-3-1 18:27 被大帅锅编辑 ,原因:
上传的附件:
收藏
点赞13
打赏
分享
最新回复 (11)
雪    币: 964
活跃值: (1250)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_fssslkzs 2024-3-1 17:34
2
0
感谢分享tql
雪    币: 19299
活跃值: (28938)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-3-4 10:52
3
1
感谢分享
雪    币: 1726
活跃值: (8681)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
你瞒我瞒 2024-3-4 15:03
4
0
66666
雪    币: 887
活跃值: (2147)
能力值: ( LV4,RANK:52 )
在线值:
发帖
回帖
粉丝
夏男人 2024-3-6 10:51
5
0
tql
雪    币: 7754
活跃值: (5772)
能力值: ( LV12,RANK:418 )
在线值:
发帖
回帖
粉丝
Tokameine 3 2024-3-6 12:39
6
0

太强了

最后于 2024-3-6 12:39 被Tokameine编辑 ,原因:
雪    币: 43
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_mzboxhri 2024-3-21 18:22
7
0
还是帅锅
雪    币: 14
活跃值: (155)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
飆汗吉米 2024-4-7 20:07
8
0

我透過IDA pro打開 lib52pojie.so 使用jump to Address可以得知 start_addr = 0x1A864 就是 JNI_OnLoad

版主我想請問  end_addr = 0x1AE20 是怎麼計算得來的???


感謝版主分享優質文章


最后于 2024-4-7 20:13 被飆汗吉米编辑 ,原因: 問題沒有敘述清楚
雪    币: 16152
活跃值: (5936)
能力值: ( LV13,RANK:861 )
在线值:
发帖
回帖
粉丝
大帅锅 4 2024-4-8 10:21
9
1
飆汗吉米 我透過IDA pro打開&nbsp;lib52pojie.so&nbsp;使用jump to Address可以得知&nbsp;start_addr&nbsp;=&am ...

这种很明显就是函数的结尾

雪    币: 14
活跃值: (155)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
飆汗吉米 2024-4-8 11:01
10
0

謝謝版主
我以為0x1AE20 是另一個函數的開始,0x1AE1C才是JNI_OnLoad的結束。
我對IDApro不夠熟悉再回去多讀點書

最后于 2024-4-8 11:05 被飆汗吉米编辑 ,原因:
雪    币: 146
活跃值: (1195)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
曹无咎 1 3天前
11
0
tql
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 7小时前
12
0
符号执行yyds
游客
登录 | 注册 方可回帖
返回