Windows软件调试初探-用户态调试过程

调试器进程

本节调试器指的是用户态调试器。

调试器有俩线程,一个线程负责与用户对话,称为UI线程;另一个线程负责与被调试进程对话,称为调试器工作线程DWT或调试会话线程。WinDBG Classic的俩线程栈回溯为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UI线程:
ntdll!KiFastSystemCallRet //内核模式执行系统服务
USER32!NtUserWaitMessage //调用等待窗口消息的子系统服务
windbg!_wmainCRTStartup //程序启动函数
kernel32!BaseProcessStart //系统的进程启动函数
工作线程:
ntdll!ZwWaitForDebugEvent //调用等待调试事件的内核服务
dbgeng!LiveUserDebugServices::WaitForEvent
dbgeng!LiveUserTargetInfo::WaitForEvent
dbgeng!WaitForAnyTarget
dbgeng!RawWaitForEvent //调试引擎内部函数
dbgeng!DebugClient::WaitForEvent //等待调试事件
windbg!EngineLoop //调试事件循环
kernel32!BaseThreadStart //线程初始函数

DWT伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (bNewProcess) //建立调试对话
CreateProcess(..., DEBUG_PROCESS, ...);
else
DebugActiveProcess(dwPID);
//调试事件循环:
while (1 == WaitForDebugEvent(&DbgEvt, INFINITE)) { //等待调试事件
switch (DbgEvt.dwDebugEventCode) {
case EXIT_PROCESS_DEBUG_EVENT: {
break;
};
//...
};
ContinueDebugEvent(...); //恢复运行被调试程序
};

DWT的TEB的DbgSsReserved字段中,DbgSsReserved[0]记录被调试线程链表表头,链表每个节点为一个DBGSS_THREAD_DATA结构,描述被调试进程中的一个线程;DbgSsReserved[1]记录调试对象句柄;DbgSsReserved[2]数组记录调试器工作线程与调试子系统间通信用的同步对象和通信对象。

1
2
3
4
5
6
7
8
typedef struct _DBGSS_THREAD_DATA {
struct _DBGSS_THREAD_DATA* Next; //指向下一个节点
HANDLE ThreadHandle; //被调试进程中线程句柄
HANDLE ProcessHandle; //被调试进程句柄
DWORD ProcessId; //被调试进程ID
DWORD ThreadId; //被调试进程中线程ID
BOOLEAN HandleMarked; //退出标记
} DBGSS_THREAD_DATA, * PDBGSS_THREAD_DATA;

观察DbgSsReserved字段需要时机。用CreateProcess创建调试会话时,内部用DbgUiConnectToDbgDbgSsReserved[1]设置为非0。在CreateProcess返回后用DbgUiSetThreadDebugObject将其设为0。

观察DMT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0:015> !teb
TEB at 0000009cecad0000
ExceptionList: 0000000000000000
StackBase: 0000009cee6c0000
StackLimit: 0000009cee6bc000
SubSystemTib: 0000000000000000
FiberData: 0000000000001e00
ArbitraryUserPointer: 0000000000000000
Self: 0000009cecad0000
EnvironmentPointer: 0000000000000000
ClientId: 00000000000008c0 . 0000000000000fc8
RpcHandle: 0000000000000000
Tls Storage: 0000000000000000
PEB Address: 0000009cecb8c000
LastErrorValue: 0
LastStatusValue: 0
Count Owned Locks: 0
HardErrorMode: 0
0:015> dt -b ntdll!_Teb 0000009cecad0000 -y DbgSsReserved
+0x16a0 DbgSsReserved :
[00] ...
[01] ... //可用!handle命令查看句柄

俩DbgUi函数就是读写该字段:

1
2
3
4
5
6
7
8
9
void* DbgUiGetThreadDebugObject() {
return NtCurrentTeb()->DbgSsReserved[1];
};
struct _TEB* __fastcall DbgUiSetThreadDebugObject(void* a1) {
struct _TEB* result; // rax
result = NtCurrentTeb();
result->DbgSsReserved[1] = a1;
return result;
};

被调试进程

被调试进程与普通进程相比有些差异:EPROCESS的DebugPort不为空,用于在内核空间中判断;PEB的BeingDebugged不为0,用于在用户态判断;可能存在由调试器远程启动的远程中断线程,用于将被调试进程中断到调试器;响应调试快捷键F12使被调试进程中断到调试器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
lkd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
...
PROCESS ffffaf8c0dcce080
SessionId: 1 Cid: 1424 Peb: e4ea31d000 ParentCid: 0d54
DirBase: 6c006000 ObjectTable: ffffc0813b314e40 HandleCount: 229.
Image: notepad.exe
...
lkd> dt nt!_EPROCESS ffffaf8c0dcce080
+0x000 Pcb : _KPROCESS
...
+0x4b0 ExceptionPortData : 0xffffaf8c`0d06eca0 Void
+0x4b0 ExceptionPortValue : 0xffffaf8c`0d06eca0
+0x4b0 ExceptionPortState : 0y000
...
+0x578 DebugPort : (null)
...
lkd> !peb
PEB at 000000b38a68c000
...
BeingDebugged: No
...

调试器进程与被调试进程之间的交互称为调试会话,一次调试会话从建立调试关系开始,直到该关系解除为止。建立调试关系的标准是被调试进程与调试器进程之间通过调试端口建立的通信连接,调试关系解除是被调试进程的调试端口被清除。

调试器启动被调试程序

