返回介绍

24.3 进程控制

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

所有调试器最重要的功能都是它能够严密监控并修改(如有必要)它所调试的进程的行为。为达到这一目的,大多数调试器都提供了各种命令,用于在将控制权转交给调试器之前执行一条或多条指令。这些命令通常与断点结合起来使用,用户通过这些断点在到达一条指定的指令,或满足一个特定的条件时中断进程。

在调试器的控制下,一个进程的基本执行过程通过使用各种“单步执行”、“继续”和“运行”命令来完成。因为我们要频繁使用这些命令,因此熟悉与它们有关的工具栏按钮和热键组合将对我们有好处。与执行进程有关的工具栏按钮如图 24-8 所示。

enter image description here

图 24-8 调试器进程控制工具

下面我们描述这些命令的行为。

  • 继续(Continue ) 。继续执行一个暂停的进程。执行将继续,直到遇到一个断点、用户暂停或终止执行或者该进程自行终止。

  • 暂停(Pause) 。暂停一个正在运行的进程。

  • 终止(Terminate) 。终止一个正在运行的进程。

  • 步入(Step into ) 。仅执行下一条指令。如果下一条指令是一个函数调用,则在目标函数的第一条指令上停止执行;这个命令叫做“步入”,是因为程序步入了任何被调用的函数。

  • 跨过(Step over ) 。仅执行下一条指令。如果下一条指令是一个函数调用,则将该调用作为一条指令处理,函数返回时立即停止执行。这个命令叫做“跨过”,是因为执行步骤是跳过函数,而不是像“步入”一样是经过函数。如果遇到一个断点,执行可能会在函数调用完成前中断。如果一个函数的行为十分常见,并且没有用处,这时“跨过”就非常有用,使用它可以节省大量时间。

  • 运行至返回(Run Until Return ) 。继续执行当前函数,直到该函数返回(或遇到一个断点)时才停止。如果你多次见到一个函数并希望离开它,或者如果你无意间进入了一个你希望跨过的函数,这时就可以使用这项操作。

  • 运行至光标(Run to Cursor ) 。继续执行进程,直到执行到达当前光标位置(或遇到一个断点)时才停止。这个命令用于运行大块的代码,而不必在你希望暂停的每个位置设置一个永久性的断点。请注意,如果光标位置被跳过或永远无法到达,则程序可能不会暂停。

除了工具栏和热键外,所有执行控制命令还可以通过 Debugger 菜单访问。无论进程在执行一个单步操作或遇到一个断点后是否暂停,每次进程暂停,所有与调试器有关的窗口都会更新,以反映该进程在暂停时的状态(CPU 寄存器、标志和内存内容)。

24.3.1 断点

断点 是调试器的一个特性,它与进程执行和中断(暂停)关系密切。设置断点,是为了在程序中的特定位置中断正在执行的程序。在某种程度上说,断点是“运行至光标”这个概念的永久性扩展,因为在一个给定的地址上设置断点后,一旦执行到达这个位置,无论光标是否仍然位于这个位置,执行总是会立即中断。不过,虽然执行只能运行到一个光标位置,但我们却可以在整个程序中设置许多断点,到达任何一个断点都会使执行中断。在 IDA 中,我们通过导航到你希望使执行暂停的位置来设置断点,或使用 F2 热键(或右击并选择 Add Breakpoint)设置断点。已经设置断点的地址将以一条跨越整个反汇编行的红(默认)带突出显示。你可以再次按下 F2 键关闭一个断点,从而删除它。你可以通过 Debugger ▶Breakpoints ▶Breakpoint List 查看程序中当前已经设置的所有断点。

默认情况下,IDA 会使用 软件断点 ,它用一条软件断点指令替代断点地址所在位置的操作码字节。x86 二进制文件的软件断点指令为 int 3 指令,它使用操作码值 0xCC 。正常情况下,如果执行一条软件断点指令,操作系统会将控制权转交给任何监控被中断进程的调试器。如第 21 章所述,模糊代码可能会利用软件断点的这种行为来阻止任何附加的调试器的正常运行。

