Windows XP 用户态调试(一)被调试进程

ʕ •̀ o •́ ʔ

1 调试用到的 API

软件调试系列主要用到 kernel32.dllntdll.dllntoskrnl.exe 这几个文件。
kernel32.dll:主要是用在调试器在调试 被调试程序 前的一些建立过程上。
ntdll.dll 中的调试支持函数主要分为 3 类:

  1. DbgUi 开头的,供调试器使用;
  2. DbgSs 开头的,这一部分在 Windows 2000 之后被移除;
  3. Dbg 开头(非前两种)的,用于实现调试API,如 DbgBreakpointDebugBreak API的实现。

ntoskrnl.exe:该文件中的调试支持函数负责采集传递调试事件,以及控制被调试进程。这些内核函数都是以 Dbgk 开头的。

调试子系统主要由 3 个部分:位于ntdll.dll 中的支持函数、位于内核文件中的 Dbgk支持函数,以及在内核的调试子系统服务器组成(实际上子系统是一类服务函数的集合,所以这里指的服务器就是为调试提供服务的函数)。

2 调试器与被调试程序

2.1 调试对象

调试器是一个进程,被调试程序是一个进程,如何才能将两个进程联系到一起呢?就需要一个桥梁,在 Windows 中一般都是用一个对象来管理和连接许多事务,而调试用到的就是调试对象_DEBUG_OBJECT(进程间是相互隔离的,但是高2G往往又是相同的,因此这个桥梁可以利用内核层来实现)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _DEBUG_OBJECT {
+0x00 KEVENT EventsPresent;
+0x10 FAST_MUTEX Mutex;
+0x30 LIST_ENTRY EventList;
+0x38 ULONG Flags;
} DEBUG_OBJECT, *PDEBUG_OBJECT;

kd> dt _FAST_MUTEX -v
nt!_FAST_MUTEX
struct _FAST_MUTEX, 5 elements, 0x20 bytes
+0x000 Count : Int4B
+0x004 Owner : Ptr32 to struct _KTHREAD, 73 elements, 0x1c0 bytes
+0x008 Contention : Uint4B
+0x00c Event : struct _KEVENT, 1 elements, 0x10 bytes
+0x01c OldIrql : Uint4B
偏移 名称 作用
0x00 EventsPresent 用于指示调试事件发生的事件对象,用来同步调试器进程和被调试进程,调试子系统服务器通过设置此事件来通知调试器读取消息队列中的调试信息。调试器通过 WaitForDebugEvent 来等待此对象。
0x10 Mutex 用于同步的互斥对象,用来锁定对 StateEventListEntry 的访问,以防止向链表写数据时调试器正在取数据造成数据读写错误。
0x30 StateEventListEntry 保存调试事件的链表,被称为调试消息队列
0x38 Flags 包含多个标志位,比如,位1代表结束调试会话时是否终止被调试进程(KillProcessOnExit),位0代表调试对象是否被正在被删除。 DebugSetProcessKillOnExit 实际上设置的就是这个标志位。具体见3.5.2。

调试对象通常是在调试器进程中创建的,将调试器进程与被调试进程建立连接的过程为:

  1. 调试器进程调用函数创建调试对象,并将调试对象句柄保存到调试器当前线程 TEB.DbgSsReserved[1] 字段中。
  2. 然后进入 0 环,将调试对象地址保存在被调试进程 EPROCESS.DebugPort

2.2 调试关系建立方式

打开调试器,有两种与被调试程序建立联系的方式:

  • 在调试器中打开未运行的可执行文件:通过 kernel32!CreateProcess 建立联系。
  • 将一个正在运行的程序附加到调试器中:通过 kernel32!DebugActiveProcess 建立联系。

一、kernel32!CreateProcess

1
2
3
4
5
6
7
8
9
10
11
12
BOOL WINAPI CreateProcessA(
IN LPCSTR lpApplicationName,
IN LPSTR lpCommandLine,
IN LPSECURITY_ATTRIBUTES lpProcessAttributes,
IN LPSECURITY_ATTRIBUTES lpThreadAttributes,
IN BOOL bInheritHandles,
IN DWORD dwCreationFlags,
IN LPVOID lpEnvironment,
IN LPCSTR lpCurrentDirectory,
IN LPSTARTUPINFOA lpStartupInfo,
OUT LPPROCESS_INFORMATION lpProcessInformation
);

其中 dwCreationFlags 参数用于指定创建新进程的选项,可以是一系列标志位的组合。以下两个标志位是专门用于调试的。

1
2
#define DEBUG_PROCESS                     0x00000001
#define DEBUG_ONLY_THIS_PROCESS 0x00000002

系统在创建进程时,会检查创建标志中是否包含以上标志。如果包含,那么系统会把调用进程当作调试器(debugger)进程,把新创建的进程当作被调试(debuggee)进程,为二者建立起调试关系。

二者主要区别

  • DEBUG_PROCESS :调试器会收到被调试进程及由被调试进程创建的所有子进程中发生的所有调试事件的信息,但一般来说没有必要这样做。DEBUG_ONLY_THIS_PROCESSDEBUG_PROCESS 组合标志来禁止它。
  • DEBUG_ONLY_THIS_PROCESS:调试器将只会收到被调试进程的调试事件,而对其子进程的调试事件不予理睬。

因为操作系统将调试对象标记为在特殊模式下运行,所以可以使用 IsDebuggerPresent 函数查看进程是否在调试器下运行。《加密与解密第四版 9.3 P377》。

这种方式创建具体细节请看《软件调试第二版卷2 10.3 P201》、《64位CreateProcess逆向》、《基于Win11的CreateProcess逆向分析-3环用户层逆向分析(一)》。

两种建立联系的方式本质上区别并不大,只是第一种有一个创建进程的过程,多出一步,所以仅分析第二种通过 kernel32!DebugActiveProcess 建立联系的方式即可。

2.3 DebugActiveProcess

函数 kernel32!DebugActiveProcess:该函数允许调试器附加一个处于活动状态的进程。

1
2
3
BOOL __stdcall DebugActiveProcess(
DWORD dwProcessId //被调试进程的PID
) //返回值:TRUE-附加成功,FALSE-附加失败。使用GetLastError可获取错误码
  1. 该函数调用 ntdll!DbgUiConnectToDbg ,无需进入 0 环。先判断调试器当前线程 TEB+0xF24DbgSsReserved[1] == NULL,如果不为空则直接使用该调试对象即可,如果为空会创建一个调试对象。最后将调试对象句柄值存入调试器当前线程 TEB+0xF24,即可关联起来。
  2. 然后调用 ntdll!DbgUiDebugActiveProcess 带着被调试进程句柄、调试对象句柄进入 0 环后调用 DbgkpSetProcessDebugObject,然后将调试对象地址存入被调试进程 EPROCESS.DebugPort即可。

由于TEB是用户层的数据结构,所以此时 DbgSsReserved[1] 中保存的其实是调试对象的句柄,而不是调试对象的地址。

  • ntdll!DbgUiConnectToDbg:关联调试器-调试对象。
  • ntdll!DbgUiDebugActiveProcess --> DbgkpSetProcessDebugObject:关联被调试进程-调试对象。

当调试器与被调试进程的调试会话建立起来后,调试器进程就进入了调试事件循环,等待调试事件的发生,然后处理,然后等待,直到调试会话结束,调试器的调试事件循环如下:

1
2
3
4
5
6
while(WaitForDebugEvent(&DbgEvt, INFINITE))// 等待事件
{
// 处理等待得到的事件
// 处理后,恢复调试目标继续执行
ContinueDebugEvent(DbgEvt.dwProcessId, DbgEvt.dwThreadId, dwContinueStatus);
}

