Android:Reverse 实战 part 2 番外 IDA python
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。君は能够使用一个对象实例查看到它所有可调用的 方法 和 数据,这本身就对初学者较为友好了。


- 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)
如果想要自定义 action 的 icon 图标可以使用 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 actionself.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 viewif 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 = 0action_name = "FW:extract_function"menu_path = "Edit/ExtractFunction"wanted_name = 'Function withdraw'wanted_hotkey = ''menu = Noneicon_atm = b""# construct functiondef 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 actionself.action_name, # The action ID (see above)idaapi.SETMENU_APP) # We want to append the action after the 'Manual instruction...'# Register context menu actionsself.menu = ContextMenuHooks()self.menu.hook()return idaapi.PLUGIN_KEEPdef run(self, arg):pass# this is the deconstruct functiondef term(self):print("deconstruct")idaapi.detach_action_from_menu(self.menu_path, self.action_name)if self.menu:self.menu.unhook()returnclass 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_ALWAYSclass ContextMenuHooks(idaapi.UI_Hooks):def finish_populating_widget_popup(self, form, popup):# Add actions to the context menu of the Pseudocode viewif 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 脚本。
这里我再插入点个人观点:目前我接触到的动态调试工具 x96dbg,gdb and Frida 只有 x96dbg 有 GUI 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_Useless 和 g_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≡0then W8=0\text{if }x(x-1)\&1\equiv0\text{ then W8=0}if x(x−1)&1≡0 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≥10then W9=0\text{if }y\ge10\text{ then W9=0}if y≥10 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}\equiv0W8∣W9≡0
.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_Useless 和 g_False 的寄存器自动加入 set 中。
- 规则 1 的优先级高于 规则 2
3.2 具体实现
3.2.1 用到的 API
- idaapi.get_func_name:获取指针所在位置的函数对象,返回一个 idaapi. func_t 的对象。
- idaapi. func_t.code_items:一条条获取当前函数中的汇编指令,就如同我们在反汇编窗口中看到的那样。
- idaapi.decode_insn:解析当前位置的二进制数据为汇编指令,需要传入一个 idaapi.insn_t 对象来接收结果。
- idaapi.insn_t.ops:指令操作数构成的数组。
- 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"]
- idaapi.print_insn_mnem:获取当前指令的名称,although idaapi.insn_t.get_canon_mnem 也能够获取,但二者的结果可能不同。前者可以获取准确的名字就像反汇编窗口中看到的那样,而后者可能只会获取系列名称。For example:对于
B.EQ
,前者的结果为B.EQ
,while 后者的结果是B
。 - idautils.XrefsFrom:获取某个地址的交叉引用来自,即引用了谁,what,誰かを。返回结果为一个 idaapi.xrefblk_t 对象的 Generator。这个函数的效果与你对一个地址按 Ctrl + J 是一样的。idaapi.xrefblk_t.to 记录被引用位置,idaapi.xrefblk_t.frm 记录当前位置。
- idautils.XrefsTo:获取某个地址的交叉引用到,即被谁,what,誰かを引用了。返回结果为一个 idaapi.xrefblk_t 对象的 Generator。这个函数的效果与你对一个地址按 Ctrl + X 是一样的。idaapi.xrefblk_t.to 记录当前位置,idaapi.xrefblk_t.frm 记录引用位置。
- idc.GetDisasm:获取汇编指令,exactly same as 反汇编窗口。
- idaapi.patch_bytes:改变某处的二进制数据。
- ida_bytes.del_items:将某段数据设置为 undefined,和在反汇编窗口中按 U 一样。
- ida_funcs.add_func:将一个地址作为函数的起始地址进行分析,可自动得到函数终止地址。
- ida_bytes.create_byte:将一段数据作为 byte 进行解析。
3.2.2 脚本编写
既然是面向对象的编程,那么一定要有对象,所以还是选择使用对象进行封装。由于 ARM 的 NOP
并不 like x86 那样容易记,就使用类静态数据来记录。
可以使用 self.
访问的数据为函数名和 idaapi. func_t 对象。
class EliminateJunkCode:bNop = b"\x1f\x20\x03\xD5"# copy the junk global data address to therelistJunk = [0xE78B8, 0xE78BC, 0xE78C4, 0xE78C0]def __init__(self):screen_ea = idaapi.get_screen_ea()# function nameself.strName : str = idaapi.get_func_name(screen_ea)# func_t typeself.fnPoint : idaapi.func_t = idaapi.get_func(screen_ea)
PatchJunk 函数将传入 list 中地址所对应的指令 patch 为 NOP
。ARM 比 x86 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_eaend_ea = self.fnPoint.end_eanLen = end_ea - start_eaida_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_eaida_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:breaklistReg.append(op.reg)return strName, listReg
Eliminate 函数完成对于函数指令的分析,识别其中的 junk instructions,并调用 PatchJunk 进行 patch,完成函数分析后调用 Reanalysis 进行重新分析。
- 首先对于数据进行初始化,声明 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 instructionsetUsedReg = set()# a list of address needed to patchlistPatch = list()# a flag to represent enter the junk instructionbFlagStart = 0
- 接着遍历分析函数的每条汇编代码,如果该汇编代码的名称为
ADRP
,则查看器交叉引用中是否有被列入到 listJunk 中的全局数据。如果存在对于其中的引用,则将区间开始标志 bFlagStart 置为 1,同时将当前目的寄存器记录到 setUsedReg 中。
class EliminateJunkCode:...def Eliminate(self):...# travel each instruction in the functionfor addr in self.fnPoint.code_items():# parse instruction get mnem and oprand reg value liststrName, listReg = self.ParseInsn(addr)# a set of junk instruction start with ADRP instructionif strName == "ADRP":refs = idautils.XrefsFrom(addr, idaapi.XREF_FAR)refs : idaapi.xrefblk_t = next(refs)# there is a reference to the listJunkif refs.to in self.listJunk:# set the enter flag to 1bFlagStart = 1# add the reg to junk used reg setsetUsedReg.add(listReg[0])
- 当指令名称为
B.EQ
时,达到区间终点。调用 PatchJunk 将 listPatch 中地址进行 patch,并将 set 和 list 清空,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 instructionif bFlagStart and strName == "B.EQ":listPatch.append(addr)# patch a set of junk instructionself.PatchJunk(listPatch)# clear patched onelistPatch.clear()setUsedReg.clear()# reset the enter to 0bFlagStart = 0
- bFlagStart 为真时,说明此时位于分析区间中,需要对于指令进行寄存器分析。这里的处理较为特殊,对于有 3 个寄存器操作数的指令来说,可以严格按照之前的规则进行操作。而对于 2 个操作数的指令来说需要进行特殊处理,由于 junk instructions 中只有
LDR
,CSET
andCMP
是两个操作数的,遇到LDR
检查源操作数,而遇到CSET
andCMP
则直接加入 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 instrucitonif bFlagStart:nLen = len(listReg)# only two oprands instructionif nLen == 2:# LDR instruction, load memory from junk used reg # just like : "LDR X11, [X9]", where X9 is free and X11 is used by junkif strName == "LDR" and listReg[1] in setUsedReg:listPatch.append(addr)# release the opreand[1] regsetUsedReg.discard(listReg[1])# record the oprand[0] regsetUsedReg.add(listReg[0])elif listReg[0] in setUsedReg:# I have observed, two oprands instruction only "CSET" and "CMP" are usedif strName in ["CSET", "CMP"]:listPatch.append(addr)# three oprands instructionelif nLen == 3:# there is a junk used reg in source regif listReg[1] in setUsedReg or listReg[2] in setUsedReg:listPatch.append(addr)# release source regsetUsedReg.discard(listReg[1])setUsedReg.discard(listReg[2])# record destination regsetUsedReg.add(listReg[0])# there is no junk used reg in source reg, just release the destination regelse:setUsedReg.discard(listReg[0])else:print(f"parse {hex(addr)} : {strName} error")
- 去除完毕后,调用 Reanalysis 函数进行重新分析。
class EliminateJunkCode:...def Eliminate(self):...# complete patch all junk instruction, reanalysis the codeself.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 therelistJunk = [0xE78B8, 0xE78BC, 0xE78C4, 0xE78C0]def __init__(self):screen_ea = idaapi.get_screen_ea()# function nameself.strName : str = idaapi.get_func_name(screen_ea)# func_t typeself.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_eaend_ea = self.fnPoint.end_eanLen = end_ea - start_eaida_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_eaida_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:breaklistReg.append(op.reg)return strName, listRegdef Eliminate(self):"""* eliminate the junk instruction in a function* return value : no return value"""# a set to record current reg used by instructionsetUsedReg = set()# a list of address needed to patchlistPatch = list()# a flag to represent enter the junk instructionbFlagStart = 0# travel each instruction in the functionfor addr in self.fnPoint.code_items():# parse instruction get mnem and oprand reg value liststrName, listReg = self.ParseInsn(addr)# a set of junk instruction start with ADRP instructionif strName == "ADRP":refs = idautils.XrefsFrom(addr, idaapi.XREF_FAR)refs : idaapi.xrefblk_t = next(refs)# there is a reference to the listJunkif refs.to in self.listJunk:# set the enter flag to 1bFlagStart = 1# add the reg to junk used reg setsetUsedReg.add(listReg[0])# a set of junk instruction end with B.EQ instructionif bFlagStart and strName == "B.EQ":listPatch.append(addr)# patch a set of junk instructionself.PatchJunk(listPatch)# clear patched onelistPatch.clear()setUsedReg.clear()# reset the enter to 0bFlagStart = 0# normal occasion, bFlagStart == 1 present now in a boundry of a set of junk instrucitonif bFlagStart:nLen = len(listReg)# only two oprands instructionif nLen == 2:# LDR instruction, load memory from junk used reg # just like : "LDR X11, [X9]", where X9 is free and X11 is used by junkif strName == "LDR" and listReg[1] in setUsedReg:listPatch.append(addr)# release the opreand[1] regsetUsedReg.discard(listReg[1])# record the oprand[0] regsetUsedReg.add(listReg[0])elif listReg[0] in setUsedReg:# I have observed, two oprands instruction only "CSET" and "CMP" are usedif strName in ["CSET", "CMP"]:listPatch.append(addr)# three oprands instructionelif nLen == 3:# there is a junk used reg in source regif listReg[1] in setUsedReg or listReg[2] in setUsedReg:listPatch.append(addr)# release source regsetUsedReg.discard(listReg[1])setUsedReg.discard(listReg[2])# record destination regsetUsedReg.add(listReg[0])# there is no junk used reg in source reg, just release the destination regelse:setUsedReg.discard(listReg[0])else:print(f"parse {hex(addr)} : {strName} error")# complete patch all junk instruction, reanalysis the codeself.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):breakprint(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()returnif __name__=="__main__":main()
3.3 去除效果
- 去除前后的效果对比


- 不重新分析,尽管 CFG 得到了简化,但还是存在许多无用分支。
- 无用代码,重新分析后会有大量的无用代码,将其作为数据解析存放。这些空间也是 patch 时可以利用的位置。