Windows中有些API函数是专门用来进行调试的,被称作Debug API,或者是调试API。利用这些函数可以进行调试器的开发,调试器通过创建有调试关系的父子进程来进行调试,被调试进程的底层信息、即时的寄存器、指令等信息都可以被获取,进而用来分析。
OllyDbg调试器的功能非常强大,虽然有众多的功能,但是其基础的实现就是依赖于调试API。调试API函数的个数虽然不多,但是合理使用会产生非常大的作用。调试器依赖于调试事件,调试事件有着非常复杂的结构体。调试器有着固定的流程,由于实时需要等待调试事件的发生,其过程是一个调试循环体,非常类似于SDK开发程序中的消息循环。无论是调试事件还是调试循环,对于调试或者说调试器来说,其最根本、最核心的部分是中断,或者说其最核心的部分是可以捕获中断。
产生中断的方法是设置断点。常见的产生中断的断点方法有3种,分别是中断断点、内存断点和硬件断点。下面介绍这3种断点的不同。
中断断点,这里通常指的是汇编语言中的int 3指令,CPU执行该指令时会产生一个断点,因此也常称之为INT3断点。现在演示如何使用int 3来产生一个断点,代码如下:
- int main(int argc, char* argv[])
- {
- __asm int 3
- return 0;
- }
代码中使用了asm,在asm后面可以使用汇编指令。如果想添加一段汇编指令,方法是asm{}。通过asm可以在C语言中进行内嵌汇编语言。在__asm后面直接使用的是int 3指令,这样会产生一个异常,称为断点中断异常。对这段简单的代码进行编译连接,并且运行。运行后出现错误对话框,如图1所示。
图1 异常对话框
图1所示的异常对话框中通过链接“请单击此处”可以打开详细的异常报告。如果电脑与此处显示的对话框不同,请依次进行如下设置:在“我的电脑”上单击右键,在弹出的菜单中选择“属性”,打开“属性”对话框,选择“高级”选项卡,选择“错误报告”按钮,打开“错误汇报”界面,在该界面上选择“启用错误汇报”单选按钮,然后单击确定。通过这样的设置,就可以启动“异常对话框”了。对于分析程序的BUG、挖掘软件的漏洞,弹出异常对话框界面是非常有用的。
这个对话框可能常常见到,而且见到以后多半会很让人郁闷,通常情况是直接单击“不发送”按钮,然后关闭这个对话框。在这里,这个异常是通过int 3导致的,不要忙着关掉它。通常在写自己的软件时如果出现这样的错误,应该去寻找更多的帮助信息来修正错误。单击“请单击此处”链接,出现如图2所示的对话框。
图2 “异常基本信息”对话框
弹出“异常基本信息”对话框,因为这个对话框给出的信息实在太少了,继续单击“要查看关于错误报告的技术信息”后面的“请单击此处”链接,打开如图3所示的对话框。
图3 “错误报告内容”对话框
通常情况下,在这个报告中只关心两个内容,一是Code,二是Address。在图3中,Code后面的值为0x80000003,Address后面的值为0x0000000000401028。Code的值为产生异常的异常代码,Address是产生异常的地址。在Winnt.h中定义了关于Code的值,在这里0x80000003的定义为STATUS_BREAKPOINT,也就是断点中断。在Winnt.h中的定义为:
- #define STATUS_BREAKPOINT ((DWORD)0x80000003L)
可以看出,这里给的Address是一个VA(虚拟地址),用OD打开这个程序,直接按F9键运行,如图4和图5所示。
图4 在OD中运行后被断下
图5 OD状态栏提示
从图4中可以看到,程序执行停在了00401029位置处。从图5看到,INT3命令位于00401028位置处。再看一下图3中Address后面的值,为00401028。这也就证明了在系统的错误报告中可以给出正确的出错地址(或产生异常的地址)。这样在以后写程序的过程中可以很容易地定位到自己程序中有错误的位置。
在OD中运行自己的int 3程序时,可能OD不会停在00401029地址处,也不会给出类似图4的提示。在实验这个例子的时候需要对OD进行设置,在菜单中选择“选项”→“调试设置”,打开“调试选项”对话框,选择“异常”选项卡,取消“INT3中断”复选框的选中状态,这样就可以按照该例子进行测试了。
回到中断断点的话题上,中断断点是由int 3产生的,那么要如何通过调试器(调试进程)在被调试进程中设置中断断点呢?看图4中00401028地址处,在地址值的后面、反汇编代码的前面,中间那一列的内容是汇编指令对应的机器码。可以看出,INT3对应的机器码是0xCC。如果想通过调试器在被调试进程中设置INT3断点的话,那么只需要把要中断的位置的机器码改为0xCC即可。当调试器捕获到该断点异常时,修改为原来的值即可。
内存断点的方法同样是通过异常产生的。在Win32平台下,内存是按页进行划分的,每页的大小为4KB。每一页内存都有其各自的内存属性,常见的内存属性有只读、可读写、可执行、可共享等。内存断点的原理就是通过对内存属性的修改,本该允许进行的操作无法进行,这样便会引发异常。
在OD中关于内存断点有两种,一种是内存访问,另一种是内存写入。用OD随便打开一个应用程序,在其“转存窗口”(或者叫“数据窗口”)中随便选中一些数据点后单击右键,在弹出的菜单中选择“断点”命令,在“断点”子命令下会看到“内存访问”和“内存写入”两种断点,如图6所示。
图6 内存断点类型
下面通过简单例子来看如何产生一个内存访问异常,代码如下:
- #include <Windows.h>
- #define MEMLEN 0x100
- int main(int argc, char* argv[])
- {
- PBYTE pByte = NULL;
- pByte = (PBYTE)malloc(MEMLEN);
- if ( pByte == NULL )
- {
- return -1;
- }
- DWORD dwProtect = 0;
- VirtualProtect(pByte, MEMLEN, PAGE_READONLY, &dwProtect);
- BYTE bByte = '\xCC';
- memcpy(pByte, (const char *)&bByte, MEMLEN);
- free(pByte);
- return 0;
- }
这个程序中使用了VirtualProtect()函数,该函数与VirtualProtectEx()函数类似,不过VirtualProtect()是用来修改当前进程的内存属性的。
对这个程序编译连接,并运行起来。熟悉的出错界面又出现在眼前,如图7所示。
图7 “异常基本信息”对话框
按照前面介绍的步骤打开“错误报告内容”对话框,如图8所示。
图8 “错误报告内容”对话框
按照上面的分析方法来看一下Code和Address这两个值。Code后面的值为0xc0000005,这个值在Winnt.h中的定义如下:
- #define STATUS_ACCESS_VIOLATION ((DWORD)0xC0000005L)
这个值的意义表示访问违例。Address后面的值为0x0000000000403093,这个值是地址,但是这里的地址根据程序来考虑,值是用malloc()函数申请的,用于保存数据的堆地址,而不是用来保存代码的地址。这个地址就不进行测试了,因为是动态申请,很可能每次不同,因此大家了解就可以了。
硬件断点是由硬件进行支持的,它是硬件提供的调试寄存器组。通过这些硬件寄存器设置相应的值,然后让硬件断在需要下断点的地址。在CPU上有一组特殊的寄存器,被称作调试寄存器。该调试寄存器有8个,分别是DR0—DR7,用于设置和管理硬件断点。调试寄存器DR0—DR3用于存储所设置硬件断点的内存地址,由于只有4个调试寄存器可以用来存放地址,因此最多只能设置4个硬件断点。寄存器DR4和DR5是系统保留的,并没有公开其用处。调试寄存器DR6被称为调试状态寄存器,记录了上一次断点触发所产生的调试事件类型信息。调试寄存器DR7用于设置触发硬件断点的条件,比如硬件读断点、硬件访问断点或硬件执行断点。