返回介绍

17.1 编写插件

发布于 2024-10-11 21:05:46 字数 12909 浏览 0 评论 0 收藏 0

所有 IDA 模块(包括插件)都以适用于执行插件的平台的共享库组件实现。在 IDA 的模块化体系结构下,模块不需要导出任何函数。而每个模块必须导出某个特定类的一个变量。就插件而言,这个类叫做 plugin_t ,它在 SDK 的 loader.hpp 文件中定义。

IDA API 发展历程

自 SDK 4.9 以来,Hex-Rays 一直努力在发布新的 IDA 版本时避免更改现有的 API 函数。因此,旧版 IDA 中的二进制插件通常可以直接复制到新版 IDA 中,并且能够正常运行。然而,每个新版本都会引入新函数和新选项来利用 IDA 不断扩展的功能,因此 IDA API 的规模也随之不断增长。随着 SDK 不断发展,Hex-Rays 已废弃了一些很少使用的 API 函数。在废弃某个函数(或任何其他符号)时,Hex-Rays 会将其移至 NO_OBSOLETE_FUNCS 测试宏包含的代码块内。如果你希望确保你的插件(或其他模块)没有使用任何废弃的函数,在包括任何 SDK 头文件之前,你应当定义 NO_OBSOLETE_FUNCS。

为了了解如何创建插件,必须首先了解 plugin_t 类以及其中的数据字段(这个类没有成员函数)。 plugin_t 类的布局如下所示,其中的注释摘自 loader.hpp 文件:

class plugin_t {  
public:  
  int version;          // Should be equal to IDP_INTERFACE_VERSION  
  int flags;            // Features of the plugin  
  int (idaapi* init)(void); // Initialize plugin  
  void (idaapi* term)(void);   // Terminate plugin. This function will be called  
                            // when the plugin is unloaded. May be NULL.  
  void (idaapi* run)(int arg); // Invoke plugin  
  char *comment;               // Long comment about the plugin  
  char *help;           // Multiline help about the plugin  
  char *wanted_name;    // The preferred short name of the plugin  
  char *wanted_hotkey;  // The preferred hotkey to run the plugin  
};

每个插件都必须导出一个名为 PLUGINplugin_t 对象。导出 PLUGIN 对象由 loader.hpp 文件处理,而声明和初始化具体的对象则由你自己负责。因为成功创建插件取决于正确初始化这个对象,下面我们描述它的每个成员的作用。请注意,即使你宁愿使用 IDA 新引入的脚本化插件功能,你仍然需要了解这里的每一个字段,因为它们也用在脚本化插件中。

  • version 。这个成员指出用于构建插件的 IDA 的版本号。通常,它被设置为在 idp.hpp 文件中声明的 IDP_INTERFACE_VERSION 常量。自 SDK 4.9 版对 API 进行标准化以来,这个常量的值一直没有改变。使用这个字段的最初目的是防止由早期版本的 SDK 创建的插件加载到由更新版本的 SDK 创建的 IDA 中。

  • flags 。这个字段包含各种标志,它们规定 IDA 在不同的情况下该如何处理插件。这些标志使用在 loader.hpp 文件中定义的 PLUGIN_XXX 常量的按位组合来设置。一般来说,将这个字段赋值为零就够了。请参阅 loader.hpp 文件,了解每个标志位的意义。

  • init 。这是 plugin_t 类所包含的 3 个函数指针中的第一个指针。这个特殊的成员是一个指向插件的初始化函数的指针。该函数没有参数,返回一个 int 。IDA 调用这个函数,允许加载你的插件。插件初始化将在 17.1.2 节讨论。

  • term 。这个成员是另一个函数指针。当插件卸载时,IDA 将调用相关函数。该函数没有参数,也不返回任何值。在 IDA 卸载你的插件之前,这个函数用于执行插件所需的任何清理任务(释放内存、结束处理、保存状态等)。如果在插件被卸载时,你不需要执行任何操作,你可以将这个字段设置为 NULL。

  • run 。这个成员指向一个函数,只要用户激活(通过热键、菜单项或脚本调用)你的插件,都应调用这个函数。这个函数是任何插件的核心组件,因为用户正是通过它定义插件行为的。将脚本与插件进行比较时,这个函数的行为与脚本语言的行为极相似。这个函数接受唯一一个整数参数(将在 17.1.4 节讨论)且不返回任何值。

  • comment 。这个成员是指向一个字符串的指针,这个字符串代表插件的一条注释。IDA 并不直接使用这个成员,你完全可以将它设置为 NULL。

  • help 。这个成员是指向一个字符串的指针,这个字符串充当一个多行帮助字符串。IDA 并不直接使用这个成员,你完全可以将它设置为 NULL。

  • wanted_name 。这个成员是指向一个字符串的指针,这个字符串保存插件的名称。当一个插件被加载时,这个字符串被添加到 Edit ▶Plugins 菜单中,提供一种激活该插件的方法。对于已加载的插件,你没有必要对它们使用唯一的名称,但是,从菜单中选择一个插件名称后,如果有两个插件全都使用这个名称,你很难确定到底哪一个插件被激活。

  • wanted_hotkey 。这个成员是指向一个字符串的指针,这个字符串保存 IDA 尝试与插件关联的热键(如 ALT+F8)的名称。同样,这时 IDA 也不要求你对已加载的插件使用唯一的名称。但是,如果插件的名称并不唯一,热键将与请求关联的最后一个插件相关联。17.4 节讨论了用户该如何重写 wanted_hotkey 值。

