Windows XP 用户态调试(二)调试器进程

ʕ •̀ o •́ ʔ

1 调试事件循环

在上一篇文章中,已经了解了调试器如何与被调试进程建立调试会话(本文后续还会补充说明建立调试会话的细节),也了解了调试进程发送杜撰调试消息、被调试进程发送调试消息的过程。

本文将会研究调试器如何接收调试消息,并处理调试消息(在 0 环时叫做调试消息,在 3 环叫做调试事件)。

当调试会话建立好之后,调试器将进入如下的接收调试事件–处理调试事件–回复调试事件的循环,直到被调试进程终止或者调试器终止调试会话。

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
DEBUG_EVENT DbgEvt; 

while(WaitForDebugEvent(&DbgEvt, INFINITE)) // 等待调试事件
{
switch (DbgEvt.dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT:
printf("发生异常调试事件\n");
break;

case CREATE_THREAD_DEBUG_EVENT:
printf("创建线程调试事件\n");
break;

case CREATE_PROCESS_DEBUG_EVENT:
printf("创建进程调试事件\n");
break;

case EXIT_THREAD_DEBUG_EVENT:
printf("退出线程调试事件\n");
break;

case EXIT_PROCESS_DEBUG_EVENT:
printf("退出进程调试事件\n");
break;

case LOAD_DLL_DEBUG_EVENT:
printf("加载DLL调试事件\n");
break;

case UNLOAD_DLL_DEBUG_EVENT:
printf("卸载DLL调试事件\n");
break;

default:
break;
}

//DBG_CONTINUE 表示调试器已处理该异常
//DBG_EXCEPTION_NOT_HANDLED 表示调试器没有处理该异常,转回到用户态中执行,寻找可以处理该异常的异常处理器
//ContinueDebugEvent 告诉被调试程序让被调试程序继续执行
bRet = ContinueDebugEvent(DbgEvt.dwProcessId, DbgEvt.dwThreadId, DBG_CONTINUE);

// 处理后,恢复调试目标继续执行
ContinueDebugEvent(DbgEvt.dwProcessId, DbgEvt.dwThreadId, dwContinueStatus);
}

2 等待调试事件

2.1 WaitForDebugEvent函数定义

函数 WaitForDebugEvent:等待正在调试的进程中发生调试事件。

1
2
3
4
BOOL WaitForDebugEvent(
[out] LPDEBUG_EVENT lpDebugEvent, //指向DEBUG_EVENT结构的指针,用来保存收到的调试事件。
[in] DWORD dwMilliseconds //指定要等待的毫秒数,或者使用常量INFINITE(0xFFFFFFFF),则一直等待。
);

注意:

  • 该函数第一个参数是 OUT,不像 WaitForSingleObject(IN HANDLE hHandle, IN DWORD dwMilliseconds) 来等待某个可等待对象。
  • 不要向在处理 WaitForDebugEvent 函数的线程插入 APC,该函数虽然会阻塞线程,但不是用来进行线程调度的。

调用 WaitForDebugEvent 会导致所在线程阻塞,直到有调试事件发生,或等待时间已到或发生错误才返回。这也是大多数调试器使用多线程的原因,可使用其他线程处理 UI 更新和用户对话。

2.2 DEBUG_EVENT结构

Windows XP 用户态调试(一)被调试进程 3.5.3已经提到,DbgkpSendApiMessage 发送的调试消息为 DBGKM_APIMSG,而 DbgkpQueueMessage 会将 DBGKM_APIMSG 扩展成 DEBUG_EVENT(为了区分我重命名为DBGKM_DEBUG_EVENT)并挂到调试对象 DEBUG_OBJECT.EventList 消息队列中。

结构 DEBUG_EVENT 在 0 环和 3 环同名,但是并不是同一个结构,在 3 环的结构如下:

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;

其中 dwDebugEventCode 用来标识调试事件的类型(取值范围是一个联合体),联合体 u 则是由9种不同的事件详细信息构成。根据 dwDebugEventCode 的不同,决定了 u 中包含的结构,具体对应关系如下:

事件类型(dwDebugEventCode) 说明 详细信息所使用的结构(u)
EXCEPTION_DEBUG_EVENT 1 异常 EXCEPTION_DEBUG_INFO
CREATE_THREAD_DEBUG_EVENT 2 创建线程 CREATE_THREAD_DEBUG_INFO
CREATE_PROCESS_DEBUG_EVENT 3 创建进程 CREATE_PROCESS_DEBUG_INFO
EXIT_THREAD_DEBUG_EVENT 4 线程退出 EXIT_THREAD_DEBUG_INFO
EXIT_PROCESS_DEBUG_EVENT 5 进程退出 EXIT_PROCESS_DEBUG_INFO
LOAD_DLL_DEBUG_EVENT 6 映射DLL LOAD_DLL_DEBUG_INFO
UNLOAD_DLL_DEBUG_EVENT 7 卸载DLL UNLOAD_DLL_DEBUG_INFO
OUTPUT_DEBUG_STRING_EVENT 8 输出调试信息 OUTPUT_DEBUG_STRING_INFO
RIP_EVENT 9 内部错误 RIP_INFO

在 0 环的 DEBUG_EVENT 结构为:

1
2
3
4
5
6
7
8
9
10
11
12
// 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;

2.3 WaitForDebugEvent源码分析

函数 WaitForDebugEvent 主要有三个功能:

  1. 将调试进程 TEB.DbgSsReserved[1] 的调试对象句柄传到 0 环,调用 KeWaitForSingleObject 等待调试对象的同步事件 DebugObject->EventsPresent
  2. 当调试对象的同步事件有信号时,会先从调试对象消息队列的队头取出结构为 DBGKM_DEBUG_EVENTDbgkmDebugEvent 节点。然后对该消息节点再判断一次,满足条件就继续往下执行。将 DBGKM_DEBUG_EVENT 结构的消息转换成 DBGUI_WAIT_STATE_CHANGE 结构的消息,最后将该消息返回 3 环。
  3. 在 3 环将 DBGUI_WAIT_STATE_CHANGE 结构的消息转换成 DEBUG_EVENT 结构的消息,并且根据消息类型对 TEB.DbgSsReserved[0] 链表上结构为 TMPHANDLES 的节点进行维护(上一篇2.4有讲解),方便 ContinueDebugEvent 回复消息时使用。注意,该结构的内存是在当前进程堆栈上进行申请的,每次新建进程/线程时都会将新的 TMPHANDLES 节点插入链表头。