这种启动方式通过调用创建进程API时指定DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS标志。典型API为CreateProcess,其超集有CreateProcessAsUserCreateProcessWithTokenWCreateProcessWithLogonW等,内部过程如下。

在执行NtCreateProcess(Ex)前用DbgUiConnectToDbg使调用线程与调试子系统建立连接,后者用ZwCreateDebugObject创建DEBUG_OBJECFT内核对象,并将其保存在TEB的DbgSsReserved[1]字段中。

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
x64dbg:
0:022> k
# Child-SP RetAddr Call Site
00 0000006e`bacfdf78 00007fff`3dc93db3 ntdll!DbgUiConnectToDbg
01 0000006e`bacfdf80 00007fff`3dc06ab6 KERNELBASE!CreateProcessInternalW+0x8c613
02 0000006e`bacff490 00007fff`3f69cbb4 KERNELBASE!CreateProcessW+0x66
03 0000006e`bacff500 00007fff`196f4776 KERNEL32!CreateProcessWStub+0x54
04 0000006e`bacff560 00007fff`190e7b01 TitanEngine!InitDebugW+0x486
05 0000006e`bacff640 00007fff`190dba5e x64dbg!plugin_logprintf+0x120c1
06 0000006e`bacff840 00007fff`3f697034 x64dbg!plugin_logprintf+0x601e
07 0000006e`bacff870 00007fff`400dcec1 KERNEL32!BaseThreadInitThunk+0x14
08 0000006e`bacff8a0 00000000`00000000 ntdll!RtlUserThreadStart+0x21

windbg:
0:014> k
# Child-SP RetAddr Call Site
00 00000086`1627dd38 00007fff`3dc93db3 ntdll!DbgUiConnectToDbg //连接调试子系统
01 00000086`1627dd40 00007fff`3dc06ab6 KERNELBASE!CreateProcessInternalW+0x8c613 //创建进程的内部函数
02 00000086`1627f250 00007fff`3f69cbb4 KERNELBASE!CreateProcessW+0x66 //创建进程
03 00000086`1627f2c0 00007fff`1a2a4e5e KERNEL32!CreateProcessWStub+0x54
04 00000086`1627f320 00007fff`19f92014 dbgeng!LiveUserDebugServices::CreateProcessW+0x27e
05 00000086`1627f490 00007fff`19f3f50b dbgeng!LiveUserTargetInfo::StartCreateProcess+0x114
06 00000086`1627f4f0 00007ff6`be96e12c dbgeng!DebugClient::CreateProcessAndAttach2Wide+0x11b
07 00000086`1627f5b0 00007ff6`be96eab8 windbg!StartSession+0x808 //开始调试会话
08 00000086`1627fa80 00007fff`3f697034 windbg!EngineLoop+0xb8 //调试事件循环
09 00000086`1627faf0 00007fff`400dcec1 KERNEL32!BaseThreadInitThunk+0x14 ///调试器工作线程初始?
0a 00000086`1627fb20 00000000`00000000 ntdll!RtlUserThreadStart+0x21

用进程创建内核服务NtCreateProcess(Ex)时,将DbgSsReserved[1]字段中对象句柄以第7个参数的方式传给内核进程管理器。内核进程创建函数PspCreateProcess检查该句柄不为空则获取其对象指针,并设置到EPROCESS的DebugPort字段中。栈回溯如下:

1
2
3
4
5
6
nt!PspCreateProcess
nt!NtCreateProcessEx
nt!KiSystemService
SharedUserData!SystemCallStub
ntdll!ZwCreateProcessEx
kernel32!CreateProcessInternalW

PspCreateProcessMmCreatePeb创建新PEB时检查EPROCESS的DebugPort不为空则设置BeingDebugged字段为真。此时调试对话建立完成。

一个新创建进程的初始线程从KiThreadStartup开始执行,其将线程IRQL降到APC级后将执行权交给PspUserThreadStartup。后者用DbgkCreateThread向调试子系统通知新线程创建事件。DWT等待调试事件时收到进程创建事件,调试器为该新线程作准备,并用ContinueDebugEvent回复调试事件,被调试事件开始继续执行。

新进程初始线程在自己上下文中初始化时,ntdll!LdrpInitializeProcess检查正在初始化的进程是否处于被调试状态,也就是查询PEB的BeingDebugged字段。如果处于被调试状态则用DbgBreakPoint触发断点异常,中断到调试器,称为初始断点。这时被调试程序的主函数还未执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
0:000> k
# Child-SP RetAddr Call Site
00 0000009a`c48ff3b0 00007ff8`b9d43aa2 ntdll!LdrpDoDebuggerBreak+0x30
01 0000009a`c48ff3f0 00007ff8`b9d3211b ntdll!LdrpInitializeProcess+0x1f42
02 0000009a`c48ff820 00007ff8`b9ce47c3 ntdll!_LdrpInitialize+0x4d93f
03 0000009a`c48ff8c0 00007ff8`b9ce476e ntdll!LdrpInitialize+0x3b
04 0000009a`c48ff8f0 00000000`00000000 ntdll!LdrInitializeThunk+0xe
0:007> k
# Child-SP RetAddr Call Site
00 0000009a`c4fffeb8 00007ff8`b9d3c88e ntdll!DbgBreakPoint
01 0000009a`c4fffec0 00007ff8`b9bc7034 ntdll!DbgUiRemoteBreakin+0x4e
02 0000009a`c4fffef0 00007ff8`b9cbcec1 KERNEL32!BaseThreadInitThunk+0x14
03 0000009a`c4ffff20 00000000`00000000 ntdll!RtlUserThreadStart+0x21

