Android:Reverse 实战 part 2 番外 IDA python

发布于:2025-07-28 ⋅ 阅读:(14) ⋅ 点赞:(0)

1. 环境配置

1.1 QScript

QScrpit 是一款好用的 IDA script 管理工具,which 能够重新运行 script when it 发生更改时。同时能够避免重启 IDA 库 to 重新运行脚本,是一款实用的 IDA python 插件。

强烈推荐,Strongly Recommendation!!! After installing it,将会 make you 使用和测试 IDA python 脚本的效率大为提高。

1.2 自动补全

面向对象的编程语言,Object Oriental Program,最大的好处就是存在 对象,给管理和学习提供了极大的 convenience。君は能够使用一个对象实例查看到它所有可调用的 方法数据,这本身就对初学者较为友好了。

pic1 pic1
  • python 自动补全 in VSCode。
    在 VSCode 搜索栏中输入 > setting 打开 Workspace Settings
    在这里插入图片描述
    接着在词条 python.analysis.extraPath 中填入 %IDAPATH%/python/3 即可
    在这里插入图片描述
  • C++ 自动补全 in VSCode
    一般用 VSCode 打开一个 C/C++ 项目之后就会自动帮你生成一个 c_cpp_properties.json 文件 in .vscode 文件夹中。如果没有可以自己创建一个。创建好后在 includePath 中填入自己的头文件目录即可。
    在这里插入图片描述

2. How to write

2.1 IDA Python Framework

我在一年前的一篇文章中写了相同的内容,只不过那篇文章是我 private,目前还没有 public,所以我直接从那里把这部分搬运过来了。

这是 plugin 的编写框架,只有当一个 eastwest 具有较好的通用性的时候才需要 write like this。如果只是针对特定问题只需要写 script 就好了不需要这么 trouble。

IDA Python 有一个入口函数,名称为 PLUGIN_ENTRY,IDA 在加载 Python 插件时会调用这个 interface,就如同 C/C++ 中的 main

PLUGIN_ENTRY 需要返回一个 Python 对象,其书写格式要按照 IDA 的要求进行编写。在构造函数中,即 Python 中的 init(self) 函数,需要完成功能注册,和插件持续时间的选择,同时也可以选择 上下文菜单toolbar 添加。

2.1.1 注册行为

Before 注册一个功能,需要首先获取功能的描述符,idaapi 中提供了 action_desc_t 对象来进行获取。他的构造函数如下所示,注释中已经对各个参数进行了详细说明。其中最重要的就是 FWHandler 它是一个类,which will 进行具体的处理过程。

action_name = "FW:extract_function"
action_desc = idaapi.action_desc_t(
            self.action_name,               # The action name. This acts like an ID and must be unique
            'Extract function',             # The action text.
            FWHandler(),                    # The action handler.
            "Ctrl+Alt+L",                   # Optional: the action shortcut
            'extract all the dependency of current function',  # Optional: the action tooltip (available in menus/toolbar)
            icon_mikasa_id)                 # Optional: the action icon (shows when in menus/toolbars)
        

如果想要自定义 actionicon 图标可以使用 idaapi 中的 load_custom_icon 函数。它有三个参数,第一个参数可以传入一个图片路径,这样后面的连个参数就会失效。也可以将图片二进制数据保存在 bytes 对象中,通过 data=format= 来实现加载,这种方式更具有通用性,because 图标的路径在不同环境中都会变化。

icon_mikasa = b"..."	# fill with your own picture data
icon_mikasa_id = idaapi.load_custom_icon(data=icon_mikasa, format="png")

获取 action_desc_t 对象对象后需要使用 idaapi.register_action 的方式来注册该插件。

idaapi.register_action(action_desc)

2.1.2 添加工具栏

注册 Toolbar 的方式十分简单,只需要设置好想要添加的位置 in 字符串中,使用 **‘/’**进行路径分隔,例如下面的 meanu_path 就在 IDA 的 Edit 中添加了一个 ExtractFunction 的功能。