下面是一个初始化 plugin_t 对象的例子:

int idaapi idaboook_plugin_init(void);  
void idaapi idaboook_plugin_term(void);  
void idaapi idaboook_plugin_run(int arg);  

char idabook_comment[] = "This is an example of a plugin";  
char idabook_name[] = "Idabook";  
char idabook_hotkey = "Alt-F9";  

plugin_t PLUGIN = {  
   IDP_INTERFACE_VERSION, 0, idaboook_plugin_init, idaboook_plugin_term,  
    idaboook_plugin_run, idabook_comment, NULL, idabook_name, idabook_hotkey  
};  

plugin_t 类所包含的函数指针允许 IDA 定位你的插件所需的函数,而不需要你导出这些函数,或为它们选择特定的名称。

17.1.1 插件生命周期

一般的 IDA 会话从启动 IDA 应用程序本身开始,然后是加载和自动分析一个新的二进制文件或现有的数据库,最后等待用户交互。在这个过程中,IDA 为插件提供了 3 个加载的机会。

  1. 插件可以在 IDA 启动后立即加载,而不管数据库是否加载。PLUGIN.flags 中的 FLUGIN_FIX 位控制这种加载方式。

  2. 插件可以在处理器模块加载后立即加载,并且在处理器模块卸载前一直驻留在内存中。 PLUGIN.flags 中的 FLUGIN_PROC 位控制插件与处理器模块之间的关联。

  3. 如果不存在上面提到的标志位,则每次 IDA 打开一个数据库,IDA 都为插件提供加载机会。

IDA 通过调用 PLUGIN.init 为插件提供加载机会。一旦被调用, init 函数应根据 IDA 的当前状态,决定是否加载插件。加载插件时,“当前状态”的意义取决于上述 3 种情形中的适用情形。插件可能感兴趣的状态包括输入文件类型(例如,插件可能专门为 PE 文件设计)和处理器类型(插件可能专门为 x86 二进制文件设计)。

为了向 IDA 表达它的“愿望”, PLUGIN.init 必须返回以下在 loader.hpp 文件中定义的一个值。

  • PLUGIN_SKIP 。返回这个值表示不应加载插件。

  • PLUGIN_OK 。返回这个值告诉 IDA 为当前数据库加载插件。如果用户使用一个菜单操作或热键激活插件,IDA 将加载该插件。

  • PLUGIN_KEEP 。返回这个值告诉 IDA 为当前数据库加载插件,并且使插件驻留在内存中。

插件加载后,你可以通过两种方式激活它。使用菜单项或热键是激活插件的最常用方法。每次以这种方式激活插件,IDA 将调用 PLUGIN.run ,将控制权转交给插件。另一种激活插件的方法是使插件“钩住”IDA 的事件通知系统。在这种情况下,插件必须对一种或多种类型的 IDA 事件表示兴趣,并注册一个回调函数,以便在发生有趣的事件时供 IDA 调用。

