win内核调试原理揭秘4

Posted by Qmeimei10086 on July 12, 2026

这次我们来看被调试进程是如何发送调试事件的,以及送大家一个有趣反调试
拿异常举例,当发生异常,如果检测到有调试器,会先发给调试器,调用这个函数

DbgkForwardException 分析

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
BOOLEAN DbgkForwardException(PEXCEPTION_RECORD ExceptionRecord, BOOLEAN DebugException, BOOLEAN SecondChance)
{
  struct _ETHREAD *CurrentThread; // rax
  struct _EPROCESS *Process; // rsi
  struct _DMA_ADAPTER *DebugPort; // rbx
  char v9; // r14
  int v11; // esi
  NTSTATUS ReturnedStatus; // eax
  int info[4]; // [rsp+20h] [rbp-E0h] BYREF
  struct _DBGKM_APIMSG v14; // [rsp+30h] [rbp-D0h] BYREF

  *(_QWORD *)info = 0;
  memset(&v14, 0, 0x110u);
  if ( SecondChance )
  {
    info[0] = 1;
    PsSetProcessFaultInformation((ULONG_PTR)KeGetCurrentThread()->ApcState.Process, info);
  }
  v14.ApiNumber = DbgKmExceptionApi;
  v14.h.u1.Length = 13631656;
  v14.h.u2.ZeroInit = 8;
  CurrentThread = (struct _ETHREAD *)KeGetCurrentThread();
  Process = (struct _EPROCESS *)CurrentThread->Tcb.ApcState.Process;
  if ( DebugException )
  {
    if ( (*(_DWORD *)(&KeGetCurrentThread()[1].SwapListEntry + 1) & 4) != 0 )
      DebugPort = 0;
    else
      DebugPort = (struct _DMA_ADAPTER *)Process->DebugPort;
    v9 = 0;
  }
  else
  {
    DebugPort = (struct _DMA_ADAPTER *)PsCaptureExceptionPort(CurrentThread->Tcb.ApcState.Process);
    v14.h.u2.ZeroInit = 7;
    v9 = 1;
  }
  if ( !DebugPort && DebugException )
    return 0;
  KeCopyExceptionRecord(&v14.u, ExceptionRecord);
  v14.u.Exception.FirstChance = SecondChance == 0;
  if ( v9 )
  {
    if ( DebugPort )
    {
      v11 = DbgkpSendApiMessageLpc((__int64)&v14, (int)DebugPort, DebugException);
      HalPutDmaAdapter(DebugPort);
    }
    else
    {
      v11 = 0;
      v14.ReturnedStatus = -2147418111;
    }
  }
  else
  {
    v11 = DbgkpSendApiMessage(Process, DebugException != 0, &v14);
  }
  if ( v11 < 0 )
    return 0;
  ReturnedStatus = v14.ReturnedStatus;
  if ( v14.ReturnedStatus == -2147418111 )
  {
    if ( DebugException )
      return 0;
    ReturnedStatus = DbgkpSendErrorMessage(ExceptionRecord, 2, &v14);
  }
  return ReturnedStatus >= 0;
}

这里填写了根据异常apimsg的类型啦,一些异常记录,然后主动调用DbgkpSendApiMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall DbgkpSendApiMessage(struct _EPROCESS *Object, char a2, struct _DBGKM_APIMSG *a3)
{
  int v6; // ebp
  NTSTATUS v7; // esi

  if ( (PerfGlobalGroupMask & 0x400000) != 0 )
    EtwTraceDebuggerEvent(KeGetCurrentThread()->ApcState.Process, KeGetCurrentThread(), 1);
  do
  {
    v6 = 0;
    if ( Object == (struct _EPROCESS *)KeGetCurrentThread()->ApcState.Process && (a2 & 1) != 0 )
      v6 = (unsigned __int8)DbgkpSuspendProcess(Object);
    a3->ReturnedStatus = 259;
    v7 = DbgkpQueueMessage(Object, (PETHREAD)KeGetCurrentThread(), a3, 32 * (a2 & 2), 0);
    if ( v6 )
    {
      PsThawProcess(Object, 0);
      KeLeaveCriticalRegion();
    }
  }
  while ( v7 >= 0 && a3->ReturnedStatus == 1073807361 );
  return (unsigned int)v7;
}