一些 CPU(如 x86,具体为 386 及更高版本)支持硬件辅助的 断点 (hardware-assisted breakpoint )以替代软件断点。通常,硬件断点使用专用的 CPU 寄存器配置。在 x86 CPU 中,这些寄存器叫做 DR0-7(调试寄存器 0~7)。使用 x86 寄存器 DR0-3,最多可以指定 4 个硬件断点。剩下的 x86 调试寄存器用于指定每个断点的其他限制。如果启用了一个硬件断点,也就没有必要替换被调试的程序中的某条特殊指令。CPU 会根据调试寄存器中包含的值,决定是否应中断执行。

设置一个断点后,你可以修改它行为的各个方面。除简单地中断进程外,调试器往往还支持 条件断点 (conditional breakpoint )的概念,用户可指定一个条件,在实践断点之前,这个条件必须得到满足。如果到达了这样一个断点,而相关的条件没有得到满足,调试器会自动继续执行相关程序。这样做的总体想法是:相关条件会在将来的某个时候得到满足,因此,只有当你感兴趣的条件得到满足时,程序才会中断。

IDA 调试器支持条件断点和硬件断点。要修改一个断点的默认(无条件的基于软件的)行为,你必须在设置断点后对它进行编辑。要访问“断点编辑”对话框,你必须右击一个现有的断点,并在出现的菜单中选择 Edit Breakpoint。得到的“断点设置”对话框如图 24-9 所示。

enter image description here

图 24-9 “断点设置”对话框

Location 框指定被编辑断点的地址,而 Enabled 复选框说明该断点当前是否处于活动状态。如果一个断点被禁用,无论哪个与该断点有关的条件得到满足,该断点都不会被实践。Hardware 复选框用于请求以硬件断点(而非软件断点)实现该断点。

警告  关于硬件断点的一个警告:虽然任何时刻仅支持 4 个硬件断点,但在编写本书时(IDA6.1),IDA 仍然允许你指定 4 个以上的硬件断点。但是,其中只有 4 个断点被实践,其他的任何断点将被忽略。

在指定一个硬件断点时,你必须使用 Hardware 单选按钮指定断点的行为:是执行则中断、写入则中断,还是读取/写入则中断。后两类行为(写入则中断和读取/写入则中断)可以创建一类特殊的断点,它们只有在某个特定的内存位置(通常为一个数据位置)被访问时才会触发,而不论访问发生时到底执行的是什么指令。如果你更感兴趣的是程序何时访问一组数据,而不是这些数据在什么地方被访问,那么这类断点就非常有用。

除了为硬件断点指定一种模式外,你还必须指定一个大小。执行类断点的大小必须为 1 字节,写入类或读取/写入类断点的大小可以设置为 1、2 或 4 字节。如果将大小设置为 2 字节,则断点的地址必须为字对齐(2 字节的整数倍)。同样,4 字节断点的地址必须为双字对齐(4 字节的整数倍)。硬件断点的大小和它的地址共同构成了触发这类断点的地址范围。下面举例说明。以在地址 0804C834h 处设置的一个 4 字节写入式断点为例,这个断点将由 1 字节写入 0804C837h 、2 字节写入 0804C836h 、4 字节写入 0804C832h 等操作触发。在上述情形中, 0804C834h0804C837h 间至少有一个字节被写入。有关 x86 硬件断点行为的更多信息,请参阅 Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3B: System Programming Guide,Part 21

1. 参见 http://www.intel.com/products/processor/manuals/

在“断点设置”对话框的 Condition 输入框中提供一个表达式,即可创建条件断点。条件断点是一种调试器特性,而不是一种指令集或 CPU 特性。如果一个断点被触发,这时调试器将对任何相关的条件表达式求值,决定是应暂停程序(条件得到满足),还是继续执行程序(条件未满足)。因此,你可以为软件和硬件断点指定条件。

IDA 断点条件使用 IDC (而非 Python )表达式指定。值为非零的表达式被视为真,它们满足断点条件并触发断点。值为零的表达式被视为假,它们无法满足断点条件,因而无法触发相关断点。为了便于创建断点表达式,IDA 提供了一些特殊的寄存器变量,用于直接访问断点表达式中的寄存器内容。这些变量以寄存器本身的名称命名,包括 EAXEBXECXEDXESIEDIEBPESPEFLAXBXCXDXSIDIBPSPALAHBLBHCLCHDLDH 。只有调试器激活时,才能访问这些寄存器变量。