在卸载插件时,IDA 将调用 PLUGIN.term (假设它不是 NULL )。卸载插件的情形因 PLUGIN.flags 中设置的位而异。没有指定标志位的插件将根据 PLUGIN.init 返回的值进行加载。如果要加载插件的数据库关闭,插件也随之卸载。

如果一个插件指定了 PLUGIN_UNL 标志位,则每次调用 PLUGIN.run 后,该插件将被卸载。随后每次激活这些插件时,都必须重新加载(导致调用 PLUGIN.init )它们。如果插件指定了 PLUGIN_PROCS 标志位,在它们为其加载的处理器模块卸载后,它们也随之卸载。一旦数据库关闭,处理器模块也随之卸载。最后,指定了 PLUGIN_FIX 标志位的插件只有在 IDA 本身终止时才会卸载。

17.1.2 插件初始化

插件分两个阶段初始化。插件的静态初始化发生在编译时,而动态初始化则在加载时通过在 PLUGIN.init 中执行的操作来完成。如前所述, PLUGIN.flags 字段(在编译时初始化)规定了插件的几个行为。

在 IDA 启动时,它会检查<IDADIR>/plugins 目录中每个插件的 PLUGIN.flags 字段。IDA 为每个指定了 PLUGIN_FIX 标志的插件调用 PLUGIN.init 函数。 PLUGIN_FIX 插件在任何其他 IDA 模块之前加载,因此,它们有机会获知 IDA 能生成的任何事件,包括由加载器模块和处理器模块生成的通知。一般而言,这些插件的 PLUGIN.init 函数应返回 PLUGIN_OKPLUGIN_KEEP ,因为如果要 PLUGIN.init 返回 PLUGIN_SKIP ,那么在 IDA 启动时请求加载这些插件就没有任何意义。

但是,如果插件用于在 IDA 启动时执行一次性的初始化任务,你可以考虑在插件的 init 函数中执行这个任务,并返回 PLUGIN_SKIP ,指出你不再需要这个插件。

每次加载一个处理器模块时,IDA 将对每一个可用的插件进行 PLUGIN_PROC 标志取样,并为每一个设置 PLUGIN_PROC 标志的插件调用 PLUGIN.initPLUGIN_PROC 标志允许将要加载的插件响应处理器模块生成的通知,从而补充这些模块的行为。这些模块的 PLUGIN.init 函数可以访问全局 processor_t 对象 ph ,检查这个对象,并根据检查结果决定应忽略还是保留插件。例如,如果 IDA 加载的是 x86 处理器模块,则专门供 MIPS 处理器模块使用的插件可能会返回 PLUGIN_SKIP ,如下所示:

int idaapi mips_init() {  
   if (ph.id != PLFM_MIPS) return PLUGIN_SKIP;  
   else return PLUGIN_OK;  //or, alternatively PLUGIN_KEEP  
}

最后,每次加载或创建一个数据库时,IDA 都调用每个尚未加载的插件的 PLUGIN.init 函数,以确定是否应加载这些插件。这时,每个插件可能会使用许多标准来决定 IDA 是否应保留自己。如果插件提供特定于某些文件类型(ELF 、PE、Mach-O 等)、处理器类型或编译器类型的行为,这些插件即属于专用插件。

无论出于什么原因,如果一个插件决定返回 PLUGIN_OK (或 PLUGIN_KEEP ),则 PLUGIN.init 函数还应执行一次性初始化操作,以确保插件在最初被激活时能够正常运行。 PLUGIN.init 请求的任何资源都应在 PLUGIN.term 中释放。 PLUGIN_OKPLUGIN_KEEP 的一个主要不同在于, PLUGIN_KEEP 可防止一个插件被反复加载和卸载,因而不必像一个指定了 PLUGIN_OK 的插件那样,需要分配、释放和重新分配资源。作为一条通用原则,如果将来对插件的调用取决于之前调用插件过程中积累的状态, PLUGIN.init 应返回 PLUGIN_KEEP 。为了避免这种情况,插件可以使用诸如网络节点之类的永久存储机制,将状态信息存储在打开的 IDA 数据库中。使用这种技巧,随后的插件调用就可以定位和利用早期的插件调用存储的数据。这种方法具有很大的优点,它可以为整个插件调用过程乃至所有 IDA 会话提供永久性存储。