DbgkpSetProcessDebugObject 函数内部除了将调试对象赋给 EPROCESS 结构的 DebugPort字段,还会调用 DbgkpMarkProcessPeb 函数设置进程环境块 PEB.BeingDebugged 字段(用于在用户态判断进程是否正在被调试):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VOID DbgkpMarkProcessPeb(PEPROCESS Process)
{
if(ExAcquireRundownProtection (&Process->RundownProtect))
{
KeStackAttachProcess(&Process->Pcb, &ApcState);
ExAcquireFastMutex (&DbgkpProcessDebugPortMutex);

Process->Peb->BeingDebugged = (BOOLEAN)(Process->DebugPort != NULL ? TRUE : FALSE);

ExReleaseFastMutex (&DbgkpProcessDebugPortMutex);
KeUnstackDetachProcess(&ApcState);
}
ExReleaseRundownProtection (&Process->RundownProtect);
}

将调试器进程附加到已处于运行的进程中,使用 kernel32!DebugActiveProcess,需要注意两点:

  1. 在 NT内核中,当试图通过 DebugActiveProcess 函数将调试器捆绑到一个创建时带有安全描述符的进程上时,将被拒绝。《加密与解密第四版 P378》
  2. kernel32!DebugActiveProcess 调用 ProcessIdToHlandle 函数,获得指定 ID 的进程句柄,这个函数内部会调用 OpenProcess 函数, 进而调用 NtOpenProcess 内核服务。在执行这一步时需要调用进程与目标进程有同样或更高的权限,否则这一步便会失败,调用 GetLastError(返回的错误码通常是 0x5,意思是 Access is denied,即访问被拒绝。如果调试器进程具有 SE_DEBUG_NAME 权限,那么它通常有权限调试系统内的任何进程。《软件调试第二版 卷2 P206》

DebugActiveProcess的副本.png

2.4 调试器TEB.DbgSsReserved

TEB 结构的 DbgSsReservedr[2] 数组就是专门用来记录调试器工作线程与调试子系统之间通信用的同步对象和通信对象的。

  • DbgSsReservear[0]:是一个链表头,指向所有被调试线程。这个链表的每个节点是一个 DBGSS_THREAD_DATA 结构或 TMPHANDLES 结构(XP SP3使用),每个节点用来描述被调试进程中的一个线程。
  • DbgSsReservear[1]:存放调试对象句柄。

关于 DBGSS_THREAD_DATA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct _DBGSS_THREAD_DATA
{
struct _DBGSS_THREAD_DATA *Next; // 指向下一个节点
HANDLE ThreadHandle; // 线程句柄(被调试进程中)
HANDLE ProcessHandle; // 被调试进程的句柄
DWORD ProcessId; // 被调试进程的 ID
DWORD ThreadId; // 线程ID(被调试进程中)
BOOLEAN HandleMarked; // 退出标记,TRUE为退出进程/线程
} DBGSS_THREAD_DATA, *PDBGSS_THREAD_DATA;

typedef struct _TMPHANDLES {
struct _TMPHANDLES *Next; //指向下一个节点
HANDLE Thread; // 线程句柄(被调试进程中)
HANDLE Process; // 被调试进程的句柄
DWORD dwProcessId; // 被调试进程的 ID
DWORD dwThreadId; // 线程ID(被调试进程中)
BOOLEAN DeletePending; // 退出标记,TRUE为退出进程/线程
} TMPHANDLES, *PTMPHANDLES;

调试时间处理的 WaitForDebugEventContinueDebugEvent 函数会维护这个链表。

2.5 被调试进程PEB.BeingDebugged

DbgkpSetProcessDebugObject 函数内部除了将调试对象赋给被调试进程 EPROCESS 结构的 DebugPort字段,还会调用 DbgkpMarkProcessPeb 函数设置被调试进程环境块PEB.BeingDebugged字段(用于在用户态判断进程是否正在被调试):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VOID DbgkpMarkProcessPeb(PEPROCESS Process)
{
if(ExAcquireRundownProtection (&Process->RundownProtect))
{
KeStackAttachProcess(&Process->Pcb, &ApcState);
ExAcquireFastMutex (&DbgkpProcessDebugPortMutex);

Process->Peb->BeingDebugged = (BOOLEAN)(Process->DebugPort != NULL ? TRUE : FALSE);

ExReleaseFastMutex (&DbgkpProcessDebugPortMutex);
KeUnstackDetachProcess(&ApcState);
}
ExReleaseRundownProtection (&Process->RundownProtect);
}

如果一个进程不在被调试状态,那么其 PEB 结构的 Being Debugged 字段为 0;否则,为 1。ISDebuggerPresent API 就是通过判断 BeingDebugged 字段实现的。

2.6 反调试

在掌握了调试原理后,自然也就可以总结出一些反调试的手段:

  1. 清零DebugPort,只要起一个线程不断的检查当前进程的DebugPort,一旦有值就退出程序或者将其清零,这样可以中断调试对象与被调试进程的联系,以达到反调试的目的。
  2. 遍历所有进程TEB+0xF24处,看有没有值,若有值,一定就是调试器,则退出程序。
  3. Hook NtCreateDebugObject,不让它创建调试对象。

2.7 反反调试

有反调试,自然就有反反调试,正所谓道高一尺魔高一丈,针对各类反调试手段,也会衍生出各类的反反调试,攻防领域永远都在交替上升:

  1. 针对Hook NtCreateDebugObject的反调试方式,可以自己分配一个内存给_DEBUG_OBJECT,并为它的成员赋值。
  2. 针对清零DebugPort的反调试方式,可以不使用DebugPort的位置,在进程中另找一个区域存放_DEBUG_OBJECT的地址。把原先+0xbc的值都选为新的偏移处。
  3. 重写整个DebugActiveProcess函数

3 调试消息的采集-0环

3.1 调试消息是什么

调试消息(事件):调试器进程如何才能知道被调试进程发生甚么事了?于是就有了调试消息这么一个概念,用来描述被调试进程的某些行为,当被调试进程做出了一些行为后,如果属于调试事件中的一类,就会借助调试对象告知调试器。参考下图:

1.png

调试事件的采集是在内核进行的,使用以 Dbgk 开头的函数来进行采集。

创建/退出进程、线程时,进行消息采集的线程由被调试进程来提供。

在内核中,调试事件有时也称为调试消息,并使用一个名为 DBGKM_APIMSG 的结构来描述。实际上在 0 环侧一般叫调试消息。

调试事件是在 3 环的叫法,调试消息是在 0 环的叫法。

对于在3 环的调试器,调试 API 使用的是一个名为 DEBUG_EVENT 的结构。因为这两个结构是不同的,所以需要一个转化过程,这个工作是由调试子系统服务器和 ntdll.dll 中的用户态函数来完成的。简单来说,子系统服务器会将自己使用的结构转化为 ntdll.dll 使用的DBGULWAIT_STATE_CHANGEntdll.dll 再将这个结构转化为调试器使用的 DEBUG_EVENT 结构。

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
32
33
34
35
36
37
typedef struct _DEBUG_OBJECT {
KEVENT EventsPresent; //+0x00,用于指示有调试事件发生的事件对象
FAST_MUTEX Mutex; //+0x10,用于同步读取消息的互斥对象
LIST_ENTRY EventList; //+0x30,保存调试消息的链表
ULONG Flags; //+0x38,标志位,调试消息是否已读取
} DEBUG_OBJECT, *PDEBUG_OBJECT;

// 3环使用
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode; //事件类型,对应消息类型
DWORD dwProcessId; //被调试进程PID
DWORD dwThreadId; //被调试线程TID
union { //根据不同的事件类型,对应于不同的结构
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

// 0环使用
typedef struct _DEBUG_EVENT {
+0x00 LIST_ENTRY EventList; // 与兄弟节点相互链接的节点结构,注意这不是调试消息队列
+0x08 KEVENT ContinueEvent; // 用于等待调试器回复的事件对象
+0x18 CLIENT_ID ClientId; // 调试事件所属的线程 ID 和进程 ID
+0x20 PEPROCESS Process; // 被调试进程的 EPROCESS 结构地址
+0x24 PETHREAD Thread; // 被调试进程中触发调试事件的线程的 ETHREAD 地址
+0x28 NTSTATUS Status; // 对调试事件的处理结果
+0x2C ULONG Flags; // 标志(调试消息是否已经被读取)
+0x30 PETHREAD BackoutThread;// 产生杜撰消息(faked message)的线程
+0x38 DBGKM_APIMSG ApiMsg; // 调试消息的详细信息
} DEBUG_EVENT, *PDEBUG_EVENT;

3.2 调试消息种类

不能说执行了什么代码(例如打印了某个字符,申请了一块内存),都产生一个调试事件(消息)发送给 _DEBUG_OBJECT,那样链表也就过于复杂了,所以调试事件设定了以下7种类型(对应的常量在 3 环使用):

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

消息类型 采集方式
进程和线程创建消息 创建进程的时候,都需要创建主线程。而创建用户模式线程的时候,会执行PspUserThreadStartup例程,在该函数中会通过调用DbgkCreateThread来采集创建消息
进程和线程退出消息 进程的退出其实就是将其所有线程都退出,而线程退出的最终函数是PspExitThread。该函数会判断退出的线程是否是最后一个线程,如果不是则会调用DbgkExitThread来采集线程退出消除;如果是最后一个线程,就会通过调用DbgkExitProcess来采集进程退出消息
DLL映射消息 Windows内核使用NtMapViewOfSection来将一个模块对象映射到指定的进程空间中时,NtMapViewOfSection会调用DbgkMapViewOfSection来采集模块映射的消息
DLL卸载消息 NtUnMapViewOfSection则会在卸载模块的时候,调用DbgkUnMapViewOfSection来采集模块卸载消息
异常消息 当出现异常的时候,KiDispatchException会完成对异常的分发,而该函数会判断是否具有调试器,如果有调试器,则会通过调用DbgkForwardException函数来采集异常消息

3.3 调试消息结构

为了让调试进程得知被调试进程的状态,内核会将被调试进程所有的调试消息都收集起来发送给调试子系统。

不同的 Dbgk 采集例程会根据当前进程的 DebugPort 字段来判断是否处于被调试状态。如果不是,便会忽略这次调用;如果处于调试状态,便会产生一个0环的 DBGKM_APIMSG 消息结构,该结构的定义如下:

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
typedef struct _DBGKM_APIMSG {
+0x00 PORT_MESSAGE h; // LPC端口消息结构,Windows XP之前使用,但是该成员一直在(0x18字节)
+0x18 DBGKM_APINUMBER ApiNumber; //消息类型
+0x1C NTSTATUS ReturnedStatus; // 调试器的回复状态
+0x20 union {
+0x20 DBGKM_EXCEPTION Exception; // 异常
+0x20 DBGKM_CREATE_THREAD CreateThread; // 创建线程
+0x20 DBGKM_CREATE_PROCESS CreateProcessInfo; // 创建进程
+0x20 DBGKM_EXIT_THREAD ExitThread; // 线程退出
+0x20 DBGKM_EXIT_PROCESS ExitProcess; // 进程退出
+0x20 DBGKM_LOAD_DLL LoadDll; // 映射DLL
+0x20 DBGKM_UNLOAD_DLL UnloadDll; // 卸载DLL
+0x20 } u;
} DBGKM_APIMSG, *PDBGKM_APIMSG;

kd> dt _PORT_MESSAGE -v
nt!_PORT_MESSAGE
struct _PORT_MESSAGE, 7 elements, 0x18 bytes
+0x000 u1 : union __unnamed, 2 elements, 0x4 bytes
+0x004 u2 : union __unnamed, 2 elements, 0x4 bytes
+0x008 ClientId : struct _CLIENT_ID, 2 elements, 0x8 bytes
+0x008 DoNotUseThisField : Float
+0x010 MessageId : Uint4B
+0x014 ClientViewSize : Uint4B
+0x014 CallbackId : Uint4B

其中 ApiNumber 的种类枚举值取自 DBGKM_APINUMBER,不同类型的消息将会封装成 u 中不同的结构。

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
32
typedef struct _DBGKM_CREATE_THREAD {
ULONG SubSystemKey;
PVOID StartAddress;
} DBGKM_CREATE_THREAD, *PDBGKM_CREATE_THREAD;

typedef struct _DBGKM_CREATE_PROCESS {
ULONG SubSystemKey;
HANDLE FileHandle;
PVOID BaseOfImage;
ULONG DebugInfoFileOffset;
ULONG DebugInfoSize;
DBGKM_CREATE_THREAD InitialThread;
} DBGKM_CREATE_PROCESS, *PDBGKM_CREATE_PROCESS;

typedef struct _DBGKM_EXIT_THREAD {
NTSTATUS ExitStatus;
} DBGKM_EXIT_THREAD, *PDBGKM_EXIT_THREAD;

typedef struct _DBGKM_EXIT_PROCESS {
NTSTATUS ExitStatus;
} DBGKM_EXIT_PROCESS, *PDBGKM_EXIT_PROCESS;

typedef struct _DBGKM_LOAD_DLL {
HANDLE FileHandle;
PVOID BaseOfDll;
ULONG DebugInfoFileOffset;
ULONG DebugInfoSize;
} DBGKM_LOAD_DLL, *PDBGKM_LOAD_DLL;

typedef struct _DBGKM_UNLOAD_DLL {
PVOID BaseAddress;
} DBGKM_UNLOAD_DLL, *PDBGKM_UNLOAD_DLL;

3.4 调试消息的采集

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

2.png

针对不同类型的调试事件,均有对应的调试消息采集函数。图中:

  • 黑色字体的为导致调试消息产生的函数
  • 紫色字体的为生成调试消息的_采集函数_;
  • 红色字体的则为调试消息_写入函数_。

并且调试消息采集函数均在导致调试事件发生的函数执行的必经之路上,从而捕获到被调试进程的行为。下面以前4类调试事件为例,分析调试事件采集函数的执行过程。

3.4.1 创建进程、线程

从线程角度来说,创建进程就是创建第一个线程,可以参考《64位CreateProcess逆向》、《基于Win11的CreateProcess逆向分析-3环用户层逆向分析(一)》。

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

  1. PspUserThreadStartup 中如果满足 !DeadThread && !HideFromDebugger 就会调用 DbgkCreateThread

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /*VOID __stdcall PspUserThreadStartup(
    IN PKSTART_ROUTINE StartRoutine, //忽略,并没有使用到(反汇编的代码直接将其忽略)
    IN PVOID StartContext //线程开始执行的地址
    )
    */
    ...
    PAGE:004F8E52 loc_4F8E52: // CODE XREF: PspUserThreadStartup(x,x)+44↑j
    PAGE:004F8E52 xor cl, cl // NewIrql
    PAGE:004F8E54 call ds:__imp_@KfLowerIrql@4 // KfLowerIrql(x)
    PAGE:004F8E5A test byte ptr [esi+_ETHREAD.CrossThreadFlags], 6 // DeadThread:2(创建失败)|
    //HideFromDebugger:4(该线程对于调试器不可见)
    PAGE:004F8E61 jnz short loc_4F8E6B
    PAGE:004F8E63 push [ebp+arg_4]
    PAGE:004F8E66 call _DbgkCreateThread@4 // DbgkCreateThread(x)
    PAGE:004F8E6B loc_4F8E6B: // CODE XREF: PspUserThreadStartup(x,x)+93↑j
    PAGE:004F8E6B cmp [ebp+var_19], 0
    PAGE:004F8E6F jz short loc_4F8E7E
    PAGE:004F8E71 push 0C000004Bh // ExitStatus
    PAGE:004F8E76 push esi // Object
    PAGE:004F8E77 call _PspTerminateThreadByPointer@8

    从代码分析来看,只要不是创建失败且不对调试器隐藏的线程,就一定会调用函数 DbgkCreateThread。如果一个线程不走 DbgkCreateThread 这条路线是不被 CPU 跑起来的,所以是必经之路。

  2. 进入 DbgkCreateThread,进来后,可以看到有一个判断,判断当前进程的 DebugPort 的值是否为空(ebx先前会被清零),这是每个 Dbgk 系列的函数都会做的判断;如果 DebugPort 的值不为空,说明当前进程正在被调试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /* VOID DbgkCreateThread(
    PVOID StartAddress //Supplies the start address for the thread that is starting.
    )
    */
    PAGE:0056C5A9 loc_56C5A9: // CODE XREF: DbgkCreateThread(x)+26↑j
    PAGE:0056C5A9 // DbgkCreateThread(x)+2F↑j
    PAGE:0056C5A9 cmp [esi+_EPROCESS.DebugPort], ebx
    PAGE:0056C5AF jz loc_56C815
    ...
    PAGE:0056C815 loc_56C815: // CODE XREF: DbgkCreateThread(x)+12B↑j
    PAGE:0056C815 // DbgkCreateThread(x)+343↑j ...
    PAGE:0056C815 call __SEH_epilog
    PAGE:0056C81A retn 4

    在判断 DebugPort == 0? 前线程已经调用 PsCallImageNotifyRoutines。如果正在被调试则做两件事:第一件事,通过判断创建的线程是不是第一个线程来判断此时属于创建进程还是创建线程(DbgKmCreateThreadApi/DbgKmCreateProcessApi),第二件事,针对该调试事件将其打包成一个结构体。最终调用 DbgkpSendApiMessage,它的第一个参数,就是刚刚打包的调试事件结构体。可参考《软件调试第二版卷2 9.2.2》

小结:一个线程、进程被创建时,执行到 PspUserThreadStartup ,只要线程创建成功,且该线程不对调试设置隐藏就一定会调用 DbgkCreateThread,以便让调试子系统得到处理机会。在 DbgkCreateThread 中只要 DebugPort != 0 就一定会调用函数 DbgkpSendApiMessage

用户态调试.png

3.4.2 退出进程、线程

在《Terminate/Suspend/ResumeThread函数分析》中已经分析过结束一个线程的执行路径了,线程自杀,也就是退出进程时会调用 PspExitThread。他杀是通过插入APC实现的。

  1. 如下,当 EPROCESS.Flags = 0x8(ProcessDelete) 即退出进程时,会设置变量 var_19 = 1, 这里的变量最后用来指示是退出线程还是退出进程(是否是最后一个线程)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // VOID PspExitThread( IN NTSTATUS ExitStatus )
    ...
    PAGE:004FB180 loc_4FB180: // CODE XREF: PspExitThread(x)+F1↑j
    PAGE:004FB180 dec [edi+_EPROCESS.ActiveThreads]
    PAGE:004FB186 jnz loc_4FB29E
    PAGE:004FB18C push 8 //如果已经是最后一个线程
    PAGE:004FB18E pop eax
    PAGE:004FB18F lea ecx, [edi+_EPROCESS.Flags]
    PAGE:004FB195 lock or [ecx], eax // Flags.ProcessDelete = 1
    PAGE:004FB195 // +0x248 Flags : Uint4B
    PAGE:004FB195 // +0x248 CreateReported : Pos 0, 1 Bit
    PAGE:004FB195 // +0x248 NoDebugInherit : Pos 1, 1 Bit
    PAGE:004FB195 // +0x248 ProcessExiting : Pos 2, 1 Bit
    PAGE:004FB195 // +0x248 ProcessDelete : Pos 3, 1 Bit
    PAGE:004FB195 // +0x248 Wow64SplitPages : Pos 4, 1 Bit
    PAGE:004FB198 mov [ebp+var_19], 1 // if(Flags.ProcessDelete == 1) var_19 = 1;
  2. 如下图,只要对应的条件满足,一定会给内核调试器和用户调试器机会对即将退出的线程进行调试(这里就用到变量var_19)。

    3.png

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // VOID DbgkExitProcess( NTSTATUS ExitStatus )
    PAGE:0056C8ED push 0 //因为进程马上就要退出了,无需再进程挂起和恢复
    //因为进程管理器已经对该线程做了删除标记
    PAGE:0056C8EF lea eax, [ebp+var_78]
    PAGE:0056C8F2 push eax
    PAGE:0056C8F3 mov [ebp+var_78], 78000Ch
    PAGE:0056C8FA mov [ebp+var_74], 8
    PAGE:0056C901 mov [ebp+var_60], 4
    PAGE:0056C908 call _DbgkpSendApiMessage@8 // DbgkpSendApiMessage(x,x)

    // VOID DbgkExitThread( NTSTATUS ExitStatus )
    PAGE:0056C877 call _DbgkpSuspendProcess@0 // 先将进程挂起
    PAGE:0056C87C mov bl, al
    PAGE:0056C87E push 0
    PAGE:0056C880 lea eax, [ebp+var_78]
    PAGE:0056C883 push eax
    PAGE:0056C884 call _DbgkpSendApiMessage@8 // DbgkpSendApiMessage(x,x)

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

消息采集_退出进程_线程的副本.png

3.4.3 模块的加载、卸载

3.4.4 异常消息

3.5 调试消息的发送

在 3.4 中讲解了调试消息的采集,产生调试消息的被调试进程的线程调用相关 Dbgk 函数,将相应的调试消息封装成 DBGKM_APIMSG 结构,然后调用 DbgkpSendApiMessage 来传送该调试消息给调试器

3.5.1 DbgkpSendApiMessage

函数 DbgkpSendApiMessage :该函数将一条调试消息发送到调试子系统服务器。

1
2
3
4
NTSTATUS DbgkpSendApiMessage(
IN OUT PDBGKM_APIMSG ApiMsg, // 要发送的消息
IN BOOLEAN SuspendProcess // 是否挂起当前进程
)
参数 含义
ApiMsg 消息结构,每种消息都有自己的消息结构,由不同的调试事件采集函数创建,共有7种类型。
SuspendProcess 如果 SuspendProcessTRUE,那么这个函数会先调用 DbgkpSuspendProcess 函数挂起本进程内除了自己之外的其他所有线程,然后发送消息,消息发送后将当前线程也挂起。等收到消息回复后先唤醒消息处理的线程,然后再调用 DbgkpResumeProcess 函数唤醒当前进程的其他所有线程。

4.png

KeFreezeAllThreads :当被调试进程中断到调试器中时,它当前线程的 FreezeCount 通常为0,其他线程的 FreezeCount 通常为1。因为 KeFreezeAllThreads 不会冻结当前线程,包括 WinDBG 在内的调试器在收到调试事件后,会对被调试进程中的所有线程依次调用 SuspendThread,这样所有线程的 SuspendCount 计数通常都为 1

DbgkpSendApiMessage.png

关于 KTHREAD.FrezeeCountKTHREAD.SuspendCount

  • KTHREAD.FrezeeCount:该字段由 KeFreezeAllThreads函数和 KeThawAllThreads 操作。当一个线程的进程被中断到调试器之后,当前线程FreezeCount == 0,其余线程FreezeCount == 1。即该字段为 1 时表示这个线程处于冻结状态。《软件调试第二版卷2 9.3.3 P175》
  • KTHREAD.SuspendCount:当前线程被挂起的次数,次数小于1则会恢复执行线程。线程活动时该值为0。该字段加减次数由SuspendThreadResumeThread (对应于 NtSuspendThread 内核服务KeSuspendThread)操作。
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
PAGE:0056C1F0 // =============== S U B R O U T I N E =======================================
PAGE:0056C1F0
PAGE:0056C1F0 // Attributes: bp-based frame
PAGE:0056C1F0
PAGE:0056C1F0 // __stdcall DbgkpSendApiMessage(x, x)
PAGE:0056C1F0 _DbgkpSendApiMessage@8 proc near // CODE XREF: DbgkForwardException(x,x,x)+8A↓p
PAGE:0056C1F0 // DbgkCreateThread(x)+218↓p ...
PAGE:0056C1F0
PAGE:0056C1F0 arg_ApiMsg = dword ptr 8
PAGE:0056C1F0 arg_bSuspendProcess= byte ptr 0Ch
PAGE:0056C1F0
PAGE:0056C1F0 mov edi, edi
PAGE:0056C1F2 push ebp
PAGE:0056C1F3 mov ebp, esp
PAGE:0056C1F5 push ebx
PAGE:0056C1F6 xor ebx, ebx
PAGE:0056C1F8 cmp [ebp+arg_bSuspendProcess], bl
PAGE:0056C1FB push esi
PAGE:0056C1FC jz short loc_56C206
PAGE:0056C1FE call _DbgkpSuspendProcess@0 // DbgkpSuspendProcess()
PAGE:0056C203 mov [ebp+arg_bSuspendProcess], al
PAGE:0056C206
PAGE:0056C206 loc_56C206: // CODE XREF: DbgkpSendApiMessage(x,x)+C↑j
PAGE:0056C206 mov edx, [ebp+arg_ApiMsg]
PAGE:0056C209 mov dword ptr [edx+1Ch], 103h // DBGKM_APIMSG.ReturnedStatus, STATUS_PENDING
PAGE:0056C209 // 表示调试器正在等待调试消息
PAGE:0056C210 mov eax, large fs:_KPCR.PrcbData.CurrentThread
PAGE:0056C216 mov ecx, [eax+_KTHREAD.ApcState.Process]
PAGE:0056C219 xor eax, eax
PAGE:0056C21B inc eax // PS_PROCESS_FLAGS_CREATE_REPORTED(Create process debug call has occurred)
PAGE:0056C21C lea esi, [ecx+_EPROCESS.Flags]
PAGE:0056C222 lock or [esi], eax
PAGE:0056C225 mov eax, large fs:_KPCR.PrcbData.CurrentThread
PAGE:0056C22B push ebx // PFAST_MUTEX
PAGE:0056C22C push ebx // PRKEVENT
PAGE:0056C22D push edx // &ApiMsg
PAGE:0056C22E push eax // &CurrentThread
PAGE:0056C22F push ecx // &CurrentProcess
PAGE:0056C230 call _DbgkpQueueMessage@20 // NTSTATUS DbgkpQueueMessage (
PAGE:0056C230 // IN PEPROCESS Process,
PAGE:0056C230 // IN PETHREAD Thread,
PAGE:0056C230 // IN OUT PDBGKM_APIMSG ApiMsg,
PAGE:0056C230 // IN ULONG Flags,
PAGE:0056C230 // IN PDEBUG_OBJECT TargetDebugObject
PAGE:0056C230 // )
PAGE:0056C235 push ebx // NumberOfBytesToFlush
PAGE:0056C236 push ebx // BaseAddress
PAGE:0056C237 push 0FFFFFFFFh // ProcessHandle
PAGE:0056C239 mov esi, eax
PAGE:0056C23B call _ZwFlushInstructionCache@12 // ZwFlushInstructionCache(x,x,x)
PAGE:0056C240 cmp [ebp+arg_bSuspendProcess], bl
PAGE:0056C243 jz short loc_56C24A
PAGE:0056C245 call _DbgkpResumeProcess@0 // DbgkpResumeProcess()
PAGE:0056C24A
PAGE:0056C24A loc_56C24A: // CODE XREF: DbgkpSendApiMessage(x,x)+53↑j
PAGE:0056C24A mov eax, esi
PAGE:0056C24C pop esi
PAGE:0056C24D pop ebx
PAGE:0056C24E pop ebp
PAGE:0056C24F retn 8
PAGE:0056C24F _DbgkpSendApiMessage@8 endp

3.5.2 DbgkpQueueMessage

函数 DbgkpQueueMessage :将扩展的调试消息 Queue 到调试对象 DEBUG_OBJECT 的消息队列 EventList,以便用户模式调试器获取消息。

1
2
3
4
5
6
7
NTSTATUS __stdcall DbgkpQueueMessage (
IN PEPROCESS Process, //被调试进程对象地址
IN PETHREAD Thread, //被调试进程的线程对象地址
IN OUT PDBGKM_APIMSG ApiMsg, //要传送的调试消息
IN ULONG Flags, //NOWAIT 标志,Flags=0x2时是异步调试消息发送。Flags!=0x2时是同步消息发送,需要等待
IN PDEBUG_OBJECT TargetDebugObject //异步发送消息时有效,是一个调试对象
)

3.5.3 _DBGKM_DEBUG_EVENT

注意调试对象的消息队列 DEBUG_OBJECT.EventList 串着的都是 DEBUG_EVENT 结构,该结构是对 DBGKM_APIMSG 的拓展,但是 0 环和 3 环该结构的定义不一样

为了区分使用,所以本文借鉴《软件调试》中的命名方式,将 0 环使用的结构名称换为_DBGKM_DEBUG_EVENT

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
// 0环使用, _DBGKM_DEBUG_EVENT
typedef struct _DEBUG_EVENT {
+0x00 LIST_ENTRY EventList; // 与兄弟节点相互链接的节点结构,注意这不是调试消息队列
+0x08 KEVENT ContinueEvent; // 用于等待调试器回复的事件对象
+0x18 CLIENT_ID ClientId; // 调试事件所属的线程 ID 和进程 ID
+0x20 PEPROCESS Process; // 被调试进程的 EPROCESS 结构地址
+0x24 PETHREAD Thread; // 被调试进程中触发调试事件的线程的 ETHREAD 地址
+0x28 NTSTATUS Status; // 对调试事件的处理结果
+0x2C ULONG Flags; // 标志(调试消息是否已经被读取)
+0x30 PETHREAD BackoutThread;// 产生杜撰消息(faked message)的线程
+0x38 DBGKM_APIMSG ApiMsg; // 调试消息的详细信息
} DEBUG_EVENT, *PDEBUG_EVENT;

// 3环使用
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode; //事件类型,对应消息类型
DWORD dwProcessId; //被调试进程PID
DWORD dwThreadId; //被调试线程TID
union { //根据不同的事件类型,对应于不同的结构
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

需要说明的是:

  • 在 0 环:

    • DebugObject->EventsPresent:该成员在 0 环将其信号设置为 1,通知 3 环的调试器来取调试消息。

    • DbgkmDebugEvent.ContinueEvent:该成员在 0 环,用来等待调试器传来的信号。并且这里使用 KeWaitForSingleObject 将被调试进程的最后一个活动线程也挂起,当这个线程获得信号后返回到 DbgkpSendApiMessage 中调用 DbgkpResumeProcess 唤醒其他所有线程。

    • 在 0 环的调用结构是这样:

      1
      2
      KeSetEvent(&DebugObject->EventsPresent, 0, 0)
      KeWaitForSingleObject(&DbgkmDebugEvent->ContinueEvent, Executive, KernelMode, FALSE, NULL);
  • 我猜在 3 环的调用结构应该是这样:

    1
    2
    KeWaitForSingleObject(&DebugObject->EventsPresent, Executive, UserMode, FALSE, NULL);
    KeSetEvent(&DbgkmDebugEvent->ContinueEvent, 0, 0)

DbgkpQueueMessage.png

参考内容:《Windows调试流程分析-Win10 1511 Build:10586》《科锐三阶段项目-跟踪调试框架》《读书笔记|Windows 调试原理学习|持续更新》《win7 x64内核调试函数逆向还原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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
PAGE:0056AFDB // ---------------------------------------------------------------------------
PAGE:0056AFDE db 6 dup(0CCh)
PAGE:0056AFE4
PAGE:0056AFE4 // =============== S U B R O U T I N E =======================================
PAGE:0056AFE4
PAGE:0056AFE4 // NTSTATUS DbgkpQueueMessage (
PAGE:0056AFE4 // IN PEPROCESS Process,
PAGE:0056AFE4 // IN PETHREAD Thread,
PAGE:0056AFE4 // IN OUT PDBGKM_APIMSG ApiMsg,
PAGE:0056AFE4 // IN ULONG Flags,
PAGE:0056AFE4 // IN PDEBUG_OBJECT TargetDebugObject
PAGE:0056AFE4 // )
PAGE:0056AFE4 // Attributes: bp-based frame
PAGE:0056AFE4
PAGE:0056AFE4 // int __stdcall DbgkpQueueMessage(PVOID, PVOID, int, PRKEVENT, PFAST_MUTEX)
PAGE:0056AFE4 _DbgkpQueueMessage@20 proc near // CODE XREF: DbgkpPostFakeThreadMessages(x,x,x,x,x)+150↓p
PAGE:0056AFE4 // DbgkpPostFakeModuleMessages(x,x,x)+136↓p ...
PAGE:0056AFE4
PAGE:0056AFE4 var_B8 = byte ptr -0B8h
PAGE:0056AFE4 var_8C = dword ptr -8Ch
PAGE:0056AFE4 var_DBGKM_DEBUG_EVENT.ApiMsg= dword ptr -8
PAGE:0056AFE4 var_Flags_NOWAIT= dword ptr -4
PAGE:0056AFE4 arg_Process = dword ptr 8
PAGE:0056AFE4 arg_Thread = dword ptr 0Ch
PAGE:0056AFE4 arg_pApiMsg = dword ptr 10h
PAGE:0056AFE4 arg_Flags_0_DebugObj= dword ptr 14h
PAGE:0056AFE4 arg_TargetDebugObject_0_DEBUG_OBJECT.Mutex= dword ptr 18h
PAGE:0056AFE4
PAGE:0056AFE4 mov edi, edi
PAGE:0056AFE6 push ebp
PAGE:0056AFE7 mov ebp, esp
PAGE:0056AFE9 sub esp, 0B8h
PAGE:0056AFEF push ebx
PAGE:0056AFF0 push esi
PAGE:0056AFF1 mov esi, [ebp+arg_Flags_0_DebugObj]
PAGE:0056AFF4 mov [ebp+var_Flags_NOWAIT], esi
PAGE:0056AFF7 and [ebp+var_Flags_NOWAIT], 2 // var_4 == 0
PAGE:0056AFFB jz short loc_56B045
PAGE:0056AFFD push 45676244h // Tag
PAGE:0056B002 push 0B0h // NumberOfBytes
PAGE:0056B007 push 8 // PoolType
PAGE:0056B009 call _ExAllocatePoolWithQuotaTag@12 // PVOID KsiAllocatePoolWithQuotaTag(
PAGE:0056B009 // IN POOL_TYPE PoolType,
PAGE:0056B009 // IN SIZE_T NumberOfBytes,
PAGE:0056B009 // IN ULONG Tag)
PAGE:0056B00E mov ebx, eax
PAGE:0056B010 test ebx, ebx
PAGE:0056B012 jnz short loc_56B01E
PAGE:0056B014 mov eax, 0C000009Ah
PAGE:0056B019 jmp loc_56B192
PAGE:0056B01E // ---------------------------------------------------------------------------
PAGE:0056B01E
PAGE:0056B01E loc_56B01E: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+2E↑j
PAGE:0056B01E mov ecx, [ebp+arg_Process] // Object
PAGE:0056B021 or esi, 4
PAGE:0056B024 mov [ebx+DBGKM_DEBUG_EVENT.Flags], esi
PAGE:0056B027 call @ObfReferenceObject@4 // ObfReferenceObject(x)
PAGE:0056B02C mov ecx, [ebp+arg_Thread] // Object
PAGE:0056B02F call @ObfReferenceObject@4 // ObfReferenceObject(x)
PAGE:0056B034 mov eax, large fs:124h
PAGE:0056B03A mov [ebx+DBGKM_DEBUG_EVENT.BackoutThread], eax
PAGE:0056B03D mov eax, [ebp+arg_TargetDebugObject_0_DEBUG_OBJECT.Mutex]
PAGE:0056B040 mov [ebp+arg_Flags_0_DebugObj], eax
PAGE:0056B043 jmp short loc_56B0A2 // KeInitializeEvent()
PAGE:0056B045 // ---------------------------------------------------------------------------
PAGE:0056B045
PAGE:0056B045 loc_56B045: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+17↑j
PAGE:0056B045 mov ecx, offset _DbgkpProcessDebugPortMutex // FastMutex
PAGE:0056B04A lea ebx, [ebp+var_B8]
PAGE:0056B050 mov [ebp+var_8C], esi // esi == 0
PAGE:0056B056 call ds:__imp_@ExAcquireFastMutex@4 // ExAcquireFastMutex(x)
PAGE:0056B05C mov eax, [ebp+arg_Process]
PAGE:0056B05F mov eax, [eax+_EPROCESS.DebugPort] // 此时DebugPort是调试对象地址
PAGE:0056B065 mov [ebp+arg_Flags_0_DebugObj], eax
PAGE:0056B068 mov eax, [ebp+arg_pApiMsg]
PAGE:0056B06B mov eax, [eax+18h] // eax = DBGKM_APIMSG.ApiNumber
PAGE:0056B06E cmp eax, 1 // 1、对于DbgKmCreateThreadApi、DbgKmCreateProcessApi判断:
PAGE:0056B06E // 如果Thread->CrossThreadFlags==PS_CROSS_THREAD_FLAGS_SKIP_CREATION_MSG
PAGE:0056B06E // 则不向调试器发送创建消息。
PAGE:0056B06E // 2、对于DbgKmExitThreadApi、DbgKmExitProcessApi判断:
PAGE:0056B06E // 如果Thread->CrossThreadFlags==PS_CROSS_THREAD_FLAGS_SKIP_CREATION_MSG
PAGE:0056B06E // 则不向调试器发送终止消息。
PAGE:0056B071 jz short loc_56B078 // 直接跳到 0x56B0A2 即可
PAGE:0056B073 cmp eax, 2
PAGE:0056B076 jnz short loc_56B088
PAGE:0056B078
PAGE:0056B078 loc_56B078: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+8D↑j
PAGE:0056B078 mov ecx, [ebp+arg_Thread]
PAGE:0056B07B test byte ptr [ecx+_ETHREAD.CrossThreadFlags], 80h
PAGE:0056B082 jz short loc_56B088
PAGE:0056B084 and [ebp+arg_Flags_0_DebugObj], 0
PAGE:0056B088
PAGE:0056B088 loc_56B088: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+92↑j
PAGE:0056B088 // DbgkpQueueMessage(x,x,x,x,x)+9E↑j
PAGE:0056B088 cmp eax, 3
PAGE:0056B08B jz short loc_56B092
PAGE:0056B08D cmp eax, 4
PAGE:0056B090 jnz short loc_56B0A2 // KeInitializeEvent()
PAGE:0056B092
PAGE:0056B092 loc_56B092: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+A7↑j
PAGE:0056B092 mov eax, [ebp+arg_Thread]
PAGE:0056B095 test byte ptr [eax+249h], 1
PAGE:0056B09C jz short loc_56B0A2 // KeInitializeEvent()
PAGE:0056B09E and [ebp+arg_Flags_0_DebugObj], 0
PAGE:0056B0A2
PAGE:0056B0A2 loc_56B0A2: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+5F↑j
PAGE:0056B0A2 // DbgkpQueueMessage(x,x,x,x,x)+AC↑j ...
PAGE:0056B0A2 and [ebx+DBGKM_DEBUG_EVENT.ContinueEvent.DISPATCHER_HEADER.SignalState], 0 // KeInitializeEvent()
PAGE:0056B0A6 mov esi, [ebp+arg_pApiMsg]
PAGE:0056B0A9 mov [ebx+DBGKM_DEBUG_EVENT.ContinueEvent.Type], 1 // EventSynchronizationObject
PAGE:0056B0AD mov [ebx+DBGKM_DEBUG_EVENT.ContinueEvent.Size], 4
PAGE:0056B0B1 lea eax, [ebx+DBGKM_DEBUG_EVENT.ContinueEvent.WaitListHead]
PAGE:0056B0B4 mov [eax+_LIST_ENTRY.Blink], eax
PAGE:0056B0B7 mov [eax+_LIST_ENTRY.Flink], eax
PAGE:0056B0B9 mov eax, [ebp+arg_Process]
PAGE:0056B0BC push edi
PAGE:0056B0BD mov [ebx+DBGKM_DEBUG_EVENT.Process], eax
PAGE:0056B0C0 mov eax, [ebp+arg_Thread]
PAGE:0056B0C3 lea edi, [ebx+38h]
PAGE:0056B0C6 push 1Eh
PAGE:0056B0C8 mov [ebx+DBGKM_DEBUG_EVENT.Thread], eax
PAGE:0056B0CB mov [ebp-8], edi // 局部变量2,&_DBGKM_DEBUG_EVENT.ApiMsg
PAGE:0056B0CE pop ecx
PAGE:0056B0CF rep movsd
PAGE:0056B0D1 mov ecx, [ebp+arg_Flags_0_DebugObj]
PAGE:0056B0D4 mov esi, eax
PAGE:0056B0D6 mov eax, [esi+_ETHREAD.Cid.UniqueProcess]
PAGE:0056B0DC mov [ebx+DBGKM_DEBUG_EVENT.ClientId.UniqueProcess], eax
PAGE:0056B0DF mov eax, [esi+_ETHREAD.Cid.UniqueThread]
PAGE:0056B0E5 xor edi, edi
PAGE:0056B0E7 cmp ecx, edi // 判断 EPROCESS.DebugPort == 0 ?
PAGE:0056B0E9 mov [ebx+DBGKM_DEBUG_EVENT.ClientId.UniqueThread], eax
PAGE:0056B0EC jnz short loc_56B0F7 // ecx == &DEBUG_OBJECT.Mutex
PAGE:0056B0EE mov [ebp+arg_Thread], 0C0000353h // STATUS_PORT_NOT_SET
PAGE:0056B0F5 jmp short loc_56B13E
PAGE:0056B0F7 // ---------------------------------------------------------------------------
PAGE:0056B0F7
PAGE:0056B0F7 loc_56B0F7: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+108↑j
PAGE:0056B0F7 add ecx, 10h // ecx == &DEBUG_OBJECT.Mutex
PAGE:0056B0FA mov [ebp+arg_TargetDebugObject_0_DEBUG_OBJECT.Mutex], ecx
PAGE:0056B0FD call ds:__imp_@ExAcquireFastMutex@4 // ExAcquireFastMutex(x)
PAGE:0056B103 mov edx, [ebp+arg_Flags_0_DebugObj]
PAGE:0056B106 test byte ptr [edx+38h], 1 // -----------------------------------
PAGE:0056B106 // 如果此时 DEBUG_OBJECT.Flags == 2,代表结束调试会话时是否终止被调试进程
PAGE:0056B106 // #define DEBUG_OBJECT_DELETE_PENDING (0x1) // 调试对象是否被正在被删除
PAGE:0056B106 // #define DEBUG_OBJECT_KILL_ON_CLOSE (0x2) // Kill all debugged processes on close
PAGE:0056B106 // -----------------------------------
PAGE:0056B10A jnz short loc_56B12E // STATUS_DEBUGGER_INACTIVE
PAGE:0056B10C cmp [ebp+var_Flags_NOWAIT], edi // var_4 == 0,edi == 0
PAGE:0056B10F lea eax, [edx+30h] // eax = &DEBUG_OBJECT.EventList
PAGE:0056B112 mov ecx, [eax+DBGKM_DEBUG_EVENT.EventList.Blink] // 将刚采集的 DBGKM_DEBUG_EVENT 插入到调试对象的 EventList链尾
PAGE:0056B115 mov [ebx+DBGKM_DEBUG_EVENT.EventList.Flink], eax
PAGE:0056B117 mov [ebx+DBGKM_DEBUG_EVENT.EventList.Blink], ecx
PAGE:0056B11A mov [ecx], ebx
PAGE:0056B11C mov [eax+4], ebx
PAGE:0056B11F jnz short loc_56B129 // KeSetEvent 使事件有信号,让调试器尽快处理调试事件(消息)
PAGE:0056B121 push edi // Wait
PAGE:0056B122 push edi // Increment
PAGE:0056B123 push edx // Event
PAGE:0056B124 call _KeSetEvent@12 // KeSetEvent(x,x,x)
PAGE:0056B129
PAGE:0056B129 loc_56B129: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+13B↑j
PAGE:0056B129 mov [ebp+arg_Thread], edi
PAGE:0056B12C jmp short loc_56B135
PAGE:0056B12E // ---------------------------------------------------------------------------
PAGE:0056B12E
PAGE:0056B12E loc_56B12E: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+126↑j
PAGE:0056B12E mov [ebp+arg_Thread], 0C0000354h // STATUS_DEBUGGER_INACTIVE
PAGE:0056B135
PAGE:0056B135 loc_56B135: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+148↑j
PAGE:0056B135 mov ecx, [ebp+arg_TargetDebugObject_0_DEBUG_OBJECT.Mutex] // FastMutex
PAGE:0056B138 call ds:__imp_@ExReleaseFastMutex@4 // ExReleaseFastMutex(x)
PAGE:0056B13E
PAGE:0056B13E loc_56B13E: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+111↑j
PAGE:0056B13E cmp [ebp+var_Flags_NOWAIT], edi
PAGE:0056B141 jnz short loc_56B173
PAGE:0056B143 mov ecx, offset _DbgkpProcessDebugPortMutex // FastMutex
PAGE:0056B148 call ds:__imp_@ExReleaseFastMutex@4 // ExReleaseFastMutex(x)
PAGE:0056B14E cmp [ebp+arg_Thread], edi
PAGE:0056B151 jl short loc_56B18E
PAGE:0056B153 push edi // Timeout
PAGE:0056B154 push edi // Alertable
PAGE:0056B155 push edi // WaitMode
PAGE:0056B156 push edi // WaitReason
PAGE:0056B157 lea eax, [ebx+8]
PAGE:0056B15A push eax // Object
PAGE:0056B15B call _KeWaitForSingleObject@20 // KeWaitForSingleObject(x,x,x,x,x)
PAGE:0056B160 mov eax, [ebx+DBGKM_DEBUG_EVENT.Status] // 这里将被调试进程的最后一个活动线程也挂起
PAGE:0056B163 mov esi, [ebp+var_DBGKM_DEBUG_EVENT.ApiMsg]
PAGE:0056B166 mov edi, [ebp+arg_pApiMsg]
PAGE:0056B169 push 1Eh
PAGE:0056B16B pop ecx
PAGE:0056B16C mov [ebp+arg_Thread], eax // 接收从调试器传回来的消息
PAGE:0056B16C // Status = DebugEvent->Status//
PAGE:0056B16C // *ApiMsg = DebugEvent->ApiMsg//
PAGE:0056B16F rep movsd
PAGE:0056B171 jmp short loc_56B18E
PAGE:0056B173 // ---------------------------------------------------------------------------
PAGE:0056B173
PAGE:0056B173 loc_56B173: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+15D↑j
PAGE:0056B173 cmp [ebp+arg_Thread], edi
PAGE:0056B176 jge short loc_56B18E
PAGE:0056B178 mov ecx, [ebp+arg_Process] // Object
PAGE:0056B17B call @ObfDereferenceObject@4 // --
PAGE:0056B17B // 减少指定对象的对象引用计数,并在计数变为零时进行清理工作。
PAGE:0056B17B // 不同于句柄引用计数
PAGE:0056B17B // --
PAGE:0056B180 mov ecx, esi // Object
PAGE:0056B182 call @ObfDereferenceObject@4 // --
PAGE:0056B182 // 减少指定对象的对象引用计数,并在计数变为零时进行清理工作。
PAGE:0056B182 // 不同于句柄引用计数
PAGE:0056B182 // --
PAGE:0056B187 push edi // Tag
PAGE:0056B188 push ebx // P
PAGE:0056B189 call _ExFreePoolWithTag@8 // ExFreePoolWithTag(x,x)
PAGE:0056B18E
PAGE:0056B18E loc_56B18E: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+16D↑j
PAGE:0056B18E // DbgkpQueueMessage(x,x,x,x,x)+18D↑j ...
PAGE:0056B18E mov eax, [ebp+arg_Thread]
PAGE:0056B191 pop edi
PAGE:0056B192
PAGE:0056B192 loc_56B192: // CODE XREF: DbgkpQueueMessage(x,x,x,x,x)+35↑j
PAGE:0056B192 pop esi
PAGE:0056B193 pop ebx
PAGE:0056B194 leave
PAGE:0056B195 retn 14h
PAGE:0056B195 _DbgkpQueueMessage@20 endp

4 杜撰的调试消息

在 2.3 中讲了如何将调试器附加到一个已经处于运行状态的被调试进程,但是还有两个细节没有说(没有逆向分析)。本节是对 2.3 的补充,同时本节用来和 3.4.1 作对比。

当使用附加的方式来调试一个进程时,调试子系统会“假装”此时正在创建一个新进程,模仿像创建进程那样给调试器发送杜撰的消息(faked debug message)。但是有一点区别:

  1. 在被调试进程 DbgkCreateThread 中,当判断是创建进程时,会调用两次 DbgkpSendApiMessage 来分别发送 DbgKmCreateProcessApi-DbgKmLoadDllApiDbgKmCreateThreadApi 消息。发送调试信息的线程属于被调试进程。可以挂起当前进程。消息发送是同步的。
  2. 在发送杜撰消息时,调试器的线程直接使用 DbgkpQueueMessage 来发送上面这三类消息。发送调试信息的线程属于调试器进程。不会挂起被调试进程,且消息发送是异步的(指定了 NO_WAIT )。

nt!NtDebugActiveProcess 中函数的主要调用顺序如下(由于该函数调用的子函数太多了,所以本节主要是参考源码和软件调试第二版卷2 9.4.5 来精简总结):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NTSTATUS NtDebugActiveProcess(IN HANDLE ProcessHandle, IN HADNLE DebugObjectHandle)
{
PDEBUG_OBJECT DebugObject;
PEPROCESS Process;
NTSTATUS Status;

Status = ObReferenceObjectByHandle(ProcessHandle,PROCESS_SET_PORT,PsProcessType,PreviousMode,&Process,NULL);
Status = ObReferenceObjectByHandle(DebugObjectHandle,DEBUG_PROCESS_ASSIGN,DbgkDebugObjectType,PreviousMode,
&DebugObject,NULL);
// 发送杜撰的进程/线程创建消息
Status = DbgkpPostFakeProcessCreateMessages(Process,DebugObject,&LastThread);

Status = DbgkpSetProcessDebugObject(Process,DebugObject,Status,LastThread);
return Status;
}

函数 DbgkpSetProcessDebugObject 用来将被调试进程和调试对象进行关联,但是在关联之前会调用 DbgkpPostFakeProcessCreateMessages 来发送上面提到的杜撰的消息。

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
NTSTATUS DbgkpPostFakeProcessCreateMessages (
IN PEPROCESS Process, //被调试的进程对象
IN PDEBUG_OBJECT DebugObject, //调试对象
IN PETHREAD *pLastThread
)
{
NTSTATUS Status;
KAPC_STATE ApcState;
PETHREAD Thread;
PETHREAD LastThread;

// 将当前属于调试器的线程附加到被调试进程地址空间,CR3。
// 调试会话建立的过程是由调试器发起的,所以当前线程属于调试器进程。
KeStackAttachProcess(&Process->Pcb, &ApcState);

// 发送 DbgKmCreateProcessApi/DbgKmCreateThreadApi 消息
Status = DbgkpPostFakeThreadMessages(Process, DebugObject, NULL, &Thread, &LastThread);

// 发送 DbgKmLoadDllApi 消息
if(NT_SUCCESS(Status))
{
Status = DbgkpPostFakeModuleMessages(Process, Thread, DebugObject);
}

return Status;
}

DbgkpPostFakeProcessCreateMessages 会先调用 DbgkpPostFakeThreadMessages,后者会遍历被调试进程的所有线程,以向调试对象的消息队列中投放杜撰的进程和线程来创建消息。而后 DbgkpPostPakeProcessCreateMessages 会调用DbgkpPostFakeModuleMessages 来投放杜撰的模块加载消息。

DbgkpPostFakeThreadMessagesDbgkpPostFakeModuleMessages 都是调用 DbgkpQueueMessage 来向消息队列添加调试消息的。

因为在参数中指定了不需等待的标志NOWAIT,异步),所以 DbgkpQueueMessage 将事件放人队列后便会返回,不会设置 EventsPresent 对象以避免它通知调试器来读取。

DbgkpSetProcessDebugObject 函数内部除了将调试对象赋给被调试进程 EPROCESS 结构的 DebugPort字段,还会调用 DbgkpMarkProcessPeb 函数设置被调试进程环境块PEB.BeingDebugged字段(用于在用户态判断进程是否正在被调试):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VOID DbgkpMarkProcessPeb(PEPROCESS Process)
{
if(ExAcquireRundownProtection (&Process->RundownProtect))
{
KeStackAttachProcess(&Process->Pcb, &ApcState);
ExAcquireFastMutex (&DbgkpProcessDebugPortMutex);

Process->Peb->BeingDebugged = (BOOLEAN)(Process->DebugPort != NULL ? TRUE : FALSE);

ExReleaseFastMutex (&DbgkpProcessDebugPortMutex);
KeUnstackDetachProcess(&ApcState);
}
ExReleaseRundownProtection (&Process->RundownProtect);
}

如果一个进程不在被调试状态,那么其 PEB 结构的 Being Debugged 字段为 0;否则,为 1。ISDebuggerPresent API 就是通过判断 BeingDebugged 字段实现的。

5 清除调试对象

当调试结束后需要撤销调试会话时,系统会调用 DbgkClearProcessDebugObject 将被调试进程的 DebugPort 字段恢复为 NULL。恢复时,这个函数会遍历调试对象的消息队列,将关于这个进程的调试事件清除。这个函数并不破坏调试对象,因为一个调试器可以同时调试多个被调试进程,这个调试对象可能还在被其他被调试进程所使用。

以前的 TP 保护就是起一个线程不断地扫描要保护的程序的 DebugPort 字段,来检测是否被调试,然后将 DebugPort 不断清零。后来逐渐演变,有的调试器在建立调试会话时,会将 DebugPort 移位到其他一些不常用的偏移处或保留位置(如 ExitTime)。也有的是将 EPROCESS 结构进行扩展,然后将相关调试结构移位到扩展的位置。

还有的检测手段就是,起一个线程,不断地遍历每一个线程 TEB.DbgSsReserved 出来判断其是否是调试器。

可以参考:

6 用户态调试全景

5.png