返回介绍

25.5 处理异常

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

有时候,程序希望自行处理它们在执行过程中生成的任何异常。如第 21 章所述,模糊程序常常有意生成异常,以此作为一种反控制流和反调试技巧。但异常通常表示存在问题,而调试器的目的则是帮助你解决这些问题。因此,调试器往往希望处理在程序运行过程中发生的所有异常,以帮助你找到 bug。

如果程序希望自行处理异常,我们需要阻止调试器拦截这些异常,或者在异常被拦截时,我们至少需要采取办法,让调试器将异常转交给我们控制的进程。好在 IDA 的调试器能够传递各个异常,或者自动传递指定类型的所有异常。

自动异常处理通过 Debugger ▶Debugger Options 命令配置,其对话框如图 25-7 所示。

enter image description here

图 25-7 “调试器设置”对话框

我们可以配置几个事件自动中止调试器,并可以将大量事件自动记录到 IDA 的消息窗口中,除此以外,“调试器设置”对话框可用于配置调试器的异常处理行为。Edit exceptions 按钮打开如图 25-8 所示的异常配置对话框。

enter image description here

图 25-8  异常配置对话框

对于调试器已知的每一种异常类型,这个对话框列出了一个特定于操作系统的异常代码、异常的名称、调试器是否中止进程( Stop/No ),以及调试器是否会处理异常,或自动将异常传递给应用程序处理( Debugger/Application )。 <IDADIR>/cfg/exceptions.cfg 文件中包含一个主要异常列表以及处理每个异常的默认设置。此外,这个配置文件中还包含一些消息,如果调试器正在执行进程时发生给定类型的异常,这些消息将显示出来。你可以使用一个文本编辑器编辑 exceptions.cfg 文件,更改调试器的默认异常处理行为。在 exceptions.cfg 中,值 stopnostop 用于指出:当一个给定异常发生时,调试器是否应将进程挂起。

你还可以通过异常配置对话框编辑各个异常,逐个会话地(也就是说,在打开特定数据库的同时)配置异常处理。要修改调试器对于一个给定异常类型的行为,在“异常配置”对话框中右击需要修改的异常,并选择 Edit ,得到的“异常处理”对话框如图 25-9 所示。

enter image description here

图 25-9 “异常处理”对话框

其中的两个选项对应于 exceptions.cfg 文件中的两个可配置选项,你可以为任何异常配置这些选项。通过第一个选项,你可以指定,当指定类型的异常发生时,调试器是否应中止进程,或者执行是否继续。需要注意的是,如果让调试器处理异常,允许进程继续执行会导致无限的异常生成循环。

通过第二个配置选项可以决定,是否应将一个给定类型的异常传递给被调试的应用程序,以便该应用程序有机会使用它自己的异常处理程序处理这个异常。如果一个应用程序的正常运行需要这类异常处理程序被执行,你应该选择将相关类型的异常传递给该应用程序处理。在分析模糊代码,如第 21 章介绍的 tElock 实用工具(它注册有它自己的异常处理程序)生成的模糊代码时,你可能需要这样做。

除非你已配置 IDA 继续执行并向应用程序传递特定的异常类型,否则 IDA 将暂停执行,并在发生异常时向你报告异常。如果你选择继续执行程序,IDA 将显示如图 25-10 所示的 Exception Handling(异常处理)对话框。

enter image description here

图 25-10 Exception Handling 对话框

这时,你可以选择更改 IDA 处理给定异常类型的方式(Change exception definition)、向应用程序传递异常(Ye s),或允许 IDA 忽略异常(No)。如果向应用程序传递异常,应用程序将使用任何已配置的异常处理程序来处理异常。如果选择“No”,IDA 将尝试继续执行,但如果你没有更正负责引发异常的条件,这样做可能会导致故障。

如果你正在遍历代码,并且 IDA 确定你即将执行的指令会生成异常,这时会出现一种特殊情况,就像是将设置跟踪标记的 int 3icebppopf 一样。此时 IDA 会显示如图 25-11 所示的对话框。

enter image description here

图 25-11 异常确认对话框