对于插件而言,如果每次调用都与前一次调用无关, PLUGIN.init 通常会返回 PLUGIN_OK 。这样,由于加载到内存中的插件更少,IDA 的内存占用也更少。

17.1.3 事件通知

用户经常通过菜单选择(Edit ▶Plugins )或热键直接激活插件,不过 IDA 的事件通知功能提供了另一种激活插件的方法。

如果希望插件知道 IDA 中发生的某事件,你必须注册一个回调函数,对这类事件表示兴趣。 hook_to_notification_point 函数用于告诉 IDA 你对某类事件感兴趣,并且每次在指定的类中发生这类事件时,IDA 应调用该函数。使用 hook_to_notification_point 函数对数据库事件表示兴趣的例子如下所示:

//typedef for event hooking callback functions (from loader.hpp)  
typedef int idaapi hook_cb_t(void *user_data, int notification_code, va_list va);  
//prototype for  hook_to_notification_point (from loader.hpp)  
bool hook_to_notification_point(hook_type_t hook_type,  
                                hook_cb_t *callback,  
                                void *user_data);  
int idaapi idabook_plugin_init() {  
   //Example call to  hook_to_notification_point  
   hook_to_notification_point(HT_IDB, idabook_database_cb, NULL);  
}

通知共分为 4 大类:处理器通知(ida.hpp 中的 idp_nofityHT_IDP )、用户界面通知(kernwin.hpp 中的 ui_notification_tHT_UI )、调试器事件(dbg.hpp 中的 dbg_notificationHT_DBG )和数据库事件(idp.hpp 中的 idp_event_tHT_IDB )。在每一类事件中,都有大量通知代码用来表示你将会收到通知的特定事件。数据库( HT_IDB )通知的例子包括 idb_event::byte_ patched ,它指出一个数据库字节已被修补;还包括 idb_event::cmt_changed ,它指出一个常规注释或可重复注释已被修改。每次发生事件时,IDA 都会调用你注册的回调函数,传递特定的事件通知代码和特定于该通知代码的其他参数。定义每段通知代码的 SDK 头文件详细说明了向每段通知代码提供的参数。

继续前面的例子,我们可以定义一个回调函数处理数据库事件,如下所示:

   int idabook_database_cb(void *user_data, int notification_code, va_list va) {  
      ea_t addr;  
      ulong original, current;  
      switch (notification_code) {  
         case idb_event::byte_patched:  
➊          addr = va_arg(va, ea_t);  
            current = get_byte(addr);  
            original = get_original_byte(addr);  
            msg("%x was patched to %x.  Original value was %x\n",  
                 addr, current, original);  
            break;  
      }  
    return 0;  
  }  

这个特殊的例子仅识别 byte_patched 通知消息,它打印被修补字节的地址、该字节的新值以及该字节的初始值。通知回调函数利用 C++ 可变参数列表 va_list ,根据自己收到的通知代码,访问一组数量可变的参数。定义每一段通知代码的头文件指定了为每一段通知代码提供的参数的数量和类型。 byte_patched 通知代码在 loader.hpp 文件中定义,接收它的 va_list 中的一个 ea_t 类型的参数。C++ va_arg 宏可用于从 va_list 中检索连续的参数。在前面的例子中,被修补的字节的地址从➊处的 va_list 中检索出来。

下面是对数据库通知事件解除挂钩的一个例子:

void idaapi idabook_plugin_term() {  
   unhook_from_notification_point(HT_IDB, idabook_database_cb, NULL);  
}

只要卸载了功能正常的插件,就应解除它与任何通知之间的挂钩。这也是 PLUGIN.term 函数的作用之一。如果未能对所有活动的通知解除挂钩,几乎可以肯定,IDA 会在你的插件卸载后不久崩溃。