action_name = "FW:extract_function"
menu_path = "Edit/ExtractFunction"
idaapi.attach_action_to_menu(
            self.menu_path,                 # The relative path of where to add the action
            self.action_name,               # The action ID (see above)
            idaapi.SETMENU_APP)             # We want to append the action after the 'Manual instruction...'

2.1.3 添加上下文菜单

Context Menu 就是你在IDA的窗口中使用鼠标右键点击后产生的菜单。这个的注册过程有些许麻烦,因为需要将其中的一个 menu 成员执行一个类。

menu = None
# Register context menu actions
self.menu = ContextMenuHooks()
self.menu.hook()

上面的 ContextMenuHooks 类需要单独进行编写,同时这也是一个模板化的类,需要符合IDA的 standard

通过idaapi.get_widget_type 函数确定了当前窗口的类型是 反编译反汇编 就会调用 idaapi.attach_action_to_popup 弹出上下文菜单。

class ContextMenuHooks(idaapi.UI_Hooks):
    def finish_populating_widget_popup(self, form, popup):
        # Add actions to the context menu of the Pseudocode view
        if idaapi.get_widget_type(form) == idaapi.BWN_PSEUDOCODE or idaapi.get_widget_type(form) == idaapi.BWN_DISASM:
            idaapi.attach_action_to_popup(form, popup, idaFWPlugin.action_name, "")

2.1.4 All in one

class idaFWPlugin(idaapi.plugin_t):
    flags = 0
    action_name = "FW:extract_function"
    menu_path = "Edit/ExtractFunction"
    wanted_name = 'Function withdraw'
    wanted_hotkey = ''
    menu = None
    icon_atm = b""
    # construct function
    def init(self):
        icon_atm_id = idaapi.load_custom_icon(data=self.icon_atm, format="png")
        action_desc = idaapi.action_desc_t(
            self.action_name,               # The action name. This acts like an ID and must be unique
            'Extract function',             # The action text.
            FWHandler(),                    # The action handler.
            "Ctrl+Alt+L",                   # Optional: the action shortcut
            'extract all the dependency of current function',  # Optional: the action tooltip (available in menus/toolbar)
            icon_atm_id)                            # Optional: the action icon (shows when in menus/toolbars)
        idaapi.register_action(action_desc)
        idaapi.attach_action_to_menu(
            self.menu_path,                 # The relative path of where to add the action
            self.action_name,               # The action ID (see above)
            idaapi.SETMENU_APP)             # We want to append the action after the 'Manual instruction...'

        # Register context menu actions
        self.menu = ContextMenuHooks()
        self.menu.hook()

        return idaapi.PLUGIN_KEEP

    def run(self, arg):
        pass

    # this is the deconstruct function
    def term(self):
        print("deconstruct")
        idaapi.detach_action_from_menu(self.menu_path, self.action_name)
        if self.menu:
            self.menu.unhook()
        return
    
class FWHandler(idaapi.action_handler_t):
    def __init__(self):
        idaapi.action_handler_t.__init__(self)

    def activate(self, ctx):
        me = idaFW()
        me.FunctionWithdraw()
        return 1

    # This action is always available.
    def update(self, ctx):
        return idaapi.AST_ENABLE_ALWAYS
    
class ContextMenuHooks(idaapi.UI_Hooks):
    def finish_populating_widget_popup(self, form, popup):
        # Add actions to the context menu of the Pseudocode view
        if idaapi.get_widget_type(form) == idaapi.BWN_PSEUDOCODE or idaapi.get_widget_type(form) == idaapi.BWN_DISASM:
            idaapi.attach_action_to_popup(form, popup, idaFWPlugin.action_name, "")


def PLUGIN_ENTRY():
    return idaFWPlugin()

if __name__=="__main__":
    PLUGIN_ENTRY()

2.2 编写技巧

