吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1172|回复: 14
上一主题 下一主题
收起左侧

[原创] 模拟执行去除花指令

[复制链接]
跳转到指定楼层
楼主
REG737 发表于 2024-12-26 13:16 回帖奖励
本帖最后由 REG737 于 2024-12-26 13:19 编辑

idaemu插件

https://github.com/zengfr/idaemu
插件提供了一些易于在ida使用的unicorn engine模拟执行函数的封装,可以指定模拟执行开始和结束的地址、设置跳过指令、hook函数、hook指令等,便于在ida中调用。

模拟执行去花指令

显示执行路径和有效指令

模拟执行可以轻量化的执行可执行文件中的指令,而花指令会混淆IDA对于指令的识别,那是否可以通过模拟执行记录下来执行的每个指令的地址,然后再告诉IDA哪些指令才是真正需要识别的呢?
插件自带打印执行路径的功能:

a = Emu(UC_ARCH_X86, UC_MODE_32)
a.setTrace(TRACE_CODE|TRACE_INTR)
a.eBlock(start,end)
a.showTrace()


实现方式呢是在模拟开始之前添加了一个UC_HOOK_CODE

....
def _hook_code(self, uc, address, size, user_data):
        ....
        self._addTrace("### Trace:0x%08x, size = %u" % (address, size))
        ....
....
uc.hook_add(UC_HOOK_INTR, self.hook_interrupt)
....

新加一个list来记录每次执行的指令地址和指令的size,模拟执行结束之后调用ida的api来重新将模拟执行过的指令地址识别一遍

....
self.traceIns  = []
self.traceInsLen = []
def _hook_code(self, uc, address, size, user_data):
        ....
        self._addTrace("### Trace:0x%08x, size = %u" % (address, size))
        self.traceIns.append(address)
        self.traceInsLen.append(size)
        ....
....
uc.hook_add(UC_HOOK_INTR, self.hook_interrupt)
....
def dBlock(self, codeStart=None, codeEnd=None,timeout=None):
        ....
        self.traceIns  = []
        self.traceInsLen = []
        ....
        length = len(self.traceIns)
        for i in range(length):
            addr = self.traceIns[i]
            size = self.traceInsLen[i]
            ida_bytes.del_items(addr,nbytes=size)
            idc.create_insn(addr)
        del self.traceIns
        del self.traceInsLen

如何只执行必要的指令

模拟执行过程中可能会执行到一下导入函数里,导致模拟时间过长或者报错,如何只模拟执行必要的指令?

跳到代码段外执行的函数跳过

在elf文件中unicorn执行call func@plt的时候会跳转到plt段去执行指令,在hook_code中做个判断,判断代码执行是否还在代码段即可,如果不在就跳过当前函数直接返回,这样我们需要一个list变量来记录每次call的返回地址。

...
def dBlock(self, codeStart=None, codeEnd=None,timeout=None):
        self.rangeStart = idc.get_segm_start(codeStart)
        self.rangeEnd = idc.get_segm_end(codeStart)
        ...
...
self.cap = Cs(CS_ARCH_X86,CS_MODE_16)
...
def _hook_code(self, uc, address, size, user_data):
    if self.traceOption & TRACE_CODE:
        #lib function
        if (address > self.rangeEnd or address < self.rangeStart) and len(self.retaddrs) != 0:
            sp = uc.reg_read(self.REG_SP)
            sp += self.step
            uc.reg_write(self.REG_SP,sp)
            uc.reg_write(self.REG_PC,self.retaddrs[-1])
            self.retaddrs = self.retaddrs[:-1] 

        cs_gen = self.cap.disasm(uc.mem_read(address, size), address)
        try:
            cs_instr = cs_gen.__next__()
        except:
            cs_instr = None
        if cs_instr and cs_instr.mnemonic == "call":
                try:
                    calladdr = int(cs_instr.op_str,16)
                except:
                    calladdr = 0
                if calladdr not in self.altFunc.keys():
                    self.retaddrs.append(address + cs_instr.size)
...
库函数跳过

可以通过ida的API识别导入的库函数,直接hook成空函数

...
def dBlock(self, codeStart=None, codeEnd=None,timeout=None):
        ...
        THUNK_LIST = []
        for func in idautils.Functions():
            flags = idc.get_func_attr(func,idc.FUNCATTR_FLAGS)
            if flags & idc.FUNC_THUNK :
                THUNK_LIST.append(func)

        for func in idautils.Functions():
            flags = idc.get_func_attr(func,idc.FUNCATTR_FLAGS)
            if flags & idc.FUNC_LIB :
                self.alt(func,nullfunc,2)
                # print(hex(func))
                xrefs = list(idautils.XrefsTo(func))
                for xref in xrefs:
                    if xref.frm in THUNK_LIST:
                        self.alt(xref.frm,nullfunc,2)
        ...
无效指令跳过

