avatar

Catalog
软件断点

前言

在之前的学习中,已经了解了调试器与被调试对象之间如何建立调试关系,也了解了调试事件的采集与处理的过程,在前一篇则回顾了调试与异常之间的联系,本篇将基于以上知识点展开,进一步了解调试相关的细节。

调试的本质

调试的本质,就是在被调试进程中触发异常,并由调试器接管异常的过程

其中有3种触发异常的方式:

  • 软件断点
  • 内存断点
  • 硬件断点

本篇就来分析一下软件断点的实现细节与执行流程。

软件断点的本质

软件断点,就是我们常说的INT3,它的本质就是将下断处的机器码修改为0xCC(INT3对应的机器码)下面来验证这一观点:

  1. 任意打开一个程序,在某处下断,如下图所示,可以看到此时下断处地址0x004270EF处的机器码并不是0xCC,这主要是调试器为了给用户一个比较直观的感受,并没有修改这里的值,而实际上,这里的值已经为0xCC。下面打开CheatEngine来验证一下。

  2. 进入CE,找到0x004270EF处地址的值,转换为16进制。可以看到原先断点处的0x74 0x12变成的0xCC 0x12。这也就验证了INT3的本质,就是将下断处的机器码修改为0xCC。

软件断点执行流程(被调试进程角度)

下面来看软件断点的执行流程,触发软件断点的过程,实际上就是CPU异常分发的过程,所以说了解异常是学习调试的基础。 又因为在CPU异常记录一篇中,已经以除零异常为例,分析了异常记录的过程,软件断点的情况也类似,因此这部分就不详细展开。执行流程如下:

  1. CPU检测到INT3指令

  2. 在中断描述符表中找到3号中断处理函数KiTrap03

  3. 中断处理函数内部会调用CommonDispatchException

  4. CommonDispatchException内部又会调用KiDispatchException。以上流程均可在CPU异常记录一篇中找到。

  5. 进入KiDispatchException,之前在用户异常分发内核异常分发过程中分析过这个函数,由于是模拟用户层的软件断点,所以这里直接进入处理用户层异常的跳转,在处理用户异常时,如果不存在0环调试器或者0环调试器未处理异常,就会调用DbgkForwardException试图发送给3环调试器。

  6. DbgkForwardException内部最终会调用DbgkpSendApiMessage,在调试事件的采集一篇中分析过,它是将调试事件发送给调试对象的函数。

  7. 进入DbgkpSendApiMessage,刚开始会判断第二个参数的值,若该值不为0,则调用DbgkpSuspendProcess将本进程(被调试进程)内除自己外的其它进程挂起,像本例的INT3引起的异常就会挂起

  8. 挂起进程后,调试事件会被发送到调试对象中,调试器将会在循环中取出调试事件,并根据异常调试事件结构体列出相应信息(当前寄存器的值,内存情况),接下来便交由用户处理。

以上就是调试器下了INT3断点后,被调试进程执行到INT3时,内部执行的具体流程,总的来说还是以异常分发为基础,只不过这次不是分发给异常处理函数,而是分发给调试器。总体流程可以参考下图(来自张嘉杰的笔记)

软件断点的处理(调试器角度)

下断点

为了实现软件断点的功能,需要设置一个软件断点进行测试,手动编写一个SetInt3BreakPoint函数,实现如下:

c
1
2
3
4
5
6
7
VOID SetInt3BreakPoint(PCHAR pAddress){
CHAR cInt3 = 0xCC;
//1.备份
ReadProcessMemory(hDebugeeProcess, pAddress, &OriginalCode, 1, NULL);
//2.修改
WriteProcessMemory(hDebugeeProcess, pAddress, &cInt3, 1, NULL);
}

下断点的位置位于OEP,可以放到判断调试事件的switch…case中,当触发创建进程事件时,下该软件断点

软件断点处理函数

下面,以代码的形式梳理一下软件断点的处理流程,这里只贴上软件断点部分的代码,在依次学习完各类断点与调试手段后,将会单独开一篇文章写一个功能简单的调试器。

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO* pExceptionInfo)
{
BOOL bRet = FALSE;
CONTEXT Context;

//1.将INT3修复为原来的数据(如果是系统断点,不用修复)
if(IsSystemInt3(pExceptionInfo))
return TRUE;
else
WriteProcessMemory(hDebugeeProcess,pExceptionInfo->ExceptionRecord.ExceptionAddress,&OriginalCode,1,NULL);

//2.显示断点位置
printf("int 3断点: 0x%p \n",pExceptionInfo->ExceptionRecord.ExceptionAddress);

//3.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebugeeThread,&Context);

//4.修复EIP
Context.Eip--;
SetThreadContext(hDebugeeThread, &Context);

//5.显示反汇编

//6.等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}
return bRet;
}

下面来梳理一下每一步做的事:

  1. 由于INT3是将机器码修改为0xCC,因此重新执行时需要将此机器码恢复。调用IsSytemInt3()函数判断当前INT3是否为系统断点,若为系统断点则不需要修复。IsSystemInt3()由自己实现,系统断点由当前系统环境决定,断点处的地址则可以通过pDebugEvent(调试事件)->u.Exception->ExceptionRecord.ExceptionAddress来获取。然后调用WriteProcessMemory恢复原来的数据。

  2. 显示断点的位置,该值保存在ExceptionRecord.ExceptionAddress的地址。

  3. 获取线程上下文环境,调用Windows提供的API GetThreadContext获取,获得到线程上下文环境后,就可以获取到当前状态下各个寄存器的值,在用户APC执行过程一篇中有分析到。

  4. 接下来需要修复EIP,原因是对于不同类型的断点,断下后EIP的位置会有所不同,对于软件断点INT3,断下后EIP会位于原先地址+1字节的位置,因此这里需要将EIP-1,修复EIP。

  5. 显示反汇编,对于常规调试器,要能够实时看到程序的反汇编代码,所以断下后,至少要能够显示断点周围的反汇编代码,这个功能后面看情况决定是否加上。

  6. 等待用户命令,调试器最主要的一个特征就是对代码进行调试,包括但不限于单步,步进,执行等操作。这里通过while循环等待用户执行的命令,若用户未执行命令,就一直等下去。这里参考了KiDispatchException在调用完DbgkForwardException后也会等待处理结果,通过al的值判断异常是否得到了处理,若未被处理则会分发给VEH或SEH去处理。而我们这里调试器就会一直等待WaitUserForCommand传回来的结果,该函数将手动实现,后面涉及到单步操作时会学习到。

参考资料

参考文档:

参考教程:

  • 海哥逆向中级预习班课程
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/17/%E8%BD%AF%E4%BB%B6%E6%96%AD%E7%82%B9/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