python 是一种语言 which 不需要类型定义 when 声明一个 variable。However, it is not convenient for a new comer, because you can’t get all the method through . operator with autocomplete.

因此声明一个变量时让其自带类型对于我们刚开始使用 IDA python API 来说更为方便,我们可以这样做,在一个变量后面使用 : 来指定其解析的类型

下面代码就是这样操作的一个实例,将 idaapi.get_func 会返回一个 idaapi.func_t 类型的对象。如果不在变量 fnCuror 定义时指定其类型为 idaapi.func_t,那么 VSCode 就无法正常的提供 autocomplete 列表。

screen_ea = idaapi.get_screen_ea()
# explicitly assign type to a variable
fnCuror : idaapi.func_t = idaapi.get_func(screen_ea)

一开始俺達は是不知道函数的返回值类型的,IDA python API 接口没有对于参数和返回值进行任何限制,所以我们可能需要自己去确定一个变量的是什么类型的,方便使用 . 来获取自动补全列表。
在这里插入图片描述
使用 python の内置对象 type 来获取变量的类型,注意这是一个 对象,不是 函数 所以是绿色的而不是函数的黄色

screen_ea = idaapi.get_screen_ea()
fnCuror = idaapi.get_func(screen_ea)
# get the type of fnCuror
print(type(fnCuror))

在这里插入图片描述

3. 自动化去除方案

Above two Chapters 介绍了 环境配置编写技巧 下面将回到主线任务,来解决 Android:Reverse 实战 part 2 脱壳反调篇 中的具体问题——自动化去除 永假表达式

首先要有信仰:IDA GUI 中能够进行的操作一定可以通过 script 完成。换句话说お前は对 IDA 使用的熟练程度决定了君の上限 when 编写 IDA python 脚本。

这里我再插入点个人观点:目前我接触到的动态调试工具 x96dbggdb and Frida 只有 x96dbgGUI which means its 上手难度是最低的。通过对于 x96dbg 的熟练掌握我是 firmly 相信 any debugger 都会有基础的 反汇编内存读/写断点栈回溯模块列表 等基础功能。只不过在 gdb 中要通过指令,而在 frida 中则要自分自身编写脚本。所以从从手难度来说 x96dbg < gdb < frida

3.1 去除方案

在手动去除时只会对其中的一条指令进行修改,使得F5反编译的结果是正确的,但这样在查看汇编代码时还是会被干扰。这是因为手动去除比较麻烦。既然都决定自动去除了,那么目标就要定高一点:将绝大部分的 junk instructions 识别出来,然后使用 nop 指令来进行 patch

3.1.1 问题分析

首先观察需要去除的 junk instructions,从对于全局变量 g_Uselessg_False 的引用开始,到 B.EQ 指令结束。那么这样我们就能够确定一个去除区间 [ADBP, B.EQ]