1
2
3
4
5
6
7
8
9
10
11
typedef struct _TMPHANDLES {
struct _TMPHANDLES *Next; //指向下一个节点
HANDLE Thread; // 线程句柄(被调试进程中)
HANDLE Process; // 被调试进程的句柄
DWORD dwProcessId; // 被调试进程的 ID
DWORD dwThreadId; // 线程ID(被调试进程中)
BOOLEAN DeletePending; // 退出标记,TRUE为退出进程/线程
} TMPHANDLES, *PTMPHANDLES;

PTMPHANDLES Tmp;
Tmp = RtlAllocateHeap (RtlProcessHeap(), 0, sizeof (TMPHANDLES));

关于 DBGUI_WAIT_STATE_CHANGE 结构:

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
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;

// NewState 取值的枚举常量
typedef enum _DBG_STATE {
DbgIdle,
DbgReplyPending,
DbgCreateThreadStateChange,
DbgCreateProcessStateChange,
DbgExitThreadStateChange,
DbgExitProcessStateChange,
DbgExceptionStateChange,
DbgBreakpointStateChange,
DbgSingleStepStateChange,
DbgLoadDllStateChange,
DbgUnloadDllStateChange
} DBG_STATE, *PDBG_STATE;

关于该函数详细流程分析:《Windows调试内幕系列一之Windows用户模式调试内幕》、《Windows内核学习笔记之调试(下)》、《内核用户模式调试支持(DBGK)》。

WaitForDebugEvent1.png

WaitForDebugEvent的副本2.png

WaitForDebugEvent的.png

关于调试器、被调试进程的同步问题:

发送消息:

1
2
3
// DbgkpQueueMessage
KeSetEvent(&DebugObject->EventsPresent, 0, 0)
KeWaitForSingleObject(&DbgkmDebugEvent->ContinueEvent, Executive, KernelMode, FALSE, NULL);

接收消息:

1
2
// WaitForDebugEvent
KeWaitForSingleObject(&DebugObject->EventsPresent,x,x,x,x)

回复被调试进程:

1
2
// ContinueDebugEvent
KeSetEvent(&DbgkmDebugEvent->ContinueEvent, 0, 0)

3 初始断点