也可以实现在被调试进程运行前,自动启动指定调试器。可在注册表“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image FIle Execution Options”下新建一个以要执行的映像文件名命名的子键,子键下新建Debugger键值,内容可以为:

1
C:\WinDBG\WinDBG.exe -g

-g选项表示忽略初始断点,被调试进程不中断到调试器并继续运行。例如在“Image FileExecution Options”表键下建立子键“calc.exe”且Debugger键值同上,表示将启动时命令行拼接在Debugger键值内容之后并执行,这时无论以什么方式打开calc.exe都会先启动WinDBG并忽略初始断点。该方法不能递归,即Debugger表键指示的调试器不能以这种方式被设置自动启动。显然Debugger键值所设置的调试器不能是自己,否则死循环。

对于附加到已启动进程中时,通过DebugActiveProcess,参数只需指定被调试进程ID即可。该函数内部原理如下。

先用DbgUiConnectToDbg获得一个调试通信对象并放在当前TEB的DbgSsReserved数组中,使调用进程与调试子系统建立连接;再用ProcessIdToHandle获取指定ID进程句柄,其内部用OpenProcess;最后用ntdll!DbgUiDebugActiveProcess,其内部用NtDebugActiveProcess内核服务。

上述ntdll!DbgUiDebugActiveProcess将被调试进程句柄和调试对象句柄作为参数依次传递给NtDebugActiveProcess内核服务,后者主要执行有:获取被调试进程EPROCESS结构和调试对象指针;向调试对象发送杜撰调试事件;用DbgkpSetProcessDebugObject将调试对象设置到被调试进程调试端口DebugPort字段,并用DbgkpMarkProcessPeb设置BeingDebugged字段。栈帧回溯如下:

1
2
3
4
5
6
7
8
9
10
11
nt!DbgkpMarkProcessPeb //标记PEB
nt!DbgkpSetProcessDebugObject //设置调试端口
nt!NtDebugActiveProcess //系统服务
nt!KiSystemService //系统服务分发
SharedUserData!SysteCallStub
ntdll!ZwDebugActiveProcess //系统服务残根
ntdll!DbgUiDebugActiveProcess
kernel32!DebugActiveProcess //附加到已运行进程
TinyDbgr!main //主函数
TinyDbgr!mainCRTStartup //编译器插入的启动函数
kernel32!BaseProcessStart //线程启动

