标题 | 简介 | 类型 | 公开时间 | ||||||||||
|
|||||||||||||
|
|||||||||||||
详情 | |||||||||||||
[SAFE-ID: JIWO-2025-3275] 作者: 小螺号 发表于: [2023-02-23]
本文共 [263] 位读者顶过
在逆向工程期间,你可能会遇到可用工具尚不支持你正在使用的架构的情况。 [出自:jiwo.org] 在本文中,我们探讨了这种情况并展示了扩展 IDA 功能的示例。我们探索如何实现用于反汇编新 Xtensa 指令的 IDA 插件。本指南对于从事涉及逆向工程的项目并希望了解如何充分利用逆向工具的开发人员很有帮助。 为什么要创建自定义 IDA 插件?在我们之前的一篇文章中,我们讨论了研究固件架构的重要性。在那种情况下,我们设法找到了支持我们需要分析的设备架构的反汇编工具。现在,我们要解决逆向工程工具不支持你在项目中使用的架构的问题。让我们以 IDA 为例来探讨本文。 Interactive Disassembler (IDA)是一种软件反汇编程序,可从机器可执行代码生成汇编语言源代码并执行自动代码分析。这个逆向工程工具通过众多插件提供了广泛的反汇编和调试功能。 IDA 还使用一组称为处理器模块的插件将原始字节代码转换为反汇编文本。每个插件都是为特定的硬件架构设计的。 由于反汇编插件的可用性在很大程度上取决于架构的流行程度,因此某些处理器模块的更新频率高于其他处理器模块。而对新指令的支持可能需要 IDA 开发人员实施。 那么,如果你当前使用的体系结构没有插件,你该怎么办? 好消息是不必等待 IDA 开发人员实现对特定指令集的支持。你可以自己创建自定义 IDA 插件或使用 Python 通过 IDA SDK 实现相关插件。 让我们探索一个为逆向工程目的实现 IDA 插件的示例,并使用新的 Xtensa 架构指令,在撰写本文时 IDA 7.7 不支持这些指令。由于这些指令未在 IDA 中反汇编,因此你将它们视为原始字节:
屏幕截图 1. Xtensa 指令在 IDA 中显示为原始字节 但是如果你使用其他支持反汇编新 Xtensa 指令的软件,比如Lauterbach Trace32 模拟器,你可以看到这些字节是无符号商 (QUOU) 指令:
一旦你知道这些字节是什么,你就可以找到 QUOU 指令的描述,并为 IDA 实现一个插件来扩展现有处理器模块的功能。让我们探讨如何做到这一点。 使用新插件向 IDA 处理器模块添加指令让我们使用NECromancer插件,它为 NEC V850 CPU 扩展了 IDA 的处理器模块。 使用此插件的目的是挂钩处理器模块的事件处理程序并执行你自己的处理例程而不是现有的处理例程。该插件将允许处理器模块处理默认情况下无法处理的未知指令。 让我们来看看一个空的插件。下面是在 IDA 引擎中注册插件并挂钩处理器模块所需的最少代码: class XtensaESP(plugin_t): flags = PLUGIN_PROC | PLUGIN_HIDE comment = "" wanted_hotkey = "" help = "Adds support for additional Xtensa instructions" wanted_name = "XtensaESP" def __init__(self): self.prochook = None def init(self): if ph_get_id() != PLFM_XTENSA: return PLUGIN_SKIP self.prochook = xtensa_idp_hook_t() self.prochook.hook() print ("%s initialized." % XtensaESP.wanted_name) return PLUGIN_KEEP def run(self, arg): pass def term(self): if self.prochook: self.prochook.unhook() #-------------------------------------------------------------------------- def PLUGIN_ENTRY(): return XtensaESP() 为确保 IDA 仅在加载 Xtensa CPU 处理器模块时运行插件,插件执行以下检查: if ph_get_id() != PLFM_XTENSA NECromancer 插件还需要 xtensa_idp_hook_t 挂钩类来安装处理器模块事件的处理程序。下面是钩子类主体的样子: class xtensa_idp_hook_t(IDP_Hooks): def __init__(self): IDP_Hooks.__init__(self) def ev_ana_insn(self, insn): pass def ev_out_mnem(self, outctx): pass def ev_out_operand(self, outctx, op): pass 此代码段的关键元素是:
让我们一一实现这三种方法。 1.实现ev_ana_insn方法使用 NECromancer 插件的目的是添加对 QUOU(无符号商)指令的支持。这意味着你需要知道 CPU 实际上是如何解析代表 QUOU 指令的字节的。 [你可以在Xtensa 指令集架构 (ISA) 参考手册 PDF]中找到此信息:
在这种特殊情况下,你不需要详细了解指令的作用。目标是了解 CPU 如何知道一组字节实际上是 QUOU 指令。 IDA 将此 QUOU 指令显示为字节序列:0xC0, 0x22, 0xC2。指令的第一个字节------------0xC0在文档中表示如下:
让我们解释一下这意味着什么:
指令的第二个字节指定了另外两个标记为r和 的参数s:
在我们的例子中,第二个字节是0x22,这意味着r = 0x2和s = 0x2。因此,第一个和第二个操作数都是a2。 最后,第三个字节是0xC2。根据文档,它将始终是一个常量:
由于1100 0010 = 0xC2,你可以使用此字节进行 QUOU 指令标识。 现在,一切准备就绪,可以开始实施 ev_ana_insn 方法了。 首先,创建新的指令 ID,这将允许 IDA 引擎区分新指令和现有指令: class NewInstructions: (NN_quou, NN_last) = range(CUSTOM_INSN_ITYPE, CUSTOM_INSN_ITYPE+2) 其次,让我们使用从值开始的 IDCUSTOM_INSN_ITYPE。 最后,运行指令分析方法,如下所示: def ev_ana_insn(self, insn): buf = get_bytes(insn.ea, 3) if buf[2] == 0xC2 and (buf[0] & 0xF) == 0: insn.itype = NewInstructions.NN_quou insn.size = 3 return True return False 让我们解释一下这段代码的作用:
在这种情况下,我们检查第三个字节是否在第一个字节中,0xC2低 4 个字节是否在文档中定义。0最后,你需要在 ev_ana_insn方法的 insn 参数中填写有关 QUOU 指令的信息:至少指定指令 ID 和以字节为单位的指令大小。 然后,如果 ev_ana_insn 方法能够在建议的地址找到指令,则它必须返回 True ; 否则,它必须返回 False 。 即使你将努力保持在绝对最低限度(如上所示),IDA 也已经能够识别新指令。但我们也想向你展示如何让 IDA 也知道指令参数,否则指令将显示为没有参数。为此,你需要对 ev_ana_insn() 方法进行改进: def ev_ana_insn(self, insn): buf = get_bytes(insn.ea, 3) if buf[2] == 0xC2 and (buf[0] & 0xF) == 0: insn.itype = NewInstructions.NN_quou insn.size = 3 insn.Op1.type = o_reg insn.Op1.reg = buf[1] >> 4 insn.Op2.type = o_reg insn.Op2.reg = buf[1] & 0xF insn.Op3.type = o_reg insn.Op3.reg = buf[0] >> 4 return True return False 这段新代码实现了指令参数的定义。这些是代码从指令字节中提取的r、s和值。t 解析完成后,就可以设置反汇编文本的输出了。 2.实现ev_out_mnem方法要生成反汇编文本,你可以完全重用ev_out_mnem 方法的 NECromancer 插件的现有代码: DEBUG_PLUGIN = True NEWINSN_COLOR = COLOR_MACRO if DEBUG_PLUGIN else COLOR_INSN class NewInstructions: (NN_quou, NN_last) = range(CUSTOM_INSN_ITYPE, CUSTOM_INSN_ITYPE+2) lst = {NN_quou:"quou"} def ev_out_mnem(self, outctx): insntype = outctx.insn.itype global NEWINSN_COLOR if (insntype >= CUSTOM_INSN_ITYPE) and (insntype in NewInstructions.lst): mnem = NewInstructions.lst[insntype] outctx.out_tagon(NEWINSN_COLOR) outctx.out_line(mnem) outctx.out_tagoff(NEWINSN_COLOR) # TODO: how can MNEM_width be determined programmatically? MNEM_WIDTH = 8 width = max(1, MNEM_WIDTH - len(mnem)) outctx.out_line(' ' * width) return True return False 我们来解释一下这个例子的要点:
3.实现ev_out_operand方法要实现指令操作数的生成,你还可以重用ev_out_operand 方法的现成代码: def ev_out_operand(self, outctx, op): insn = outctx.insn if insn.itype in [NewInstructions.NN_ld_hu, NewInstructions.NN_st_h]: if op.type == o_displ: outctx.out_value(op, OOF_ADDR) outctx.out_register(ph_get_regnames()[op.reg]) return True return False 此代码检查指令是否是你之前添加的指令。如果指令包含参数,则需要打印操作数。然后,你将获得寄存器的名称,插件将打印它。 现在,所有准备工作都已完成,你可以开始测试你创建的插件了。 测试插件将插件放在\IDA\plugins目录中,以便 IDA 可以运行它。然后,加载包含未定义字节的 IDA 数据库,将光标置于其上,然后按 C 键创建指令:
屏幕截图 3. 在 IDA 中创建新的 QUOU 指令 添加对 QUOU 指令的支持后,IDA 会在遇到该指令时自动识别它。此处指令的颜色与其余指令不同,因为我们DEBUG_PLUGIN为此示例启用了标志。如果你决定禁用该标志,指令的颜色将与代码的其余部分相同。 我们示例的完整 NECromancer 插件源代码如下: from ida_lines import COLOR_INSN, COLOR_MACRO from ida_idp import CUSTOM_INSN_ITYPE, IDP_Hooks, ph_get_regnames, ph_get_id, PLFM_XTENSA from ida_bytes import get_bytes from ida_idaapi import plugin_t, PLUGIN_PROC, PLUGIN_HIDE, PLUGIN_SKIP, PLUGIN_KEEP from ida_ua import o_displ, o_reg, o_imm, dt_dword, OOF_ADDR from struct import unpack DEBUG_PLUGIN = True NEWINSN_COLOR = COLOR_MACRO if DEBUG_PLUGIN else COLOR_INSN class NewInstructions: (NN_quou, NN_muluh) = range(CUSTOM_INSN_ITYPE, CUSTOM_INSN_ITYPE+2) lst = {NN_quou:"quou", NN_muluh:"muluh"} #-------------------------------------------------------------------------- class xtensa_idp_hook_t(IDP_Hooks): def __init__(self): IDP_Hooks.__init__(self) def decode_instruction(self, insn): buf = get_bytes(insn.ea, 3) #print("%08X bytes %X %X %X" % (insn.ea , buf[2] , buf[1] , buf[0])) if buf[2] == 0xC2 and (buf[0] & 0xF) == 0: insn.itype = NewInstructions.NN_quou insn.size = 3 insn.Op1.type = o_reg insn.Op1.reg = buf[1] >> 4 insn.Op2.type = o_reg insn.Op2.reg = buf[1] & 0xF insn.Op3.type = o_reg insn.Op3.reg = buf[0] >> 4 return True if buf[2] == 0xA2 and (buf[0] & 0xF) == 0: insn.itype = NewInstructions.NN_muluh insn.size = 3 insn.Op1.type = o_reg insn.Op1.reg = buf[1] >> 4 insn.Op2.type = o_reg insn.Op2.reg = buf[1] & 0xF insn.Op3.type = o_reg insn.Op3.reg = buf[0] >> 4 return True return False def ev_ana_insn(self, insn): return self.decode_instruction(insn) def ev_out_mnem(self, outctx): insntype = outctx.insn.itype global NEWINSN_COLOR if (insntype >= CUSTOM_INSN_ITYPE) and (insntype in NewInstructions.lst): mnem = NewInstructions.lst[insntype] outctx.out_tagon(NEWINSN_COLOR) outctx.out_line(mnem) outctx.out_tagoff(NEWINSN_COLOR) # TODO: how can MNEM_width be determined programmatically? MNEM_WIDTH = 8 width = max(1, MNEM_WIDTH - len(mnem)) outctx.out_line(' ' * width) return True return False def ev_out_operand(self, outctx, op): insn = outctx.insn if insn.itype in [NewInstructions.NN_ld_hu, NewInstructions.NN_st_h]: if op.type == o_displ: outctx.out_value(op, OOF_ADDR) outctx.out_register(ph_get_regnames()[op.reg]) return True return False #-------------------------------------------------------------------------- class XtensaESP(plugin_t): flags = PLUGIN_PROC | PLUGIN_HIDE comment = "" wanted_hotkey = "" help = "Adds support for additional Xtensa instructions" wanted_name = "XtensaESP" def __init__(self): self.prochook = None def init(self): if ph_get_id() != PLFM_XTENSA: return PLUGIN_SKIP self.prochook = xtensa_idp_hook_t() self.prochook.hook() print ("%s initialized." % XtensaESP.wanted_name) return PLUGIN_KEEP def run(self, arg): pass def term(self): if self.prochook: self.prochook.unhook() #-------------------------------------------------------------------------- def PLUGIN_ENTRY(): return XtensaESP() 这就是你如何使用 Python 为 IDA 实现插件。现在你知道了:IDA 能够反汇编新的 Xtensa 指令。 结论尽管像 IDA 这样的工具支持大多数流行的架构,但了解如何扩展逆向工程工具的功能将增加你的机会。这些知识还将在你使用新架构和解决重要任务时提供帮助。 |