打开网易新闻 查看精彩图片

在逆向工程过程中,您可能会遇到可用工具尚不支持您正在使用的架构的情况。

在本文中,我们将探讨这样的案例并展示扩展 IDA 功能的示例。我们探索如何实现 IDA 插件来反汇编新的 Xtensa 指令。

为什么要创建自定义 IDA 插件?
打开网易新闻 查看精彩图片
为什么要创建自定义 IDA 插件?

在我们之前的一篇文章中,我们讨论了研究固件架构的重要性。在这种情况下,我们设法找到了一个支持我们需要分析的设备架构的反汇编工具。现在,我们想要解决逆向工程工具不支持您在项目中使用的架构的问题。让我们以 IDA 为例来探讨本文。

交互式反汇编器 (IDA)是一种软件反汇编器,可从机器可执行代码生成汇编语言源代码并执行自动代码分析。该逆向工程工具通过众多插件提供广泛的反汇编和调试功能。

IDA 还使用一组称为处理器模块的插件将原始字节代码转换为反汇编文本。每个插件都是针对特定的硬件架构设计的。

由于反汇编插件的可用性很大程度上取决于架构的流行程度,因此某些处理器模块的更新频率比其他模块要高。而对新指令的支持可能需要 IDA 开发人员花一些时间才能实现。

那么,如果您当前正在使用的架构没有插件,您该怎么办?

好消息是,无需等待 IDA 开发人员实现对特定指令集的支持。您可以自己创建自定义 IDA 插件,也可以使用 Python 通过 IDA SDK 实现相关插件。
让我们探讨一个实现逆向工程 IDA 插件的示例,并使用新的 Xtensa 架构指令,在撰写本文时,IDA 7.7 尚不支持这些指令。由于这些指令未在 IDA 中反汇编,因此您将它们视为原始字节

打开网易新闻 查看精彩图片

屏幕截图 1. Xtensa 指令在 IDA 中显示为原始字节

但如果您使用其他支持反汇编新Xtensa指令的软件,例如Lauterbach Trace32模拟器,您可以看到这些字节是商无符号(QUOU)指令:

打开网易新闻 查看精彩图片

屏幕截图 2.Xtensa 指令看起来像 Lauterbach Trace32 模拟器中的 QUOU 指令

一旦知道这些字节是什么,您就可以找到 QUOU 指令的描述并实现 IDA 插件来扩展现有处理器模块的功能。让我们探讨一下如何做到这一点。

使用新插件向 IDA 处理器模块添加指令
打开网易新闻 查看精彩图片
使用新插件向 IDA 处理器模块添加指令

让我们使用NECromancer插件,它为 NEC V850 CPU 扩展了 IDA 的处理器模块。

使用此插件的目标是挂钩处理器模块的事件处理程序并执行您自己的处理例程而不是现有的处理例程。该插件将允许处理器模块处理默认情况下无法处理的未知指令。

让我们看一下一个空插件。以下是在 IDA 引擎中注册插件并挂接处理器模块所需的最少代码:

(Python)

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

该代码片段的关键元素是:

ev_ana_insn方法,帮助您分析字节码并创建指令类

ev_out_mnem方法,允许您创建指令的可视化表示,即生成反汇编文本

ev_out_operand方法,实现指令操作数生成为文本以便反汇编

让我们一一实现这三种方法。

1. 实现ev_ana_insn方法
打开网易新闻 查看精彩图片
1. 实现ev_ana_insn方法

使用 NECromancer 插件的目标是添加对 QUOU(无符号商)指令的支持。这意味着您需要知道 CPU 实际上如何解析表示 QUOU 指令的字节。

您可以在Xtensa 指令集架构 (ISA) 参考手册 [PDF]中找到此信息:

指令词:

打开网易新闻 查看精彩图片

所需的配置选项:32 位整数除法选项。

汇编语法:QUOU ar, as, at

说明:将地址寄存器的内容除以地址寄存器的内容QUOU进行 32 位无符号除法,并将商写入地址寄存器。如果地址寄存器的内容引发整数除以零异常而不是写入结果。asatarat0, QUOU

在这种特殊情况下,您不需要详细了解指令的作用。目标是了解 CPU 如何知道一组字节实际上是 QUOU 指令。

IDA 将这条 QUOU 指令显示为字节序列:0xC0, 0x22, 0xC2。指令的第一个字节0xC0——在文档中表示如下:

打开网易新闻 查看精彩图片

让我们解释一下这意味着什么:

1、标记为 的顶部四个字节t的值为0xC。

2、低四个字节始终等于0。

3、的值t是用作指令第三个参数的寄存器的索引。

4、0xC = 12,这意味着第三个参数是a12。

指令的第二个字节指定了另外两个标记为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)

其次,让我们使用从值开始的 ID CUSTOM_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

我们来解释一下这段代码的作用:

get_bytes ()函数从 IDA 期望的下一条指令的地址处的二进制文件中读取原始字节。

然后,它检查指令字节是否实际上看起来像 QUOU 指令。

在这种情况下,我们检查第三个字节0xC2和较低的 4 个字节是否0在第一个字节中,如文档中所定义。最后,您需要使用有关 QUOU 指令的信息填充ev_ana_insn方法的insn参数:至少指定指令 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和值。

解析完成后,就可以设置反汇编文本的输出了。

2. 实现ev_out_mnem方法
打开网易新闻 查看精彩图片
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

我们来解释一下这个例子的要点:

ev_out_mnem从NewInstructions类获取指令名称并andoutctx.out_line在 IDA 反汇编窗口中显示文本。

标志DEBUG_PLUGIN改变文本的颜色。您可以将其设置为默认颜色或宏的颜色,以使新指令脱颖而出,这在调试插件时非常方便。

ev_out_mnem输出八个空格,为引擎输出指令参数做好准备。因此,所有内容——代码、空格、代码注释——都将被对齐。

3. 实现ev_out_operand方法
打开网易新闻 查看精彩图片
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_XTENSAfrom ida_bytes import get_bytesfrom ida_idaapi import plugin_t, PLUGIN_PROC, PLUGIN_HIDE, PLUGIN_SKIP, PLUGIN_KEEPfrom ida_ua import o_displ, o_reg, o_imm, dt_dword, OOF_ADDRfrom struct import unpackDEBUG_PLUGIN = TrueNEWINSN_COLOR = COLOR_MACRO if DEBUG_PLUGIN else COLOR_INSNclass 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 等工具支持大多数流行的架构,但了解如何扩展逆向工程工具的功能将增加您的机会。这些知识还将在您使用新架构和解决重要任务时提供帮助。

参考及来源:https://www.apriorit.com/dev-blog/reverse-extend-ida-capabilities-with-python