可以看到调用了DbgkpQueueMessage,我们前面分析过了

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
NTSTATUS DbgkpQueueMessage(
        PEPROCESS Process,
        PETHREAD Thread,
        PDBGKM_APIMSG ApiMsg,
        ULONG Flags,
        PDEBUG_OBJECT TargetDebugObject)
{
  __int64 v10; // r12
  DEBUG_EVENT *alloc_debug_event; // rax
  DEBUG_EVENT *p_debug_event; // r14
  DBGKM_APINUMBER ApiNumber; // ecx
  DBGKM_APIMSG *p_ApiMsg; // rbx
  DBGKM_APIMSG *ApiMsg_From_Debug_Event; // rax
  PDBGKM_APIMSG In_ApiMsg; // rcx
  __int64 v18; // rdx
  __int128 v19; // xmm1
  NTSTATUS Status; // esi
  struct _FAST_MUTEX *p_Mutex; // r12
  struct _LIST_ENTRY *Blink; // rcx
  __int128 v23; // xmm1
  ULONG is_no_wait; // [rsp+30h] [rbp-1C8h]
  DEBUG_EVENT debug_event; // [rsp+40h] [rbp-1B8h] BYREF
                                                //                                                 #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
                                                //                                                 
                                                //                                              
  memset(
    &debug_event,
    0,
    0x168u);
  v10 = 2;
  is_no_wait = Flags & 2;
  if ( (Flags & 2) == 0 )                       // 需等待事件
  {
    debug_event.Flags = Flags;
    p_debug_event = &debug_event;
    ExAcquireFastMutex(&DbgkpProcessDebugPortMutex);
    ApiNumber = ApiMsg->ApiNumber;
    TargetDebugObject = (PDEBUG_OBJECT)Process->DebugPort;// 需等待事件必须有debug port
    if ( (unsigned int)(ApiNumber - 1) <= 1 && (Thread->CrossThreadFlags & 0x40) != 0 )
      TargetDebugObject = 0;
    if ( ApiNumber == DbgKmLoadDllApi )
    {
      if ( ((unsigned __int8)Flags & Thread->CrossThreadFlags & 0x40) == 0 )
      {
LABEL_14:
        KeInitializeEvent(&debug_event.ContinueEvent, SynchronizationEvent, 0);
        goto LABEL_15;
      }
      TargetDebugObject = 0;
    }
    if ( (unsigned int)(ApiNumber - 3) <= 1 && SLOBYTE(Thread->CrossThreadFlags) < 0 )
      TargetDebugObject = 0;
    goto LABEL_14;
  }
  alloc_debug_event = (DEBUG_EVENT *)ExAllocatePoolWithQuotaTag((POOL_TYPE)520, 0x168u, 'EgbD');
  p_debug_event = alloc_debug_event;
  if ( !alloc_debug_event )
    return 0xC000009A;                          // STATUS_INSUFFICIENT_RESOURCES
  alloc_debug_event->Flags = Flags | 4;
  ObfReferenceObjectWithTag(Process, 0x4F676244u);
  ObfReferenceObjectWithTag(Thread, 0x4F676244u);
  p_debug_event->BackoutThread = (PETHREAD)KeGetCurrentThread();
LABEL_15:
  p_ApiMsg = &p_debug_event->ApiMsg;
  p_debug_event->Process = Process;
  ApiMsg_From_Debug_Event = &p_debug_event->ApiMsg;
  p_debug_event->Thread = Thread;
  In_ApiMsg = ApiMsg;
  v18 = 2;
  do                                            // 复制
  {
    *(_OWORD *)&ApiMsg_From_Debug_Event->h.u1.s1.DataLength = *(_OWORD *)&In_ApiMsg->h.u1.s1.DataLength;
    *(union _PORT_MESSAGE::$BD8137A324A8476723CC573F97133CB1 *)((char *)&ApiMsg_From_Debug_Event->h.8 + 8) = *(union _PORT_MESSAGE::$BD8137A324A8476723CC573F97133CB1 *)((char *)&In_ApiMsg->h.8 + 8);
    *(_OWORD *)&ApiMsg_From_Debug_Event->h.ClientViewSize = *(_OWORD *)&In_ApiMsg->h.ClientViewSize;
    *(_OWORD *)&ApiMsg_From_Debug_Event->u.Exception.ExceptionRecord.ExceptionCode = *(_OWORD *)&In_ApiMsg->u.Exception.ExceptionRecord.ExceptionCode;
    *((_OWORD *)&ApiMsg_From_Debug_Event->u.UnloadDll + 1) = *((_OWORD *)&In_ApiMsg->u.UnloadDll + 1);
    *((_OWORD *)&ApiMsg_From_Debug_Event->u.UnloadDll + 2) = *((_OWORD *)&In_ApiMsg->u.UnloadDll + 2);
    *((_OWORD *)&ApiMsg_From_Debug_Event->u.UnloadDll + 3) = *((_OWORD *)&In_ApiMsg->u.UnloadDll + 3);
    ApiMsg_From_Debug_Event = (DBGKM_APIMSG *)((char *)ApiMsg_From_Debug_Event + 128);
    v19 = *((_OWORD *)&In_ApiMsg->u.UnloadDll + 4);
    In_ApiMsg = (PDBGKM_APIMSG)((char *)In_ApiMsg + 128);
    *((_OWORD *)&ApiMsg_From_Debug_Event[-1].u.UnloadDll + 9) = v19;
    --v18;
  }
  while ( v18 );
  *(_OWORD *)&ApiMsg_From_Debug_Event->h.u1.s1.DataLength = *(_OWORD *)&In_ApiMsg->h.u1.s1.DataLength;
  p_debug_event->ClientId = Thread->Cid;
  if ( TargetDebugObject )
  {
    p_Mutex = &TargetDebugObject->Mutex;
    ExAcquireFastMutex(&TargetDebugObject->Mutex);
    if ( (TargetDebugObject->Flags & 1) != 0 )
    {
      Status = 0xC0000354;
    }
    else
    {
      Blink = TargetDebugObject->EventList.Blink;
      if ( Blink->Flink != &TargetDebugObject->EventList )
        __fastfail(3u);                         // 检查完整性
      p_debug_event->EventList.Flink = &TargetDebugObject->EventList;
      p_debug_event->EventList.Blink = Blink;
      Blink->Flink = &p_debug_event->EventList; // 尾插法
      TargetDebugObject->EventList.Blink = &p_debug_event->EventList;
      if ( !is_no_wait )                        // 需等待,假消息不走这
        KeSetEvent(&TargetDebugObject->EventsPresent, 0, 0);
      Status = 0;
    }
    KeReleaseGuardedMutex(p_Mutex);
    v10 = 2;
  }
  else
  {
    Status = 0xC0000353;                        // STATUS_PORT_NOT_SET
  }
  if ( is_no_wait )
  {
    if ( Status < 0 )
    {
      ObfDereferenceObjectWithTag(Process, 0x4F676244u);
      ObfDereferenceObjectWithTag(Thread, 0x4F676244u);
      ExFreePoolWithTag(p_debug_event, 0);
    }
  }
  else
  {
    KeReleaseGuardedMutex(&DbgkpProcessDebugPortMutex);
    if ( Status >= 0 )
    {
      KeWaitForSingleObject(&p_debug_event->ContinueEvent, Executive, 0, 0, 0);
      Status = p_debug_event->Status;
      do
      {
        *(_OWORD *)&ApiMsg->h.u1.s1.DataLength = *(_OWORD *)&p_ApiMsg->h.u1.s1.DataLength;
        *(union _PORT_MESSAGE::$BD8137A324A8476723CC573F97133CB1 *)((char *)&ApiMsg->h.8 + 8) = *(union _PORT_MESSAGE::$BD8137A324A8476723CC573F97133CB1 *)((char *)&p_ApiMsg->h.8 + 8);
        *(_OWORD *)&ApiMsg->h.ClientViewSize = *(_OWORD *)&p_ApiMsg->h.ClientViewSize;
        *(_OWORD *)&ApiMsg->u.Exception.ExceptionRecord.ExceptionCode = *(_OWORD *)&p_ApiMsg->u.Exception.ExceptionRecord.ExceptionCode;
        *((_OWORD *)&ApiMsg->u.UnloadDll + 1) = *((_OWORD *)&p_ApiMsg->u.UnloadDll + 1);
        *((_OWORD *)&ApiMsg->u.UnloadDll + 2) = *((_OWORD *)&p_ApiMsg->u.UnloadDll + 2);
        *((_OWORD *)&ApiMsg->u.UnloadDll + 3) = *((_OWORD *)&p_ApiMsg->u.UnloadDll + 3);
        ApiMsg = (PDBGKM_APIMSG)((char *)ApiMsg + 128);
        v23 = *((_OWORD *)&p_ApiMsg->u.UnloadDll + 4);
        p_ApiMsg = (DBGKM_APIMSG *)((char *)p_ApiMsg + 128);
        *((_OWORD *)&ApiMsg[-1].u.UnloadDll + 9) = v23;
        --v10;
      }
      while ( v10 );
      *(_OWORD *)&ApiMsg->h.u1.s1.DataLength = *(_OWORD *)&p_ApiMsg->h.u1.s1.DataLength;
    }
  }
  return Status;
}