多数情况下,Run 选项都是最适当的选项,如果没有依附调试器,这时应用程序会看到它所期待的行为(见图 25-11 所示的对话框)。通过此对话框,你确认某个异常即将生成。如果你选择 Run,你会立即收到发生异常的通知;当你继续执行时,你将会看到图 25-10 中的 Exception Handling 对话框,以确定应如何处理异常。

要确定应用程序如何处理异常,我们需要了解如何跟踪异常处理程序,这又需要我们知道如何定位异常处理程序。在一篇名为“Tracing exception handler”1 的博客文章中,Ilfak 讨论了如何跟踪 Windows SEH 处理程序。其基本的概念是搜索应用程序的已安装异常处理程序列表,定位其中有用的异常处理程序。对于 Windows SEH 异常,有一个指向该列表顶部的指针,它是线程环境块(TEB )中的第一个双字。异常处理程序列表是一个标准的链表数据结构,其中包含一个指向链中下一个异常处理程序的指针,以及一个指向处理生成的异常的函数的指针。异常在列表中由一个处理程序往下传递给另一个处理程序,直到选中一个处理程序来处理异常,并通知操作系统进程将继续正常执行。如果没有选中已安装的异常处理程序来处理当前的异常,则操作系统会终止进程,或者当进程被调试时,操作系统会通知调试器,被调试的进程中发生了一个异常。

1. 参见 http://www.hexblog.com/2005/12/tracing_exception_handlers.html

在 IDA 调试器中,TEB 被映射到一个名为 TIB [NNNNNNNN] 的 IDA 数据库段中,这里的 NNNNNNNN 是线程标识号的 8 位十六进制表示形式。这段中的第一个双字如下所示:

    TIB[000009E0]:7FFDF000 TIB_000009E0_ segment byte public 'DATA' use32  
    TIB[000009E0]:7FFDF000 assume cs:TIB_000009E0_  
    TIB[000009E0]:7FFDF000 ;org 7FFDF000h  
➊  TIB[000009E0]:7FFDF000 dd offset dword_22FFE0

前 3 行显示该段的摘要信息,而第四行(➊)包含该段的第一个双字,它指出:第一个异常处理程序记录可以在地址 22FFE0h``(offset dword_22FFE0) 处找到。如果 IDA 没有为这个特殊的线程安装异常处理程序,则 TEB 中的第一个双字将包含值 0FFFFFFFFh ,表示已经到达异常处理程序链的结尾部分。在这个例子中,分析地址 22FFE0h 处的两个双字,得到以下结果:

Stack[000009E0]:0022FFE0 ➊dword_22FFE0 dd 0FFFFFFFFh ;DATA XREF:TIB[000009E0]:7FFDF000↓o
Stack[000009E0]:0022FFE4             ➋ dd offset loc_7C839AA8

第一个双字(➊)包含值 0FFFFFFFFh ,表示这是链中的最后一个异常处理程序记录。第二个双字(➋)包含地址 7C839AA8h(offset loc_7C839AA8) ,表示应调用 loc_7C839AA8 处的函数来处理进程执行过程中发生的任何异常。如果要跟踪这个进程如何处理异常,首先可以在地址 7C839AA8h 处设置一个断点。

由于搜索 SEH 链是一个相对简单的任务,调试器可以执行一个有用的功能:在一个窗口中显示为当前线程安装的 SEH 处理程序链。通过这样一个窗口,你可以轻易导航到每一个 SEH 处理程序,这时你可以决定是否在处理程序中插入一个断点。不过,这是 OllyDbg 的另一个功能,而 IDA 的调试器并没有这个功能。为了弥补这个缺点,我们开发了一个 SEH 链插件,如果从调试器中调用,这个插件可以在一个窗口中显示为当前线程安装的异常处理程序列表。图 25-12 是这个窗口的一个示例。

enter image description here

图 25-12 SEH 链窗口

这个插件利用 SDK 的 choose2 函数显示一个非模式对话框,列出当前的异常处理程序链。对于每一个已安装的异常处理程序,对话框显示异常处理程序记录的地址(双字列表记录)及对应的异常处理程序的地址。双击一个异常处理程序,活动反汇编窗口(IDA View-EIP 或 IDA View-ESP)将跳转到该 SEH 异常处理函数的地址。这个插件的唯一目的在于简化定位异常处理程序的过程。读者可以在本书的网站上找到 SEH 链插件的源代码。

