2.1.5 断点
常用的断点有INT 3断点、硬件断点、内存断点等。调试时,合理使用断点,能大大提高效率。
1.INT 3断点
当执行一个INT 3断点时,该地址处的内容被调试器用INT 3指令替换了,此时OllyDbg将INT 3隐藏了,显示出来仍是下断前的指令,如图2.12按F2键下的断点。实际上,4013A5处的指令68已被替换成CC了:
这个INT 3指令,其机器码是CCh,也常称为CC指令。当被调试进程执行INT 3指令导致一个异常时,调试器就会捕捉这个异常从而停在断点处,然后将断点处的指令恢复成原来指令。当然,如果自己写调试器,也可用其他一些指令代替INT 3来触发异常。
用INT 3断点的好处是可以设置无数个断点,缺点是改变了原程序指令,容易被软件检测到。例如为了防范API被下断,一些软件会检测API的首地址是否为CCh,以此来判断是否被下了断点。在这用C语言来实现这个检测,方法是取得检测函数的地址,然后读取它的第一个字节,判断它是否等于“CCh”。下面这段代码就是对MessageBoxA函数进行的断点检测:
FARPROC Uaddr ; BYTE Mark = 0; (FARPROC&) Uaddr =GetProcAddress ( LoadLibrary("user32.dll"), "MessageBoxA"); Mark = *((BYTE*)Uaddr); // 取MessageBoxA函数第一字节 if(Mark ==0xCC) // 如该字节为CC,则认为MessageBoxA函数被下断 return TRUE // 发现断点 |
程序编译后,对MessageBoxA设断,程序将会发现自己被设断跟踪。当然躲过检测的方法是将断点下在函数内部或末尾,例如可以将断点下在函数入口的下一行,就可躲过检测了。
2.硬件断点
硬件断点和DRx调试寄存器有关。从Intel CPU体系架构手册中,可以找到DRx调试寄存器的介绍,如图2.25所示。
|
| 图2.25 Intel调试寄存器示意图 |
DRx调试寄存器总共有8个,从DR0到DR7。每个寄存器的特性如下:
DR0~DR3:调试地址寄存器,保存需要监视的地址,如设置硬件断点;
DR4~DR5:保留,未公开具体作用;
DR6:调试寄存器组状态寄存器;
DR7:调试寄存器组控制寄存器。
硬件断点原理是使用4个调试寄存器(DR0,DR1,DR2,DR3)来设定地址,以及DR7设定状态,因此最多只能设置4个断点。
OllyDbg支持调试寄存器,其称为硬件断点。设断方法是在指定的代码行单击鼠标右键,执行“Breakpoint/Hardware,on execution(断点/硬件执行)”命令。
为了便于理解,这里演示一下。加载实例TraceMe.exe,右键单击寄存器面板窗口,执行“View debug registers(查看调试寄存器)”,接着在4013AA这行设置硬件断点。按F9键执行程序,程序就会中断在4013AA这一行,查看调试寄存器,会发现DR0的值为4013AA,如图2.26所示。
|
| (点击查看大图)图2.26 演示硬件断点 |
设置断点后,OllyDbg实际上就是将DR0~DR3其中的一个设置为44013AA,然后在DR7中设定相应的控制位。这样当被调试进程运行到4013AA时,CPU就会给OllyDbg发送异常信息,OllyDbg将该信息做初步处理后,中断下来,让用户继续进行操作。
硬件断点删除稍有些麻烦,单击菜单“Debug/Hardware breakpoints(调试/硬件断点)”,打开硬件断点面板,如图2.27所示,然后单击“Delete”按钮删除相应的硬件断点。
|
| 图2.27 删除硬件断点 |
OllyDbg提供了一个快捷键F4,可以执行到光标所在的行,也是利用调试寄存器原理,中断后自动删除,相当于一次性硬件断点。
硬件断点优点是速度快,在INT 3断点容易被发现的地方,使用硬件断点来代替会有很好的效果;缺点就是最多能使用4个断点。
3.内存断点
OllyDbg可以设置内存访问断点或内存写入断点,原理是对所设的地址设为不可访问/不可写属性,这样当访问/写入的时候就会产生异常,OllyDbg截获异常后比较异常地址是不是断点地址,如果是就中断,让用户继续进行操作。
内存断点会降低OllyDbg速度,因为每次异常时都要通过比较来确定是否应该停下,也许OllyDbg可能在速度上做了考虑而只实现一个内存断点。
程序运行时会有3种状态:读取、写入、执行。
004013D0 mov dword ptr [405528], edx ;对[405528]地址处的内存写入 004013D0 mov dword ptr edx,[405528] ;对[405528]地址处的内存读取
|
用OllyDbg加载实例TraceMe.exe,看到4013D0一行有一个写内存的指令:
004013D0 8915 28554000 mov dword ptr [405528], edx
|
就用这个地址来演示一下如何下内存断点。在数据窗口对405528下内存写断点,方法是将光标移到405528地址处,选中需要下断点的地址区域,单击鼠标右键,执行“Breakpoint/Memory,on write(断点/内存写入)”,如图2.28所示。
|
(点击查看大图)图2.28 设置内存写入断点
|
下了内存写断点后,按F9键让程序跑起来,会马上中断在“4013D0 mov [405528], edx”这行。如果要清除内存断点,单击鼠标右键,执行“Breakpoint/Remove memory breakpoint(断点/删除内存断点)”。同样,内存访问断点操作类似。
对代码也可下内存访问断点。在OllyDbg里重新加载实例,随意定位一行代码,如4013D6,单击鼠标右键,执行“Breakpoint/Memory,on access(断点/内存访问)”,如图2.29所示。
|
| (点击查看大图)图2.29 设置内存访问断点 |
当然要执行内存地址4013D6的代码时需要“访问”它,因此按F9键让实例在OllyDbg里跑起来,就会中断在4013D6这行所下的内存访问断点上。这个实验表明内存执行的地方,也可以用内存访问中断。
内存断点不修改原代码。它不会像INT 3断点那样,因为修改代码被程序校验而导致中断失败,因此在遇到代码校验,并且硬件断点失灵情况下,可以用内存断点来代替。
4.内存访问一次性断点
Windows对内存使用段页式的管理,在OllyDbg里按“Alt+M”键显示内存,可以看到许多段,每个段都有不可访问、读、写、执行属性。在相应的段上单击右键,如图2.30所示,会发现一个命令“Set break-on-access(在访问上设置断点)”,其快捷键是F2键,对整个内存块设置该类断点。这个断点是一次性断点,当所在段被读取或执行时就中断,中断发生以后,断点将被删除。想捕捉调用或返回到某个模块时,如后面章节中的脱壳时,该类断点就显得特别有用。右键中的“Set memory breakpoint on access(设置内存访问断点)”和“Set break-on-access”功能一样,所不同的是它不是一次性断点。这类断点仅在NT架构下可用。
|
| (点击查看大图)图2.30 对区块设置内存断点 |
5.消息断点
Windows本身是由消息驱动的,如果调试时没有合适的断点,可以尝试消息断点。消息断点使得当某个特定窗口函数接收到某个特定消息时程序中断。
当用户单击一个按钮、移动鼠标或向文本框中键入文字时,一条消息就会被发送给当前的窗体。所有发送的消息都有4个参数:一个窗口句柄(hwnd),一个消息编号(msg),还有两个32位长度(Long)的参数。Windows通过句柄来标识它代表的对象,比如单击某个按钮,Windows就是通过句柄来判断是点击了哪一个按钮,然后发送相应的消息通知程序。
用实例TraceMe.exe来演示一下如何下消息断点。在OllyDbg里运行实例,输入用户名与序列号,单击菜单“View/Windows(查看/窗口)”或单击工具栏中的 按钮,列出窗口相关参数,如图2.31所示。如果界面无内容显示,此时执行鼠标右键菜单中的“Actualize(刷新)”命令。
|
| (点击查看大图)图2.31 列出窗口相关参数 |
这里用于列出所有属于被调试程序窗口及其窗口相关的重要参数,比如按钮、对应的ID以及句柄(Handle)等。现在要对Check按钮下断点,当单击按钮时中断。在Check条目上单击鼠标右键,如图2.32所示。
|
| 图2.32 设置消息断点 |
在弹出的右键菜单中,执行“Message breakpoint on ClassProc(在ClassProc上设置消息断点)”,会弹出如图2.33所示的设置窗口。
当用鼠标左键单击按钮并松开时,会发送WM_LBUTTONUP这个消息,单击图2.33中的下拉菜单选择“202 WM_LBUTTONUP”,再单击“OK”按钮,至此消息断点设置好了。
|
| 图2.33 在WinProc上设消息断点 |
回到TraceMe界面,单击“Check”按钮,鼠标松开时,将会中断在Windows系统代码里。代码如下(不同版本的系统,代码会不同的):
77D3B00E [ESP+8]==WM_LBUTTONUP mov edi, edi 77D3B010 push ebp 77D3B011 mov ebp, esp 77D3B013 mov ecx, dword ptr [ebp+8] 77D3B016 push esi 77D3B017 call 77D184D0 |
现在消息捕捉到了,但处于系统底层代码里,这时企图使用“Alt+F9”键或“Ctrl+F9”键返回到TraceMe程序的领空代码里是徒劳的。
VC可执行文件的执行代码是存放在代码段里的,本例就是.text区块里。当从系统代码回到应用程序代码段的时候,正是代码段的执行,因此对代码段下内存断点就能返回应用程序的代码领空。按“Alt+M”键打开内存窗口,对.text区块下内存访问断点,执行右键菜单命令“Set break-on-access(在访问上设置断点)”或按快捷键F2,如图2.34所示。
|
| (点击查看大图)图2.34 对代码段下内存访问断点 |
现在按F9键运行程序,立即中断在程序的空间004010D0处,这里正是程序的消息循环处。
004010D0 sub esp, 0F4 004010D6 push esi 004010D7 push edi 004010D8 mov ecx, 5 004010DD mov esi, 00405060 …… 00401132 sub eax, 10 ; Switch (cases 10..111) 00401135 mov dword ptr [esp+10], edx 00401139 movs byte ptr es:[edi], byte ptr [esi] 0040113A je 00401314 00401140 sub eax, 100 00401145 je 004012CD 0040114B dec eax 0040114C jnz 004012C0
|
这段代码是一个消息循环,不停地处理TraceMe主界面的各类消息,此时可能不是直接处理按钮事件,如果单步跟踪,可能会跟进系统代码里去。
可以重复这个过程,在几次中断后到达处理按钮的事件代码。很快就能发现“Check”按钮事件的代码了:
最后,可以将消息断点删除,方法是按“Alt+B”键切换到断点窗口,选中消息断点,直接删除,如图2.35所示。
|
| (点击查看大图)图2.35 删除消息断点 |