DebugActiveProcess 调试流程分析(一)

在 Windows 用户层调试 API 中,DebugActiveProcess 是一个非常典型的入口函数。

从名字上看,它的作用似乎很直接:附加并调试一个已经存在的进程。

但如果顺着它的实现继续往下分析,就会发现,DebugActiveProcess 本身并不负责完成全部调试工作。

它更像是一个用户层的包装函数,主要完成三件事:

  1. 先检查当前线程是否已经保存了调试对象句柄;如果没有,就创建一个 _DEBUG_OBJECT

  2. 再根据 PID 打开目标进程,取得目标进程句柄

  3. 最后调用更底层的函数,去执行真正的调试附加

本文先分析第一部分,也就是:

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位调试器

这里的逻辑是:

  1. 取当前线程

  2. 取当前线程所属进程

  3. 检查该进程的 Wow64Process 字段

  4. 如果不为 0,说明当前调用者是 WOW64 进程

  5. 于是给 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 的职责也非常明确:

  1. 测试用户输出参数 DebugHandle合法性

  2. 先把输出句柄清零

  3. 校验 Flags,只允许 0 或 DEBUG_KILL_ON_CLOSE

  4. 通过 ObCreateObject 创建一个大小为 0x68 的 DEBUG_OBJECT

  5. 初始化其中的 Mutex、EventList、EventsPresent

  6. 根据用户输入和当前调试器类型设置内部 Flags

  7. 通过 ObInsertObject 生成句柄

  8. 把句柄写回调用者,也就是当前线程 TEB 中保存调试对象句柄的位置

因此,这一篇可以得出一个更完整的阶段性结论:

调试附加的第一步,并不是操作目标进程,而是先为调试器这一侧创建并初始化一个真正可用的 DEBUG_OBJECT

相关推荐
Java.熵减码农4 小时前
火绒安全软件误杀explorer.exe导致黑屏解决方法
windows
love530love4 小时前
不用聊天软件 OpenClaw 手机浏览器远程访问控制:Tailscale 配置、设备配对与常见问题全解
人工智能·windows·python·智能手机·tailscale·openclaw·远程访问控制
夏末蝉未鸣014 小时前
Windows环境下载并安装milvus
windows·milvus
葡萄星球5 小时前
win11右键菜单一步改成win10样式
windows
桌面运维家5 小时前
Windows/Linux云桌面:高校VDisk方案部署指南
linux·运维·windows
马士兵教育6 小时前
RocketMQ如何进行性能调优?
服务器·windows·rocketmq
fundoit7 小时前
Windows 下 YOLO 环境搭建与使用完整指南
windows·yolo
乔宕一8 小时前
windows SSH服务修改SSH登陆后的默认终端
运维·windows·ssh
love530love8 小时前
ComfyUI-3D-Pack:Windows 下手动编译 mesh_inpaint_processor C++ 加速模块
c++·人工智能·windows·python·3d·hunyuan3d·comfyui-3d-pack