代码有:

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
__int64 __fastcall DbgUiDebugActiveProcess(__int64 a1) {
int active; // ebx
active = NtDebugActiveProcess();
if (active >= 0) {
active = DbgUiIssueRemoteBreakin(a1);
if (active < 0)
ZwRemoveProcessDebug(a1, NtCurrentTeb()->DbgSsReserved[1]);
};
return (unsigned int)active;
};
__int64 NtDebugActiveProcess() {
__int64 result; // rax
result = 212LL;
if ((MEMORY[0x7FFE0308] & 1) != 0)
__asm { int 2Eh; DOS 2 + internal - EXECUTE COMMAND }
else
__asm { syscall; Low latency system call }
return result;
};
__int64 __fastcall NtDebugActiveProcess(ULONG_PTR a1, void* a2) {
KPROCESSOR_MODE PreviousMode; // r14
__int64 result; // rax
__int64 v5; // rcx
struct _KTHREAD* CurrentThread; // rax
struct _EX_RUNDOWN_REF* v7; // rbx
_KPROCESS* Process; // rsi
NTSTATUS v9; // edi
unsigned __int64 Count; // rdi
__int64 v11; // rcx
__int16 v12; // ax
__int16 v13; // ax
BOOLEAN v14; // al
struct _KEVENT* v15; // rsi
PVOID Object[2]; // [rsp+40h] [rbp-59h] BYREF
_QWORD v17[14]; // [rsp+50h] [rbp-49h] BYREF
PreviousMode = KeGetCurrentThread()->PreviousMode;
Object[0] = 0LL;
Object[1] = 0LL;
result = ObpReferenceObjectByHandleWithTag(a1, 0x4F676244u, (__int64)Object, 0LL, 0LL);
if ((int)result >= 0) {
CurrentThread = KeGetCurrentThread();
v7 = (struct _EX_RUNDOWN_REF*)Object[0];
Process = CurrentThread->ApcState.Process;
if (Object[0] == Process || Object[0] == PsInitialSystemProcess)
v9 = 0xC0000022;
else {
LOBYTE(v5) = PreviousMode;
if (PsTestProtectedProcessIncompatibility(v5, (__int64)CurrentThread->ApcState.Process, (__int64)Object[0]))
v9 = 0xC0000712;
else {
Count = v7[124].Count;
if ((Count & 1) == 0 || (memset(v17, 0, 0x68uLL), v17[1] = Count, v17[2] = 1LL, LOBYTE(v11) = 2, v9 = VslpEnterIumSecureMode(v11, 12, 0, (__int64)v17), v9 >= 0)) {
if (!Process[1].Affinity.StaticBitmap[30] || (v12 = WORD2(Process[2].Affinity.StaticBitmap[20]), || v7[176].Count && ((v13 = WORD2(v7[301].Ptr), v13 == 332) || v13 == 452)) {
Object[0] = 0LL;
v9 = ObReferenceObjectByHandle(a2, 2u, DbgkDebugObjectType, PreviousMode, Object, 0LL);
if (v9 >= 0)
v14 = ExAcquireRundownProtection_0(v7 + 139);
v15 = (struct _KEVENT*)Object[0];
if (v14) {
((void(__fastcall*)(ULONG_PTR))DbgkpPostFakeProcessCreateMessages)((ULONG_PTR)v7);
v9 = ((__int64(__fastcall*)(ULONG_PTR, PRKEVENT))DbgkpSetProcessDebugObject)((ULONG_PTR)v7, v15);
ExReleaseRundownProtection_0(v7 + 139);
}
else
v9 = 0xC000010A;
ObfDereferenceObject(v15);
};
}
else {
v9 = 0xC00000BB;
};
};
};
ObfDereferenceObjectWithTag(v7, 0x4F676244u);
return (unsigned int)v9;
};
return result;
};

NtDebugActiveProcess返回后,DbgUiDebugActiveProcessDbgUiIssueRemoteBreakin再远程进程中创建远程中断线程,使被调试进程中断到调试器。

最后DebugActiveProcess返回真,通知调用进程成功建立调试对话,调试器便进入调试事件循环。首先收到一系列杜撰调试事件,如进程创建、模块加载等,然后收到远程中断线程产生的断点事件。

一个简易调试器如下:

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
#include <stdlib.h>
#define WINVER 0x0501
#include <windows.h>
// starts a new process as a debuggee
BOOL DbgNewProcess(LPTSTR szCmdLine) {
STARTUPINFO StartupInfo;
PROCESS_INFORMATION ProcessInfo;
memset(&StartupInfo, NULL, sizeof(STARTUPINFO));
memset(&ProcessInfo, NULL, sizeof(PROCESS_INFORMATION));
StartupInfo.cb = sizeof(STARTUPINFO);
//-- create the Debuggee process
if (!CreateProcess(0L, szCmdLine, (LPSECURITY_ATTRIBUTES)0L, (LPSECURITY_ATTRIBUTES)0L, TRUE, DEBUG_PROCESS | CREATE_NEW_CONSOLE | NORMAL_PRIORITY_CLASS, (LPVOID)0L, (LPTSTR)0L, &StartupInfo, &ProcessInfo)) {
TCHAR szMsg[MAX_PATH];
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), 0 /*Default language*/, (LPTSTR)szMsg, MAX_PATH, NULL);
printf("Failed in CreateProcess() with error:\n ");
printf(szMsg);
printf("\n");
return(FALSE);
}
else {
CloseHandle(ProcessInfo.hProcess);
CloseHandle(ProcessInfo.hThread);
};
return(TRUE);
};
#define MAX_DBG_EVENT 9
LPTSTR DbgEventName[MAX_DBG_EVENT + 1] = {"EXCEPTION_DEBUG_EVENT","CREATE_THREAD_DEBUG_EVENT","CREATE_PROCESS_DEBUG_EVENT","EXIT_THREAD_DEBUG_EVENT","EXIT_PROCESS_DEBUG_EVENT","LOAD_DLL_DEBUG_EVENT","UNLOAD_DLL_DEBUG_EVENT","OUTPUT_DEBUG_STRING_EVENT","RIP_EVENT","Unknown Debug Event"};
BOOL DbgMainLoop(DWORD dwWaitMS) {
DEBUG_EVENT DbgEvt; // debugging event information
DWORD dwContinueStatus = DBG_CONTINUE; // exception continuation
BOOL bExit = FALSE;
while (!bExit) {
// Wait for a debugging event to occur. The second parameter indicates number of milliseconds to wait for a debugging event. If the parameter is INFINITE the function does not return until a debugging event occurs.
if (!WaitForDebugEvent(&DbgEvt, dwWaitMS)) {
printf("WaitForDebugEvent() returned False %d.\n", GetLastError());
bExit = TRUE;
continue;
};
// Process the debugging event code.
printf("Debug event received from process %d thread %d: %s.\n", DbgEvt.dwProcessId, DbgEvt.dwThreadId, DbgEventName[DbgEvt.dwDebugEventCode > MAX_DBG_EVENT ? MAX_DBG_EVENT : DbgEvt.dwDebugEventCode - 1]);
switch (DbgEvt.dwDebugEventCode) {
case EXCEPTION_DEBUG_EVENT: {
// Process the exception code. When handling exceptions, remember to set the continuation status parameter (dwContinueStatus). This value is used by the ContinueDebugEvent function.
printf("-Debuggee breaks into debugger; press any key to continue.\n");
getchar();
//return TRUE;
switch (DbgEvt.u.Exception.ExceptionRecord.ExceptionCode) {
case EXCEPTION_ACCESS_VIOLATION: {
// First chance: Pass this on to the system.
// Last chance: Display an appropriate error.
break;
};
case EXCEPTION_BREAKPOINT: {
// First chance: Display the current
// instruction and register values.
break;
};
case EXCEPTION_DATATYPE_MISALIGNMENT: {
// First chance: Pass this on to the system.
// Last chance: Display an appropriate error.
break;
};
case EXCEPTION_SINGLE_STEP: {
// First chance: Update the display of the
// current instruction and register values.
break;
};
case DBG_CONTROL_C: {
// First chance: Pass this on to the system.
// Last chance: Display an appropriate error.
break;
};
default: {
// Handle other exceptions.
break;
};
};
};
case CREATE_THREAD_DEBUG_EVENT: {
// As needed, examine or change the thread's registers with the GetThreadContext and SetThreadContext functions; and suspend and resume thread execution with the SuspendThread and ResumeThread functions.
break;
};
case CREATE_PROCESS_DEBUG_EVENT: {
// As needed, examine or change the registers of the process's initial thread with the GetThreadContext and SetThreadContext functions; read from and write to the process's virtual memory with the ReadProcessMemory and WriteProcessMemory functions; and suspend and resume thread execution with the SuspendThread and ResumeThread functions. Be sure to close the handle to the process image file with CloseHandle.
break;
};
case EXIT_THREAD_DEBUG_EVENT: {
// Display the thread's exit code.
break;
};
case EXIT_PROCESS_DEBUG_EVENT: {
// Display the process's exit code.
bExit = TRUE;
break;
};
case LOAD_DLL_DEBUG_EVENT: {
// Read the debugging information included in the newly loaded DLL. Be sure to close the handle to the loaded DLL with CloseHandle.
break;
};
case UNLOAD_DLL_DEBUG_EVENT: {
// Display a message that the DLL has been unloaded.
break;
};
case OUTPUT_DEBUG_STRING_EVENT: {
// Display the output debugging string.
break;
};
};
// Resume executing the thread that reported the debugging event.
ContinueDebugEvent(DbgEvt.dwProcessId, DbgEvt.dwThreadId, dwContinueStatus);
};
return TRUE;
};
void Help() {
printf("TinyDbgr <PID of Program to Debug>|\n<Full Exe File Name> [Prgram Parameters]\n");
return;
};
int main(int argc, char* argv[]) {
if (argc <= 1) {
Help();
return -1;
};
if (strstr(strupr(argv[1]), ".EXE")) {
TCHAR szCmdLine[MAX_PATH];
szCmdLine[0] = '\0';
for (int i = 1; i < argc; i++) {
strcat(szCmdLine, argv[i]);
if (i < argc)
strcat(szCmdLine, " ");
};
if (!DbgNewProcess(szCmdLine))
return -2;
}
else {
if (!DebugActiveProcess(atoi(argv[1]))) {
printf("Failed in DebugActiveProcess() with %d.\n", GetLastError());
return -2;
};
if (argc > 2 && stricmp(argv[2], "-e") == 0)
// try the DebugSetProcessKillOnExit() API
if (!DebugSetProcessKillOnExit(FALSE))
printf("Failed in DebugSetProcessKillOnExit() with %d.\n", GetLastError());
if (argc > 2 && stricmp(argv[2], "-s") == 0) {
DbgMainLoop(10);
// try the DebugActiveProcessStop() API
if (!DebugActiveProcessStop(atoi(argv[1])))
printf("Failed in DebugActiveProcessStop() with %d.\n", GetLastError());
else
printf("Detach debuggee successfully.\n");
return 0;
};
};
return DbgMainLoop(INFINITE);
};

