前言
身为一名逆向人员,ida,x64dbg几乎是我们无法离开的工具,这些调试器的背后的原理是什么
所谓各路五花八门的反调试,调试器检测,无痕hook,线程逃逸又是什么
接下来的几篇博客,我将带大家深入内核,探究windows内核里的调试体系
一个简单的调试器
我们来看一个最简的调试器代码
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
#include <windows.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <pid>\n", argv[0]);
return 1;
}
DWORD pid = (DWORD)atoi(argv[1]);
if (!DebugActiveProcess(pid)) {
printf("DebugActiveProcess failed: %lu\n", GetLastError());
return 1;
}
printf("Attached to PID %lu\n", pid);
DEBUG_EVENT de;
for (;;) {
WaitForDebugEvent(&de, INFINITE); /* debugwaitforsignal */
switch (de.dwDebugEventCode) {
case EXCEPTION_DEBUG_EVENT:
printf("[EXCEPTION] addr=%p code=%08lx\n",
de.u.Exception.ExceptionRecord.ExceptionAddress,
de.u.Exception.ExceptionRecord.ExceptionCode);
break;
case EXIT_PROCESS_DEBUG_EVENT:
printf("[EXIT_PROCESS]\n");
goto done;
}
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE); /* debugcontinue */
}
done:
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
DebugActiveProcessStop(pid);
return 0;
}
这边就涉及3个重要函数:
DebugActiveProcess WaitForDebugEvent ContinueDebugEvent
接下来这些函数就是我的分析重点
我这里不卖关子,直接告诉你这几个函数的功能
- DebugActiveProcess 激活调试,将调试器与被调试进程关联
- WaitForDebugEvent 等待调试事件
- ContinueDebugEvent 预示着调试器处理完成调试事件,被调试进程继续执行
这里我们不难想到第一个问题,调试器是如何与被调试对象关联的呢?
调试对象
当我们执行DebugActiveProcess,就会将创建一个调试对象,他既挂在被调试进程上,也挂在调试器上

什么是调试对象?我们直接从wrk里拿定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define DEBUG_OBJECT_DELETE_PENDING (0x1) // Debug object is delete pending.
#define DEBUG_OBJECT_KILL_ON_CLOSE (0x2) // Kill all debugged processes on close
typedef struct _DEBUG_OBJECT {
//
// Event thats set when the EventList is populated.
//
KEVENT EventsPresent;
//
// Mutex to protect the structure
//
FAST_MUTEX Mutex;
//
// Queue of events waiting for debugger intervention
//
LIST_ENTRY EventList;
//
// Flags for the object
//
ULONG Flags;
} DEBUG_OBJECT, *PDEBUG_OBJECT;
EventsPresent就是调试器监听的信号量,当调试事件来临,被调试进程就会给信号量置位,唤起调试器
EventList就是时一个win内核最喜欢的双向循环链表头,他串联着整个调试事件