上一章分析了调试器如何接收调试消息。我们每次不管是以新建子进程方式还是以附加方式调试一个程序,被调试程序每次都会断下来。

  1. 如下以新建子进程方式启动一个被调试程序:

    1
    2
    3
    4
    5
    6
    7
    (c4c.370): Break instruction exception - code 80000003 (first chance)
    eax=00241eb4 ebx=7ffda000 ecx=00000001 edx=00000002 esi=00241f48 edi=00241eb4
    eip=7c92120e esp=0012fb20 ebp=0012fc94 iopl=0 nv up ei pl nz na po nc
    cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
    *** ERROR: Symbol file could not be found. Defaulted to export symbols for ntdll.dll -
    ntdll!DbgBreakPoint:
    7c92120e cc int 3

    可以看到程序自动断在 ntdll!DbgBreakPoint 函数,查看此时的调用堆栈如下图(因为调试符号的原因,我的机器调用堆栈显示不正确,下图必须在Windows XP下调用堆栈才是如此,其他版本OS下 ntdll 的处理是不同的):

    6.png

  2. 当以附加形式调试一个程序时:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    (360.bd0): Break instruction exception - code 80000003 (first chance)
    *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\SYSTEM32\ntdll.dll -
    ntdll!DbgBreakPoint:
    00000000`77c4b1d0 cc int 3
    0:001> k
    Child-SP RetAddr Call Site
    00000000`0027fc78 00000000`77cd93c8 ntdll!DbgBreakPoint
    00000000`0027fc80 00000000`77c33865 ntdll!DbgUiRemoteBreakin+0x38
    00000000`0027fcb0 00000000`00000000 ntdll!RtlUserThreadStart+0x25

    被调试进程依然断在 ntdll!DbgBreakPoint 处。

当一打开/附加一个程序时,断下来的地方是系统为我们设置的一个断点,这个断点成为初始断点

下面将分别介绍调试器两种方式建立调试会话,初始断点的来源。

3.1 从调试器启动进程

这种方式是以新建子进程方式启动被调试进程,如调用 CreateProcess 函数,参数 dwCreationFlags 包含 DEBUG_PROCESSDEBUG_ONLY_THIS_PROCESS 等标志位。

3.1.1 简述进程创建过程

进程创建过程涉及到内核许多方面知识,其创建过程中调用的 API 以及调用过程是非常繁琐的。这里就根据《Windows内核情景分析 5.6 P306》与《64位CreateProcess逆向系列文章》简要总结 32 位进程创建的基本框架( 64 位系统创建进程暂不研究)。

第一阶段:在 3 环

  • 创建当前进程的子进程:

    1
    2
    //对 dwCreationFlags 等参数进行检查并处理
    kernel32!CreateProcess() -> CreateProcessInternal() -> NtCreateProcess() -> 进入 0
    • 参数检查时看 dwCreationFlags 是否包含 DEBUG_PROCESSDEBUG_ONLY_THIS_PROCESS 等标志位,如果包含则会调用 DbgUiConnectToDbg 等函数建立调试会话。
    • 然后将创建者进程 TEB.TEB.DbgSsReserved[1] 设置为调试对象的句柄,新建的被调试进程的 EPROCESS.DebugPort 设置为调试对象地址,同时设置被调试进程 PEB.BeingDebugged = TRUE
  • 创建当前进程、其他进程的子进程使用:nt!NtCreateProcess

第二阶段:在 0 环,创建内核中的进程对象

  1. 创建 EPROCESS 结构、创建进程句柄表、初始化进程地址空间、初始化 EPROCESS
  2. 将 PE 文件映射到进程用户空间。
  3. 将系统 DLL 映射到进程用户空间(ntdll.dll 这个 DLL 比较特殊,在初始化内核的此时就映射了)。
  4. 创建 PEB 、将刚创建的子进程句柄插入到当前进程的句柄表。

第三阶段:在 0 环,创建并初始化第一个线程

  1. 创建并初始化 ETHREAD
  2. 在子进程用户空间创建并初始化 TEB
  3. 将线程在用户空间的起始地址设置成:
    X86:kernel32!BaseProcessStartThunk(主线程),随后线程 BaseThreadStartupThunk
    X64:ntdll!RtlUserThreadStart(所有线程)。
  4. 设置第一个线程被调度执行开始的地方为 KiThreadStartup 函数。
  5. 调用通知回调函数。

第四阶段:在 0 环,通知 Windows 子系统

由子进程的创建者进程向 csrss.exe 子系统服务进程发通知。 在《Windows内核情景分析 5.6 P308》详细阐述了Windows子系统。

至此 CreateProcess 的操作己经完成,CreateProcess 的调用者从该函数返回,就回到了应用程序或更高层的 DLL 中。这四个阶段都是立足于创建者进程的用户空间。

第五阶段:在 0、3 环,启动第一个线程

当第一个线程(主线程)被调度时,首先会执行内核中的 KiThreadStartup,该函数把目标线程的运行级别从 DPC 级降低到 APC 级,然后调用内核函数 PspUserThreadStartup,该函数内部会调用 DbgkCreateThread 向调试子系统通知新线程创建事件,调试子系统会向调试事件队列中放入一个进程创建事件,并等待调试器来处理和回复。

最后 PspUserThreadStartupntdll!LdrInitializeThunk 作为 APC 函数挂入到 APC 队列,只要主线程一准备进入用户空间就会先执行该 APC 函数。当该 APC 函数执行完之后还是会再次回到用户空间,开始执行 kernel32!BaseProcessStartThunkkernel32!BaseThreadStartupThunk函数。对于进程中的第一个线程是前者,对于后来的线程则是后者。然后就是比较熟悉的接着调用 mainCRTStartup-main 了。

至于用户程序所提供的(线程)入口,则是作为参数提供给这两个函数的,这两个函数都会使用该指针调用由用户提供的入口函数。

第六阶段:在 3 环调用 ntdll!LdrInitializeThunk ,用户空间的初始化和 DLL 的连接

用户空间的初始化和 DLL 的动态连接是由 APC 函数 LdrInitializeThunk 在用户空间完成的。

在第二阶段 ntdll.dll 已经被映射到了用户空间,但是其他的 DLL 尚未装入,应用软件与 DLL 之间也尚未建立动态连接。函数 LdrInitializeThunk 在映像中的位置是预定的,所以在进入这个函数之前并不需要连接。

3.1.2 初始断点位置

如上第五阶段,当一个线程被调度时,将从 0 环 KiThreadStartup 函数开始执行,然后调用 PspUserThreadStartup 函数(该函数先调用DbgkCreateThread 发送调试消息),然后该函数将 ntdll!LdrInitializeThunk 作为一个 APC 函数插入到当前线程,使其只要一进入 3 环就执行该 APC 函数。

当线程一进入到 3 环,执行的是第一个 APC 函数ntdll!LdrInitializeThunk初始断点也就在这里面了。

  1. 函数 LdrInitializeThunk 只是简单调用了 LdrpInitialize

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    .text:7C921166 // __stdcall LdrInitializeThunk(x, x, x, x)
    .text:7C921166 public _LdrInitializeThunk@16
    .text:7C921166 _LdrInitializeThunk@16 proc near // DATA XREF: .text:off_7C923428↓o
    .text:7C921166
    .text:7C921166 arg_0 = dword ptr 4
    .text:7C921166 arg_C = byte ptr 10h
    .text:7C921166
    .text:7C921166 lea eax, [esp+arg_C]
    .text:7C92116A mov [esp+arg_0], eax
    .text:7C92116E xor ebp, ebp
    .text:7C921170 jmp _LdrpInitialize@12 // LdrpInitialize(x,x,x)
    .text:7C921170 _LdrInitializeThunk@16 endp // sp-analysis failed
  2. 然后 LdrpInitialize 函数接着调用 _LdrpInitialize (下划线)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    .text:7C93B057 // =============== S U B R O U T I N E =======================================
    .text:7C93B057
    .text:7C93B057 // Attributes: bp-based frame
    .text:7C93B057
    .text:7C93B057 // __stdcall LdrpInitialize(x, x, x)
    .text:7C93B057 _LdrpInitialize@12 proc near // CODE XREF: LdrInitializeThunk(x,x,x,x)+A↑j
    .text:7C93B057
    .text:7C93B057 // FUNCTION CHUNK AT .text:7C9426ED SIZE 0000000A BYTES
    .text:7C93B057
    .text:7C93B057 mov edi, edi
    .text:7C93B059 push ebp
    .text:7C93B05A mov ebp, esp
    .text:7C93B05C cmp _SecurityCookieInitialized, 0
    .text:7C93B063 jz loc_7C9426ED
    .text:7C93B069
    .text:7C93B069 loc_7C93B069: // CODE XREF: LdrpInitialize(x,x,x)+769B↓j
    .text:7C93B069 pop ebp
    .text:7C93B06A jmp __LdrpInitialize@12 // _LdrpInitialize(x,x,x)
    .text:7C93B06A _LdrpInitialize@12 endp
  3. 在函数 _LdrpInitialize 中会判断 PEB.Ldr == NULL?(Win10 21H2用常量 LdrpProcessInitialized 来判断),用来判断当前线程是否是进程的第一个线程,如果是则此时是新建进程正在启动的过程(在前面还会检查 (PEB.ProcessParameters)RTL_USER_PROCESS_PARAMETERS.Flags 并设置标志位,该 Flags 还与调试状态/反调试有关),就会调用 LdrpInitializeProcess 函数。

    1
    2
    3
    4
    5
    6
    .text:7C93AFD5                 cmp     [esi+_PEB.Ldr], ebx
    .text:7C93AFD8 jz loc_7C94106C // Peb->Ldr == NULL 则当前线程是第一个线程
    ...
    .text:7C94106C loc_7C94106C:
    ...
    .text:7C94108A call _LdrpInitializeProcess@20
  4. 函数 LdrpInitializeProcess 功能非常复杂,我们暂时只关注与初始断点相关的地方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .text:7C9410B2 loc_7C9410B2:                           // CODE XREF: LdrpInitializeProcess(x,x,x,x,x)+BCC↓j
    .text:7C9410B2 cmp [ebx+_PEB.BeingDebugged], 0
    .text:7C9410B6 jnz loc_7C95E60D
    ...
    .text:7C95E60D loc_7C95E60D: // CODE XREF: LdrpInitializeProcess(x,x,x,x,x)-4BB↑j
    .text:7C95E60D call _DbgBreakPoint@0 // DbgBreakPoint()
    .text:7C95E612 mov eax, [ebx+68h]
    .text:7C95E615 shr eax, 1
    .text:7C95E617 and al, 1
    .text:7C95E619 mov _ShowSnaps, al
    .text:7C95E61E jmp loc_7C9410BC

    这里会判断当前进程 PEB.BeingDebugged == FALSE?,如果不等于就会调用 DbgBreakPoint 函数。

  5. 函数 DbgBreakPoint 代码如下。

    1
    2
    3
    4
    .text:7C92120E _DbgBreakPoint@0 proc near                                                     
    .text:7C92120E CC int 3 // Trap to Debugger
    .text:7C92120F C3 retn
    .text:7C92120F _DbgBreakPoint@0 endp

    可以看到,该函数就是简单的 0xCC 断点,此时产生的断点异常(0x80000003第一轮分发时就会调用 DbgkForwardException 将异常消息发给用户调试器的调试端口(就是调试对象,异常端口是Windows2000 LPC通信用的)。

使用调试器打开一个进程断下来的位置,就是上面 5 个步骤分析的位置了。

该断点是系统提供的,被调试进程运行时只有在判断当前被调试,才会执行该初始断点

3.2 附加进程

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

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

  2. 观察 DbgUiRemoteBreakin 函数代码如下。

    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
    .text:7C96FFE3 // =============== S U B R O U T I N E =======================================
    .text:7C96FFE3
    .text:7C96FFE3 // Attributes: bp-based frame
    .text:7C96FFE3
    .text:7C96FFE3 // __stdcall DbgUiRemoteBreakin(x)
    .text:7C96FFE3 public _DbgUiRemoteBreakin@4
    .text:7C96FFE3 _DbgUiRemoteBreakin@4 proc near
    .text:7C96FFE3
    .text:7C96FFE3
    .text:7C96FFE3 ms_exc = CPPEH_RECORD ptr -18h
    .text:7C96FFE3 // __unwind { // __SEH_prolog
    .text:7C96FFE3 push 8
    .text:7C96FFE5 push offset stru_7C970030
    .text:7C96FFEA call __SEH_prolog
    .text:7C96FFEF mov eax, large fs:_TEB.NtTib.Self
    .text:7C96FFF5 mov eax, [eax+_TEB.ProcessEnvironmentBlock]
    .text:7C96FFF8 cmp [eax+_PEB.BeingDebugged], 0 // 判断 PEB.BeingDebugged
    .text:7C96FFFC jnz short loc_7C970007
    .text:7C96FFFE test byte ptr ds:7FFE02D4h, 2 // KUSER_SHARED_DATA.KdDebuggerEnabled
    .text:7C970005 jz short loc_7C970027
    .text:7C970007
    .text:7C970007 loc_7C970007:
    .text:7C970007 // __try { // __except at loc_7C970020
    .text:7C970007 and [ebp+ms_exc.registration.TryLevel], 0
    .text:7C97000B call _DbgBreakPoint@0 // DbgBreakPoint()
    .text:7C970010 jmp short loc_7C970023
    .text:7C970010 // } // starts at 7C970007
    .text:7C970010 // } // starts at 7C96FFE3
    .text:7C970010 _DbgUiRemoteBreakin@4 endp
    ...
    .text:7C970023
    .text:7C970023 loc_7C970023:
    .text:7C970023 // __unwind { // __SEH_prolog
    .text:7C970023 or [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
    .text:7C970027
    .text:7C970027 loc_7C970027:
    .text:7C970027 push 0
    .text:7C970029 call _RtlExitUserThread@4 // RtlExitUserThread(x)
    .text:7C97002E nop
    .text:7C97002F nop

    当被调试进程中的被插入执行 DbgUiRemoteBreakin 时,会判断 PEB.BeingDebugged == NULL && KdDebuggerEnabled != 2 时就会执行 DbgBreakPoint 从而触发断点断到调试器中,此时就出现了本章一开始贴的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    (360.bd0): Break instruction exception - code 80000003 (first chance)
    *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\SYSTEM32\ntdll.dll -
    ntdll!DbgBreakPoint:
    00000000`77c4b1d0 cc int 3
    0:001> k
    Child-SP RetAddr Call Site
    00000000`0027fc78 00000000`77cd93c8 ntdll!DbgBreakPoint
    00000000`0027fc80 00000000`77c33865 ntdll!DbgUiRemoteBreakin+0x38
    00000000`0027fcb0 00000000`00000000 ntdll!RtlUserThreadStart+0x25

