avatar

Catalog
调试事件的采集

什么是调试事件

前一篇在介绍调试对象时,分析了调试器如何通过调试对象与被调试进程建立连接,这同样产生了另一个新的问题,调试器进程如何才能知道被调试进程发生甚么事了?于是就有了调试事件这么一个概念,用来描述被调试进程的某些行为,当被调试进程做出了一些行为后,如果属于调试事件中的一类,就会借助调试对象告知调试器。参考下图:

再来回顾一下调试对象的结构:

c
1
2
3
4
5
6
typedef struct _DEBUG_OBJECT {
KEVENT EventsPresent; //+0x00,用于指示有调试事件发生的事件对象
FAST_MUTEX Mutex; //+0x10,用于同步的互斥对象
LIST_ENTRY EventList; //+0x30,保存调试事件的链表
ULONG Flags; //+0x38,标志位,调试消息是否已读取
} DEBUG_OBJECT, *PDEBUG_OBJECT;

位于_DEBUG_OBJECT(+0x30)处的成员EventList是一个链表,从被调试进程发送过来的调试事件都会存在这个链表里

调试事件的种类

前面说到调试事件,自然也有一定的边界,不能说执行了什么代码(例如打印了某个字符,申请了一块内存),都产生一个调试事件发送给_DEBUG_OBJECT,那样链表也就过于复杂了,所以调试事件设定了以下7种类型

c
1
2
3
4
5
6
7
8
9
10
11
12
typedef enum _DBGKM_APINUMBER
{
DbgKmExceptionApi = 0, //异常(例:Int3断点,硬件断点)
DbgKmCreateThreadApi = 1, //创建线程
DbgKmCreateProcessApi = 2, //创建进程
DbgKmExitThreadApi = 3, //线程退出
DbgKmExitProcessApi = 4, //进程退出
DbgKmLoadDllApi = 5, //加载DLL
DbgKmUnloadDllApi = 6, //卸载DLL
DbgKmErrorReportApi = 7, //内部错误(已废弃)
DbgKmMaxApiNumber = 8, //最大值
} DBGKM_APINUMBER;

当被调试进程做出任何一种上述类型的行为时,都会产生调试事件,并发送给_DEBUG_OBJECT.EventList。

调试事件采集函数

下面来到本篇的核心部分,前面介绍了什么是调试事件,以及调试事件的种类,那么当被调试进程产生了某些行为后,调试事件是如何被采集并写入到_DEBUG_OBJECT.EventList的呢?这一部分先研究采集的过程,也就是谁会替我们生成调试事件。

Windows系统中提供了一些调试事件的采集函数,它们以Dbgk开头,用于生成不同调试事件对应的结构体,具体如下:

针对不同类型的调试事件,均有对应的调试事件采集函数。图中黑色字体的为导致调试事件产生的函数,紫色字体的为生成调试事件的采集函数,红色字体的则为调试事件写入函数。并且调试事件采集函数均在导致调试事件发生的函数执行的必经之路上,从而捕获到被调试进程的行为。下面以前4类调试事件为例,分析调试事件采集函数的执行过程。

创建进程、线程事件采集

首先来看创建进程、线程的事件采集函数,创建进程的本质就是创建线程,其中第一次创建线程时为创建进程。因此底层调用的函数一样,均为PspUserThreadStartup,下面来分析它的执行流程:

  1. 进入PspUserThreadStartup,往下翻,就可以很容易找到其内部调用的调试事件采集函数DbgkCreateThread

  2. 进入DbgkCreateThread,进来后,可以看到有一个判断,判断当前进程的DebugPort的值是否为空(ebx先前会被清零),这是每个Dbgk系列的函数都会做的判断;如果DebugPort的值不为空,说明当前进程正在被调试,此时会跳转到图中左侧所示的代码继续执行。

    左侧部分代码主要做两件事,第一件事,通过判断创建的线程是不是第一个线程来判断此时属于创建进程还是创建线程,第二件事,针对该调试事件将其打包成一个结构体。接着会跳转到右侧下方处的代码执行,并最终调用DbgkpSendApiMessage,这个函数会在文章后半部分进行分析,它的第一个参数,就是刚刚打包的调试事件结构体。

