返回介绍

24.4 调试器任务自动化

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

在第 15 章~第 19 章中,我们讨论了 IDA 脚本和 IDA SDK 的基础知识,并说明了这些功能在静态分析二进制文件时的作用。当你启动一个进程,并且在调试器这样更加动态的环境中工作时,脚本和插件仍然能够发挥重要的作用。脚本和插件的自动化应用包括:在调试进程时分析运行时数据,执行复杂的断点条件,采取措施破坏反调试技巧。

24.4.1 为调试器操作编写脚本

在使用 IDA 调试器时,我们在第 15 章讨论的 IDA 脚本功能仍然有效。脚本可以通过 File 菜单启动,与热键关联,由 IDA 的脚本命令行调用。此外,断点条件和跟踪终止表达式也可以引用用户创建的 IDA 函数。

基本的脚本函数可以设置、修改和枚举断点,读取和写入寄存器与内存值。内存访问功能由 DbgBytePatchDbgByteDbgWordPatchDbgWordDbgDwordPatchDbgDword 函数提供(类似于第 15 章中描述的 ByteWordDwordPatchXXX 函数)。寄存器和断点操作由以下函数(请参阅 IDA 帮助文件了解全部函数)实现。

  • long GetRegValue``(string reg) 。如前所述,返回已命名寄存器的值,如 EAX。只有在 IDC 中,寄存器的值还可以通过在 IDC 表达式中使用该寄存器的名称来访问。

  • bool SetRegValue``(number val, string name) 。返回已命名寄存器的值,如 EAX。如果正在使用 IDC ,通过使用赋值语句左侧的相关寄存器名称,你还可以修改寄存器值。

  • bool AddBpt``(long addr) 。在指定的地址添加一个软件断点。

  • bool AddBptEx``(long addr, long size, long type) 。在指定的地址添加一个指定大小和类型的断点。断点类型应为 idc.idc 或 IDA 帮助文件中描述的一个 BPT_XXX 常量。

  • bool DelBpt``(long addr) 。删除指定地址处的一个断点。

  • long GetBptQty``() 。返回在程序中设置的断点的总数。

  • long GetBptEA``(long bpt_num) 。返回指定断点所在的地址。

  • long/string GetBptAttr``(long addr, number attr) 。返回与指定地址处的断点有关的一个断点属性。根据你请求的属性值,返回值可能为一个数字或字符串。使用 idc.idc 文件或者 IDA 帮助文件描述的一个 BPTATTR_XXX 值可指定属性。

  • bool SetBptAttr``(long addr, number attr, long value) 。将指定断点的指定属性设置为指定值。不要使用这个函数设置断点条件表达式(而应使用 SetBptCnd 设置)。

  • bool SetBptCnd``(long addr, string cond) 。将断点条件设置为所提供的条件表达式,这个表达式必须为一个有效的 IDC 表达式。

  • long CheckBpt``(long addr) 获取指定位置的断点状态。返回值指示是否没有断点,是否已禁用断点,是否已启用断点或断点是否处于活动状态。活动断点指在调试器也处于活动状态时启用的断点。

下面的脚本说明如何在当前光标位置安装一个自定义的 IDC 断点处理函数:

#include   
/*  
 * The following should return 1 to break, and 0 to continue execution.  
 */  
static my_breakpoint_condition() {  
   return AskYN(1, "my_breakpoint_condition activated, break now?") == 1;  
}  
/*  
 * This function is required to register my_breakpoint_condition  
 * as a breakpoint conditional expression  
 */  
static main() {  
   auto addr;  
   addr = ScreenEA();  
   AddBpt(addr);  
   SetBptCnd(addr, "my_breakpoint_condition()");  
}

my_breakpoint_condition 的复杂程度完全由你自己决定。在这个例子中,每次遇到一个新的断点,都会显示一个对话框,询问用户是想要继续执行进程,还是在当前位置暂停。调试器使用 my_breakpoint_condition 返回的值决定是实践断点,还是忽略断点。