有关异常处理过程的另一个问题在于异常处理程序如何将控制权转交(如果它选择这样做)给其中发生异常的应用程序。如果操作系统调用一个异常处理函数,它将允许该函数访问 CPU 寄存器在发生异常时设置的所有内容。在处理异常的过程中,该函数可能会修改一个或几个 CPU 寄存器值,然后将控制权转交给应用程序。这样做是为了让异常处理程序有机会修复进程的状态,从而使进程继续正常执行。如果异常处理程序决定让该进程继续执行,它将使用异常处理程序所做的修改向操作系统发出通知,并还原该进程的寄存器值。如第 21 章所述,一些反逆向工程实用工具通过在异常处理阶段修改指令指针的保存值,利用异常处理程序更改进程的执行流。这时如果操作系统将控制权转交给该进程,这个进程将在修改后的指令指针指定的地址处恢复执行。

在有关跟踪异常的博客文章中,Ilfak 讨论了一个事实,即 Windows SEH 异常处理程序通过 ntdll.dll 函数 NtContinue (也叫做 ZwContinue )将控制权转交给受影响的进程。由于 NtContinue 已经访问了该进程保存的所有寄存器值(通过它的一个参数),因此通过分析 NtContinue 中保存的指令指针所包含的值,我们可以确定该进程到底在什么地方恢复执行。只要知道该进程将在什么地方恢复执行,我们就可以设置一个断点,以避免步入操作系统代码,并在进程恢复执行前尽早令进程中止。上述过程可以按以下步骤执行。

  1. 定位 NtContinue 并在它的第一条指令上设置一个非中止断点。

  2. 给这个断点添加一个断点条件。

  3. 到达该断点时,通过读取栈中 CONTEXT 指针的内容获得所保存的寄存器的地址。

  4. CONTEXT 记录中获取该进程保存的指令指针的值。

  5. 在得到的地址上设置一个断点,并让程序继续执行。

使用一个与隐藏调试器脚本类似的进程,我们可以自动完成所有这些任务,并将它们与启动一个调试会话关联起来。下面的代码说明如何在调试器中启动一个进程,并在 NtContinue 上设置一个断点:

 static main() {  
   auto func;  
   RunTo(BeginEA());  
   GetDebuggerEvent(WFNE_SUSP, -1);  
   func = LocByName("ntdll_NtContinue");  
   AddBpt(func);  
   SetBptCnd(func, "bpt_NtContinue()");  
}

这段代码的目的很简单,即在 NtContinue 的入口处设置一个条件断点。该断点的行为通过下面的 IDC 函数 bpt_NtContinue 执行:

   static bpt_NtContinue() {  
➊     auto p_ctx = Dword(ESP + 4);            //get CONTEXT pointer argument  
➋     auto next_eip = Dword(p_ctx + 0xB8);    //retrieve eip from CONTEXT  
➌     AddBpt(next_eip);                  //set a breakpoint at the new eip  
➍     SetBptCnd(next_eip, "Warning(\"Exception return hit\") || 1");
       return 0;           //don’t stop
    }

这个函数首先定位指向上述进程保存的寄存器上下文信息的指针(➊),从 CONTEXT 结构体中偏移量为 0xB8 处获取所保存的指令指针值(➋),然后在该地址上设置一个断点(➌)。为了使用户清楚知道执行为什么会中止,这个函数增加了一个断点条件(始终为真),以向用户显示一条消息(➍)。我们之所以这样做,是因为该断点并不是由用户显式设置,而且用户可能没有将这一事件与异常处理程序的返回关联起来。

这个例子提供了一个简单的方法,说明如何处理异常返回。我们可以在断点函数 bpt_Nt- Continue 中添加更加复杂的逻辑。例如,如果你怀疑一个异常处理程序正操纵调试寄存器的内容来阻止你设置硬件断点,那么在将控制权返还给被调试的进程之前,你可以将调试寄存器的值恢复到一个已知正确的值。

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

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

发布评论

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