17.1.4 插件执行

迄今为止,我们已经讨论了几个实例,说明 IDA 如何调用属于某插件的函数。插件加载和卸载操作分别需要调用 PLUGIN.initPLUGIN.term 函数。用户通过 Edit ▶Plugins 菜单或与插件关联的热键激活插件后,IDA 将调用 PLUGIN.run 函数。最后,你可能需要调用插件注册的回调函数,以响应 IDA 中发生的各种事件。

无论以何种方式执行插件,必须记住几个重要的事实。插件函数从 IDA 的主要事件处理循环中调用。如果插件正在执行,IDA 将无法处理事件,包括已排序的分析任务和用户界面更新。因此,你的插件必须尽可能迅速地执行它的任务,然后将控制权返还给 IDA 。否则,IDA 将完全无法响应,也就没有办法重新获得控制权。换句话说,一旦插件开始执行,你就很难让它中断。你要么等待插件完成执行,要么终止 IDA 进程。在后一种情况下,你可能已经打开了一个数据库,该数据库可能会受到破坏,而 IDA 却不一定能修复。SDK 提供了 3 种函数帮助你解决这个问题。可以调用 show_wait_box 函数显示一个对话框,其中显示消息“Please wait…”以及一个 Cancel 按钮。你可以调用 wasBreak 函数,定期验证用户是否单击了 Cancel 按钮。使用这种方法的好处在于,一旦 wasBreak 被调用,IDA 将利用这个机会更新用户界面,你的插件也有机会决定是否终止它所执行的操作。无论如何,你必须调用 hide_wait_box 从窗口中移除等待对话框。

请不要尝试对插件进行任何创新,不要让 PLUGIN.run 函数创建一个新的线程来处理插件所执行的任务。IDA 不是一个线程安全的应用程序。它没有锁定机制来同步对 IDA 使用的许多全局变量的访问,也没有任何锁定机制来确保数据库事务的“原子性”。换言之,如果你确实创建了一个新的线程,并且使用 SDK 函数通过该线程修改了数据库,那么你可能会破坏数据库。因为这时 IDA 可能正在修改数据库,这一操作将与你要做的修改产生冲突。

请记住这些限制。对多数插件而言,由插件完成的大部分工作将在 PLUGIN.run 中执行。基于我们前面初始化的 PLUGIN 对象, PLUGIN.run 的最短(也令人乏味)实现代码如下所示:

void idaapi idabook_plugin_run(int arg) {  
   msg("idabook plugin activated!\n");  
}

每个插件都拥有供其使用的 C++ 和 IDA API 。将插件与特定于平台的库链接起来,你还可以实现其他功能。例如,为 Windows 版本的 IDA 开发的插件可以使用全部的 Windows API。如果除了在消息窗口中打印一条消息外,还想实现更加复杂的功能,你需要了解如何利用可用的 IDA SDK 函数来完成任务。例如,利用代码清单 16-6 可以开发出以下函数:

void idaapi extended_plugin_run(int arg) {  
   func_t *func = get_func(get_screen_ea());  //get function at cursor location  
   msg("Local variable size is %d\n", func->frsize);  
   msg("Saved regs size is %d\n", func->frregs);  
   struc_t *frame = get_frame(func);          //get pointer to stack frame  
   if (frame) {  
      size_t ret_addr = func->frsize + func->frregs;  //offset to return address  
      for (size_t m = 0; m < frame->memqty; m++) {    //loop through members  
         char fname[1024];  
         get_member_name(frame->members[m].id, fname, sizeof(fname));  
         if (frame->members[m].soff < func->frsize) {  
            msg("Local variable ");  
         }  
         else if (frame->members[m].soff > ret_addr) {  
            msg("Parameter ");  
         }  
         msg("%s is at frame offset %x\n", fname, frame->members[m].soff);  
         if (frame->members[m].soff == ret_addr) {  
            msg("%s is the saved return address\n", fname);  
         }  
      }  
   }  
}

使用这个函数,现在我们有了插件的核心组件,每次你激活这个插件时,它将存储与当前选定的函数有关的栈帧信息。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文