总结:在 NtDebugActiveProcess 成功返回后,DbgUiDebugActiveProccss 会调用 DbgUiIssueRemoteBreakin,目的是在远程进程中创建远程中断线程,使被调试进程中断到调试器中。以上操作都成功后,DebugActiveProcess 会返回真,通知调用进程已经成功建立调试对话。
接下来调试器便进入调试事件循环,开始接收和处理调试事件了。它首先会接收到一系列杜撰的调试事件,包括进程创建、模块加载等事件。然后收到远程中断线程产生的断点事件。调试器收到这一事件后,通常会停下来报告给用户。

DebugActiveProcess的副本.png

4 异常与调试

Windows XP 异常处理(二)异常分发中详细分析过,KernelMode/UserMode 产生的异常在 KiDispatchException 中都会有两次异常分发的机会,本节只针对 UserMode 的异常在有用户调试器的情况下进行分析。

  1. 第一轮分发:在 KiDispatchException 中先判断 KiDebugRoutine == NULL?,内核的调试不进行处理或处理失败,会直接调用 DbgkForwardException 向用户调试器的调试端口发送异常信息。DbgkForwardException 判断如果 DebugPort == NULL 则直接返回(用户调试器不存在),否则调用 DbgkpSendApiMessage 发送异常调试消息,然后使进程被挂起。
  2. 顶层异常处理(最后一道防线):当用户第一次异常分发到用户时,如果用户没有注册相应的 VEH/SEH,则系统会调用顶层的异常处理。顶层异常处理的过滤器函数 UnhandledExceptionFilter,该函数会调用 NtQueryInformationProcess 来查询程序是否正在被调试,如果是则将异常返回到 0 环进行第二轮分发,如果不在被调试则会判断 kernel32!BaseCurrentTopLevelFilter 链上是否有 SetUnhandledExceptionFilter 注册的顶层异常处理函数。如果有则直接调用,否则就会弹出对话框让用户选择终止程序/启动即时调试器。
  3. 第二轮分发:对于用户异常,如果没有调试器是不会进行第二轮分发的。

5.1 实验:第一轮分发

本节写一个程序并通过编译器注册 SEH,分别在有/无调试器的状态下进行对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "stdafx.h"