例如运行后挂靠被调试进程,系统模拟以前发生的事件,帮助调试器补充历史信息:

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
C:\Users\User\Desktop>tinydbgr 2708
Debug event received from process 2708 thread 816: CREATE_PROCESS_DEBUG_EVENT.
Debug event received from process 2708 thread 816: LOAD_DLL_DEBUG_EVENT.
Debug event received from process 2708 thread 4680: CREATE_THREAD_DEBUG_EVENT.
Debug event received from process 2708 thread 1484: CREATE_THREAD_DEBUG_EVENT.
Debug event received from process 2708 thread 4172: CREATE_THREAD_DEBUG_EVENT.
Debug event received from process 2708 thread 816: LOAD_DLL_DEBUG_EVENT.
...
Debug event received from process 2708 thread 816: LOAD_DLL_DEBUG_EVENT.
Debug event received from process 2708 thread 4792: CREATE_THREAD_DEBUG_EVENT. //DbgUiIssueRemoteBreakin创建的远程线程
Debug event received from process 2708 thread 4792: EXCEPTION_DEBUG_EVENT. //远程中断线程触发的断点异常
-Debuggee breaks into debugger; press any key to continue.

Debug event received from process 2708 thread 4448: CREATE_THREAD_DEBUG_EVENT.
...
Debug event received from process 2708 thread 4792: EXIT_THREAD_DEBUG_EVENT. //远程中断线程退出
C:\Users\User\Desktop>tinydbgr aheadlibex.exe
Debug event received from process 292 thread 1532: CREATE_PROCESS_DEBUG_EVENT.
Debug event received from process 292 thread 1532: LOAD_DLL_DEBUG_EVENT.
...
Debug event received from process 292 thread 1532: OUTPUT_DEBUG_STRING_EVENT.
...
Debug event received from process 292 thread 1532: EXCEPTION_DEBUG_EVENT. //LdrpInitializeProcess用DbgBreakPoint产生初始断点
-Debuggee breaks into debugger; press any key to continue.

Debug event received from process 292 thread 1532: UNLOAD_DLL_DEBUG_EVENT.
...
Debug event received from process 292 thread 3768: EXIT_THREAD_DEBUG_EVENT.
Debug event received from process 292 thread 1532: EXIT_PROCESS_DEBUG_EVENT.