计算最后传入DbgkpQueueMessage的flag是0,也就是要wait的需等待事件,与nowait走一些不一样的路,最终会走到这里

1
KeWaitForSingleObject(&p_debug_event->ContinueEvent, Executive, 0, 0, 0);

这时候这个线程就开死等直到调试器处理完成,然后还会传出ApiMsg供外面的函数分析使用

有趣的反调试

我们看DbgkForwardException的这里

1
2
    if ( (*(_DWORD *)(&KeGetCurrentThread()[1].SwapListEntry + 1) & 4) != 0 )
      DebugPort = 0;

汇编准确一点

mov     rax, gs:610o
mov     ecx, [rax+_ETHREAD.___u21.CrossThreadFlags]

这个是CrossThreadFlags的ThreadHideFromDebugger位,如果该位置位,直接就是DebugPort = 0,啥都发不过去了
这里给出利用代码

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
#include <windows.h>
#include <winternl.h>
#include <stdio.h>

// 未公开的 ThreadInformationClass 常量
#define ThreadHideFromDebugger 0x11

// NtSetInformationThread 函数原型
typedef NTSTATUS (NTAPI *pNtSetInformationThread)(
    HANDLE ThreadHandle,
    THREADINFOCLASS ThreadInformationClass,
    PVOID ThreadInformation,
    ULONG ThreadInformationLength
);

