在 Windows 用户层调试 API 中,DebugActiveProcess 是一个非常典型的入口函数。
从名字上看,它的作用似乎很直接:附加并调试一个已经存在的进程。
但如果顺着它的实现继续往下分析,就会发现,DebugActiveProcess 本身并不负责完成全部调试工作。
它更像是一个用户层的包装函数,主要完成三件事:
-
先检查当前线程是否已经保存了调试对象句柄;如果没有,就创建一个
_DEBUG_OBJECT -
再根据 PID 打开目标进程,取得目标进程句柄
-
最后调用更底层的函数,去执行真正的调试附加
本文先分析第一部分,也就是:
c
kernelbase!DebugActiveProcess
-> ntdll!DbgUiConnectToDbg
-> nt!NtCreateDebugObject
这一篇先把_DEBUG_OBJECT 是如何创建出来的 讲清楚。
至于真正把目标进程挂到调试对象上的过程,也就是 DbgUiDebugActiveProcess 的逻辑,放到下一篇再分析。
一、先看 DebugActiveProcess 的整体流程
先看 kernelbase!DebugActiveProcess 的整体逻辑。
c
.text:0000000078D86BD0 ; BOOL __stdcall DebugActiveProcess(DWORD dwProcessId)
.text:0000000078D86BD0 public DebugActiveProcess
.text:0000000078D86BD0 DebugActiveProcess proc near ; DATA XREF: .rdata:off_78DC0024↓o
.text:0000000078D86BD0 ; .pdata:0000000078E33758↓o ...
.text:0000000078D86BD0
.text:0000000078D86BD0 arg_0 = qword ptr 8
.text:0000000078D86BD0
.text:0000000078D86BD0 mov [rsp+arg_0], rbx
.text:0000000078D86BD5 push rdi
.text:0000000078D86BD6 sub rsp, 20h
.text:0000000078D86BDA mov ebx, ecx ; PID
.text:0000000078D86BDC call DbgUiConnectToDbg_0 ; 创建调试对象
.text:0000000078D86BE1 test eax, eax
.text:0000000078D86BE3 jns short loc_78D86BF0 ; 创建成功就跳转
.text:0000000078D86BE5 mov ecx, eax
.text:0000000078D86BE7
.text:0000000078D86BE7 loc_78D86BE7: ; CODE XREF: DebugActiveProcess+48↓j
.text:0000000078D86BE7 call BaseSetLastNTError ; 设置错误码,然后返回
.text:0000000078D86BEC
.text:0000000078D86BEC loc_78D86BEC: ; CODE XREF: DebugActiveProcess+2D↓j
.text:0000000078D86BEC xor eax, eax
.text:0000000078D86BEE jmp short loc_78D86C25
.text:0000000078D86BF0 ; ---------------------------------------------------------------------------
.text:0000000078D86BF0
.text:0000000078D86BF0 loc_78D86BF0: ; CODE XREF: DebugActiveProcess+13↑j
.text:0000000078D86BF0 mov ecx, ebx
.text:0000000078D86BF2 call ProcessIdToHandle ; 打开一个进程
.text:0000000078D86BF7 mov rbx, rax ; 返回进程句柄
.text:0000000078D86BFA test rax, rax
.text:0000000078D86BFD jz short loc_78D86BEC ; 如果进程句柄为0,就跳转返回
.text:0000000078D86BFF mov rcx, rax
.text:0000000078D86C02 call DbgUiDebugActiveProcess_0
.text:0000000078D86C07 mov rcx, rbx ; Handle
.text:0000000078D86C0A mov edi, eax
.text:0000000078D86C0C test eax, eax
.text:0000000078D86C0E jns short loc_78D86C1A
.text:0000000078D86C10 call cs:__imp_NtClose ; 关闭进程句柄
.text:0000000078D86C16 mov ecx, edi
.text:0000000078D86C18 jmp short loc_78D86BE7
.text:0000000078D86C1A ; ---------------------------------------------------------------------------
.text:0000000078D86C1A
.text:0000000078D86C1A loc_78D86C1A: ; CODE XREF: DebugActiveProcess+3E↑j
.text:0000000078D86C1A call cs:__imp_NtClose
.text:0000000078D86C20 mov eax, 1
.text:0000000078D86C25
.text:0000000078D86C25 loc_78D86C25: ; CODE XREF: DebugActiveProcess+1E↑j
.text:0000000078D86C25 mov rbx, [rsp+28h+arg_0]
.text:0000000078D86C2A add rsp, 20h
.text:0000000078D86C2E pop rdi
.text:0000000078D86C2F retn
.text:0000000078D86C2F ; ---------------------------------------------------------------------------
.text:0000000078D86C30 align 20h
.text:0000000078D86C30 DebugActiveProcess endp
从反汇编可以整理出如下伪代码:
c
BOOL DebugActiveProcess(DWORD pid)
{
NTSTATUS status;
HANDLE hProcess;
status = DbgUiConnectToDbg();
if (!NT_SUCCESS(status))
{
BaseSetLastNTError(status);
return FALSE;
}
hProcess = ProcessIdToHandle(pid);
if (!hProcess)
return FALSE;
status = DbgUiDebugActiveProcess(hProcess);
NtClose(hProcess);
if (!NT_SUCCESS(status))
{
BaseSetLastNTError(status);
return FALSE;
}
return TRUE;
}
从这个伪代码可以看出,DebugActiveProcess 的主线其实非常清楚:
-
先调用 DbgUiConnectToDbg
-
再根据 PID 打开目标进程
-
然后调用 DbgUiDebugActiveProcess 执行真正附加
-
最后关闭临时打开的进程句柄
所以,DebugActiveProcess 并不是"直接调试目标进程",而是:
先把调试器这一侧需要的调试对象准备好,再去附加目标进程。
二、DbgUiConnectToDbg:检查并准备调试对象句柄
在 DebugActiveProcess中,第一步调用的是 ntdll!DbgUiConnectToDbg。
这个函数,先检查当前线程是否已经保存了调试对象句柄;如果没有,再调用 NtCreateDebugObject 创建一个。
先看它的反汇编:
c
.text:0000000078F01DA0 DbgUiConnectToDbg proc near ; DATA XREF: .text:off_78F64078↓o
.text:0000000078F01DA0 ; .pdata:0000000078F8EFBC↓o
.text:0000000078F01DA0
.text:0000000078F01DA0 var_38 = dword ptr -38h
.text:0000000078F01DA0 var_20 = dword ptr -20h
.text:0000000078F01DA0
.text:0000000078F01DA0 mov r11, rsp
.text:0000000078F01DA3 sub rsp, 58h
.text:0000000078F01DA7 mov rax, gs:_TEB.NtTib.Self
.text:0000000078F01DB0 xor ecx, ecx
.text:0000000078F01DB2 cmp [rax+16A8h], rcx ; 当前线程在 TEB 中保存的调试对象句柄位置
.text:0000000078F01DB9 jnz short loc_78F01DFD ; 如果不为0,说明当前线程已经有可用的_DEBUG_OBJECT句柄,
.text:0000000078F01DB9 ; 直接返回STATUS_SUCCESS == 0
.text:0000000078F01DBB mov [rsp+58h+var_38], 30h ; '0' ; 初始化 _OBJECT_ATTRIBUTES结构
.text:0000000078F01DC3 mov [r11-30h], rcx
.text:0000000078F01DC7 mov [rsp+58h+var_20], ecx
.text:0000000078F01DCB mov [r11-28h], rcx
.text:0000000078F01DCF mov [r11-18h], rcx
.text:0000000078F01DD3 mov [r11-10h], rcx
.text:0000000078F01DD7 mov rcx, gs:_TEB.NtTib.Self
.text:0000000078F01DE0 lea r8, [r11-38h] ; 参数3:POBJECT_ATTRIBUTE
.text:0000000078F01DE4 mov r9d, 1 ; 参数4:Flags
.text:0000000078F01DEA add rcx, 16A8h ; 参数1:DebugHandle,保存调试对象句柄的位置
.text:0000000078F01DF1 mov edx, 1F000Fh ; 参数2:DesiredAccess
.text:0000000078F01DF6 call NtCreateDebugObject
.text:0000000078F01DFB mov ecx, eax
.text:0000000078F01DFD
.text:0000000078F01DFD loc_78F01DFD: ; CODE XREF: DbgUiConnectToDbg+19↑j
.text:0000000078F01DFD mov eax, ecx
.text:0000000078F01DFF add rsp, 58h
.text:0000000078F01E03 retn
.text:0000000078F01E03 DbgUiConnectToDbg endp
1. 先检查当前线程是否已经有调试对象句柄
先检查当前线程是否已经有调试对象句柄
c
.text:0000000078F01DA7 mov rax, gs:_TEB.NtTib.Self
.text:0000000078F01DB0 xor ecx, ecx
.text:0000000078F01DB2 cmp [rax+16A8h], rcx ; 当前线程在 TEB 中保存的调试对象句柄位置
.text:0000000078F01DB9 jnz short loc_78F01DFD ; 如果不为0,说明当前线程已经有可用的_DEBUG_OBJECT句柄
这里的 [TEB+0x16A8],从行为上看,就是当前线程在 TEB 中保存的调试对象句柄位置。
这段逻辑的含义是:
-
先把 ecx 清零
-
判断 [TEB+0x16A8] 是否为 0
-
如果 不为 0,说明当前线程已经保存了调试对象句柄
-
于是直接跳到结尾返回成功:STATUS_SUCCESS == 0
也就是说:
如果当前线程已经保存了调试对象句柄,就不重复创建,直接返回成功。
2. 如果还没有句柄,就初始化 _OBJECT_ATTRIBUTES
如果 [TEB+0x16A8] == 0,说明当前线程还没有调试对象句柄,于是函数继续往下走:
c
.text:0000000078F01DBB mov [rsp+58h+var_38], 30h ; '0' ; 初始化 _OBJECT_ATTRIBUTES结构
.text:0000000078F01DC3 mov [r11-30h], rcx
.text:0000000078F01DC7 mov [rsp+58h+var_20], ecx
.text:0000000078F01DCB mov [r11-28h], rcx
.text:0000000078F01DCF mov [r11-18h], rcx
.text:0000000078F01DD3 mov [r11-10h], rcx
_OBJECT_ATTRIBUTES 结构如下:
c
//0x30 bytes (sizeof)
struct _OBJECT_ATTRIBUTES
{
ULONG Length; //0x0
VOID* RootDirectory; //0x8
struct _UNICODE_STRING* ObjectName; //0x10
ULONG Attributes; //0x18
VOID* SecurityDescriptor; //0x20
VOID* SecurityQualityOfService; //0x28
};
这几条指令本质上就是在栈上手动初始化一个 _OBJECT_ATTRIBUTES 结构:
-
Length = 0x30
-
其余字段全部清零
也就是:
c
OBJECT_ATTRIBUTES ObjectAttributes;
ObjectAttributes.Length = sizeof(OBJECT_ATTRIBUTES);
ObjectAttributes.RootDirectory = NULL;
ObjectAttributes.ObjectName = NULL;
ObjectAttributes.Attributes = 0;
ObjectAttributes.SecurityDescriptor = NULL;
ObjectAttributes.SecurityQualityOfService = NULL;
3. 调用 NtCreateDebugObject
接下来是整个函数的核心:
c
.text:0000000078F01DD7 mov rcx, gs:_TEB.NtTib.Self
.text:0000000078F01DE0 lea r8, [r11-38h] ; 参数3:POBJECT_ATTRIBUTE
.text:0000000078F01DE4 mov r9d, 1 ; 参数4:Flags
.text:0000000078F01DEA add rcx, 16A8h ; 参数1:DebugHandle,保存调试对象句柄的位置
.text:0000000078F01DF1 mov edx, 1F000Fh ; 参数2:DesiredAccess
.text:0000000078F01DF6 call NtCreateDebugObject
按 x64 调用约定,这里等价于:
c
NtCreateDebugObject(
(PHANDLE)(Teb + 0x16A8), // 输出句柄地址
0x1F000F, // DesiredAccess
&ObjectAttributes, // ObjectAttributes
1 // Flags
);
也可以把整个函数整理成如下伪代码:
c
NTSTATUS DbgUiConnectToDbg(void)
{
PTEB Teb = NtCurrentTeb();
if (*(HANDLE *)((PUCHAR)Teb + 0x16A8) != 0)
return STATUS_SUCCESS;
OBJECT_ATTRIBUTES oa;
oa.Length = sizeof(OBJECT_ATTRIBUTES);
oa.RootDirectory = NULL;
oa.ObjectName = NULL;
oa.Attributes = 0;
oa.SecurityDescriptor = NULL;
oa.SecurityQualityOfService = NULL;
return NtCreateDebugObject(
(PHANDLE)((PUCHAR)Teb + 0x16A8),
0x1F000F,
&oa,
1
);
}
4. 这一节的结论
从 DbgUiConnectToDbg 的实现可以看出,这个函数的职责非常明确:
先检查当前线程的 TEB 中是否已经保存了调试对象句柄;如果已经有了,就直接返回 STATUS_SUCCESS;如果还没有,就构造
_OBJECT_ATTRIBUTES,调用 NtCreateDebugObject 创建一个新的_DEBUG_OBJECT,并把句柄直接写回当前线程的 TEB。
三、NtCreateDebugObject:真正创建内核中的调试对象
在上一节里,DbgUiConnectToDbg 最终调用了 NtCreateDebugObject。
到了这里,才真正进入内核,开始创建 _DEBUG_OBJECT。
c
PAGE:0000000140419BB0 ; NTSTATUS __stdcall NtCreateDebugObject(PHANDLE DebugHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, ULONG Flags)
PAGE:0000000140419BB0 NtCreateDebugObject proc near ; DATA XREF: .text:0000000140097880↑o
PAGE:0000000140419BB0 ; .pdata:0000000140293638↑o
PAGE:0000000140419BB0
PAGE:0000000140419BB0 var_58 = qword ptr -58h
PAGE:0000000140419BB0 ObjectBodySize = qword ptr -50h
PAGE:0000000140419BB0 var_48 = dword ptr -48h
PAGE:0000000140419BB0 var_40 = dword ptr -40h
PAGE:0000000140419BB0 var_38 = qword ptr -38h
PAGE:0000000140419BB0 var_28 = qword ptr -28h
PAGE:0000000140419BB0 DebugHandle = qword ptr -20h
PAGE:0000000140419BB0 DEBUG_OBJECT = qword ptr -18h
PAGE:0000000140419BB0 var_8 = byte ptr -8
PAGE:0000000140419BB0 arg_0 = qword ptr 8
PAGE:0000000140419BB0 arg_8 = qword ptr 10h
PAGE:0000000140419BB0
PAGE:0000000140419BB0 ; FUNCTION CHUNK AT PAGE:0000000140419D00 SIZE 00000014 BYTES
PAGE:0000000140419BB0 ; FUNCTION CHUNK AT PAGE:0000000140419D20 SIZE 00000014 BYTES
PAGE:0000000140419BB0
PAGE:0000000140419BB0 ; __unwind { // __C_specific_handler
PAGE:0000000140419BB0 mov [rsp+arg_0], rbx
PAGE:0000000140419BB5 mov [rsp+arg_8], rsi
PAGE:0000000140419BBA push rdi
PAGE:0000000140419BBB sub rsp, 70h
PAGE:0000000140419BBF mov edi, r9d
PAGE:0000000140419BC2 mov esi, edx
PAGE:0000000140419BC4 mov rbx, rcx
PAGE:0000000140419BC7 mov rax, gs:_KPCR.Prcb.CurrentThread
PAGE:0000000140419BD0 mov r10b, [rax+_ETHREAD.Tcb.PreviousMode]
PAGE:0000000140419BD7
PAGE:0000000140419BD7 loc_140419BD7: ; DATA XREF: .text:00000001401BD220↑o
PAGE:0000000140419BD7 test r10b, r10b
PAGE:0000000140419BDA jz short loc_140419BF0 ; PreviousMode == KernelMode,就直接跳过
PAGE:0000000140419BDC mov rax, cs:MmUserProbeAddress
PAGE:0000000140419BE3 cmp rcx, rax ; 无符号比较,判断PHANDLE是否超过合法用户地址范围
PAGE:0000000140419BE6 cmovnb rcx, rax ; 超过则rcx=rax
PAGE:0000000140419BEA mov rax, [rcx] ; 校验参数是否可读可写
PAGE:0000000140419BED mov [rcx], rax
PAGE:0000000140419BF0
PAGE:0000000140419BF0 loc_140419BF0: ; CODE XREF: NtCreateDebugObject+2A↑j
PAGE:0000000140419BF0 and qword ptr [rbx], 0 ; 先把输出句柄清零,方便失败路径统一处理
PAGE:0000000140419BF4
PAGE:0000000140419BF4 loc_140419BF4: ; DATA XREF: .text:00000001401BD220↑o
PAGE:0000000140419BF4 test r9d, 0FFFFFFFEh ; 校验 Flags,只允许 0 或 1
PAGE:0000000140419BFB jz short loc_140419C07 ; 如果r9d是1或者0就跳走
PAGE:0000000140419BFB ;
PAGE:0000000140419BFB ; 如果 Flags 除了 bit0 以外还有其他位被设置
PAGE:0000000140419BFB ;
PAGE:0000000140419BFB ; 就直接返回 0xC000000D
PAGE:0000000140419BFB ; 也就是STATUS_INVALID_PARAMETER
PAGE:0000000140419BFD mov eax, 0C000000Dh
PAGE:0000000140419C02 jmp loc_140419CEB
PAGE:0000000140419C07 ; ---------------------------------------------------------------------------
PAGE:0000000140419C07
PAGE:0000000140419C07 loc_140419C07: ; CODE XREF: NtCreateDebugObject+4B↑j
PAGE:0000000140419C07 lea rax, [rsp+78h+var_28]
PAGE:0000000140419C0C mov [rsp+40h], rax ; 参数9:[rsp+40h] = PDEBUG_OBJECT
PAGE:0000000140419C11 and dword ptr [rsp+38h], 0 ; 参数8:[rsp+38h] = NonPagedPoolCharge
PAGE:0000000140419C16 and dword ptr [rsp+30h], 0 ; 参数7:[rsp+30h] = PagedPoolCharge
PAGE:0000000140419C1B mov dword ptr [rsp+78h+ObjectBodySize], 68h ; 'h' ; 参数6:[rsp+28h] = ObjectBodySize
PAGE:0000000140419C23 and qword ptr [rsp+20h], 0 ; 参数5:[rsp+20h] = ParseContext
PAGE:0000000140419C29 mov r9b, r10b ; 参数4:r9 = OwnershipMode
PAGE:0000000140419C29 ; 参数3:r8 = ObjectAttributes
PAGE:0000000140419C2C mov rdx, cs:DbgkDebugObjectType ; 参数2:rdx = ObjectType
PAGE:0000000140419C33 mov cl, r10b ; 参数1:rcx = ProbeMode
PAGE:0000000140419C36 call ObCreateObject ; 创建DEBUG_OBJECT
PAGE:0000000140419C3B mov r9, [rsp+78h+var_28]
PAGE:0000000140419C40 mov [rsp+78h+DEBUG_OBJECT], r9
PAGE:0000000140419C45 test eax, eax
PAGE:0000000140419C47 js loc_140419CEB
PAGE:0000000140419C4D mov r10d, 1
PAGE:0000000140419C53 mov [r9+_DEBUG_OBJECT.Mutex.Count], r10d
PAGE:0000000140419C57 and [r9+_DEBUG_OBJECT.Mutex.Owner], 0
PAGE:0000000140419C5C and [r9+_DEBUG_OBJECT.Mutex.Contention], 0
PAGE:0000000140419C61 lea rcx, [r9+_DEBUG_OBJECT.Mutex.Event] ; Event
PAGE:0000000140419C65 xor r8d, r8d
PAGE:0000000140419C68 mov edx, r10d
PAGE:0000000140419C6B call KeInitializeEvent
PAGE:0000000140419C70 lea rcx, [r9+_DEBUG_OBJECT.EventList]
PAGE:0000000140419C74 mov [rcx+LIST_ENTRY.Blink], rcx
PAGE:0000000140419C78 mov [rcx+LIST_ENTRY.Flink], rcx
PAGE:0000000140419C7B xor edx, edx
PAGE:0000000140419C7D mov rcx, r9
PAGE:0000000140419C80 call KeInitializeEvent
PAGE:0000000140419C85 test r10b, dil
PAGE:0000000140419C88 jz short loc_140419C94
PAGE:0000000140419C8A mov [r9+_DEBUG_OBJECT.Flags], 2
PAGE:0000000140419C92 jmp short loc_140419C98
PAGE:0000000140419C94 ; ---------------------------------------------------------------------------
PAGE:0000000140419C94
PAGE:0000000140419C94 loc_140419C94: ; CODE XREF: NtCreateDebugObject+D8↑j
PAGE:0000000140419C94 and [r9+_DEBUG_OBJECT.Flags], edx
PAGE:0000000140419C98
PAGE:0000000140419C98 loc_140419C98: ; CODE XREF: NtCreateDebugObject+E2↑j
PAGE:0000000140419C98 mov rax, gs:_KPCR.Prcb.CurrentThread
PAGE:0000000140419CA1 mov rcx, [rax+70h] ; kthread._KAPC_STATE._KPROCESS
PAGE:0000000140419CA5 cmp [rcx+320h], rdx ; _ERPCOESS.Wow64Process
PAGE:0000000140419CA5 ; 判断是否为32位调试器
PAGE:0000000140419CAC jz short loc_140419CB3 ; 64位调试器跳转
PAGE:0000000140419CAE or [r9+_DEBUG_OBJECT.Flags], 4 ; 也就是DEBUG_OBJECT.Flags的bit3,为1表示32位调试器
PAGE:0000000140419CB3
PAGE:0000000140419CB3 loc_140419CB3: ; CODE XREF: NtCreateDebugObject+FC↑j
PAGE:0000000140419CB3 lea rax, [rsp+78h+DebugHandle]
PAGE:0000000140419CB8 mov [rsp+78h+ObjectBodySize], rax ; 参数5:DebugHandle
PAGE:0000000140419CBD and [rsp+78h+var_58], rdx
PAGE:0000000140419CC2 xor r9d, r9d ; 参数4:ObjectPointerBias
PAGE:0000000140419CC5 mov r8d, esi ; 参数3:DesiredAccess
PAGE:0000000140419CC5 ; 参数2:PassedAccessState
PAGE:0000000140419CC8 mov rcx, [rsp+78h+var_28] ; 参数1:DEBUG_OBJECT
PAGE:0000000140419CCD call ObInsertObject
PAGE:0000000140419CD2 mov r11d, eax
PAGE:0000000140419CD5 test eax, eax
PAGE:0000000140419CD7 js short loc_140419CEB
PAGE:0000000140419CD9
PAGE:0000000140419CD9 loc_140419CD9: ; DATA XREF: .text:00000001401BD230↑o
PAGE:0000000140419CD9 mov rax, [rsp+78h+DebugHandle]
PAGE:0000000140419CDE mov [rbx], rax ; rbx就是(Teb + 0x16A8)
PAGE:0000000140419CE1 jmp short loc_140419CE6
PAGE:0000000140419CE3 ; ---------------------------------------------------------------------------
PAGE:0000000140419CE3
PAGE:0000000140419CE3 loc_140419CE3: ; DATA XREF: .text:00000001401BD230↑o
PAGE:0000000140419CE3 mov r11d, eax
PAGE:0000000140419CE6
PAGE:0000000140419CE6 loc_140419CE6: ; CODE XREF: NtCreateDebugObject+131↑j
PAGE:0000000140419CE6 mov eax, r11d
PAGE:0000000140419CE9 jmp short $+2
PAGE:0000000140419CEB ; ---------------------------------------------------------------------------
PAGE:0000000140419CEB
PAGE:0000000140419CEB loc_140419CEB: ; CODE XREF: NtCreateDebugObject+52↑j
PAGE:0000000140419CEB ; NtCreateDebugObject+97↑j ...
PAGE:0000000140419CEB lea r11, [rsp+78h+var_8]
PAGE:0000000140419CF0 mov rbx, [r11+10h]
PAGE:0000000140419CF4 mov rsi, [r11+18h]
PAGE:0000000140419CF8 mov rsp, r11
PAGE:0000000140419CFB pop rdi
PAGE:0000000140419CFC retn
PAGE:0000000140419CFC ; } // starts at 140419BB0
PAGE:0000000140419CFC NtCreateDebugObject endp
函数原型如下:
c
NTSTATUS
NtCreateDebugObject(
OUT PHANDLE DebugObjectHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN ULONG Flags
);
1. 先取参数,并读取 PreviousMode
先看函数开头:
c
PAGE:0000000140419BBF mov edi, r9d
PAGE:0000000140419BC2 mov esi, edx
PAGE:0000000140419BC4 mov rbx, rcx
PAGE:0000000140419BC7 mov rax, gs:_KPCR.Prcb.CurrentThread
PAGE:0000000140419BD0 mov r10b, [rax+_ETHREAD.Tcb.PreviousMode]
这里对应关系很清楚:
-
rbx = DebugObjectHandle
-
esi = DesiredAccess
-
edi = Flags
-
r10b = PreviousMode
2. 如果来自用户层,先探测输出指针是否可写
接下来就是这段:
c
PAGE:0000000140419BD7 test r10b, r10b
PAGE:0000000140419BDA jz short loc_140419BF0 ; PreviousMode == KernelMode,就直接跳过
PAGE:0000000140419BDC mov rax, cs:MmUserProbeAddress
PAGE:0000000140419BE3 cmp rcx, rax ; 无符号比较,判断PHANDLE是否超过合法用户地址范围
PAGE:0000000140419BE6 cmovnb rcx, rax ; 超过则rcx=rax
PAGE:0000000140419BEA mov rax, [rcx] ; 校验参数是否可读可写
PAGE:0000000140419BED mov [rcx], rax
这段代码的意思是:
-
如果
PreviousMode == KernelMode,说明参数来自内核,直接跳过探测 -
如果来自用户层,内核就不会直接相信
DebugObjectHandle这个指针,而是先做一次"试探性访问"
其中:
c
mov rax, cs:MmUserProbeAddress
cmp rcx, rax
cmovnb rcx, rax
表示:
-
MmUserProbeAddress是用户空间地址上界 -
如果 DebugObjectHandle >= MmUserProbeAddress
-
就把它钳制到 MmUserProbeAddress
随后:
c
mov rax, [rcx]
mov [rcx], rax
它的目的不是修改数据,而是:
-
通过读取,验证这个地址是否可读
-
通过写回,验证这个地址是否可写
如果地址无效,第一条就会异常;
如果地址是只读页,第二条就会异常。
所以这一段可以概括为:
如果系统调用来自用户层,先对输出参数
DebugObjectHandle做一次 probe,确认这个用户指针确实可以被写入。
3. 先把输出句柄清零
探测完成后,马上执行:
c
在PAGE:0000000140419BF0 and qword ptr [rbx], 0 ; 先把输出句柄清零,方便失败路径统一处理
也就是:
c
*DebugObjectHandle = 0;
这一步的意义很简单:
先把输出句柄置空,避免后续失败路径给调用者留下垃圾值。
所以 NtCreateDebugObject 的失败路径是很干净的:
只要后面任何一步失败,调用者看到的输出句柄都还是 0。
4. 校验 Flags,只允许 0 或 1
接下来是参数校验:
c
PAGE:0000000140419BF4 test r9d, 0FFFFFFFEh ; 校验 Flags,只允许 0 或 1
PAGE:0000000140419BFB jz short loc_140419C07 ; 如果r9d是1或者0就跳走
PAGE:0000000140419BFB ;
PAGE:0000000140419BFB ; 如果 Flags 除了 bit0 以外还有其他位被设置
PAGE:0000000140419BFB ;
PAGE:0000000140419BFB ; 就直接返回 0xC000000D
PAGE:0000000140419BFB ; 也就是STATUS_INVALID_PARAMETER
PAGE:0000000140419BFD mov eax, 0C000000Dh
PAGE:0000000140419C02 jmp loc_140419CEB
这段的逻辑是:
-
用 0xFFFFFFFE 屏蔽掉 bit0
-
如果 Flags 除了 bit0 以外还有其他位被设置
-
就直接返回 0xC000000D
STATUS_INVALID_PARAMETER
所以这里Flags允许的情况只有两种:
c
Flags = 0;
Flags = 1; // DEBUG_KILL_ON_CLOSE
5. 调用 ObCreateObject 创建 DEBUG_OBJECT
参数检查通过以后,开始创建对象:
c
PAGE:0000000140419C07 loc_140419C07: ; CODE XREF: NtCreateDebugObject+4B↑j
PAGE:0000000140419C07 lea rax, [rsp+78h+var_28]
PAGE:0000000140419C0C mov [rsp+40h], rax ; 参数9:[rsp+40h] = PDEBUG_OBJECT
PAGE:0000000140419C11 and dword ptr [rsp+38h], 0 ; 参数8:[rsp+38h] = NonPagedPoolCharge
PAGE:0000000140419C16 and dword ptr [rsp+30h], 0 ; 参数7:[rsp+30h] = PagedPoolCharge
PAGE:0000000140419C1B mov dword ptr [rsp+78h+ObjectBodySize], 68h ; 'h' ; 参数6:[rsp+28h] = ObjectBodySize
PAGE:0000000140419C23 and qword ptr [rsp+20h], 0 ; 参数5:[rsp+20h] = ParseContext
PAGE:0000000140419C29 mov r9b, r10b ; 参数4:r9 = OwnershipMode
PAGE:0000000140419C29 ; 参数3:r8 = ObjectAttributes
PAGE:0000000140419C2C mov rdx, cs:DbgkDebugObjectType ; 参数2:rdx = ObjectType
PAGE:0000000140419C33 mov cl, r10b ; 参数1:rcx = ProbeMode
PAGE:0000000140419C36 call ObCreateObject ; 创建DEBUG_OBJECT
ObCreateObject 原型如下:
c
NTSTATUS
ObCreateObject (
__in KPROCESSOR_MODE ProbeMode,
__in POBJECT_TYPE ObjectType,
__in POBJECT_ATTRIBUTES ObjectAttributes,
__in KPROCESSOR_MODE OwnershipMode,
__inout_opt PVOID ParseContext,
__in ULONG ObjectBodySize,
__in ULONG PagedPoolCharge,
__in ULONG NonPagedPoolCharge,
__out PVOID *Object
);
结合寄存器和栈参数,这里可以准确整理成:
c
ObCreateObject(
PreviousMode, // rcx = ProbeMode
DbgkDebugObjectType, // rdx = ObjectType
ObjectAttributes, // r8 = ObjectAttributes
PreviousMode, // r9 = OwnershipMode
NULL, // [rsp+20h] = ParseContext
0x68, // [rsp+28h] = ObjectBodySize
0, // [rsp+30h] = PagedPoolCharge
0, // [rsp+38h] = NonPagedPoolCharge
&DebugObject // [rsp+40h] = Object
);
这里最关键的信息有两个:
-
创建的对象类型是 DbgkDebugObjectType
这说明 NtCreateDebugObject 创建是
DEBUG_OBJECT类型对象。 -
对象体大小是 0x68
-
也就是说,内核这里分配的对象体大小为 0x68 字节。
这和DEBUG_OBJECT 结构正好对应:
c
typedef struct _DEBUG_OBJECT {
KEVENT EventsPresent; // 0x00
FAST_MUTEX Mutex; // 0x18
LIST_ENTRY EventList; // 0x50
ULONG Flags; // 0x60
} DEBUG_OBJECT, *PDEBUG_OBJECT; // sizeof = 0x68
所以,从这里已经可以看出:
NtCreateDebugObject 的核心,就是通过 ObCreateObject 分配一个大小为 0x68 的 DEBUG_OBJECT 对象体。
6. 创建成功后,初始化 DEBUG_OBJECT
ObCreateObject 成功后,返回的对象地址保存在局部变量 var_28 中,然后又转存到 DEBUG_OBJECT 局部变量里:
c
PAGE:0000000140419C3B mov r9, [rsp+78h+var_28]
PAGE:0000000140419C40 mov [rsp+78h+DEBUG_OBJECT], r9
PAGE:0000000140419C45 test eax, eax
PAGE:0000000140419C47 js loc_140419CEB
后面开始手工初始化这个对象的各个成员。
6.1 始化 FAST_MUTEX
先看这段:
c
PAGE:0000000140419C4D mov r10d, 1
PAGE:0000000140419C53 mov [r9+_DEBUG_OBJECT.Mutex.Count], r10d
PAGE:0000000140419C57 and [r9+_DEBUG_OBJECT.Mutex.Owner], 0
PAGE:0000000140419C5C and [r9+_DEBUG_OBJECT.Mutex.Contention], 0
PAGE:0000000140419C61 lea rcx, [r9+_DEBUG_OBJECT.Mutex.Event] ; Event
PAGE:0000000140419C65 xor r8d, r8d
PAGE:0000000140419C68 mov edx, r10d
PAGE:0000000140419C6B call KeInitializeEvent
这部分对应的是 DEBUG_OBJECT.Mutex 的初始化,也就是 _FAST_MUTEX:
-
Count = 1
-
Owner = 0
-
Contention = 0
然后继续初始化其中的 Event 字段。
这里调用 KeInitializeEvent 时,参数是:
-
rcx = &DebugObject->Mutex.Event
-
edx = 1
-
r8d = 0
也就是:
c
KeInitializeEvent(&DebugObject->Mutex.Event, 1, 0);
结合 KeInitializeEvent 的原型:
c
VOID KeInitializeEvent(PRKEVENT Event, EVENT_TYPE Type, BOOLEAN State);
所以这一段整体上做的事情,就是:
把 DEBUG_OBJECT 内部的 FAST_MUTEX 初始化成可用状态,并把其中用于竞争等待的事件对象初始化为一个未触发的同步事件。
6.2 初始化 EventList
接着是:
c
PAGE:0000000140419C70 lea rcx, [r9+_DEBUG_OBJECT.EventList]
PAGE:0000000140419C74 mov [rcx+LIST_ENTRY.Blink], rcx
PAGE:0000000140419C78 mov [rcx+LIST_ENTRY.Flink], rcx
是标准的双向循环链表头初始化:
c
InitializeListHead(&DebugObject->EventList);
初始化完成后:
-
Flink = &EventList
-
Blink = &EventList
表示当前调试事件链表为空。
6.3 初始化 EventsPresent
然后是这一段:
c
PAGE:0000000140419C7B xor edx, edx
PAGE:0000000140419C7D mov rcx, r9
PAGE:0000000140419C80 call KeInitializeEvent
这里 rcx = r9,也就是直接把 DEBUG_OBJECT 起始地址当成一个 KEVENT 来初始化。
这正好对应:
c
KEVENT EventsPresent; // 0x00
DEBUG_OBJECT 开头的 EventsPresent 是一个通知事件(NotificationEvent),初始状态为未触发;它的作用是告诉调试器"当前是否存在待处理的调试事件"。
7. 根据当前调试器是否为 WOW64,再补一个内部标志
接下来还有一段非常关键:
c
PAGE:0000000140419C98 mov rax, gs:_KPCR.Prcb.CurrentThread
PAGE:0000000140419CA1 mov rcx, [rax+70h] ; kthread._KAPC_STATE._KPROCESS
PAGE:0000000140419CA5 cmp [rcx+320h], rdx ; _ERPCOESS.Wow64Process
PAGE:0000000140419CA5 ; 判断是否为32位调试器
PAGE:0000000140419CAC jz short loc_140419CB3 ; 64位调试器跳转
PAGE:0000000140419CAE or [r9+_DEBUG_OBJECT.Flags], 4 ; 也就是DEBUG_OBJECT.Flags的bit3,为1表示32位调试器
这里的逻辑是:
-
取当前线程
-
取当前线程所属进程
-
检查该进程的 Wow64Process 字段
-
如果不为 0,说明当前调用者是 WOW64 进程
-
于是给 DEBUG_OBJECT.Flags 再额外 OR 一个 0x4
该版本 DEBUG_OBJECT.Flags 中的 bit2(值 0x4)表示当前调试器是 WOW64 / 32 位调试器。
8. 调用 ObInsertObject 生成句柄
对象初始化完成以后,继续执行:
c
PAGE:0000000140419CB3 loc_140419CB3: ; CODE XREF: NtCreateDebugObject+FC↑j
PAGE:0000000140419CB3 lea rax, [rsp+78h+DebugHandle]
PAGE:0000000140419CB8 mov [rsp+78h+ObjectBodySize], rax ; 参数5:DebugHandle
PAGE:0000000140419CBD and [rsp+78h+var_58], rdx
PAGE:0000000140419CC2 xor r9d, r9d ; 参数4:ObjectPointerBias
PAGE:0000000140419CC5 mov r8d, esi ; 参数3:DesiredAccess
PAGE:0000000140419CC5 ; 参数2:PassedAccessState
PAGE:0000000140419CC8 mov rcx, [rsp+78h+var_28] ; 参数1:DEBUG_OBJECT
PAGE:0000000140419CCD call ObInsertObject
ObInsertObject 原型是:
c
NTSTATUS __stdcall ObInsertObject(
PVOID Object,
PACCESS_STATE PassedAccessState,
ACCESS_MASK DesiredAccess,
ULONG ObjectPointerBias,
PVOID *NewObject,
PHANDLE Handle
);
因此这里可以准确整理成:
c
ObInsertObject(
DebugObject, // 参数1:Object
NULL, // 参数2:PassedAccessState
DesiredAccess, // 参数3:DesiredAccess
0, // 参数4:ObjectPointerBias
NULL, // 参数5:NewObject
&DebugHandle // 参数6:Handle
);
这一步的意义非常重要:
前面的 ObCreateObject 只是创建出了内核对象体;真正把它变成一个调用者可用的句柄,还需要通过 ObInsertObject 把它插入对象管理器的句柄表。
也就是说:
-
ObCreateObject:创建对象体
-
ObInsertObject:生成句柄
9. 把句柄写回调用者
如果 ObInsertObject 成功,就继续执行:
c
PAGE:0000000140419CD9 mov rax, [rsp+78h+DebugHandle]
PAGE:0000000140419CDE mov [rbx], rax ; rbx就是(Teb + 0x16A8)
其中:
-
rbx 一开始保存的就是参数 DebugHandle
-
在当前这条调用链里,这个参数实际上传进来的是:
(Teb + 0x16A8)
所以这里等价于:
c
*DebugHandle = ReturnedHandle;
放回 DbgUiConnectToDbg 的上下文里看,就是:
内核最终把创建出来的调试对象句柄写回到了当前线程 TEB 的 +0x16A8 位置。
这也正好和前面 DbgUiConnectToDbg 一开始检查 [TEB+0x16A8] 的逻辑首尾呼应。
10. 把整个函数整理成伪代码
结合前面的分析,NtCreateDebugObject 可以整理成下面这样:
c
NTSTATUS NtCreateDebugObject(
PHANDLE DebugHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
ULONG Flags
)
{
PDEBUG_OBJECT DebugObject;
HANDLE ReturnedHandle;
KPROCESSOR_MODE PreviousMode;
PreviousMode = KeGetPreviousMode();
if (PreviousMode != KernelMode)
{
// probe DebugHandle 是否可写
}
*DebugHandle = NULL;
if (Flags & ~1)
return STATUS_INVALID_PARAMETER;
Status = ObCreateObject(
PreviousMode,
DbgkDebugObjectType,
ObjectAttributes,
PreviousMode,
NULL,
sizeof(DEBUG_OBJECT),
0,
0,
&DebugObject
);
if (!NT_SUCCESS(Status))
return Status;
// 初始化 EventsPresent
// 初始化 Mutex
// 初始化 EventList
if (Flags & 1)
DebugObject->Flags = DEBUG_OBJECT_KILL_ON_CLOSE;
else
DebugObject->Flags = 0;
if (PsGetCurrentProcess()->Wow64Process != NULL)
DebugObject->Flags |= 0x4;
Status = ObInsertObject(
DebugObject,
NULL,
DesiredAccess,
0,
NULL,
&ReturnedHandle
);
if (!NT_SUCCESS(Status))
return Status;
*DebugHandle = ReturnedHandle;
return Status;
}
四、总结
到这里,DebugActiveProcess -> DbgUiConnectToDbg -> NtCreateDebugObject 这条链路就已经比较清楚了。
DebugActiveProcess 本身并不直接完成全部调试附加逻辑。
它的第一步,是调用 DbgUiConnectToDbg,先检查当前线程是否已经保存了调试对象句柄;如果没有,就进入内核调用 NtCreateDebugObject。
而 NtCreateDebugObject 的职责也非常明确:
-
测试用户输出参数 DebugHandle合法性
-
先把输出句柄清零
-
校验 Flags,只允许 0 或 DEBUG_KILL_ON_CLOSE
-
通过 ObCreateObject 创建一个大小为 0x68 的 DEBUG_OBJECT
-
初始化其中的 Mutex、EventList、EventsPresent
-
根据用户输入和当前调试器类型设置内部 Flags
-
通过 ObInsertObject 生成句柄
-
把句柄写回调用者,也就是当前线程 TEB 中保存调试对象句柄的位置
因此,这一篇可以得出一个更完整的阶段性结论:
调试附加的第一步,并不是操作目标进程,而是先为调试器这一侧创建并初始化一个真正可用的
DEBUG_OBJECT。