有些指令识别不了,比如rdrand
在hook_code里加个识别行了

            if size == 0xf1f1f1f1:
                cs_gen = self.cap.disasm(uc.mem_read(address, 16), address)
                try:
                    cs_instr = cs_gen.__next__()
                except:
                    cs_instr = None
                if cs_instr and cs_instr.mnemonic == "rdrand":
                    uc.reg_write(self.REG_PC, address + cs_instr.size)
                    return

修改返回地址的函数


花指令的一种,既然前面我们已经记录下来了每个call的返回地址,在hook_code函数中检测到执行ret指令的时候检查一下当前的返回地址1与记录的返回地址0是否吻合即可,不吻合的话直接把从call指令到返回地址1之间的所有指令全都nop掉。

            if cs_instr and cs_instr.mnemonic == "call":
                try:
                    calladdr = int(cs_instr.op_str,16)
                except:
                    calladdr = 0
                if calladdr not in self.altFunc.keys():
                    self.retaddrs.append(address + cs_instr.size)
            elif cs_instr and cs_instr.mnemonic == "ret":
                if len(self.retaddrs) != 0:
                    sp = uc.reg_read(self.REG_SP)
                    retaddr = unpack(self.pack_fmt,uc.mem_read(sp,self.step))[0]
                    if retaddr != self.retaddrs[-1]:
                        orange = self.retaddrs[-1]
                        now = retaddr
                        if now > orange:
                            triggerLog(1,address,orange,now)
                            for i in range(orange-5,now):
                                ida_bytes.patch_byte(i,0x90)
                    self.retaddrs = self.retaddrs[:-1]

原地tp的jmp


执行到jmp的时候检查一下

            elif cs_instr and cs_instr.mnemonic == "jmp":
                try:
                    targetaddr = int(cs_instr.op_str,16)
                except:
                    targetaddr = 0
                if targetaddr - address == 5:
                    triggerLog(2,address,address,address+5)
                    for i in range(address,address+5):
                        ida_bytes.patch_byte(i,0x90)

永远为真的跳转


执行到je或者jne指令的时候检查下一句是不是对应的jne或者je,再检查一下跳转的地方是不是同一个,是的话直接把中间所有指令nop掉

            elif cs_instr and cs_instr.mnemonic == "jne":
                cs_gen_1 = self.cap.disasm(uc.mem_read(address+size, size), address+size)
                try:
                    cs_instr_1 = cs_gen_1.__next__()
                except:
                    cs_instr_1 = None
                if cs_instr_1 and cs_instr_1.mnemonic == "je":
                    try:
                        targetaddr0 = int(cs_instr.op_str,16)
                        targetaddr1 = int(cs_instr_1.op_str,16)
                    except:
                        targetaddr0 = 0
                        targetaddr1 = 0
                    if targetaddr0 == targetaddr1:
                        triggerLog(4,address,address,targetaddr0)
                        for i in range(address,targetaddr0):
                            ida_bytes.patch_byte(i,0x90)
            elif cs_instr and cs_instr.mnemonic == "je":
                cs_gen_1 = self.cap.disasm(uc.mem_read(address+size, size), address+size)
                try:
                    cs_instr_1 = cs_gen_1.__next__()
                except:
                    cs_instr_1 = None
                if cs_instr_1 and cs_instr_1.mnemonic == "jne":
                    try:
                        targetaddr0 = int(cs_instr.op_str,16)
                        targetaddr1 = int(cs_instr_1.op_str,16)
                    except:
                        targetaddr0 = 0
                        targetaddr1 = 0
                    if targetaddr0 == targetaddr1:
                        triggerLog(3,address,address,targetaddr0)
                        for i in range(address,targetaddr0):
                            ida_bytes.patch_byte(i,0x90)

永远用不着的跳转