调试事件DEBUG_EVENT结构如下,详细自己去查MSDN。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode; //事件代码
DWORD dwProcessId; //发生调试事件进程ID
DWORD dwThreadId; //发生调试事件线程ID
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; //映射DLL事件
UNLOAD_DLL_DEBUG_INFO UnloadDll; //反映射DLL事件
OUTPUT_DEBUG_STRING_INFO DebugString; //输出调试字符串事件
RIP_INFO RipInfo; //内部错误事件
} u;
} DEBUG_EVENT, * LPDEBUG_EVENT;

调试器用kernel32!WaitForDebugEvent等待和接收调试事件:

1
2
3
4
BOOL WaitForDebugEvent(
_Out_ LPDEBUG_EVENT lpDebugEvent, //接收收到的调试事件
_In_ DWORD dwMilliSeconds //等待时间 INFINITE无限等待
);

该函数将阻塞当前进程,直至发生调试事件或等待时间已过或发生错误。其内部先用ntdll!DbgUiWaitStateChange,再用ntdll!DbgUiConvertStateChangeStructure将DBGUI_WAIT_STATE_CHANGE结构调试事件转为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
typedef struct _DBGUI_WAIT_STATE_CHANGE {
DBG_STATE NewState; //新调试状态
CLIENT_ID AppClientId; //进程线程句柄等
union { //详细信息
DBGKM_EXCEPTION Exception; //异常
DBGUI_CREATE_THREAD CreateThread; //创建线程
DBGUI_CREATE_PROCESS CreateProcessInfo; //创建进程
DBGKM_EXIT_THREAD ExitThread; //线程退出
DBGKM_EXIT_PROCESS ExitProcess; //进程退出
DBGKM_LOAD_DLL LoadDll; //映射模块
DBGKM_UNLOAD_DLL UnloadDll; //反映射模块
}StateInfo;
}DBGUI_WAIT_STATE_CHANGE, * PDBGUI_WAIT_STATE_CHANGE;
typedef enum _DBG_STATE {
DbgIdle,
DbgReplyPending,
DbgCreateThreadStateChange,
DbgCreateProcessStateChange,
DbgExitThreadStateChange,
DbgExitProcessStateChange,
DbgLoadDllStateChange,
DbgUnloadDllStateChange
}DBG_STATE,*PDBG_STATE;

总结来说,对于用来描述调试事件的结构,内核中使用DBGKM_APIMSG、DbgUi使用DBGUI_WAIT_STATE_CHANGE、调试API使用DEBUG_EVENT。前两者之间转用DbgkpConvertKernelToUserStateChange,后两者之间转用DbgUiConvertStateChangeStructure

调试器用ContinueDebugEvent向调试子系统回复处理结果:

1
2
3
4
5
BOOL ContinueDebugEvent(
DWORD dwProcessId, //进程ID
DWORD dwThreadId, //线程ID
DWORD dwContinueStatus
);

dwContinueStatus可以为DBG_CONTINUE或DBG_EXCEPTION_NOT_HANDLED,当不为EXCEPTION_DEBUG_EVENT异常事件时,这俩选项没有区别,调试子系统都会用DbgkpResumeProcess恢复运行被调试进程。

在异常事件时,对于DBG_CONTINUE:表示调试器处理了该异常。异常事件由DbgkForwardException接收并传给子系统,其收到次返回值后向其调用者KiDispatchException返回真,后者便结束对该异常的分发。

对于DBG_EXCEPTION_NOT_HANDLED:表示调试器不处理该异常。这将导致DbgkForwardException返回假给KiDispatchExceptionKiDispatchException会多次调用DbgkForwardException,对于第一轮处理机会时若该函数返回假,则继续分发过程并寻找异常处理块;对于第二轮处理机会时若该函数返回假,则会再次用DbgkForwardException发送给异常端口,此时调试器通常返回DBG_CONTINUE来假装处理并导致异常分发过程结束,产生异常的代码再次被执行且又发生异常,反复不断。

若在第一次处理机会时,手动将错误改正,并在Pass exception时选择No表示已处理异常情况且不必继续分发异常。

中断到调试器

有个Windows API用于触发断点异常:

1
VOID DebugBreak(VOID);

例如通常这么用:

1
2
if (IsDebuggerPresent()/*&& 中断条件*/)
DebugBreak();

IsDebuggerPresent等函数从KERNEL32.DLL中转发出去,到某前端API集DLL中,通常用Dependencies软件查找其实现代码所在模块,这里为KernelBase.dll。

此外,根据用户即时需要而将被调试进程中断到调试器的功能称为异步阻停。做法是用CreateRemoteThread在被调试进程中创建一个远程线程,该线程已运行便执行断点指令,把调试进程中断到调试器。该函数即为ntdll!DbgUiRemoteBreakin,其内部在SEH块中不断调用DbgBreakPoint,伪代码如下,目前该函数已封装到DebugBreakProcess中。

1
2
3
4
5
6
7
8
9
10
DWORD WINAPI DbgUiRemoteBreakin(LPVOID lpParameter) {
__try {
if (NtCurrentPeb()->BeingDebugged)
DbgBreakPoint();
}
__except (EXCEPTION_EXECUTE_HANDLER) { //调试器不处理
return 1;
};
RtlExitUserThread(0); //非被调试状态 强制退出当前线程
};

还可以在线程当前执行位置设置断点。首先用SuspendThread将被调试进程中所有线程挂起,再用GetThreadContext得到线程的PC寄存器值,保存原1字节并写uINT 3机器码,然后用ResumeThread让其继续执行触发断点。断点命中后调试器清除断点。