每当发生调试事件,被调试进程就会进行如下操作
1.检测是否被调试
2.创建调试事件(DEBUG_EVENT),并且挂入链表
3.置位调试对象的EventsPresent,调试器会被激活
4.开始死等ContinueEvent的信号量
5.收到来自调试器的ContinueEvent信号量,恢复执行,同时reset调试对象的EventsPresent(也有可能不会)
调试器流程:
1.我用WaitForDebugEvent死等
2.我的EventsPresent被激活,我检测EventList串起来的一大串链表是否有东西
3.发现有东西,开始处理,处理完成一个就发送ContinueEvent信号量,然后摘除这个节点
4.处理到链表为空,reset调试对象的EventsPresent
5.WaitForDebugEvent继续死等
DEBUG_EVENT定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define DEBUG_EVENT_READ (0x01) // Event had been seen by win32 app
#define DEBUG_EVENT_NOWAIT (0x02) // No waiter one this. Just free the pool
#define DEBUG_EVENT_INACTIVE (0x04) // The message is in inactive. It may be activated or deleted later
#define DEBUG_EVENT_RELEASE (0x08) // Release rundown protection on this thread
#define DEBUG_EVENT_PROTECT_FAILED (0x10) // Rundown protection failed to be acquired on this thread
#define DEBUG_EVENT_SUSPEND (0x20) // Resume thread on continue
#define DBGKP_FIELD_FROM_IMAGE_OPTIONAL_HEADER(hdrs,field) \
((hdrs)->OptionalHeader.##field)
typedef struct _DEBUG_EVENT {
LIST_ENTRY EventList; // Queued to event object through this
KEVENT ContinueEvent;
CLIENT_ID ClientId;
PEPROCESS Process; // Waiting process
PETHREAD Thread; // Waiting thread
NTSTATUS Status; // Status of operation
ULONG Flags;
PETHREAD BackoutThread; // Backout key for faked messages
DBGKM_APIMSG ApiMsg; // Message being sent
} DEBUG_EVENT, *PDEBUG_EVENT;
从图结合定义我们可用看到DEBUG_EVENT和DEBUG_OBJECT都有EventList,像一条链子一样把DEBUG_OBJECT和一堆DEBUG_EVENT串起来
当然你可能现在云里雾里,我们结合代码分析
DebugActiveProcess分析
DebugActiveProcess为kernelBase.dll的导出函数,在ida里是
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
BOOL __stdcall DebugActiveProcess(DWORD dwProcessId)
{
NTSTATUS v2; // eax
__int64 v3; // rcx
HANDLE hProcess; // rax
HANDLE hProcess_copy; // rbx
NTSTATUS active; // edi
v2 = DbgUiConnectToDbg(); // 创建调试对象并且挂到上面去
if ( v2 < 0 )
{
v3 = (unsigned int)v2;
LABEL_3:
BaseSetLastNTError(v3);
return 0;
}
hProcess = ProcessIdToHandle(dwProcessId);
hProcess_copy = hProcess;
if ( !hProcess )
return 0;
active = DbgUiDebugActiveProcess(hProcess);
if ( active < 0 )
{
NtClose(hProcess_copy);
v3 = (unsigned int)active;
goto LABEL_3;
}
NtClose(hProcess_copy);
return 1;
}
DbgUiConnectToDbg的功能就是初始化调试对象,并且先将调试对象挂到调试器上
我们来到ntdll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 DbgUiConnectToDbg()
{
unsigned int v0; // ecx
OBJECT_ATTRIBUTES ObjectAttributes; // [rsp+20h] [rbp-38h] BYREF
v0 = 0;
if ( !NtCurrentTeb()->DbgSsReserved[1] )
{
memset(&ObjectAttributes.RootDirectory, 0, 20);
*(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0;// set ObjectAttributes
ObjectAttributes.Length = 48;
return (unsigned int)NtCreateDebugObject(&NtCurrentTeb()->DbgSsReserved[1], 0x1F000Fu, &ObjectAttributes, 1u);
}
return v0;
}
emmm,很简单的中转一下直接进入内核
注意看一下&NtCurrentTeb()->DbgSsReserved[1]的汇编
mov rcx, gs:30h
add rcx, 16A8h ; DebugObjectHandle
16A8的偏移应该刻进dna,这就是调试器该被挂入调试对象的地方,放的是个句柄
NtCreateDebugObject分析
我们来看ntoskrnl
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
NTSTATUS NtCreateDebugObject(
PHANDLE DebugObjectHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
ULONG Flags)
{
char Flags_copy; // si
KPROCESSOR_MODE PreviousMode; // r10
__int64 v8; // rcx
NTSTATUS result; // eax
PDEBUG_OBJECT pDebugObject_copy; // rbx
_EWOW64PROCESS *is_wow64; // rax
unsigned __int16 machine; // ax
char *v13; // [rsp+20h] [rbp-68h]
__int64 v14; // [rsp+20h] [rbp-68h]
PDEBUG_OBJECT pDebug_Object; // [rsp+58h] [rbp-30h] BYREF
void *handle; // [rsp+60h] [rbp-28h]
Flags_copy = Flags;
handle = 0;
pDebug_Object = 0;
PreviousMode = KeGetCurrentThread()->PreviousMode;
if ( PreviousMode )
{
v8 = 0x7FFFFFFF0000LL;
if ( (unsigned __int64)DebugObjectHandle < 0x7FFFFFFF0000LL )
v8 = (__int64)DebugObjectHandle;
*(_QWORD *)v8 = *(_QWORD *)v8;
}
*DebugObjectHandle = 0;
if ( (Flags & 0xFFFFFFFE) != 0 )
return 0xC000000D; // STATUS_INVALID_PARAMETER
result = ObCreateObjectEx(
PreviousMode,
DbgkDebugObjectType, // 创建调试对象
ObjectAttributes,
PreviousMode,
v13, // optional
0x68u,
0,
0,
(PVOID *)&pDebug_Object);
if ( result >= 0 )
{
pDebugObject_copy = pDebug_Object;
pDebug_Object->Mutex.Count = 1;
pDebugObject_copy->Mutex.Owner = 0;
pDebugObject_copy->Mutex.Contention = 0;
KeInitializeEvent(&pDebugObject_copy->Mutex.Event, SynchronizationEvent, 0);// 同步事件
pDebugObject_copy->EventList.Blink = &pDebugObject_copy->EventList;
pDebugObject_copy->EventList.Flink = &pDebugObject_copy->EventList;// 清空链表
KeInitializeEvent(&pDebugObject_copy->EventsPresent, NotificationEvent, 0);
if ( (Flags_copy & 1) != 0 )
pDebugObject_copy->Flags = 2; // #define DEBUG_OBJECT_KILL_ON_CLOSE (0x2) // Kill all debugged processes on close
else
pDebugObject_copy->Flags = 0;
is_wow64 = (_EWOW64PROCESS *)KeGetCurrentThread()->ApcState.Process[1].AffinityPadding[10];// 判断是否wow64
if ( is_wow64 )
{
machine = is_wow64->Machine;
if ( machine == 332 || machine == 452 )
pDebugObject_copy->Flags |= 4u;
}
LODWORD(v14) = 0;
result = ObInsertObjectEx(pDebug_Object, 0, DesiredAccess, 0, (PVOID *)v14, 0);// ObInsertObjectEx(pDebug_Object, 0, DesiredAccess, 0, (PVOID *)v14, 0,handle);
if ( result >= 0 )
*DebugObjectHandle = handle; // 调试对象挂上去了
}
return result;
}
代码不难
主要就是创建调试对象,初始化一下,然后挂上去,注意时先挂调试器
当然里面的结构体我都是导入的,你们看起来肯定没这么好看,之后我会把我自己用的idb文件放重来供大家学习
创建好了就要激活
DbgUiDebugActiveProcess分析
我们来到ntdll
1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall DbgUiDebugActiveProcess(HANDLE hProcess)
{
int active; // ebx
active = NtDebugActiveProcess(hProcess, NtCurrentTeb()->DbgSsReserved[1]);
if ( active >= 0 )
{
active = DbgUiIssueRemoteBreakin(hProcess);
if ( active < 0 )
ZwRemoveProcessDebug(hProcess, NtCurrentTeb()->DbgSsReserved[1]);
}
return (unsigned int)active;
}
NtDebugActiveProcess就是将调试对象与被调试进程关联的地方,在ntoskrnl里比较复杂,我们先看DbgUiIssueRemoteBreakin
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
__int64 __fastcall DbgUiIssueRemoteBreakin(HANDLE hProcess)
{
int v1; // ebx
__int64 v3; // [rsp+30h] [rbp-48h]
__int128 v4; // [rsp+60h] [rbp-18h] BYREF
HANDLE Handle; // [rsp+88h] [rbp+10h] BYREF
v1 = RtlpCreateUserThreadEx((__int64)hProcess, 0, 2, 0, 0, 0x4000, v3, (__int64)DbgUiRemoteBreakin, 0, &Handle, &v4);
if ( v1 >= 0 )
NtClose(Handle);
return (unsigned int)v1;
}
void __noreturn DbgUiRemoteBreakin()
{
if ( (NtCurrentPeb()->BeingDebugged || (MEMORY[0x7FFE02D4] & 2) != 0) && (NtCurrentTeb()->SameTebFlags & 0x20) == 0 )
{
if ( UseWOW64 )
{
if ( g_LdrpWow64PrepareForDebuggerAttach )
g_LdrpWow64PrepareForDebuggerAttach();
}
DbgBreakPoint();
}
RtlExitUserThread(0);
}
嗯,很简单,就是创建一个远程线程,然后这个线程有个int3会把程序中断下来
解释了为什么我们附加会让程序暂停下来
当然这也是一个反调试点,如果我们让DbgUiRemoteBreakin没有int3,不就断不下来了?
NtDebugActiveProcess
接下来时最复杂的
1
active = NtDebugActiveProcess(hProcess, NtCurrentTeb()->DbgSsReserved[1]);
NtCurrentTeb()->DbgSsReserved[1]依旧16A8
我们来到ntoskrnl
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
//hProcess为被调试进程
NTSTATUS __fastcall NtDebugActiveProcess(HANDLE hProcess, HANDLE hDebugObject)
{
KPROCESSOR_MODE PreviousMode; // bp
NTSTATUS result; // eax
__int64 v5; // rcx
struct _KTHREAD *CurrentThread; // rax
PEPROCESS pProcess_copy; // rdi
PEPROCESS Process; // rsi
int ntstatus; // ebx
unsigned __int64 v10; // rax
__int16 v11; // cx
unsigned __int64 v12; // rax
__int16 v13; // cx
BOOLEAN v14; // al
struct _DEBUG_OBJECT *pDebugObject_copy; // rsi
NTSTATUS Messages; // eax
PETHREAD LastThread[5]; // [rsp+40h] [rbp-28h] BYREF
_EPROCESS *pProcess; // [rsp+80h] [rbp+18h] BYREF
_DEBUG_OBJECT *pDebugObject; // [rsp+88h] [rbp+20h] BYREF
pProcess = 0;
PreviousMode = KeGetCurrentThread()->PreviousMode;
LastThread[0] = 0;
result = ObReferenceObjectByHandleWithTag(
hProcess,
0x800u,
(POBJECT_TYPE)PsProcessType,
PreviousMode,
'OgbD',
(PVOID *)&pProcess,
0);
if ( result >= 0 )
{
CurrentThread = KeGetCurrentThread();
pProcess_copy = &pProcess->Pcb;
Process = CurrentThread->ApcState.Process;
if ( pProcess == (_EPROCESS *)Process || pProcess == (_EPROCESS *)PsInitialSystemProcess )// 这个就是system进程
{
ntstatus = 0xC0000022; // STATUS_ACCESS_DENIED
}
else
{
LOBYTE(v5) = PreviousMode;
if ( (unsigned __int8)PsTestProtectedProcessIncompatibility(v5, CurrentThread->ApcState.Process, pProcess) )
{
ntstatus = 0xC0000712;
}
else if ( (pProcess_copy->?.SecureHandle & 1) == 0
|| (ntstatus = PsRequestDebugSecureProcess(pProcess_copy), ntstatus >= 0) )
{
v10 = Process[1].AffinityPadding[10];
if ( !v10
|| (v11 = *(_WORD *)(v10 + 8), v11 != 332) && v11 != 452
|| (v12 = pProcess_copy[1].AffinityPadding[10]) != 0
&& ((v13 = *(_WORD *)(v12 + 8), v13 == 332) || v13 == 452) )
{
pDebugObject = 0;
ntstatus = ObReferenceObjectByHandle(
hDebugObject,
2u,
DbgkDebugObjectType,
PreviousMode,
(PVOID *)&pDebugObject,
0);
if ( ntstatus >= 0 )
{
v14 = ExAcquireRundownProtection((PEX_RUNDOWN_REF)&pProcess_copy[1].ProfileListHead.Blink);// 获取rundown保护锁,防止你在我们挂调试对象时退出
pDebugObject_copy = pDebugObject;
if ( v14 )
{
Messages = DbgkpPostFakeProcessCreateMessages(pProcess_copy, pDebugObject, LastThread);
ntstatus = DbgkpSetProcessDebugObject(pProcess_copy, pDebugObject_copy, Messages, LastThread[0]);
ExReleaseRundownProtection((PEX_RUNDOWN_REF)&pProcess_copy[1].ProfileListHead.Blink);
}
else
{
ntstatus = 0xC000010A; // STATUS_PROCESS_IS_TERMINATING
}
HalPutDmaAdapter((PADAPTER_OBJECT)pDebugObject_copy);
}
}
else
{
ntstatus = 0xC00000BB; // STATUS_NOT_SUPPORTED
}
}
}
ObfDereferenceObjectWithTag(pProcess_copy, 'OgbD');
return ntstatus;
}
return result;
}
进行了前面一些无关紧要的操作,什么增加引用计数,system和psp保护的进程不让你调试,然后获取目标进程rundown保护锁,最关键两个操作是
1
2
Messages = DbgkpPostFakeProcessCreateMessages(pProcess_copy, pDebugObject, LastThread);
ntstatus = DbgkpSetProcessDebugObject(pProcess_copy, pDebugObject_copy, Messages, LastThread[0]);
DbgkpPostFakeProcessCreateMessages是发假消息,就是每次你用ida附加,不是底下会刷屏一大堆信息,什么创建了几个线程,加载了什么dll,exe镜像在哪,就是这个发的
DbgkpSetProcessDebugObject就是将被调试进程也挂上调试对象的函数
接下来的知识以后再来探索吧,先写这么多(