// 隐藏当前线程,使调试器无法接收该线程的异常
void HideCurrentThreadFromDebugger() {
    // 1. 动态获取 ntdll!NtSetInformationThread 地址
    HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
    if (!hNtdll) {
        printf("[!] 无法加载 ntdll.dll\n");
        return;
    }

    auto NtSetInformationThread = (pNtSetInformationThread)GetProcAddress(hNtdll, "NtSetInformationThread");
    if (!NtSetInformationThread) {
        printf("[!] 无法获取 NtSetInformationThread 地址\n");
        return;
    }

    // 2. 调用未公开功能,隐藏当前线程
    NTSTATUS status = NtSetInformationThread(
        GetCurrentThread(),                // 当前线程句柄
        (THREADINFOCLASS)ThreadHideFromDebugger, // 0x11
        nullptr,                           // 无额外数据
        0                                  // 长度0
    );

    if (status == 0) {
        printf("[+] 当前线程已成功从调试器隐藏\n");
    } else {
        printf("[!] 隐藏失败,NTSTATUS = 0x%08X\n", status);
    }
}

int main() {
    // 执行反调试:隐藏自身线程
    HideCurrentThreadFromDebugger();

    // 正常业务代码(示例:每隔一秒打印一次)
    printf("[*] 程序正常运行,请尝试用调试器附加并触发异常...\n");
    while (true) {
        Sleep(1000);
        // 可以取消下面注释来触发一个异常,验证调试器是否还能捕获
        // __try { *(int*)0 = 0; } __except(EXCEPTION_EXECUTE_HANDLER) {}
        // 如果线程已隐藏,调试器将收不到异常,程序会自己处理。
    }

    return 0;
}

我们直接使用NtSetInformationThread这个导出函数就可以设置线程teb的某些值

结语

至此,所有的调试原理都讲完了
本博客只作为学习过程上的记录,可能不太详细,大家可以去看oxygen或者火哥的教程