但是,并没有可用于访问处理器标志位的变量。要访问 CPU 标志,你需要调用 IDC 的 GetRegValue 函数,获得其相关标志位的值,如 CF 。如果你需要了解可用的寄存器和标志名称,请参考“通用寄存器”窗口左右边缘的标记。断点表达式的一些示例如下所示:

EAX == 100             // break if eax holds the value 100  
ESI > EDI              // break if esi is greater than edi  
Dword(EBP-20) == 10    // Read current stack frame (var_20) and compare to 10  
GetRegValue("ZF")      // break if zero flag is set  
EAX = 1                // Set EAX to 1, this also evaluates to true (non-zero)  
EIP = 0x0804186C       // Change EIP, perhaps to bypass code

关于断点表达式,有两个问题需要注意。第一,你可以调用 IDC 函数访问进程信息(只要该函数返回一个值);第二,在进程执行过程中,你可以通过赋值的方式修改特定位置的寄存器值。在一个重写函数返回值的例子2 中,Ilfak 亲自说明了这个技巧。

2. 参见 http://www.hexblog.com/2005/11/simple_trick_to_hide_ida_debug.htmlhttp://www.hexblog.com/2005/11/stealth_plugin_1.html

在“断点设置”对话框中,最后一个可以配置的断点选项是对话框右侧的 Action 框。Break 复选框指定在到达断点时,是否应暂停执行程序(假设相关的条件均为真)。创建一个不会中断的断点,这种做法并不多见。但是,如果你希望在每次到达一条指令时修改某个内存或寄存器值,而又不希望在这时暂停程序,那么你就可以创建不会中断的断点。如果选择 Trace 复选框,则每次触发断点时,都会记录一个跟踪事件。

24.3.2 跟踪

跟踪是一种记录方法,用于记录一个进程在执行过程中发生的特定事件。跟踪事件被记录到一个固定大小的跟踪缓冲区中,或记录到一个跟踪文件中。跟踪分为两类:指令跟踪和函数跟踪。如果启用 指令跟踪 (Debugger ▶Tracing▶Instruction Tracing ),IDA 将记录被指令更改的地址、指令和任何寄存器(EIP 除外)的值。指令跟踪将大大减慢被调试进程的执行速度,因为调试器必须单步执行这个进程,以监视和记录所有寄存器的值。 函数跟踪 (Debugger ▶Tracing▶Function Tracing)是指令跟踪的一个子集,它仅记录函数调用(并选择性地记录返回值),而不记录寄存器的值。

跟踪事件也分为 3 类:写入跟踪事件、读取/写入跟踪事件和执行跟踪事件。顾名思义,它们分别代表在某个指定的地址发生一项特定的操作时记录的一类跟踪事件。这些跟踪是在设置 trace 选项的前提下使用非中断断点实现的。写入跟踪和读取/写入跟踪使用硬件断点实现,因此它受到与前面提到的硬件断点相同的限制,更重要的是,任何时候都不能有 4 个以上的硬件辅助的断点或跟踪处于活动状态。默认情况下,执行跟踪使用软件断点实现,因此一个程序可以设置的执行跟踪的数量并无限制。

用于配置调试器跟踪操作的“跟踪选项”(Tracing options)对话框(Debugger ▶Tracing▶Tracing options)如图 24-10 所示。

enter image description here

图 24-10 “跟踪选项”对话框

这里指定的选项仅适用于函数和指令跟踪。这些选项对于各个跟踪事件不会造成影响。Trace buffer size 选项指定在任何给定的时刻显示的跟踪事件的最大数量。对于给定的缓冲区大小 n ,则显示最近发生的 n 个跟踪事件。命名一个日志文件,则所有跟踪事件将附加到这个文件后面。

在指定日志文件时,并没有文件对话框显示出来,因此,你必须自己指定该日志文件的完整路径。你可以输入一个 IDC 表达式作为“停止条件”。在跟踪每条指令之前,调试器会对这个表达式求值。如果表达式的值为真,执行会立即暂停。这个表达式充当一个与任何特定的位置无关的条件断点。