一般用户发起中断命令时PC位于系统内核中,所以这时中断在系统服务返回后的RET被改为INT 3,该RET调试符号为ntdll!KiFastSystemCallRet。断点命中后调试器清除断点。

还有一种方法是动态调用远程函数,该函数再指定断点指令。先将目标线程挂起或锁定在稳定内核状态,再将kernel32!BaseAttachCompleteThunk地址作为调用点参数传递给ntdll!RtlRemoteCall,其中kernel32!BaseAttachCompleteThunk调用BaseAttachCompletentdll!RtlRemoteCall内部用NtGetContextThread内核服务获取目标线程上下文得到栈地址,然后调整栈指针将上下文结构和参数用NtWriteVirtualMemroy写到目标线程栈上,接着将线程上下文结构中RIP寄存器设为参数指定的调用地址。然后用NtSetContextThread将上下文结构设置回目标线程,再用NtResumeThread恢复目标线程运行,其一运行便执行调用点指向的代码。BaseAttachComplete当当前进程正被调试则用DbgBreakPoint触发断点。

当WinDBG使用远程线程中断时超时30秒,则尝试挂起中断。由于远程线程中断在内核服务返回处设置动态断点,若线程因同步对象死锁而无法创建/启动新线程,即挂在内核态,则尝试强行将被调试进程所有线程挂起,进入准调试状态,该状态只能查询不能运行。

除了调试器发出Break命令,也可对被调试程序按F12。原理是Windows子系统内核接收此加速键,通过LPC请求csrss!SrvActivateDebugger服务。该服务检查要调试的进程为自己时,且自己处于被调试状态,则用DbgBreakPoint中断到调试器。可修改该注册表项来指定其他调试快捷键:“HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AeDebug”。

kernel32!OutputDebugString来输出调试字符串。该函数将字符串发送给调试器,若程序不再被调试则发送给系统内核调试器,若系统调试器也没被激活则啥也不干。该函数用RaiseException产生DBG_PRINTEXCEPTION_C调试打印异常,返回后产生标准异常结构EXCEPTION_RECORD并用内核服务将该模拟的异常发送到内核进行分发。ANSI版伪代码如下:

1
2
3
4
5
6
7
8
__try {
ExceptionArguments[0] = strlen(lpOutputString) + 1;
ExceptionArguments[1] = (ULONG_PTR)lpOutputString;
RaiseException(DBG_PRINTEXCEPTION_C, 0, 2, ExceptionArguments);
}
__except{
//...
};

内核异常分发函数KiDispatchException调用用户态调试内核例程DbgkForwardException向调试子系统通报异常,后者检查当前进程正被调试则将该异常通过调试子系统服务器发送给调试器。

调试器工作线程用WaitForDebugEvent调用ntdll!DbgUiWaitStateChange等待调试事件。收到异常事件后,用DbgUiConvertStateChangeStructure将DBGUI_WAIT_STATE_CHANGE组装成DEBUG_EVENT结构。此时若异常代码为DBG_PRINTEXCEPTION_C,则该组装函数将dwDebugEventCode事件代码字段设为OUTPUT_DEBUG_STRING_EVENT,并将异常参数中调试信息写到DEBUG_EVENT的DebugString子结构中,该子结构为OUTPUT_DEBUG_STRING_INFO结构。

1
2
3
4
5
typedef struct _OUTPUT_DEBUG_STRING_INFO {
LPSTR lpDebugStringData; //调试信息字符串地址
WORD fUnicode; //是否为Unicode 目前总为FALSE
WORD nDebugStringLength; //调试信息字符串长度
} OUTPUT_DEBUG_STRING_INFO, * LPOUTPUT_DEBUG_STRING_INFO;

调试器显示后用ContinueDebugEvent回复此异常事件已被处理,此时KiDispatchException结束异常分发。

调试会话终止

程序退出通常执行内核函数PspExitThread来退出线程。该函数内部通过EPROCESS的DebugPort字段检查当前进程是否在被调试,是则根据当前线程是否为最后一个线程而调用DbgkExitProcessDbgkExitThread来通知调试子系统。栈回溯如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
nt!DbgkExitProcess //通知调试子系统
nt!PspExitThread //进程管理器的线程退出函数
nt!PspTerminateThreadByPointer //终止进程
nt!NtTerminateProcess //终止进程的内核服务
nt!KiSystemService //内核服务分发函数
SharedUserData!SystemCallStub //调用内核服务
ntdll!NtTerminateProcess //内核服务残根
kernel32!_ExitProcess //Kernel32进程退出函数
kernel32!TerminateProcess //终止进程API
msvcrt!__crtExitProcess //C运行时库的进程退出函数
msvcrt!_cinit //参照点 实际为doexit
msvcrt!exit //C运行时库的退出函数
notepad!WinMainCRTStartup //编译器插入的启动函数
kernel32!BaseProcessStart //进程启动函数

最后进程管理器的工作线程执行PspProcessDelete来完成进程的清理和删除工作,其内部检查EPROCESS结构DebugPort不为空则用ObDereferenceObject取消引用,前者再用内存管理器中的MmDeleteProcessAddressSpace删除进程地址空间,从此该进程彻底在系统中消失。

