前言
在之前的学习中,已经了解了调试器与被调试对象之间如何建立调试关系,也了解了调试事件的采集与处理的过程,在前一篇则回顾了调试与异常之间的联系,本篇将基于以上知识点展开,进一步了解调试相关的细节。
调试的本质
调试的本质,就是在被调试进程中触发异常,并由调试器接管异常的过程。
其中有3种触发异常的方式:
- 软件断点
- 内存断点
- 硬件断点
本篇就来分析一下软件断点的实现细节与执行流程。
软件断点的本质
软件断点,就是我们常说的INT3,它的本质就是将下断处的机器码修改为0xCC(INT3对应的机器码)下面来验证这一观点:
任意打开一个程序,在某处下断,如下图所示,可以看到此时下断处地址0x004270EF处的机器码并不是0xCC,这主要是调试器为了给用户一个比较直观的感受,并没有修改这里的值,而实际上,这里的值已经为0xCC。下面打开CheatEngine来验证一下。
进入CE,找到0x004270EF处地址的值,转换为16进制。可以看到原先断点处的0x74 0x12变成的0xCC 0x12。这也就验证了INT3的本质,就是将下断处的机器码修改为0xCC。
软件断点执行流程(被调试进程角度)
下面来看软件断点的执行流程,触发软件断点的过程,实际上就是CPU异常分发的过程,所以说了解异常是学习调试的基础。 又因为在CPU异常记录一篇中,已经以除零异常为例,分析了异常记录的过程,软件断点的情况也类似,因此这部分就不详细展开。执行流程如下:
CPU检测到INT3指令
在中断描述符表中找到3号中断处理函数KiTrap03
中断处理函数内部会调用CommonDispatchException
CommonDispatchException内部又会调用KiDispatchException。以上流程均可在CPU异常记录一篇中找到。
进入KiDispatchException,之前在用户异常分发与内核异常分发过程中分析过这个函数,由于是模拟用户层的软件断点,所以这里直接进入处理用户层异常的跳转,在处理用户异常时,如果不存在0环调试器或者0环调试器未处理异常,就会调用DbgkForwardException试图发送给3环调试器。
DbgkForwardException内部最终会调用DbgkpSendApiMessage,在调试事件的采集一篇中分析过,它是将调试事件发送给调试对象的函数。
进入DbgkpSendApiMessage,刚开始会判断第二个参数的值,若该值不为0,则调用DbgkpSuspendProcess将本进程(被调试进程)内除自己外的其它进程挂起,像本例的INT3引起的异常就会挂起。
挂起进程后,调试事件会被发送到调试对象中,调试器将会在循环中取出调试事件,并根据异常调试事件结构体列出相应信息(当前寄存器的值,内存情况),接下来便交由用户处理。
以上就是调试器下了INT3断点后,被调试进程执行到INT3时,内部执行的具体流程,总的来说还是以异常分发为基础,只不过这次不是分发给异常处理函数,而是分发给调试器。总体流程可以参考下图(来自张嘉杰的笔记)
软件断点的处理(调试器角度)
下断点
为了实现软件断点的功能,需要设置一个软件断点进行测试,手动编写一个SetInt3BreakPoint函数,实现如下:
1 | VOID SetInt3BreakPoint(PCHAR pAddress){ |
下断点的位置位于OEP,可以放到判断调试事件的switch…case中,当触发创建进程事件时,下该软件断点
软件断点处理函数
下面,以代码的形式梳理一下软件断点的处理流程,这里只贴上软件断点部分的代码,在依次学习完各类断点与调试手段后,将会单独开一篇文章写一个功能简单的调试器。
1 | BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO* pExceptionInfo) |
下面来梳理一下每一步做的事:
由于INT3是将机器码修改为0xCC,因此重新执行时需要将此机器码恢复。调用IsSytemInt3()函数判断当前INT3是否为系统断点,若为系统断点则不需要修复。IsSystemInt3()由自己实现,系统断点由当前系统环境决定,断点处的地址则可以通过pDebugEvent(调试事件)->u.Exception->ExceptionRecord.ExceptionAddress来获取。然后调用WriteProcessMemory恢复原来的数据。
显示断点的位置,该值保存在ExceptionRecord.ExceptionAddress的地址。
获取线程上下文环境,调用Windows提供的API GetThreadContext获取,获得到线程上下文环境后,就可以获取到当前状态下各个寄存器的值,在用户APC执行过程一篇中有分析到。
接下来需要修复EIP,原因是对于不同类型的断点,断下后EIP的位置会有所不同,对于软件断点INT3,断下后EIP会位于原先地址+1字节的位置,因此这里需要将EIP-1,修复EIP。
显示反汇编,对于常规调试器,要能够实时看到程序的反汇编代码,所以断下后,至少要能够显示断点周围的反汇编代码,这个功能后面看情况决定是否加上。
等待用户命令,调试器最主要的一个特征就是对代码进行调试,包括但不限于单步,步进,执行等操作。这里通过while循环等待用户执行的命令,若用户未执行命令,就一直等下去。这里参考了KiDispatchException在调用完DbgkForwardException后也会等待处理结果,通过al的值判断异常是否得到了处理,若未被处理则会分发给VEH或SEH去处理。而我们这里调试器就会一直等待WaitUserForCommand传回来的结果,该函数将手动实现,后面涉及到单步操作时会学习到。
参考资料
参考文档:
- 张嘉杰笔记
- My classmates笔记
参考教程:
- 海哥逆向中级预习班课程