从 SDK 及通过使用脚本均能够以编程方式控制调试器。在 SDK 内,IDA 利用事件驱动模型并在发生特定的调试器事件时向插件提供回调通知。遗憾的是,IDA 的脚本功能并不便于在脚本中使用事件驱动范型。因此,Hex-Rays 引入了许多用于从脚本中同步控制调试器的脚本函数。使用脚本驱动调试器的基本方法是开始一个调试器操作,然后等待对应的调试器事件代码。记住,调用一个同步调试器函数(这是你在脚本中能够执行的所有操作)时,IDA 的所有其他操作将被阻止,直到这个调用完成。下面详细说明了几个调试扩展。

  • long GetDebuggerEvent``(long wait_evt, long timeout) 。在指定的秒数内(1 表示永远等待)等待一个调试器事件(由 wait_evt 指定)发生。返回一个事件类型,指出收到的事件的类型。使用一个或几个 WFNE_XXX (WFNE 表示 Wait For Next Event)标志指定 wait_evt 。请参阅 IDA 帮助文件了解可能的返回值。

  • bool RunTo(long addr) 。运行进程,直到到达指定的位置或遇到一个断点。

  • bool StepInto() 。按指令逐步运行进程,步入任何函数调用。

  • bool StepOver() 。按指令逐步运行进程,跨过任何函数调用。遇到断点时该调用可能提前终止。

  • bool StepUntilRet() 。运行进程,直到当前函数调用返回或遇到一个断点。

  • bool EnableTracing(long trace_level, long enable) 。启用(或禁用)跟踪事件的生成。 trace_level 参数应设置为在 idc.idc 中定义的一个 TRACE_ XXX 常量。

  • long GetEventXXX() 。有许多函数可用于获取与当前调试事件有关的信息。其中一些函数仅对特定的事件类型有效。你应该测试 GetDebuggerEvent 的返回值,以确保某个 GetEventXXX 函数可用。

为了获取调试器的事件代码,在每一个使进程执行的函数返回后,必须调用 GetDebugger- Event 。如果不这样做,随后单步执行或运行进程的尝试将会失败。例如,下面的代码只会单步执行调试器一次,因为它没有在两次调用 StepOver 之间调用 GetDebuggerEvent ,以清除上一个事件类型。

StepOver();  
StepOver();    //this and all subsequent calls will fail  
StepOver();  
StepOver();

正确的执行方法是在每次调用 StepOver 之后调用一次 GetDebuggerEvent ,如下所示:

StepOver();  
GetDebuggerEvent(WFNE_SUSP, -1);  
StepOver();  
GetDebuggerEvent(WFNE_SUSP, -1);  
StepOver();  
GetDebuggerEvent(WFNE_SUSP, -1);  
StepOver();  
GetDebuggerEvent(WFNE_SUSP, -1);

调用 GetDebuggerEvent 后,即使你选择忽略 GetDebuggerEvent 的返回值,执行仍将继续。事件类型 WFNE_SUSP 表示我们要等待一个使被调试的进程挂起的事件,如一个异常或断点。你可能已经注意到,并没有函数可以恢复执行一个被挂起的进程1 。但是,通过在调用 GetDebugger-Event 时使用 WFNE_CONT 标志,我们可以达到相同的目的,如下所示:

1. 实际上有一个定义为 GetDebuggerEvent(WFNE_CONT|WFNE_NOWAIT,O) 的宏 Resume Process

GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1);

这个特殊的调用通过继续由当前指令位置执行进程,等待初步恢复执行后的下一个可用的挂起事件。

其他函数可用于自动启动调试器和依附正在运行的进程。请参阅 IDA 帮助文件,了解这些函数的更多信息。

下面是一个简单的调试器脚本,通过它可以收集与被提取的指令的地址有关的统计信息(假如调试器已经启用),如下所示:

  static main() {  
     auto ca, code, addr, count, idx;  
➊    ca = GetArrayId("stats");  
     if (ca != -1) {  
         DeleteArray(ca);  
     }  
     ca = CreateArray("stats");  
➋    EnableTracing(TRACE_STEP, 1);  
➌    for (code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, -1); code > 0  
             code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, -1)) {  
➍         addr = GetEventEa();  
➎         count = GetArrayElement(AR_LONG, ca, addr) + 1;  
➏         SetArrayLong(ca, addr, count);  
      }  
     EnableTracing(TRACE_STEP, 0);  