小结:经过分析,可以得出,若一个进程正在被调试,当它调用PspUserThreadStartup创建线程时就会被采集调试事件,并调用DbgkpSendApiMessage。

退出线程、进程事件采集

一个例子往往不够说明问题,再进入PspExitThread分析一下:

  1. 进入PspExitThread,往下翻,可以看到这里在PspExitThread就已经判断了DebugPort的值,同样是如果不为空就跳转。跳转后,会判断要退出的线程是不是最后一个线程了,如果是就调用DbgkExitProcess,如果不是就调用DbgkExitThread。

  2. 参考下图,无论是DbgkExitThread还是DbgkExitProcess,内部都会调用DbgkpSendApiMessage,当然在调用之前,这两个函数都会先生成一个该函数对应的调试事件结构体

小结:在退出线程或进程的必经之路上会调用DbgkExitThread或者DbgkExitProcess生成相应的调试事件结构体,并最终调用DbgkpSendApiMessage。其余的调试事件采集函数不再作分析,结论相同,可自行分析。

调试事件写入函数

经过前文的分析,会发现调试事件采集函数内部会创建一个调试事件结构体,并且最终都会调用DbgkpSendApiMessage,进入IDA,对该函数进行交叉引用,验证结论。

DbgkpSendApiMessage执行流程

显然,这个函数就是用来将创建好的调试事件发送到调试对象的事件链表里的,接下来就分析一下这个函数的执行流程:

  1. 进入DbgkpSendApiMessage,它调用了一个函数DbgkpQueueMessage,该函数有一个参数Event,这是调试事件采集函数创建的结构体,并且有一个互斥体参数,与调试对象的第一个成员一样,所以从这里进入分析。

  2. 进入DbgkpQueueMessage,可以看到,在执行到一半时,会先从自己进程的EPROCESS中拿出调试对象(之前判断不为空,才会执行到这里,因此当前进程一定是正在被调试的进程),并在之后的部分从调试对象中取出EventList成员的首地址,并将ebx保存的节点插入到EventList的第一个位置,可以推测ebx为经过处理后的调试事件。这部分执行完后,调试事件已经被写入到函数中

再看_DEBUG_OBJECT

至此调试事件写入部分结束,我们在回过头看一下调试对象的各个成员,应该能更好理解一些:

c
1
2
3
4
5
6
typedef struct _DEBUG_OBJECT {
KEVENT EventsPresent; //+0x00,用于指示有调试事件发生的事件对象
FAST_MUTEX Mutex; //+0x10,用于同步的互斥对象
LIST_ENTRY EventList; //+0x30,保存调试事件的链表
ULONG Flags; //+0x38,标志位,调试消息是否已读取
} DEBUG_OBJECT, *PDEBUG_OBJECT;
  • EventsPresent:在调用DbgkpQueueMessage将调试事件添加到链表后,_DEBUG_OBJECT.EventsPresent将会修改状态,然后调试器就会从EventList中把调试事件取出来。
  • Mutex:调试器从EventList中取调试事件会修改链表,DbgkpSendApiMessage往EventList链表写数据也会改,所以需要有一个Mutex来互斥一下。
  • Flags:用于标识这个事件调试器有没有读取。

DbgkpSendApiMessage参数说明

该函数有两个参数,这里简单说明一下:

  • 参数1:消息结构,每种消息都有自己的消息结构,由不同的调试事件采集函数创建,共有7种类型。
  • 参数2:是否需要把本进程除了自己之外的其它线程挂起,有些调试事件需要把其它线程挂起,比如int3断点;有些调试事件不需要把线程挂起,比如模块加载

小结:DbgkpSendApiMessage是调试事件采集的总入口,如果在这里挂钩子,调试器将无法调试。

参考资料

参考教程:

  • 海哥逆向中级预习班

参考链接:

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/13/%E8%B0%83%E8%AF%95%E4%BA%8B%E4%BB%B6%E7%9A%84%E9%87%87%E9%9B%86/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