改进一下上面的检查,跳转地址如果不一致,就检查跳转到的两个地址中有没有无效指令,如果有的话做个标记,执行完跳转指令之后将没有跳转到的那个地址与没有执行的j指令一起nop掉

            #jz jnz
            elif cs_instr and cs_instr.mnemonic == "je":
                cs_gen_1 = self.cap.disasm(uc.mem_read(address+size, size), address+size)
                try:
                    cs_instr_1 = cs_gen_1.__next__()
                except:
                    cs_instr_1 = None
                if cs_instr_1 and cs_instr_1.mnemonic == "jne":
                    try:
                        targetaddr0 = int(cs_instr.op_str,16)
                        targetaddr1 = int(cs_instr_1.op_str,16)
                    except:
                        targetaddr0 = 0
                        targetaddr1 = 0
                    if targetaddr0 == targetaddr1:
                        triggerLog(3,address,address,targetaddr0)
                        for i in range(address,targetaddr0):
                            ida_bytes.patch_byte(i,0x90)
                    else:
                        tmpmax = targetaddr0
                        if targetaddr1 > tmpmax:
                            tmpmax = targetaddr1
                            tmpmin = targetaddr0
                        else:
                            tmpmin = targetaddr1
                        tmpminbak = tmpmin
                        while tmpmin < tmpmax:
                            tmpcs_gen = self.cap.disasm(uc.mem_read(tmpmin, 16),tmpmin)
                            try:
                                size = tmpcs_gen.__next__().size
                                tmpmin += size
                            except:
                                break
                        if tmpmin != tmpmax: #overlap
                            self.overLap = [targetaddr0,targetaddr1] 
                            self.overLap.sort()

            elif cs_instr and cs_instr.mnemonic == "jne":
                cs_gen_1 = self.cap.disasm(uc.mem_read(address+size, size), address+size)
                try:
                    cs_instr_1 = cs_gen_1.__next__()
                except:
                    cs_instr_1 = None
                if cs_instr_1 and cs_instr_1.mnemonic == "je":
                    try:
                        targetaddr0 = int(cs_instr.op_str,16)
                        targetaddr1 = int(cs_instr_1.op_str,16)
                    except:
                        targetaddr0 = 0
                        targetaddr1 = 0
                    if targetaddr0 == targetaddr1:
                        triggerLog(4,address,address,targetaddr0)
                        for i in range(address,targetaddr0):
                            ida_bytes.patch_byte(i,0x90)
                    else:
                        tmpmax = targetaddr0
                        if targetaddr1 > tmpmax:
                            tmpmax = targetaddr1
                            tmpmin = targetaddr0
                        else:
                            tmpmin = targetaddr1
                        while tmpmin < tmpmax:
                            tmpcs_gen = self.cap.disasm(uc.mem_read(tmpmin, 16),tmpmin)
                            try:
                                size = tmpcs_gen.__next__().size
                                tmpmin += size
                            except:
                                break
                        if tmpmin != tmpmax: #overlap
                            self.overLap = [targetaddr0,targetaddr1] 
                            self.overLap.sort()

            # code over lap
            elif self.overLap != None:
                #上一步是jz或者jnz
                # 查看是现在是较大的地址还是较小的地址
                bins = self.traceIns[-1]
                bins_len = self.traceInsLen[-1]
                b_cs_instr = self.cap.disasm(uc.mem_read(bins, bins_len),bins).__next__()
                if b_cs_instr.mnemonic == "je" or b_cs_instr.mnemonic == "jne":
                    assert address in self.overLap
                    if self.overLap.index(address) == 1:
                        triggerLog(5,address,bins,address)
                        for i in range(bins,address):
                            ida_bytes.patch_byte(i,0x90)
                    else:
                         self._addTrace("Not handle")
                    self.overLap.clear()
                    self.overLap = None

效果评测

使用方法很简单,找到花指令函数开头的地址,然后使用dBlock模拟执行就行了

from idaemu import *
from unicorn import *
from unicorn.x86_const import *
from unicorn.arm_const import *
from unicorn.arm64_const import *
a = Emu(UC_ARCH_X86, UC_MODE_32)
a.setTrace(TRACE_CODE|TRACE_INTR)
target_addr = 0x4010E0
a.dBlock(target_addr,0,timeout=5000000)

确实能用,放几个前后对比:




总结

写的很烂,已知问题有:
1、不能自动把所有分支走完,遇到那种有多个分支的函数,还得把没执行过的分支手动执行地址执行一遍
2、导入函数不确定能不能全部排除掉
3、去除“修改返回地址的函数“相关逻辑有不少bug(比如遇到被调用者清理堆栈的情况,又正好把被调用的函数跳过了,就会nop一大片)
但是也算是整理了一下相关思路并验证了可行性
相关脚本和样本见附件


脚本与样本.zip

794.39 KB, 下载次数: 9, 下载积分: 吾爱币 -1 CB

免费评分

参与人数 6吾爱币 +12 热心值 +5 收起 理由
Hmily + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
MinuxCyber + 1 + 1 热心回复!
抱薪风雪雾 + 1 + 1 谢谢@Thanks!
Fiftyisnt100 + 1 + 1 用心讨论,共获提升!
Li1y + 1 + 1 我很赞同!
hanghaidongchen + 1 好文

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| REG737 发表于 2024-12-27 11:20 |楼主
li083m 发表于 2024-12-27 09:46
写的很好,请问对于安卓的so文件同样适用吗

现在这个是针对x86指令写的,如果搞别的架构的话得照着思路适配一下(因为跳转指令、调用约定啥的都不太一样)
沙发
xixicoco 发表于 2024-12-26 22:45
3#
WordCreater 发表于 2024-12-27 07:52
4#
li083m 发表于 2024-12-27 09:46
写的很好,请问对于安卓的so文件同样适用吗
6#
抱薪风雪雾 发表于 2024-12-27 17:15
强人啊,厉害
7#
Mysky6 发表于 2024-12-27 21:38
很厉害啊,感谢分享!
8#
pjdada 发表于 2024-12-28 15:24
很厉害啊,感谢分享!
9#
hebingcan 发表于 2024-12-29 11:05
谢谢分享
10#
laotzudao0 发表于 2024-12-29 12:08
厉害厉害学到
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-1-1 18:12

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表