➐    for (idx = GetFirstIndex(AR_LONG, ca);  
             idx != BADADDR;  
             idx = GetNextIndex(AR_LONG, ca, idx)) {  
        count = GetArrayElement(AR_LONG, ca, idx);  
        Message("%x: %d\n", idx, count);  
     }  
➑    DeleteArray(ca);  
    }

这个脚本首先测试一个名为 stats 的全局数组是否存在(➊ )。如果存在,则删除该数组,再重建一个,以便我们可以使用一个空数组。接下来,启用单步跟踪(➋ ),然后进入一个循环(➌ )开始单步执行进程。每次生成一个调试事件,将获取相关事件的地址(➍ ),相关地址的当前总数从全局数组中获取,并不断递增(➎ ),该数组将根据新的总数更新(➏)。注意,这里的指令指针用做稀疏全局数组的索引,使我们免于查找其他数据结构的地址,从而节省大量时间。整个过程结束后,再使用第二个循环(➐ )获取并打印所有拥有有效值的数组位置的值。在这个例子中,拥有有效值的数组索引代表被提取指令的地址。最后,这个脚本删除用于收集统计信息的全局数组(➑)。这个脚本的示例输出如下所示:

401028: 1  
40102b: 1  
40102e: 2  
401031: 2  
401034: 2  
401036: 1  
40103b: 1

稍作修改后,前面的例子可用于收集有关指令类型的统计信息,即在一个进程执行过程中,有哪些类型的指令被执行。下面的例子说明如何对第一个循环进行必要的修改,以收集指令类型数据(而非地址数据):

   for (code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, -1); code > 0;  
        code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, -1)) {  
      addr = GetEventEa();  
➊   mnem = GetMnem(addr);  
➋   count = GetHashLong(ht, mnem) + 1;  
➌   SetHashLong(ht, mnem, count);  
   }

我们并没有对各个操作码分类,而是选择按助记符将指令分组(➊)。由于助记符是字符串,所以我们利用全局数组的散列表特性获取与一个给定助记符有关的当前总数(➋),并将更新后的总数(➌)保存到对应的散列表条目中。这个修改后的脚本的样本输出如下所示:

add:   18  
and:   2  
call:  46  
cmp:   16  
dec:   1  
imul:  2  
jge:   2  
jmp:   5  
jnz:   7  
js:    1  
jz:    5  
lea:   4  
mov:   56  
pop:   25  
push:  59  
retn:  19  
sar:   2  
setnz: 3  
test:  3  
xor:   7

在第 25 章中,我们将讨论如何利用调试器的迭代功能对二进制文件进行去模糊处理。

24.4.2 使用 IDA 插件实现调试器操作自动化

在第 16 章中,我们了解到,IDA 的 SDK 提供了非常强大的功能,用于开发各种复杂的编译扩展,这些扩展可以与 IDA 集成,并能够访问全部 IDA API 。IDA API 提供了 IDC 的所有功能,调试扩展也不例外。 API 的调试器扩展在<SDKDIR>/dbg.hpp 文件中声明,其中包含与我们迄今为止所讨论的 IDC 函数对应的 C++ 函数,以及全面的异步调试器接口功能。

为了实现异步交互,插件通过“钩住” HT_DBG 通知类型(参见 loader.hpp 文件)访问调试器通知。调试器通知在 dbg.hpp 文件中的 dbg_notification_t 枚举中声明。

在调试器 API 中,用于与调试器交互的命令通常成对定义,一个函数用于同步交互,另一个函数则用于异步交互。一般而言,一个函数的同步形式命名为 COMMAND() ,而对应的异步形式则命名为 request_COMMAND()rehttp://www.ituring.com.cn/article/50699quest_XXX 函数用于对调试器操作排队,以便于随后的处理。排列好异步请求后,必须调用 run_requests 函数,开始处理请求队列。在处理请求时,调试器通知将被传递给你通过 hook_to_notification_point 注册的任何回调函数。