如果选中 Mark consecutive traced events with same IP(标明 IP 相同的连续跟踪事件)选项,则使用一个等号标明源自同一条指令(这里的 IP 表示指令指针)的连续跟踪事件。源自同一个指令地址的连续事件大多与使用 x86 REP 3 前缀的指令有关。为便于一个指令跟踪显示它在同一个指令地址处的每次重复,还必须选中 Log if same IP(如果 IP 相同则记录)选项。如果不选择这个选项,则每次遇到以 REP 为前缀的指令时,该指令仅列出一次。下面显示的是一个指令跟踪的部分结果,该跟踪使用的是默认跟踪设置:

3. REP 前缀是一个指令修饰符,它会根据 ECX 寄存器中的一个计数值重复某些 x86 字符串指令,如 movsscas

     Thread   Address             Instruction    Result  
     ------   -------             -----------    ------  
➊   00000150 .text:sub_401320+17 rep movsb      ECX =00000000 ESI=0022FE2C EDI=0022FCF4
     00000150 .text:sub_401320+19 pop esi        ESI=00000000 ESP=0022FCE4

请注意, movsb 指令(➊)仅列出一次。

在下面的代码中,选择了 Log if same IP 选项,因此 rep 循环的每次重复都被记录下来:

Thread   Address             Instruction   Result  
------   -------             -----------   ------  
000012AC .text:sub_401320+17 rep movsb     ECX=0000000B ESI=0022FE21 EDI=0022FCE9 EFL=00010206 RF=1  
000012AC .text:sub_401320+17 rep movsb     ECX=0000000A ESI=0022FE22 EDI=0022FCEA  
000012AC .text:sub_401320+17 rep movsb     ECX=00000009 ESI=0022FE23 EDI=0022FCEB  
000012AC .text:sub_401320+17 rep movsb     ECX=00000008 ESI=0022FE24 EDI=0022FCEC  
000012AC .text:sub_401320+17 rep movsb     ECX=00000007 ESI=0022FE25 EDI=0022FCED  
000012AC .text:sub_401320+17 rep movsb     ECX=00000006 ESI=0022FE26 EDI=0022FCEE
000012AC .text:sub_401320+17 rep movsb     ECX=00000005 ESI=0022FE27 EDI=0022FCEF  
000012AC .text:sub_401320+17 rep movsb     ECX=00000004 ESI=0022FE28 EDI=0022FCF0  
000012AC .text:sub_401320+17 rep movsb     ECX=00000003 ESI=0022FE29 EDI=0022FCF1
000012AC .text:sub_401320+17 rep movsb     ECX=00000002 ESI=0022FE2A EDI=0022FCF2
000012AC .text:sub_401320+17 rep movsb     ECX=00000001 ESI=0022FE2B EDI=0022FCF3 
000012AC .text:sub_401320+17 rep movsb     ECX=00000000 ESI=0022FE2C EDI=0022FCF4 EFL=00000206 RF=0
000012AC .text:sub_401320+19 pop esi       ESI=00000000 ESP=0022FCE4

最后,在下面的代码中,选择了 Mark consecutive traced events with same IP 选项,因此,其中的特殊标记体现了一个事实,即不同指令之间的指令指针并未发生变化。

Thread   Address             Instruction  Result  
------   -------             -----------  ------  
000017AC .text:sub_401320+17 rep movsb    ECX=0000000B ESI=0022FE21 EDI=0022FCE9 EFL=00010206 RF=1  
=        =                   =            ECX=0000000A ESI=0022FE22 EDI=0022FCEA  
=        =                   =            ECX=00000009 ESI=0022FE23 EDI=0022FCEB  
=        =                   =            ECX=00000008 ESI=0022FE24 EDI=0022FCEC  
=        =                   =            ECX=00000007 ESI=0022FE25 EDI=0022FCED  
=        =                   =            ECX=00000006 ESI=0022FE26 EDI=0022FCEE  
=        =                   =            ECX=00000005 ESI=0022FE27 EDI=0022FCEF  
=        =                   =            ECX=00000004 ESI=0022FE28 EDI=0022FCF0  
=        =                   =            ECX=00000003 ESI=0022FE29 EDI=0022FCF1  
=        =                   =            ECX=00000002 ESI=0022FE2A EDI=0022FCF2  
=        =                   =            ECX=00000001 ESI=0022FE2B EDI=0022FCF3  
=        =                   =            ECX=00000000 ESI=0022FE2C EDI=0022FCF4 EFL=00000206 RF=0  
000017AC .text:sub_401320+19 pop esi      ESI=00000000 ESP=0022FCE4

