win内核调试原理揭秘1

Posted by Qmeimei10086 on July 12, 2026

前言

身为一名逆向人员,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,就会将创建一个调试对象,他既挂在被调试进程上,也挂在调试器上 img1
什么是调试对象?我们直接从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内核最喜欢的双向循环链表头,他串联着整个调试事件
img2
每当发生调试事件,被调试进程就会进行如下操作
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就是将被调试进程也挂上调试对象的函数

接下来的知识以后再来探索吧,先写这么多(