int main(int argc, char* argv[])
{
int x = 100;
int y = 0;
int z;

_try{
z = x/y;
printf("无法执行的代码!\n");
}
_except(1){
printf("SEH异常处理代码!\n");
}
getchar();
return 0;
}
  1. 在不被调试的状态下,程序输出”SEH异常处理代码!“。

  2. 在被调试的状态下,程序首先断在初始断点处(注意该实验要用原版OllyDbg),然后 F9 运行,程序将会断在如下位置,无论如何按F9,都无法继续执行。原因是KiDispatchException检测到了调试器的存在,因此会先交予调试器处理。

    7.png

    修改方法有很多,可以修改堆栈上局部变量的除数(此时修改ECX寄存器无效,因为这个不是异常结构里Context.Ecx)。修改除数为1:

    8.png

    然后 F7 单步运行如下(此时EIP已经指向下一行了):

    9.png

    F9 运行后输出 ”无法执行的代码!”。

  3. 如果要让 OllyDbg 忽略这些异常,可以设置:Option-Debugging Option-Exceptions,勾选上整数除0异常。

    10.png

    此时重新运行程序就不会被调试器断在除 0 异常这里了。

4.2 实验:顶层异常处理与第二轮分发

将上一节的 _except(1) 修改为 _except(0)(本异常处理块不处理该该异常,寻找其他异常处理块(不是异常处理器)),让其去找顶层处理的 SEH,同时 OllyDbg 设置“忽略整数除0异常”。修改完后,可以看到,在忽略除零异常的情况下,还是会断在 idiv这里,这是因为在第一次分发时调试器没有处理,然后分发到用户层,用户注册的SEH让找其他SEH,最后找到顶层异常处理的SEH,顶层过滤器检测到此时存在调试器,于是又发送了一次异常调试事件给调试器,即第二轮分发。如果这次还不处理,那么便会终止进程。

4.3 反调试:使用顶层处理

方法一原理:在程序中使用 SetUnhandledExceptionFilterkernel32!BaseCurrentTopLevelFilter 链表注册一个顶层异常处理函数,然后在程序中故意触发一个异常,如果异常分发的过程顶层的异常处理函数得不到执行的话,此时就认为程序正在被调试了。

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
#include "stdafx.h"
#include "windows.h"

long _stdcall callback(_EXCEPTION_POINTERS* excp)
{
excp->ContextRecord->Ecx = 1;
return EXCEPTION_CONTINUE_EXECUTION;
}

int main(int argc, char* argv[])
{
//注册一个最顶层异常处理函数
SetUnhandledExceptionFilter(callback);

//除0异常
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,0x10
idiv ecx //edx <- (eax/ecx)的余数,EDX:EAX 除以 ECX
}

//程序正常执行
printf("程序执行");

getchar();
return 0;
}

方法二原理:除了上面这种利用顶层处理函数的机制进行反调试的手段,还有一种,也是比较常规的,就是进程不断的给调试器发送异常调试事件,调试器也无法区分哪个调试事件是有用的,会一并接收,从而达到反调试的目的。

方法三:可移植性好,基于行为的方式:线程相互心跳检测。就是每个线程定时向其它线程发心跳,超过多少次超时就认为有调试行为。借鉴《由一道CTF对10种反调试的探究-评论区》。

4.4 异常处理流程回顾

异常处理流程2.png

5 回复调试消息

当调试器对调试消息进行处理后,因为当前的被调试进程还一直处于挂起的等待状态,调试器需要对消息处理结果进行反馈,并且还会告知要不要继续读取调试对象消息队列队头的消息。

调试消息回复调用 ContinueDebugEvent 函数:

1
2
3
4
5
BOOL __stdcall ContinueDebugEvent(
DWORD dwProcessId, //被调试线程所属的进程ID(DEBUG_EVENT中的PID)
DWORD dwThreadId, //被调试线程ID(DEBUG_EVENT中的TID)
DWORD dwContinueStatus //返回的当前调试消息的处理情况
)

关于参数 dwContinueStatus 可以取如下三个取值:

  1. DBG_CONTINUE(0x00010002):对于 EXCEPTION_DEBUG_EVENT 异常事件,表示当前异常已完成处理。
  2. DBG_EXCEPTION_NOT_HANDLED(0x80010001):对于 EXCEPTION_DEBUG_EVENT 异常事件,如果是第一次异常分发则会让异常分发到用户态去,如果是第二次异常分发,则会将被调试进程。
  3. DBG_REPLY_LATER(0x40010001L):此标志在Windows 10版本1507或更高版本中支持,让被调试进程对该异常消息重发一次。

对于非异常的消息,返回 DBG_CONTINUE/DBG_EXCEPTION_NOT_HANDLED 没有差异,都会让被调试线程继续运行。

返回值:如果成功返回非零,失败则返回零。要获取扩展错误信息,请调用GetLastError

5.1 ContinueDebugEvent调用流程

函数 ContinueDebugEvent 会调用 ntdll!DbgUiContinue-->ntdll!NtDebugContinue 进入 0 环。

  1. 在 0 环找到已经被处理的位于队列头的调试消息,然后将其摘除。

  2. 如下两次设置信号,分别让调试器读取下一条调试消息,让被调试进程获得信号,恢复进程。

    1
    2
    KeSetEvent(&DebugObject->EventsPresent, 0, FALSE);
    KeSetEvent(&DebugEvent->ContinueEvent, 0, FALSE);
  3. 现在整体来看一下,DebugObject->EventsPresentDebugEvent->ContinueEvent 信号的设置情况。

    1. DbgkpQueueMessage 发送消息时:

      1
      2
      KeSetEvent(&DebugObject->EventsPresent, 0, 0)
      KeWaitForSingleObject(&DbgkmDebugEvent->ContinueEvent, Executive, KernelMode, FALSE, NULL);
    2. WaitForDebugEvent 调试器取消息时:

      1
      KeWaitForSingleObject(&DebugObject->EventsPresent,x,x,x,x)
    3. ContinueDebugEvent 回复消息时:

      1
      2
      KeSetEvent(&DebugObject->EventsPresent, 0, FALSE);
      KeSetEvent(&DebugEvent->ContinueEvent, 0, FALSE);

备注:不管是取消息然后去处理,还是回复消息,都是一次只能从消息队列上处理一条消息。不是批量处理的。

ContinueDebugEvent.png

5.2 NtDebugContinue逆向分析