下面我们将讨论有关跟踪的最后两个选项:Trace over debugger segments (跟踪跨过调试器段)和 Trace over library functions(跟踪跨过库函数)。选中前者时,只要跟踪遇到一个程序段,且该段在最初加载到 IDA 中的任何二进制文件段以外,则指令和函数调用跟踪将被临时禁用。在这方面,调用共享库函数是一个最典型的例子。选中后者时,任何时候如果执行进入一个 IDA 已经识别为库函数(可能通过 FLIRT 签名匹配识别)的函数,则函数和指令跟踪将被临时禁用。链接到一个二进制文件的库函数,不能与一个二进制文件通过 DLL 之类的共享库文件访问的库函数相混淆。默认情况下,这两个选项都处于选中状态,这明显改善了跟踪的性能(因为调试器不需要步入库代码),并大大减少了所生成的跟踪事件的数量,因为进入库代码的指令跟踪会迅速填满跟踪缓冲区。

24.3.3 栈跟踪

栈跟踪 (stack trace )显示的是当前调用栈或函数调用序列,这些调用是为了使执行到达二进制文件中的一个特定位置。使用 Debugger▶Stack Trace 命令生成的一个样本栈跟踪如图 24-11 所示。

enter image description here

图 24-11 栈跟踪样本

栈跟踪的最上面一行列出当前正在执行的函数名称。第二行指出调用当前函数的函数,以及做出该调用的地址。下面的行则指出调用每一个函数的地址。调试器可以通过遍历栈并解析它遇到的每一个栈帧,从而创建一个栈跟踪窗口。IDA 调试器依靠帧指针寄存器(x86 的 EBP )的内容来定位每个栈帧的基址。定位一个栈帧后,调试器可以提取出一个指向下一个栈帧的指针(保存的帧指针),以及保存的返回地址,并使用这个地址定位用于调用当前函数的调用指令。IDA 调试器无法跟踪不使用 EBP 作为帧指针的栈帧。在函数(而非各指令)层次上,栈跟踪用于回答以下问题:“我如何到达这里?”或者更准确地说,“到达这个位置需要调用哪些函数?”

24.3.4 监视

调试进程时,你需要持续监视一个或几个变量中的值。你不需要在每次进程暂停时都导航到相关的内存位置,许多调试器都能让你指定内存位置列表,每次进程在调试器中暂停,这些内存位置的值都将显示出来。这样的列表叫做 监视列表 (watch list),因为它们可用于在程序执行过程中监视指定内存位置内容的变化情况。使用监视列表只是为了便于导航,它们不能像断点一样使执行暂停。

因为监视的对象主要为数据,所以监视点(指定要监视的地址)通常设置在二进制文件的栈、堆或数据区块。在 IDA 调试器中,你可以通过右击某个内存项,然后选择 Add Watch,从而设置监视点。确定要监视的地址可能需要费点功夫。相比于确定本地变量的地址,确定全局变量的地址要相对简单一些,因为全局变量在编译时分配有固定的地址。另一方面,在运行时之前,本地变量并不存在,即使它们存在,也只是在声明它们的函数被调用时存在。激活调试器后,只要你进入一个函数,IDA 就能够报告该函数中本地变量的地址。将鼠标放在一个名为 arg_0 的本地变量(实际上为传递给该函数的一个参数)上的结果如图 24-12 所示。

enter image description here

图 24-12 调试器解析本地变量地址

双击活动函数中的本地变量,IDA 将从主 IDA 窗口跳转到该本地变量的地址。到达该变量的地址后,你就可以使用 Add Watch(添加监视)上下文菜单项对该地址添加监视,不过,你需要在 Watch Address(监视地址)对话框中手动输入该地址。如果你命名了该内存位置,并且对其名称而不是地址应用上述菜单项,则 IDA 会自动添加监视。

所有监视点可以通过 Debugger ▶Watches▶Watch List 访问。在监视列表中,选择你想要删除的监视点并按下 DELETE ,即可删除监视点。

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

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

发布评论

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