.text:00000000000124C4                 ADRP            X9, #g_Useless@PAGE ; *
.text:00000000000124C8                 ADRP            X10, #g_False@PAGE ; *
.text:00000000000124CC                 LDR             X8, [X8]
.text:00000000000124D0                 STUR            X8, [X29,#var_28]
.text:00000000000124D4                 MOV             W8, #1 ; *
.text:00000000000124D8                 ADD             X10, X10, #g_False@PAGEOFF ; *
.text:00000000000124DC                 LDR             W10, [X10] ; *
.text:00000000000124E0                 SUBS            W8, W10, W8 ; *
.text:00000000000124E4                 MUL             W8, W10, W8 ; *
.text:00000000000124E8                 MOV             W10, #1 ; *
.text:00000000000124EC                 ADD             X9, X9, #g_Useless@PAGEOFF ; *
.text:00000000000124F0                 LDR             W9, [X9] ; *
.text:00000000000124F4                 AND             W8, W8, W10 ; *
.text:00000000000124F8                 CMP             W8, #0 ; *
.text:00000000000124FC                 CSET            W8, EQ ; *
.text:0000000000012500                 CMP             W9, #0xA ; *
.text:0000000000012504                 CSET            W9, LT ; *
.text:0000000000012508                 ORR             W8, W8, W9 ; *
.text:000000000001250C                 CMP             W8, #0 ; *
.text:0000000000012510                 B.EQ            loc_12840 ; *****

Above 的汇编代码可以划分为三个部分:

  • if  x ( x − 1 ) & 1 ≡ 0  then W8=0 \text{if }x(x-1)\&1\equiv0\text{ then W8=0} if x(x1)&10 then W8=0
.text:00000000000124C8                 ADRP            X10, #g_False@PAGE ; *
.text:00000000000124D4                 MOV             W8, #1 ; *
.text:00000000000124D8                 ADD             X10, X10, #g_False@PAGEOFF ; *
.text:00000000000124DC                 LDR             W10, [X10] ; *
.text:00000000000124E0                 SUBS            W8, W10, W8 ; *
.text:00000000000124E4                 MUL             W8, W10, W8 ; *
.text:00000000000124E8                 MOV             W10, #1 ; *
.text:00000000000124F4                 AND             W8, W8, W10 ; *
.text:00000000000124F8                 CMP             W8, #0 ; *
.text:00000000000124FC                 CSET            W8, EQ ; *
  • if  y ≥ 10  then W9=0 \text{if }y\ge10\text{ then W9=0} if y10 then W9=0
.text:00000000000124C4                 ADRP            X9, #g_Useless@PAGE ; *
.text:00000000000124EC                 ADD             X9, X9, #g_Useless@PAGEOFF ; *
.text:00000000000124F0                 LDR             W9, [X9] ; *
.text:0000000000012500                 CMP             W9, #0xA ; *
.text:0000000000012504                 CSET            W9, LT ; *
  • W8 ∣ W9 ≡ 0 \text{W8} | \text{W9}\equiv0 W8W90
.text:0000000000012508                 ORR             W8, W8, W9 ; *
.text:000000000001250C                 CMP             W8, #0 ; *
.text:0000000000012510                 B.EQ            loc_12840 ; *****

划分为三个部分后就可以很好看出他们之间联系的关键就是——寄存器

3.1.2 方案设计

我们可以设置一个集合 set setUsedReg 用来存放被 junk instructions 使用的寄存器,包含这些寄存器的指令都将被记录到一个 list listPatch 的列表中等待清除。而 setUsedReg 的更新规则如下:

  • set 中成员作为源操作数时,将指令加入 list 中,并将源操作数从 set 中删除,将目的操作数加入 当 set
  • set 中成员作为目的操作数时,将其从 set 中删除,并且该指令不加入 list
  • 初始时,引用 g_Uselessg_False 的寄存器自动加入 set 中。
  • 规则 1 的优先级高于 规则 2

3.2 具体实现

3.2.1 用到的 API

  1. idaapi.get_func_name:获取指针所在位置的函数对象,返回一个 idaapi. func_t 的对象。
  2. idaapi. func_t.code_items:一条条获取当前函数中的汇编指令,就如同我们在反汇编窗口中看到的那样。
  3. idaapi.decode_insn:解析当前位置的二进制数据为汇编指令,需要传入一个 idaapi.insn_t 对象来接收结果。
  4. idaapi.insn_t.ops:指令操作数构成的数组。
  5. idaapi.insn_t.ops[0].type:操作数类型,包括如下,序号即为其 value
listOpType = ["o_void", "o_reg", "o_mem", "o_phrase", "o_displ", 
			  "o_imm", "o_far", "o_near", "o_idpspec0", "o_idpspec1", 
			  "o_idpspec2", "o_idpspec3", "o_idpspec4", "o_idpspec5", "COND"]
  1. idaapi.print_insn_mnem:获取当前指令的名称,although idaapi.insn_t.get_canon_mnem 也能够获取,但二者的结果可能不同。前者可以获取准确的名字就像反汇编窗口中看到的那样,而后者可能只会获取系列名称。For example:对于 B.EQ,前者的结果为B.EQ,while 后者的结果是 B
  2. idautils.XrefsFrom:获取某个地址的交叉引用来自,即引用了谁,what,誰かを。返回结果为一个 idaapi.xrefblk_t 对象的 Generator。这个函数的效果与你对一个地址按 Ctrl + J 是一样的。idaapi.xrefblk_t.to 记录被引用位置,idaapi.xrefblk_t.frm 记录当前位置
  3. idautils.XrefsTo:获取某个地址的交叉引用到,即被谁,what,誰かを引用了。返回结果为一个 idaapi.xrefblk_t 对象的 Generator。这个函数的效果与你对一个地址按 Ctrl + X 是一样的。idaapi.xrefblk_t.to 记录当前位置,idaapi.xrefblk_t.frm 记录引用位置
  4. idc.GetDisasm:获取汇编指令,exactly same as 反汇编窗口。
  5. idaapi.patch_bytes:改变某处的二进制数据。
  6. ida_bytes.del_items:将某段数据设置为 undefined,和在反汇编窗口中按 U 一样。
  7. ida_funcs.add_func:将一个地址作为函数的起始地址进行分析,可自动得到函数终止地址。
  8. ida_bytes.create_byte:将一段数据作为 byte 进行解析。

3.2.2 脚本编写

既然是面向对象的编程,那么一定要有对象,所以还是选择使用对象进行封装。由于 ARMNOP 并不 like x86 那样容易记,就使用类静态数据来记录。

可以使用 self. 访问的数据为函数名和 idaapi. func_t 对象。

class EliminateJunkCode:
    bNop = b"\x1f\x20\x03\xD5"
    # copy the junk global data address to there
    listJunk = [0xE78B8, 0xE78BC, 0xE78C4, 0xE78C0]

    def __init__(self):
        screen_ea = idaapi.get_screen_ea()
        # function name
        self.strName : str = idaapi.get_func_name(screen_ea)
        # func_t type
        self.fnPoint : idaapi.func_t = idaapi.get_func(screen_ea)

PatchJunk 函数将传入 list 中地址所对应的指令 patch 为 NOPARMx86 patch 方便一点,不用先计算出指令长度。

class EliminateJunkCode:
	...
    def PatchJunk(self, listPatch : list):
       """
       * patch the address in listPath to NOP
       * parameter 0 : the list of patch address
       * return value : no return value
       """
       print(len(listPatch))
       for addr in listPatch:
           print(f"{hex(addr)} : {idc.GetDisasm(addr)}")
           idaapi.patch_bytes(addr, self.bNop)

Reanalysis 函数在去除 junk instructions 后重新分析函数的边界,并将永远不会执行的代码设置为数据。这样做的目的主要是在 CFG 图中去除无用分支。

class EliminateJunkCode:
	...
    def Reanalysis(self):
        """
        * after patch the junk instruction, reanalysis the function
        * return value : no return value
        """
        start_ea = self.fnPoint.start_ea
        end_ea = self.fnPoint.end_ea
        nLen = end_ea - start_ea
        ida_bytes.del_items(start_ea, ida_bytes.DELIT_SIMPLE, nLen)
        if ida_funcs.add_func(start_ea):
            fnPoint = idaapi.get_func(start_ea)
            new_end = fnPoint.end_ea
            ida_bytes.create_byte(new_end, end_ea - new_end)
        else:
            print("Crete function failed")

ParseInsn 函数分析传入地址指令的 名称 和 操作数中的 寄存器 操作数。

class EliminateJunkCode:
	...
    def ParseInsn(self, addr):
        """
        * parse the instruction, record the oprand's reg
        * parameter 0 : the address of the instruction
        * return value : the string of mnem and list of oprand's reg value 
        """
        insn = idaapi.insn_t()
        idaapi.decode_insn(insn, addr)
        strName = idaapi.print_insn_mnem(addr)
        listReg = list()
        for op in insn.ops:
            if op.type == idaapi.o_void:
                break
            listReg.append(op.reg)
        return strName, listReg

Eliminate 函数完成对于函数指令的分析,识别其中的 junk instructions,并调用 PatchJunk 进行 patch,完成函数分析后调用 Reanalysis 进行重新分析。

  1. 首先对于数据进行初始化,声明 set setUsedReg 用来存放被 junk instructions 使用的寄存器,list listPatch 记录需要去除的 junk instructions。
class EliminateJunkCode:
	...
    def Eliminate(self):
       """
       * eliminate the junk instruction in a function
       * return value : no return value
       """
       # a set to record current reg used by instruction
       setUsedReg = set()
       # a list of address needed to patch
       listPatch = list()
       # a flag to represent enter the junk instruction
       bFlagStart = 0
  1. 接着遍历分析函数的每条汇编代码,如果该汇编代码的名称为 ADRP,则查看器交叉引用中是否有被列入到 listJunk 中的全局数据。如果存在对于其中的引用,则将区间开始标志 bFlagStart 置为 1,同时将当前目的寄存器记录到 setUsedReg 中。
class EliminateJunkCode:
	...
    def Eliminate(self):
    	...
        # travel each instruction in the function
        for addr in self.fnPoint.code_items():
            # parse instruction get mnem and oprand reg value list
            strName, listReg = self.ParseInsn(addr)

            # a set of junk instruction start with ADRP instruction
            if strName == "ADRP":
                refs = idautils.XrefsFrom(addr, idaapi.XREF_FAR)
                refs : idaapi.xrefblk_t = next(refs)
                # there is a reference to the listJunk
                if refs.to in self.listJunk:
                    # set the enter flag to 1
                    bFlagStart = 1
                    # add the reg to junk used reg set
                    setUsedReg.add(listReg[0])
  1. 当指令名称为 B.EQ 时,达到区间终点。调用 PatchJunklistPatch 中地址进行 patch,并将 setlist 清空,bFlagStart 置为 0,以便开始下一个块 junk instructions 的识别与去除。
class EliminateJunkCode:
	...
    def Eliminate(self):
    	...
		for addr in self.fnPoint.code_items():
			...
            # a set of junk instruction end with B.EQ instruction
            if bFlagStart and strName == "B.EQ":
                listPatch.append(addr)
                # patch a set of junk instruction
                self.PatchJunk(listPatch)
                # clear patched one
                listPatch.clear()
                setUsedReg.clear()
                # reset the enter to 0
                bFlagStart = 0
  1. bFlagStart 为真时,说明此时位于分析区间中,需要对于指令进行寄存器分析。这里的处理较为特殊,对于有 3 个寄存器操作数的指令来说,可以严格按照之前的规则进行操作。而对于 2 个操作数的指令来说需要进行特殊处理,由于 junk instructions 中只有 LDR, CSET and CMP 是两个操作数的,遇到 LDR 检查源操作数,而遇到 CSET and CMP 则直接加入 listPatch
class EliminateJunkCode:
	...
    def Eliminate(self):
    	...
		for addr in self.fnPoint.code_items():
			...
			# normal occasion, bFlagStart == 1 present now in a boundry of a set of junk instruciton
            if bFlagStart:
                nLen = len(listReg)
                # only two oprands instruction
                if nLen == 2:
                    # LDR instruction, load memory from junk used reg 
                    # just like : "LDR X11, [X9]", where X9 is free and X11 is used by junk
                    if strName == "LDR" and listReg[1] in setUsedReg:
                        listPatch.append(addr)
                        # release the opreand[1] reg
                        setUsedReg.discard(listReg[1])
                        # record the oprand[0] reg
                        setUsedReg.add(listReg[0])
                    elif listReg[0] in setUsedReg:
                        # I have observed, two oprands instruction only "CSET" and "CMP" are used
                        if strName in ["CSET", "CMP"]:
                            listPatch.append(addr)
                # three oprands instruction
                elif nLen == 3:
                    # there is a junk used reg in source reg
                    if listReg[1] in setUsedReg or listReg[2] in setUsedReg:
                        listPatch.append(addr)
                        # release source reg
                        setUsedReg.discard(listReg[1])
                        setUsedReg.discard(listReg[2])
                        # record destination reg
                        setUsedReg.add(listReg[0])
                    # there is no junk used reg in source reg, just release the destination reg
                    else:
                        setUsedReg.discard(listReg[0])
                else:
                    print(f"parse {hex(addr)} : {strName} error")
  1. 去除完毕后,调用 Reanalysis 函数进行重新分析。
class EliminateJunkCode:
	...
    def Eliminate(self):
    	...
        # complete patch all junk instruction, reanalysis the code
        self.Reanalysis()

3.2.3 All in one

  • EliminateJunk.py
import idaapi
import idc
# use for xref
import idautils
# use for reanalysis
import ida_bytes
import ida_funcs

# use this flag to print debug information
DBG_FLAG = 1
# the string map to op.type
listOpType = ["o_void", "o_reg", "o_mem", "o_phrase", "o_displ", "o_imm", "o_far", "o_near", 
                  "o_idpspec0", "o_idpspec1", "o_idpspec2", "o_idpspec3", "o_idpspec4", "o_idpspec5", "COND"]

class EliminateJunkCode:
    bNop = b"\x1f\x20\x03\xD5"
    # copy the junk global data address to there
    listJunk = [0xE78B8, 0xE78BC, 0xE78C4, 0xE78C0]

    def __init__(self):
        screen_ea = idaapi.get_screen_ea()
        # function name
        self.strName : str = idaapi.get_func_name(screen_ea)
        # func_t type
        self.fnPoint : idaapi.func_t = idaapi.get_func(screen_ea)

    def __repr__(self):
        return f"function name : {self.strName}\n" \
        f"Start at 0x{self.fnPoint.start_ea:x}\nEnd at 0x{self.fnPoint.end_ea:x}" \
        f"function size : 0x{self.fnPoint.size():x}"
    
    def PatchJunk(self, listPatch : list):
        """
        * patch the address in listPath to NOP
        * parameter 0 : the list of patch address
        * return value : no return value
        """
        print(len(listPatch))
        for addr in listPatch:
            print(f"{hex(addr)} : {idc.GetDisasm(addr)}")
            idaapi.patch_bytes(addr, self.bNop)

    def Reanalysis(self):
        """
        * after patch the junk instruction, reanalysis the function
        * return value : no return value
        """
        start_ea = self.fnPoint.start_ea
        end_ea = self.fnPoint.end_ea
        nLen = end_ea - start_ea
        ida_bytes.del_items(start_ea, ida_bytes.DELIT_SIMPLE, nLen)
        if ida_funcs.add_func(start_ea):
            fnPoint = idaapi.get_func(start_ea)
            new_end = fnPoint.end_ea
            ida_bytes.create_byte(new_end, end_ea - new_end)
        else:
            print("Crete function failed")
        # ida_auto.plan_and_wait(start_ea, start_ea + 4)

    def ParseInsn(self, addr):
        """
        * parse the instruction, record the oprand's reg
        * parameter 0 : the address of the instruction
        * return value : the string of mnem and list of oprand's reg value 
        """
        insn = idaapi.insn_t()
        idaapi.decode_insn(insn, addr)
        strName = idaapi.print_insn_mnem(addr)
        listReg = list()
        for op in insn.ops:
            if op.type == idaapi.o_void:
                break
            listReg.append(op.reg)
        return strName, listReg

    def Eliminate(self):
        """
        * eliminate the junk instruction in a function
        * return value : no return value
        """
        # a set to record current reg used by instruction
        setUsedReg = set()
        # a list of address needed to patch
        listPatch = list()
        # a flag to represent enter the junk instruction
        bFlagStart = 0

        # travel each instruction in the function
        for addr in self.fnPoint.code_items():
            # parse instruction get mnem and oprand reg value list
            strName, listReg = self.ParseInsn(addr)

            # a set of junk instruction start with ADRP instruction
            if strName == "ADRP":
                refs = idautils.XrefsFrom(addr, idaapi.XREF_FAR)
                refs : idaapi.xrefblk_t = next(refs)
                # there is a reference to the listJunk
                if refs.to in self.listJunk:
                    # set the enter flag to 1
                    bFlagStart = 1
                    # add the reg to junk used reg set
                    setUsedReg.add(listReg[0])
            
            # a set of junk instruction end with B.EQ instruction
            if bFlagStart and strName == "B.EQ":
                listPatch.append(addr)
                # patch a set of junk instruction
                self.PatchJunk(listPatch)
                # clear patched one
                listPatch.clear()
                setUsedReg.clear()
                # reset the enter to 0
                bFlagStart = 0
            
            # normal occasion, bFlagStart == 1 present now in a boundry of a set of junk instruciton
            if bFlagStart:
                nLen = len(listReg)
                # only two oprands instruction
                if nLen == 2:
                    # LDR instruction, load memory from junk used reg 
                    # just like : "LDR X11, [X9]", where X9 is free and X11 is used by junk
                    if strName == "LDR" and listReg[1] in setUsedReg:
                        listPatch.append(addr)
                        # release the opreand[1] reg
                        setUsedReg.discard(listReg[1])
                        # record the oprand[0] reg
                        setUsedReg.add(listReg[0])
                    elif listReg[0] in setUsedReg:
                        # I have observed, two oprands instruction only "CSET" and "CMP" are used
                        if strName in ["CSET", "CMP"]:
                            listPatch.append(addr)
                # three oprands instruction
                elif nLen == 3:
                    # there is a junk used reg in source reg
                    if listReg[1] in setUsedReg or listReg[2] in setUsedReg:
                        listPatch.append(addr)
                        # release source reg
                        setUsedReg.discard(listReg[1])
                        setUsedReg.discard(listReg[2])
                        # record destination reg
                        setUsedReg.add(listReg[0])
                    # there is no junk used reg in source reg, just release the destination reg
                    else:
                        setUsedReg.discard(listReg[0])
                else:
                    print(f"parse {hex(addr)} : {strName} error")
        # complete patch all junk instruction, reanalysis the code
        self.Reanalysis()

def ShowInsn(nAddress):
    """
    * show the instuction information
    * parameter 0 : the address of instruction
    * return value : no return value
    """
    insn = idaapi.insn_t()
    idaapi.decode_insn(insn, nAddress)
    print(idaapi.print_insn_mnem(nAddress))
    for i in range(len(insn.ops)):
        op : idaapi.op_t = insn.ops[i]
        if(op.type == idaapi.o_void):
            break
        print(f"{listOpType[op.type]} : op.reg = 0x{op.reg:x}, op.value = 0x{op.value:x}, op.addr = 0x{op.addr}")

def main():
    idaapi.msg_clear()
    inf : idaapi.idainfo = idaapi.get_inf_structure()
    if(inf.procname != "ARM"):
        print(f"this script support ARM only, not support{inf.procname} now")
        return   

    me = EliminateJunkCode()
    print(me)
    me.Eliminate()
    return

if __name__=="__main__":
    main()

3.3 去除效果

  • 去除前后的效果对比
pic1 pic1
  • 不重新分析,尽管 CFG 得到了简化,但还是存在许多无用分支。
    在这里插入图片描述
  • 无用代码,重新分析后会有大量的无用代码,将其作为数据解析存放。这些空间也是 patch 时可以利用的位置。
    在这里插入图片描述

网站公告

今日签到

点亮在社区的每一天
去签到