nt!NtDebugContinue源代码逆向分析:

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
PAGE:0056C0A8 // =============== S U B R O U T I N E =======================================
PAGE:0056C0A8
PAGE:0056C0A8 // NTSTATUS NtDebugContinue (
PAGE:0056C0A8 // IN HANDLE DebugObjectHandle, //调试对象句柄
PAGE:0056C0A8 // IN PCLIENT_ID ClientId, //被调试线程CID
PAGE:0056C0A8 // IN NTSTATUS ContinueStatus //报告当前调试消息的处理情况
PAGE:0056C0A8 // )
PAGE:0056C0A8 // Attributes: bp-based frame
PAGE:0056C0A8
PAGE:0056C0A8 // NTSTATUS __stdcall NtDebugContinue(HANDLE DebugObject, PCLIENT_ID AppClientId, NTSTATUS ContinueStatus)
PAGE:0056C0A8 _NtDebugContinue@12 proc near // DATA XREF: .text:0042D538↑o
PAGE:0056C0A8
PAGE:0056C0A8 var_PID = dword ptr -34h
PAGE:0056C0A8 var_TID = dword ptr -30h
PAGE:0056C0A8 var_2C = dword ptr -2Ch
PAGE:0056C0A8 arv_DebugObject = dword ptr -28h
PAGE:0056C0A8 AccessMode = byte ptr -24h
PAGE:0056C0A8 var_20 = dword ptr -20h
PAGE:0056C0A8 var_19 = byte ptr -19h
PAGE:0056C0A8 ms_exc = CPPEH_RECORD ptr -18h
PAGE:0056C0A8 DebugObject = dword ptr 8
PAGE:0056C0A8 AppClientId = dword ptr 0Ch
PAGE:0056C0A8 ContinueStatus = dword ptr 10h
PAGE:0056C0A8
PAGE:0056C0A8 // __unwind { // __SEH_prolog
PAGE:0056C0A8 push 24h
PAGE:0056C0AA push offset stru_409EF8
PAGE:0056C0AF call __SEH_prolog
PAGE:0056C0B4 mov eax, large fs:_KPCR.PrcbData.CurrentThread
PAGE:0056C0BA mov al, [eax+_ETHREAD.Tcb.PreviousMode]
PAGE:0056C0C0 mov [ebp+AccessMode], al
PAGE:0056C0C3 xor ebx, ebx // ebx = 0
PAGE:0056C0C5 // __try { // __except at loc_56C1D9
PAGE:0056C0C5 mov [ebp+ms_exc.registration.TryLevel], ebx
PAGE:0056C0C8 mov ecx, [ebp+AppClientId]
PAGE:0056C0CB test al, al
PAGE:0056C0CD jz short loc_56C0DA
PAGE:0056C0CF mov eax, _MmUserProbeAddress // 对3环的PCLIENT_ID地址进行校验
PAGE:0056C0D4 cmp ecx, eax
PAGE:0056C0D6 jb short loc_56C0DA
PAGE:0056C0D8 mov [eax], ebx
PAGE:0056C0DA
PAGE:0056C0DA loc_56C0DA: // CODE XREF: NtDebugContinue(x,x,x)+25↑j
PAGE:0056C0DA // NtDebugContinue(x,x,x)+2E↑j
PAGE:0056C0DA mov eax, [ecx+CLIENT_ID.UniqueProcess]
PAGE:0056C0DC mov [ebp+var_PID], eax
PAGE:0056C0DF mov eax, [ecx+CLIENT_ID.UniqueThread]
PAGE:0056C0E2 mov [ebp+var_TID], eax
PAGE:0056C0E2 // } // starts at 56C0C5
PAGE:0056C0E5 or [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
PAGE:0056C0E9 mov eax, [ebp+ContinueStatus]
PAGE:0056C0EC cmp eax, 80010001h // DBG_EXCEPTION_NOT_HANDLED,调试器没有处理该异常,继续往下分发异常
PAGE:0056C0F1 jz short loc_56C119 // 继续处理
PAGE:0056C0F3 cmp eax, 10000h
PAGE:0056C0F8 jle short loc_56C10F // STATUS_INVALID_PARAMETER, return C000000Dh
PAGE:0056C0FA cmp eax, 10002h // DBG_CONTINUE
PAGE:0056C0FF jle short loc_56C119 // 继续处理
PAGE:0056C101 cmp eax, 40010002h // DBG_UNABLE_TO_PROVIDE_HANDLE
PAGE:0056C106 jle short loc_56C10F // STATUS_INVALID_PARAMETER, return C000000Dh
PAGE:0056C108 cmp eax, 40010004h // DBG_TERMINATE_PROCESS
PAGE:0056C10D jle short loc_56C119 // 继续处理
PAGE:0056C10F
PAGE:0056C10F loc_56C10F: // CODE XREF: NtDebugContinue(x,x,x)+50↑j
PAGE:0056C10F // NtDebugContinue(x,x,x)+5E↑j
PAGE:0056C10F mov eax, 0C000000Dh // STATUS_INVALID_PARAMETER, return C000000Dh
PAGE:0056C114 jmp loc_56C1E3 // 直接返回
PAGE:0056C119 // ---------------------------------------------------------------------------
PAGE:0056C119
PAGE:0056C119 loc_56C119: // CODE XREF: NtDebugContinue(x,x,x)+49↑j
PAGE:0056C119 // NtDebugContinue(x,x,x)+57↑j ...
PAGE:0056C119 push ebx // HandleInformation
PAGE:0056C11A lea eax, [ebp+arv_DebugObject]
PAGE:0056C11D push eax // Object
PAGE:0056C11E push dword ptr [ebp+AccessMode] // AccessMode
PAGE:0056C121 push _DbgkDebugObjectType // ObjectType
PAGE:0056C127 push 1 // DesiredAccess
PAGE:0056C129 push [ebp+DebugObject] // Handle
PAGE:0056C12C call _ObReferenceObjectByHandle@24 // NTSTATUS ObReferenceObjectByHandle (
PAGE:0056C12C // IN HANDLE Handle,
PAGE:0056C12C // IN ACCESS_MASK DesiredAccess,
PAGE:0056C12C // IN POBJECT_TYPE ObjectType OPTIONAL,
PAGE:0056C12C // IN KPROCESSOR_MODE AccessMode,
PAGE:0056C12C // OUT PVOID *Object,
PAGE:0056C12C // OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL
PAGE:0056C12C // )
PAGE:0056C131 mov [ebp+var_20], eax
PAGE:0056C134 cmp eax, ebx
PAGE:0056C136 jl loc_56C1E3 // 直接返回
PAGE:0056C13C mov [ebp+var_19], 0
PAGE:0056C140 mov edi, [ebp+arv_DebugObject]
PAGE:0056C143 lea ecx, [edi+DEBUG_OBJECT.Mutex] // FastMutex
PAGE:0056C146 call ds:__imp_@ExAcquireFastMutex@4 // ExAcquireFastMutex(x)
PAGE:0056C14C lea esi, [edi+DEBUG_OBJECT.EventList] // -----
PAGE:0056C14C // esi = &DEBUG_OBJECT.EventList
PAGE:0056C14C // eax = DEBUG_OBJECT.EventList.Flink
PAGE:0056C14C // -----
PAGE:0056C14F mov eax, [esi]
PAGE:0056C151 jmp short loc_56C181
PAGE:0056C153 // ---------------------------------------------------------------------------
PAGE:0056C153
PAGE:0056C153 loc_56C153: // CODE XREF: NtDebugContinue(x,x,x)+DB↓j
PAGE:0056C153 mov ecx, [ebp+var_PID] // 消息链表不为空
PAGE:0056C156 cmp [eax+DBGKM_DEBUG_EVENT.ClientId.UniqueProcess], ecx
PAGE:0056C159 jnz short loc_56C17F // 继续取链表上下一条调试消息
PAGE:0056C15B cmp [ebp+var_19], 0
PAGE:0056C15F jnz short loc_56C187 // ========================================================================
PAGE:0056C15F // 这里的设置很巧妙,此处摘除该消息,然后调用KeSetEvent(&DebugObject->EventsPresent,x,x)
PAGE:0056C15F // 通知调试器KeWaitForSingleObject(&DebugObject->EventsPresent,x,x,x,x),让其读取这条消息。
PAGE:0056C15F // 但是目前还没有看到发送消息时的
PAGE:0056C15F // KeWaitForSingleObject(&DbgkmDebugEvent->ContinueEvent, Executive, KernelMode, FALSE, NULL)
PAGE:0056C15F // 在哪释放信号
PAGE:0056C15F // ========================================================================
PAGE:0056C161 mov ecx, [ebp+var_TID]
PAGE:0056C164 cmp [eax+DBGKM_DEBUG_EVENT.ClientId.UniqueThread], ecx
PAGE:0056C167 jnz short loc_56C17F // 继续取链表上下一条调试消息
PAGE:0056C169 test byte ptr [eax+DBGKM_DEBUG_EVENT.Flags], 1 // 这个Flags表示调试消息是否已经被读取了
PAGE:0056C16D jz short loc_56C17F // 继续取链表上下一条调试消息
PAGE:0056C16F mov ecx, [eax+DBGKM_DEBUG_EVENT.EventList.Flink] // 将当前消息摘除
PAGE:0056C171 mov edx, [eax+DBGKM_DEBUG_EVENT.EventList.Blink]
PAGE:0056C174 mov [edx], ecx
PAGE:0056C176 mov [ecx+4], edx
PAGE:0056C179 mov ebx, eax // ebx = 摘除的消息
PAGE:0056C17B mov [ebp+var_19], 1
PAGE:0056C17F
PAGE:0056C17F loc_56C17F: // CODE XREF: NtDebugContinue(x,x,x)+B1↑j
PAGE:0056C17F // NtDebugContinue(x,x,x)+BF↑j ...
PAGE:0056C17F mov eax, [eax] // 继续取链表上下一条调试消息
PAGE:0056C181
PAGE:0056C181 loc_56C181: // CODE XREF: NtDebugContinue(x,x,x)+A9↑j
PAGE:0056C181 cmp eax, esi
PAGE:0056C183 jnz short loc_56C153 // 消息链表不为空
PAGE:0056C185 jmp short loc_56C195
PAGE:0056C187 // ---------------------------------------------------------------------------
PAGE:0056C187
PAGE:0056C187 loc_56C187: // CODE XREF: NtDebugContinue(x,x,x)+B7↑j
PAGE:0056C187 and [eax+DBGKM_DEBUG_EVENT.Flags], 0FFFFFFFBh // ========================================================================
PAGE:0056C187 // 这里的设置很巧妙,此处摘除该消息,然后调用KeSetEvent(&DebugObject->EventsPresent,x,x)
PAGE:0056C187 // 通知调试器KeWaitForSingleObject(&DebugObject->EventsPresent,x,x,x,x),让其读取这条消息。
PAGE:0056C187 // 但是目前还没有看到发送消息时的
PAGE:0056C187 // KeWaitForSingleObject(&DbgkmDebugEvent->ContinueEvent, Executive, KernelMode, FALSE, NULL)
PAGE:0056C187 // 在哪释放信号
PAGE:0056C187 // ========================================================================
PAGE:0056C18B push 0 // Wait
PAGE:0056C18D push 0 // Increment
PAGE:0056C18F push edi // DEBUG_OBJECT.EventsPresent
PAGE:0056C190 call _KeSetEvent@12 // KeSetEvent(x,x,x)
PAGE:0056C195
PAGE:0056C195 loc_56C195: // CODE XREF: NtDebugContinue(x,x,x)+DD↑j
PAGE:0056C195 lea ecx, [edi+DEBUG_OBJECT.Mutex] // FastMutex
PAGE:0056C198 call ds:__imp_@ExReleaseFastMutex@4 // ExReleaseFastMutex(x)
PAGE:0056C19E mov ecx, edi // Object
PAGE:0056C1A0 call @ObfDereferenceObject@4 // --
PAGE:0056C1A0 // 减少指定对象的对象引用计数,并在计数变为零时进行清理工作。
PAGE:0056C1A0 // 不同于句柄引用计数
PAGE:0056C1A0 // --
PAGE:0056C1A5 cmp [ebp+var_19], 0 // 此时var_19 = 1
PAGE:0056C1A9 jz short loc_56C1BD
PAGE:0056C1AB mov eax, [ebp+ContinueStatus] // ebx == 摘除的消息结构
PAGE:0056C1AE mov [ebx+DBGKM_DEBUG_EVENT.ApiMsg.ReturnedStatus], eax // 调试器返回的状态
PAGE:0056C1B1 and [ebx+DBGKM_DEBUG_EVENT.Status], 0 // 对调试事件的处理结果,STATUS_SUCCESS
PAGE:0056C1B5 push ebx // DbgkmDebugEvent
PAGE:0056C1B6 call _DbgkpWakeTarget@4 // VOID DbgkpWakeTarget (
PAGE:0056C1B6 // IN PDBGKM_DEBUG_EVENT DbgkmDebugEvent // 被摘除的消息
PAGE:0056C1B6 // )
PAGE:0056C1BB jmp short loc_56C1C4
PAGE:0056C1BD // ---------------------------------------------------------------------------
PAGE:0056C1BD
PAGE:0056C1BD loc_56C1BD: // CODE XREF: NtDebugContinue(x,x,x)+101↑j
PAGE:0056C1BD mov [ebp+var_20], 0C000000Dh
PAGE:0056C1C4
PAGE:0056C1C4 loc_56C1C4: // CODE XREF: NtDebugContinue(x,x,x)+113↑j
PAGE:0056C1C4 mov eax, [ebp+var_20]
PAGE:0056C1C7 jmp short loc_56C1E3 // 直接返回
PAGE:0056C1C9 // ---------------------------------------------------------------------------
PAGE:0056C1C9
PAGE:0056C1C9 loc_56C1C9: // DATA XREF: .text:stru_409EF8↑o
PAGE:0056C1C9 // __except filter // owned by 56C0C5
PAGE:0056C1C9 mov eax, [ebp+ms_exc.exc_ptr]
PAGE:0056C1CC mov eax, [eax]
PAGE:0056C1CE mov eax, [eax]
PAGE:0056C1D0 mov [ebp+var_2C], eax
PAGE:0056C1D3 call _ExSystemExceptionFilter@0 // ExSystemExceptionFilter()
PAGE:0056C1D8 retn
PAGE:0056C1D9 // ---------------------------------------------------------------------------
PAGE:0056C1D9
PAGE:0056C1D9 loc_56C1D9: // DATA XREF: .text:stru_409EF8↑o
PAGE:0056C1D9 // __except(loc_56C1C9) // owned by 56C0C5
PAGE:0056C1D9 mov esp, [ebp+ms_exc.old_esp]
PAGE:0056C1DC or [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
PAGE:0056C1E0 mov eax, [ebp+var_2C]
PAGE:0056C1E3
PAGE:0056C1E3 loc_56C1E3: // CODE XREF: NtDebugContinue(x,x,x)+6C↑j
PAGE:0056C1E3 // NtDebugContinue(x,x,x)+8E↑j ...
PAGE:0056C1E3 call __SEH_epilog // 直接返回
PAGE:0056C1E8 retn 0Ch
PAGE:0056C1E8 // } // starts at 56C0A8
PAGE:0056C1E8 _NtDebugContinue@12 endp

nt!DbgkpWakeTarget源代码逆向分析:

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
PAGE:0056BB16 // =============== S U B R O U T I N E =======================================
PAGE:0056BB16
PAGE:0056BB16 // VOID DbgkpWakeTarget (
PAGE:0056BB16 // IN PDBGKM_DEBUG_EVENT DbgkmDebugEvent // 被摘除的消息
PAGE:0056BB16 // )
PAGE:0056BB16 // Attributes: bp-based frame
PAGE:0056BB16
PAGE:0056BB16 // int __stdcall DbgkpWakeTarget(PVOID DbgkmDebugEvent)
PAGE:0056BB16 _DbgkpWakeTarget@4 proc near // CODE XREF: DbgkpCloseObject(x,x,x,x,x)+CB↓p
PAGE:0056BB16 // DbgkClearProcessDebugObject(x,x)+C1↓p ...
PAGE:0056BB16
PAGE:0056BB16 DbgkmDebugEvent = dword ptr 8
PAGE:0056BB16
PAGE:0056BB16 mov edi, edi
PAGE:0056BB18 push ebp
PAGE:0056BB19 mov ebp, esp
PAGE:0056BB1B push esi
PAGE:0056BB1C mov esi, [ebp+DbgkmDebugEvent]
PAGE:0056BB1F test byte ptr [esi+DBGKM_DEBUG_EVENT.Flags], 20h // DEBUG_EVENT_SUSPEND,该标志很奇怪,
PAGE:0056BB1F // 不是挂起线程,而是恢复运行线程
PAGE:0056BB23 push edi
PAGE:0056BB24 mov edi, [esi+DBGKM_DEBUG_EVENT.Thread]
PAGE:0056BB27 jz short loc_56BB31 // DEBUG_EVENT_RELEASE
PAGE:0056BB29 push 0
PAGE:0056BB2B push edi
PAGE:0056BB2C call _PsResumeThread@8 // ----
PAGE:0056BB2C // NTSTATUS PsResumeThread (
PAGE:0056BB2C // IN PETHREAD Thread,
PAGE:0056BB2C // OUT PULONG PreviousSuspendCount OPTIONAL
PAGE:0056BB2C // )
PAGE:0056BB2C // ----
PAGE:0056BB31
PAGE:0056BB31 loc_56BB31: // CODE XREF: DbgkpWakeTarget(x)+11↑j
PAGE:0056BB31 test byte ptr [esi+DBGKM_DEBUG_EVENT.Flags], 8 // DEBUG_EVENT_RELEASE
PAGE:0056BB35 jz short loc_56BB42 // DEBUG_EVENT_NOWAIT
PAGE:0056BB37 lea ecx, [edi+_ETHREAD.RundownProtect]
PAGE:0056BB3D call @ExReleaseRundownProtection@4 // ExReleaseRundownProtection(x)
PAGE:0056BB42
PAGE:0056BB42 loc_56BB42: // CODE XREF: DbgkpWakeTarget(x)+1F↑j
PAGE:0056BB42 test byte ptr [esi+DBGKM_DEBUG_EVENT.Flags], 2 // DEBUG_EVENT_NOWAIT
PAGE:0056BB46 jnz short loc_56BB57 // 异步消息
PAGE:0056BB48 push 0 // Wait
PAGE:0056BB4A push 0 // Increment
PAGE:0056BB4C add esi, 8
PAGE:0056BB4F push esi // DBGKM_DEBUG_EVENT.ContinueEvent
PAGE:0056BB50 call _KeSetEvent@12 // KeSetEvent(x,x,x)
PAGE:0056BB55 jmp short loc_56BB5D
PAGE:0056BB57 // ---------------------------------------------------------------------------
PAGE:0056BB57
PAGE:0056BB57 loc_56BB57: // CODE XREF: DbgkpWakeTarget(x)+30↑j
PAGE:0056BB57 push esi // P
PAGE:0056BB58 call _DbgkpFreeDebugEvent@4 // DbgkpFreeDebugEvent(x)
PAGE:0056BB5D
PAGE:0056BB5D loc_56BB5D: // CODE XREF: DbgkpWakeTarget(x)+3F↑j
PAGE:0056BB5D pop edi
PAGE:0056BB5E pop esi
PAGE:0056BB5F pop ebp
PAGE:0056BB60 retn 4
PAGE:0056BB60 _DbgkpWakeTarget@4 endp