使用异步通知,我们可以为前一节中用于统计地址总数的脚本开发一个异步版本。首先,我们需要配置如何“钩住”和松开调试器通知。我们使用插件的 initterm 方法完成这个任务,如下所示:

//A netnode to gather stats into  
➊ netnode stats("$ stats", 0, true);  

int idaapi init(void) {  
   hook_to_notification_point(HT_DBG, dbg_hook, NULL);  
   return PLUGIN_KEEP;  
}  

void idaapi term(void) {  
   unhook_from_notification_point(HT_DBG, dbg_hook, NULL);  
}

注意,我们还声明了一个全局网络节点(➊),用来收集统计信息。接下来需要考虑,使用分配的热键激活插件后,我们希望它执行什么任务。示例插件的 run 函数如下所示:

    void idaapi run(int arg) {  
        stats.altdel();   //clear any existing stats  
➊      request_enable_step_trace();  
➋      request_step_until_ret();  
➌      run_requests();  
    }

在这个例子中,由于我们使用的是异步技巧,首先我们必须提供一个请求,启动单步跟踪(➊ ),然后提交一个请求恢复被调试的进程。为了简化,我们仅收集与当前函数有关的统计信息,因此我们将提出一个请求,要求运行进程,直到当前函数返回(➋)对请求正确排序后,我们调用 run_requests 处理当前请求的队列(➌),开始运行这个插件。

接下来创建 HT_DBG 回调函数,处理我们希望收到的通知。下面是一个仅处理两条消息的简单回调函数:

int idaapi dbg_hook(void *user_data, int notification_code, va_list va) {  
   switch (notification_code) {  
➊         case dbg_trace:  //notification arguments are detailed in dbg.hpp  
             va_arg(va, thid_t);  
➋            ea_t ea = va_arg(va, ea_t);  
             //increment the count for this address  
➌            stats.altset(ea, stats.altval(ea) + 1);  
             return 0;  
➍         case dbg_step_until_ret:  
             //print results  
➎            for (nodeidx_t i = stats.alt1st(); i != BADNODE; i = stats.altnxt(i)) {
                  msg("%x: %d\n", i, stats.altval(i));  
             }  
             //delete the netnode and stop tracing  
➏            stats.kill();  
➐            request_disable_step_trace();  
➑            run_requests();          break;  
   }  
}

对于执行的每一条指令,我们将收到 dbg_trace 通知(➊ ),直到我们关闭跟踪。收到一个跟踪通知时,从参数列表(➋ )中获取跟踪点的地址,用于更新相应的网络节点数组索引(➌)。进程到达 return 语句,离开我们最初启动的函数时将发送 dbg_step_until_ret 通知(➍ )。这个通知是一个信号,表示我们应停止跟踪,并打印我们收集到的任何统计信息。接下来,我们使用一个循环(➎ )遍历 stats 网络节点的所有有效索引值,然后销毁该网络节点(➏ ),并请求禁用单步跟踪(➐ )。由于这个例子使用的是异步命令,禁用跟踪的请求被添加到队列中,这意味着我们必须调用 run_requests (➑)来处理队列。关于与调试器的同步交互和异步交互,有一个重要的警告:在处理异步通知消息时,你绝不能调用一个函数的同步版本。

使用 SDK 与调试器进行同步交互的方式,与为调试器操作编写脚本的做法非常类似。和我们在前几章中讨论的许多 SDK 函数一样,与调试器有关的函数名称通常与相关的脚本函数名称并不匹配,因此,你可能需要花一些时间搜索 dbg.hpp 文件,查找你需要的函数。脚本函数与 SDK 在名称上的最大差异表现在 SDK 的 GetDebuggerEvent 函数上,在 SDK 中,它叫做 wait_for_ next_event 。脚本函数与 SDK 的另一个主要差异在于 SDK 并不为你自动声明与 CPU 寄存器对应的变量。为了从 SDK 中访问 CPU 寄存器的值,你必须分别使用 get_reg_valset_reg_val 函数读取和写入寄存器。

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

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

发布评论

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