调试器工作线程退出时,PspExitThread检查TEB的DbgSsReserved[1]字段不为空则用ObCloseHandle关闭调试对象句柄。其中PspExitTheradObKillProcess清理句柄表的栈帧回溯如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
nt!DbgkpCloseObject //调用对象的关闭函数
nt!ObpDecrementHandleCount //递减关闭句柄引用
nt!ObpCloseHandleTableEntry //关闭句柄表中一项
nt!ObpCloseHandleProcedure //关闭句柄表中所有项
nt!ExSweepHandleTable //清理句柄表
nt!ObKillProcess //对象管理器针对进程退出所用函数
nt!PspExitThread //线程退出
nt!PspTerminateThreadByPointer //终止进程
nt!NtTerminateProcess //终止进程内核服务
nt!KiSystemService //系统服务分发
SharedUserData!SystemCallStub //调用系统服务
ntdll!NtTerminateProcess //系统服务残根
kernel32!_ExitProcess //进程退出函数
kernel32!TerminateProcess //终止进程API
windbg!TerminateApplication //终止应用程序
windbg!FrameWndProc //窗口过程

调试对象句柄关闭函数DbgkpCloseObject的5哥参数分别为:指向EPROCESS的指针、指向调试对象的指针、赋给句柄的权限、进程句柄计数、调试对象句柄计数。其内部检测参数中指定的调试对象句柄计数大于1则返回,ObpDecrementHandleCount先将原计数保存起来,再递减,再把原计数作为参数传给对象关闭函数。

DbgkpCloseObject列举系统所有进程,并查找相同的DebugPort字段,对这些进程分别:先将该进程DebugPort设为空,再用DbgkpMarkProcessPeb设置该进程PEB的BeingDebugged字段,最后检查调试对象的标志Flags字段包含KillOnExit便用PspTerminateProcess终止进程。

例如当退出正调试记事本程序的WinDBG时,被调试记事本随着WinDBG一同退出,大致步骤如下:调用系统服务NtTerminateProcess开始终止WinDBG进程;执行PspExitThread使DWT和UI线程退出;执行ObKillProcess清理WinDBG进程句柄表;执行DbgkpCloseObject关闭调试对象,设置DebugPort和BeingDebugged字段,并引发终止被调试进程;执行PspExitThread使记事本进程主线程退出;执行PspProcessDeleteMmDeleteProcessAddressSpace删除记事本与WinDBG进程的内核对象和进程空间。

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
void __fastcall DbgkpCloseObject(__int64 a1, __int64 a2, __int64 a3, unsigned __int64 a4) {
_QWORD* v5; // rsi
int v6; // ebx
_QWORD* NextProcess; // rdi
char v8; // bl
_DWORD* v9; // rcx
if (a4 <= 1) {
ExAcquireFastMutex((PFAST_MUTEX)(a2 + 24));
*(_DWORD*)(a2 + 96) |= 1u;
v5 = *(_QWORD**)(a2 + 80);
*(_QWORD*)(a2 + 80) = a2 + 80;
*(_QWORD*)(a2 + 88) = a2 + 80;
ExReleaseFastMutex((PFAST_MUTEX)(a2 + 24));
KeSetEvent((PRKEVENT)a2, 0, 0);
v6 = *(_DWORD*)(a2 + 96) & 2;
NextProcess = (_QWORD*)PsGetNextProcess(0LL);
if (NextProcess) {
v8 = v6 != 0 ? 2 : 0;
do {
if (NextProcess[175] == a2) {
v8 &= ~1u;
ExAcquireFastMutex(&DbgkpProcessDebugPortMutex);
if (NextProcess[175] == a2) {
NextProcess[175] = 0LL;
v8 |= 1u;
};
ExReleaseFastMutex(&DbgkpProcessDebugPortMutex);
if ((v8 & 1) != 0) {
DbgkpMarkProcessPeb((ULONG_PTR)NextProcess);
if ((v8 & 2) != 0)
PsTerminateProcess(NextProcess, 0xC0000354LL);
ObfDereferenceObject((PVOID)a2);
};
};
NextProcess = (_QWORD*)PsGetNextProcess(NextProcess);
} while (NextProcess);
};
while (v5 != (_QWORD*)(a2 + 80)) {
v9 = v5;
v5 = (_QWORD*)*v5;
v9[18] = 0xC0000354;
DbgkpWakeTarget(v9);
};
};
return;
};

调试器也可用kernel32!DebugActiveProcessStop来分离调试对话,必须在建立调试会话的线程中用该函数,否则返回0xC0000022拒绝访问错误。伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL DebugActiveProcessStop(
DWORD dwProcessId //要分离的被调试进程ID
) {
NTSTATUS nStatus;
HANDLE hProcess = ProcessIdToHandle(dwProcessId);
if (hProcess == NULL)
return FALSE;
CloseAllProcessHandles(dwProcessId);
nStatus = DbgUiStopDebugging(hProcess);
NtClose(hProcess);
if (NT_SUCCESS(nStatus))
return TRUE;
SetLastError(5);
return FALSE
};
NTSTATUS DbgUiStopDebugging(HANDLE hProcess) {
return NtRemoveProcessDebug(hProcess, NtCurrentPeb()->DbgSsReserved[1]);
};

其中NtRemoveProcessDebug内部调用调试子系统DbgkClearProcessDebugObject,后者取出调试进程的DebugPort字段对调试对象的引用并将其设为NULL,再遍历调试对象的调试事件队列,删除有关该被调试进程的事件。

此外也可用DebugSetProcessKillOnExit设置调试对象Flags字段的KillOnExit标志。当退出调试器时,调用终止进程函数前DbgkpCloseObject检查无该标志则不退出被调试进程。

1
BOOL DebugSetProcessKillOnExit(BOOL KillOnExit);