Win10_x64 21h2调试体系分析
参考 https://www.52pojie.cn/thread-1728894-1-1.html
记的笔记,发出来参考下,抛砖引玉,有错误多纠正。还望各位大佬别嘲笑。
平台如下:
版本 Windows 10 专业版
版本号 21H2
安装日期 ‎2021-‎08-‎23
操作系统内部版本 19044.2364
体验 Windows Feature Experience Pack 120.2212.4190.0
0x0参考
《WRK源码》
0x1 Windows调试体系
在Windows中,调试器是基于事件处理的,不是基于状态机的。
因此在内核中,是在进程与被调试进程之间建立通道进行通信的,即==DebugPort:调试对象==
被调试进程中产生事件时,会把事件放在DebugPort的一个事件链表中。而调试器接受事件通知,去DebugPort拿调试事件。
常见的调试事件如
- 创建进线程
- 进线程结束
- ==异常==
- 模块加卸载
- 打印日志:OutputStringA
最核心的便是异常,其他的调试事件一般是用记录的。
0x1-1 调试对象的建立
Windows调试必须先建立管道,才能在调试进程和被调试进程传递信息。而管道就是调试对象。
调试器拥有调试对象句柄从而对被调试进程进行操作。被调试进程EPROCESS.DebugPort存值以便于往里面写入DeBugEvent。
0x1-1-1 DebugActiveProcess
复制代码 隐藏代码
BOOL __stdcall DebugActiveProcess(DWORD dwProcessId);
这个函数是调试通道建立的开始,他的主要功能就是
- 创建调试对象(DEBUG_OBJECT)
- 根据传入的Pid打开句柄(==权限问题==),调用__imp_DbgUiDebugActiveProcess,把DebugPort挂上去。
DbgUiConnectToDbg即创建调试对象,判断是否创建成功。
复制代码 隐藏代码
mov [rsp+arg_0], rbx
push rdi ; 保留非易失寄存器
sub rsp, 20h
mov ebx, ecx
call cs:__imp_DbgUiConnectToDbg ; 先创建一个调试对象
nop dword ptr [rax+rax+00h]
test eax, eax
jns short DebugPortCreateSuccess ; 因此想要调试,首先得打开进程
然后根据Pid打开进程获取句柄,调用__imp_DbgUiDebugActiveProcess()
将被调试进程DEBUG_PORT端口和创建的调试对象句柄联系起来。
复制代码 隐藏代码
DebugPortCreateSuccess: ; 因此想要调试,首先得打开进程
mov ecx, ebx
call ProcessIdToHandle ; 打开进程,获取句柄
mov rbx, rax
test rax, rax
jz short OpenFailed
mov rcx, rax ; 被调试进程句柄
call cs:__imp_DbgUiDebugActiveProcess ; 初始化调试对象信息
nop dword ptr [rax+rax+00h]
mov edi, eax
mov rcx, rbx
test eax, eax
jns short InitDebugPortSuccess ; 关闭句柄返回
OpenFaild:
;清理资源,关闭句柄。
0x1-1-2 DbgUiConnectToDbg
前文提到,这个用于创建调试对象,创建过程是ntdll!DbgUiConnectToDbg->nt!NtCreateDebugObject
在这个函数中,进行了一些简单判断,判断是否已经在调试别的程序中。
复制代码 隐藏代码
mov rax, gs:_TEB.NtTib.Self
xor ecx, ecx
cmp [rax+(_TEB.DbgSsReserved+8)], rcx ; 判断是否已经有调试
jnz short HasDebugge
他判断是否有被调试进程是通过TEB.DbgSsReserved+8的位置,事实上,这个地方存的就是句柄。
如果没有,则调用NtCreateDebugObject进入内核进程创建对象。
复制代码 隐藏代码
mov [rsp+28h], rcx
lea r8, [rsp+20h] ; 传地址 其实是OBJECT_ATTRIBUTES
mov [rsp+38h], ecx
xorps xmm0, xmm0
mov [rsp+30h], rcx
mov r9d, 1 ; 传参
movdqu xmmword ptr [rsp+40h], xmm0
mov dword ptr [rsp+20h], 30h
mov edx, 1F000Fh ; 传参
mov rcx, gs:_TEB.NtTib.Self
add rcx, _TEB.DbgSsReserved+8
call NtCreateDebugObject
而NtCreateDebugObject函数声明是
复制代码 隐藏代码
NTSTATUS
NtCreateDebugObject (
OUT PHANDLE DebugObjectHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN ULONG Flags
);
由参数推出,第一个参数,DebugObjectHandle就是_TEB.DbgSsReserved+8
位置,也就是调试对象的句柄。
值得一提的是,DesiredAccess是对于调试对象句柄的权限。
0x1-1-3 nt!NtCreateDebugObject
这个函数只有两个作用
- 创建调试对象
- 根据参数DesiredAccess作为调试对象权限存入调试进程的句柄表中
首先检查一些参数,这是R3->R0的常规操作。
然后创建调试对象,使用==ObCreateObjectEx==,所有内核对象都是通过它创建的,包括ETHREAD,EPROCESS等
复制代码 隐藏代码
CreateDebugObject: ; 调试对象类型
mov rdx, cs:DbgkDebugObjectType
and qword ptr [rsp+48h], 0
lea rax, [rsp+88h+pObject]
mov [rsp+40h], rax ; pObject
and dword ptr [rsp+38h], 0
and dword ptr [rsp+30h], 0
mov dword ptr [rsp+28h], 68h ; ObjectSize
mov r9b, r10b
mov cl, r10b ; AccessMode
call ObCreateObjectEx ; 创建调试对象
test eax, eax
js Ret
调试对象的结构如下:
复制代码 隐藏代码
typedef struct _DEBUG_OBJECT {
//
// Event thats set when the EventList is populated.
//
KEVENT EventsPresent;
//
// Mutex to protect the structure
//
FAST_MUTEX Mutex;
//
// Queue of events waiting for debugger intervention
//
LIST_ENTRY EventList;
//
// Flags for the object
//
ULONG Flags;
} DEBUG_OBJECT, *PDEBUG_OBJECT;
其中EventsPresent的意义是方便让调试器的调试循环捕捉,一旦在链表中有了要处理的调试事件,就会用KeSetEvent设置事件信号(==后面会有体现==)。
而Mutex的意义便是多线程操作链表时候的同步作用。
EventList是链表头,链接DEBUG_EVENT所有事件
flags则表明DebugObject属性,如下值
复制代码 隐藏代码
#define DEBUG_OBJECT_DELETE_PENDING (0x1) // Debug object is delete pending.
#define DEBUG_OBJECT_KILL_ON_CLOSE (0x2) // Kill all debugged processes on close
若为1,说明DebugObject无效
创建对象成功后,进行简单初始化,如链表清空操作
复制代码 隐藏代码
mov rbx, [rsp+88h+pObject] ; 调试对象进行赋值
mov [rbx+_DEBUG_PORT.Mutex.Count], 1 ; 参考WRK的DEBUG_PORT对象
and [rbx+_DEBUG_PORT.Mutex.Owner], 0 ; 初始化互斥体,用于插入链表时候的同步
and [rbx+_DEBUG_PORT.Mutex.Contention], 0
lea rcx, [rbx+_DEBUG_PORT.Mutex.Event] ; Event
xor r8d, r8d ; State
lea edx, [r8+1] ; Type
call KeInitializeEvent
lea rax, [rbx+_DEBUG_PORT.EventList]
mov [rax+8], rax
mov [rax], rax ; 情况链表 自己指向自己
xor r8d, r8d ; State
xor edx, edx ; Type
mov rcx, rbx ; Event
call KeInitializeEvent
test sil, 1 ; R3 flags
jz short Equal
mov dword ptr [rbx+_DEBUG_PORT.Flags], 2
jmp short loc_140883589
Equal:
and dword ptr [rbx+_DEBUG_PORT.Flags], 0
其中sil即R3->R0传入的flags,不难发现,如果传入1,则代表调试关闭时关闭所有调试进程(==出现场景为调试子进程==),如果传入0,则不会关闭所有被调试进程。
在创建完对象之后,进行wow64进程的判断,如果调试进程是32位的,那么flags | 4
复制代码 隐藏代码
mov rax, gs:188h
mov rcx, [rax+_ETHREAD.Tcb._union_90.ApcState.Process]
mov rax, [rcx+_EPROCESS.WoW64Process] ; 这是调试器的线程
test rax, rax
jz short x64Bit
or dword ptr [rbx+_DEBUG_PORT.Flags], 4 ; 即flags & 4 就是wow64
x64Bit:
xxxxx
然后把调试对象插入到调试进程的句柄表中,其中句柄的权限就是R3传入的DesriedAccess;
顺带也可以发现,产生的句柄确实放在了_TEB.DbgSsReserved+8这个位置。
复制代码 隐藏代码
mov r8d, r14d ; r14就是R3传过来的DesriedAccess
xor edx, edx
mov rcx, [rsp+88h+pObject]
call ObInsertObjectEx ; InsertObject的作用就是把对象查到句柄表里面
mov ecx, eax
test eax, eax
js short Ret
mov rax, [rsp+88h+Handle]
mov [rdi], rax ; 这是TEB的那个位置,用于保存句柄
Ret:
;进行释放资源的操作
自此调试对象创建完毕。
0x1-2调试对象挂入被调试进程
在DebugActiveProcess中,创建完调试对象之后,则开始进行与被调试对象挂入操作。
调用如下函数进行挂入:
复制代码 隐藏代码
NTSTATUS __fastcall DbgUiDebugActiveProcess(__int64 ProcessHandle)
{
__int64 hProcess; // rdi
signed int status; // ebx
hProcess = ProcessHandle;
status = NtDebugActiveProcess(ProcessHandle, NtCurrentTeb()->DbgSsReserved[1]);
if ( status >= 0 )
{
status = DbgUiIssueRemoteBreakin(hProcess);
if ( status < 0 )
ZwRemoveProcessDebug(hProcess, NtCurrentTeb()->DbgSsReserved[1]);
}
return (unsigned int)status;
}
函数主要功能即调用NtDebugActiveProcess,传入被调试进程句柄和调试对象句柄,在内核进行挂载。
在挂入成功之后,调用DbgUiIssueRemoteBreakin()
。
这个函数的作用是创建一个远程线程,让远程线程指向int3产生异常,被调试器捕获。
这就是为什么用调试器附加进程,总是会断在一个系统断点。
复制代码 隐藏代码
__int64 __fastcall DbgUiIssueRemoteBreakin(__int64 hProcess)
{
status = RtlpCreateUserThreadEx(
hProcess,
0i64,
2,
0,
0i64,
0x4000i64,
v3,
(__int64)DbgUiRemoteBreakin, // 新建线程的地址
0i64,
&v5,
(__m128i *)&v4);
if ( (status & 0x80000000) == 0 )
NtClose(v5);
return status;
}
DbgUiRemoteBreakin
是新建线程的地址,在进行简单判断之后就会调用DbgBreakPoint();
复制代码 隐藏代码
void __noreturn DbgUiRemoteBreakin()
{
if ( (NtCurrentPeb()->BeingDebugged || MEMORY[0x7FFE02D4] & 2) && !(NtCurrentTeb()->_union_108.SameTebFlags & 0x20) )
{
if ( UseWOW64 )
{
if ( g_LdrpWow64PrepareForDebuggerAttach )
g_LdrpWow64PrepareForDebuggerAttach();
}
DbgBreakPoint();//执行Int3
}
RtlExitUserThread(0i64);
}
值得一提的是,在DbgUiDebugActiveProcess
中,如果DbgUiIssueRemoteBreakin
执行失败,则会执行
复制代码 隐藏代码
if ( status < 0 )
ZwRemoveProcessDebug(hProcess, NtCurrentTeb()->DbgSsReserved[1]);
而DbgUiIssueRemoteBreakin
执行失败只有一个原因,即创建远程线程失败。因此要调试还需要具有远程创建线程的句柄权限。
0x1-2-1 nt!NtDebugActiveProcess
它是被调试进程和调试对象建立起来联系核心函数。
声明如下:
复制代码 隐藏代码
NTSTATUS
NtDebugActiveProcess (
IN HANDLE ProcessHandle,
IN HANDLE DebugObjectHandle
);
函数首先根据句柄找到进程
复制代码 隐藏代码
mov r8, cs:PsProcessType
and qword ptr [r11+18h], 0
mov bpl, byte ptr [rax+_ETHREAD.Tcb._union_171.UserAffinity.Reserved] ; PreviousMode
lea rax, [r11+18h] ; pObject rsp+0x80
and qword ptr [r11-28h], 0
mov r9b, bpl
mov [r11-40h], rax
mov dword ptr [rsp+68h+Object], 4F676244h
call ObReferenceObjectByHandleWithTag ; 获取进程对象
然后根据进程进行一些判断,基本就是
- 是否调试自己
- 是否是系统进程
- 是否是wow64
复制代码 隐藏代码
mov rax, gs:188h
mov rdi, [rsp+68h+pDebugProcess]
mov rsi, [rax+_ETHREAD.Tcb._union_90.ApcState.Process]
cmp rdi, rsi ; 判断是不是在调试自己
jz DebugProcessErr
cmp rdi, cs:PsInitialSystemProcess ; 判断一下是不是这个进程 就是system进程
jz DebugProcessErr
mov rax, [rdi+_EPROCESS.WoW64Process]
test rax, rax
jz x64Bit
;进行调试进程被调试进程检查
; 如果被调试64 调试32 无法调试
检查无误之后,获取调试对象
复制代码 隐藏代码
GetDebugObject: ; ObjectType
mov r8, cs:DbgkDebugObjectType
lea rax, [rsp+68h+pDebugObject]
and [rsp+68h+var_40], 0
mov r9b, bpl ; AccessMode
and [rsp+68h+pDebugObject], 0
mov edx, 2 ; DesiredAccess
mov rcx, r14 ; Handle
mov [rsp+68h+Object], rax ; Object
call ObReferenceObjectByHandle
此外,获取RunDown 锁,防止进程结束
复制代码 隐藏代码
lea rbp, [rdi+_EPROCESS.RundownProtect]
mov rcx, rbp
call ExAcquireRundownProtection_0 ; 获取被调试对象的RunDown锁
mov rsi, [rsp+68h+pDebugObject] ; 这个可以反调试 但是不要用
test al, al
jz short RunDownProtectErr
之后进入核心代码,发送==假消息模拟==
调试假消息的意义在于附加时进程已经创建,无法还原线程进程创建时场景,因此采取模拟发送假消息方式进行折中,还原进程刚创建时的调试信息不会遗漏。
复制代码 隐藏代码
lea r8, [rsp+68h+var_28]
mov rdx, rsi ; DebugObject
mov rcx, rdi ; DebugProcess
call DbgkpPostFakeProcessCreateMessages ; 创建进程创建的假消息 其实就是进程已经创建过了 还得接受一下消息
; 模拟一下正常调试信息
; 注意是创建,不会发送!
mov r9, [rsp+68h+var_28]
mov r8d, eax
mov rdx, rsi ; DebugPort
mov rcx, rdi ; DebugProcess
call DbgkpSetProcessDebugObject ; 把DebugPort写入被调试进程
; 参考WRK
; 并发送消息上一个函数模拟的假消息
mov rcx, rbp
mov ebx, eax
call ExReleaseRundownProtection_0 ; 释放锁
jmp short release
核心函数便是
复制代码 隐藏代码
NTSTATUS
DbgkpPostFakeProcessCreateMessages (
IN PEPROCESS Process,
IN PDEBUG_OBJECT DebugObject,
IN PETHREAD *pLastThread
);
复制代码 隐藏代码
NTSTATUS
DbgkpSetProcessDebugObject (
IN PEPROCESS Process,
IN PDEBUG_OBJECT DebugObject,
IN NTSTATUS MsgStatus,
IN PETHREAD LastThread
);
0x1-3发送假消息
在调试器附加之后,会发送假消息模拟进程创建。这时候DebugPort还没有挂入到被调试进程。无法直接将消息写入Process.DebugPort,==Windows采取采取传入DebugPort变量,消息写入变量中==,而非Process.DebugPort中。
然后在DbgkpSetProcessDebugObject
中进行设置DebugPort挂入被调试进程。
0x1-3-1 DbgkpPostFakeProcessCreateMessages
复制代码 隐藏代码
__int64 __fastcall DbgkpPostFakeProcessCreateMessages(_EPROCESS *DebugProcess, _DEBUG_PORT *DebugObject, __int64 *a3)
{
v3 = a3;
v4 = 0i64;
pFirstThread = 0i64;
v10 = 0i64;
pLastThread = 0i64;
DebugObject_1 = DebugObject;
v11 = 0i64;
DebugProcess_1 = DebugProcess;
v12 = 0i64;
result = DbgkpPostFakeThreadMessages(DebugProcess, DebugObject, 0i64, &pFirstThread, &pLastThread);// 发送线程假消息
if ( (signed int)result >= 0 )
{
KiStackAttachProcess((ULONG_PTR)DebugProcess_1, 0, (__int64)&v10);
DbgkpPostModuleMessages(DebugProcess_1, pFirstThread, DebugObject_1);// 发送模块消息
KiUnstackDetachProcess(&v10, 0i64);
ObfDereferenceObjectWithTag((ULONG_PTR)pFirstThread);
result = 0i64;
v4 = pLastThread;
}
*v3 = (__int64)v4;
return result;
}
首先是发送假线程消息,在DbgkpPostFakeProcessCreateMessages()
中
复制代码 隐藏代码
result = DbgkpPostFakeThreadMessages(DebugProcess, DebugObject, 0i64, &pFirstThread, &pLastThread);// 发送线程假消息
在此函数中,主要进行了如下操作
- 判断是否是不能调试的进线程,入system的线程,直接跳过不发送假消息
- 遍历被调试进程所有线程,获取线程结构体,初始化==_DBGKM_APIMSG==结构体
此结构是后面用于初始化DEBUG_EVENT的结构体,DEBUG_EVENT是用于挂在DEBUG_OBJECT.EventList链表中的。
如下代码,根据ApiNumber=2,初始化ApiMsg结构体,
_DBGKM_APIMSG结构如下
复制代码 隐藏代码
typedef struct _DBGKM_APIMSG {
PORT_MESSAGE h;
DBGKM_APINUMBER ApiNumber; //枚举
NTSTATUS ReturnedStatus;
union {
DBGKM_EXCEPTION Exception;
DBGKM_CREATE_THREAD CreateThread;
DBGKM_CREATE_PROCESS CreateProcessInfo;
DBGKM_EXIT_THREAD ExitThread;
DBGKM_EXIT_PROCESS ExitProcess;
DBGKM_LOAD_DLL LoadDll;
DBGKM_UNLOAD_DLL UnloadDll;
} u;
} DBGKM_APIMSG, *PDBGKM_APIMSG;
其中h用于串口联网调试。
ApiNumber则是如下枚举
复制代码 隐藏代码
typedef enum _DBGKM_APINUMBER {
DbgKmExceptionApi,
DbgKmCreateThreadApi,
DbgKmCreateProcessApi,
DbgKmExitThreadApi,
DbgKmExitProcessApi,
DbgKmLoadDllApi,
DbgKmUnloadDllApi,
DbgKmMaxApiNumber
} DBGKM_APINUMBER;
DbgkpPostFakeThreadMessages该函数操作如下
复制代码 隐藏代码
*(_DWORD *)&ApiMsg.ApiNumber = 2;
Section = (_SECTION *)Process_2->SectionObject;
if ( Section )
*(_QWORD *)&ApiMsg.u[11] = DbgkpSectionToFileHandle(Section);// 就是返回文件句柄的
else
*(_QWORD *)&ApiMsg.u[11] = 0i64;
*(_QWORD *)&ApiMsg.u[19] = Process_2->SectionBaseAddress;// BaseOfImage
KeStackAttachProcess(Process_2, (_KAPC_STATE *)&Apc);
ntHead = (IMAGE_NT_HEADERS64 *)RtlImageNtHeader(Process_2->SectionBaseAddress);
if ( ntHead )
{
*(_QWORD *)&ApiMsg.u[43] = 0i64;
*(_DWORD *)&ApiMsg.u[27] = ntHead->FileHeader.PointerToSymbolTable;// 符号表
*(_DWORD *)&ApiMsg.u[31] = ntHead->FileHeader.NumberOfSymbols;
}
KeUnstackDetachProcess(&Apc);
status = DbgkpQueueMessage(Process_2, StartThread_1, &ApiMsg, flags, DebugObject_1);// 将信息插入DebugObject
// 这个函数就是插入DebugObject并设置DebugObject的等待位为等待
// 现在我们在发假消息的时候调用,他的作用仅仅是初始化DebugPort,填DebugEvent
在初始化完ApiMsg之后,调用DbgkpQueueMessage()
进行插入,此函数不仅是假消息插入核心,也是正常调试消息插入核心函数。
这就是在DbgkpPostFakeProcessCreateMessage中发送假线程过程,发送假dll也是在此函数中进行,原理类似。
0x1-3-2 DbgkpQueueMessage
函数声明如下
复制代码 隐藏代码
NTSTATUS
DbgkpQueueMessage (
IN PEPROCESS Process,
IN PETHREAD Thread,
IN OUT PDBGKM_APIMSG ApiMsg,
IN ULONG Flags,
IN PDEBUG_OBJECT TargetDebugObject
);
函数作用主要是
- 对于发送假消息(==本质是假消息调用此函数传入flags为NoWait,不用替换DebugObject==),直接操作TargetDebugObject。
- 对于需要等待的消息,取得Process.DebugPort,根据ApiMsg初始化DebugEvent,挂入DebugPort.EventList链表,KeWaitXXX(DebugEvent.ContinueEvent)等待
值得注意的是,此时的等待是被调试进程等待DebugEvent。而非调试进程等待,原因是如果是非NoWait消息,此时被调试进程一定有DebugPort,才可能产生这种消息,而且代表DbgkpQueueMessage
调用者是被调试进程自己而不是调试进程在模拟假消息时候的调用,因此==此时的等待是被调试进程等待DebugEvent==
KeWaitForSingleObject(&DebugEvent_1->ContinueEvent.Header, 0, 0, 0, 0i64);// 进行等待
复制ApiMsg到DebugEvent
复制代码 隐藏代码
CopyDApiMsgToDbkEvent:
v15 = &DebugEvent_1->ApiMsg;
DebugEvent_1->Process = Process_1;
DebugEvent_1_1 = &DebugEvent_1->ApiMsg;
DebugEvent_1->Thread = Thread_1;
ApiMsg_2 = ApiMsg_1;
v18 = 2i64;
do // 把ApiMsg复制过去
{
*(_OWORD *)DebugEvent_1_1->h = *(_OWORD *)ApiMsg_2->h;
*(_OWORD *)&DebugEvent_1_1->h[16] = *(_OWORD *)&ApiMsg_2->h[16];
*(_OWORD *)&DebugEvent_1_1->h[32] = *(_OWORD *)&ApiMsg_2->h[32];
*(_OWORD *)&DebugEvent_1_1->u[3] = *(_OWORD *)&ApiMsg_2->u[3];
*(_OWORD *)&DebugEvent_1_1->u[19] = *(_OWORD *)&ApiMsg_2->u[19];
*(_OWORD *)&DebugEvent_1_1->u[35] = *(_OWORD *)&ApiMsg_2->u[35];
*(_OWORD *)&DebugEvent_1_1->u[51] = *(_OWORD *)&ApiMsg_2->u[51];
DebugEvent_1_1 = (_DBGKM_APIMSG *)((char *)DebugEvent_1_1 + 128);
v19 = *(_OWORD *)&ApiMsg_2->u[67];
ApiMsg_2 = (_DBGKM_APIMSG *)((char *)ApiMsg_2 + 128);
*(_OWORD *)&DebugEvent_1_1[-1].u[211] = v19;
--v18;
}
while ( v18 );
*(_OWORD *)DebugEvent_1_1->h = *(_OWORD *)ApiMsg_2->h;
_mm_storeu_si128((__m128i *)&DebugEvent_1->Cid, (__m128i)Thread_1->Cid);
操作DebugObject,插入双向链表
值得一提,如果是被调试进程主动调用此函数(需要等待),DebugObject==Process.DebugPort,否则,代表无需等待,DebugObject=TargetObject。
复制代码 隐藏代码
Tail = DebugObject_1->EventList.Blink; // 这个算法是插到链表尾部
if ( Tail->Flink != &DebugObject_1->EventList )
__fastfail(3u);
DebugEvent_1->EventList.Flink = &DebugObject_1->EventList;
DebugEvent_1->EventList.Blink = Tail;
Tail->Flink = &DebugEvent_1->EventList;
DebugObject_1->EventList.Blink = &DebugEvent_1->EventList;
if ( !bNoWait )
KeSetEvent(&DebugObject_1->EventPresent, 0, 0);// 需要等待,设置一下DebugObject的位,当调试循环能改进行
//而我们发送假消息bNoWait是true,也就是不会KeSetEvent
status = 0;
KeSetEvent作用是让调试进程的KeWait能改等待到,说明有消息需要处理,==即阻塞调试进程的线程。==
最后判断一下是否是需要等待的DebugEvent,需要等待,KeWaitForSingleObject
,==即阻塞被调试进程的线程。==
复制代码 隐藏代码
KeReleaseGuardedMutex((ULONG_PTR)&DbgkpProcessDebugPortMutex);
if ( status >= 0 )
{
KeWaitForSingleObject(&DebugEvent_1->ContinueEvent.Header, 0, 0, 0, 0i64);// 进行等待
status = DebugEvent_1->Status; // 注意,是被调试进程在这等待!
// 等待的是DebugEvent的Event
// 而DebugObject的Event则标志着有事件要进行处理
}
0x1-3-3 DbgkpSetProcessDebugObject
函数作用为
- 设置被调试进程的DebugPort
- 遍历EventList,执行之前在DebugPort初始化的消息(==发送假消息只是初始化了这个结构体,并没有设置KeSetEvent,参见上文==)
- 清理无效的DebugEvent
- 再次遍历线程,双重保险,防止被调试进程又新建线程导致无法发送消息。
函数声明如下
复制代码 隐藏代码
NTSTATUS
DbgkpSetProcessDebugObject (
IN PEPROCESS Process,
IN PDEBUG_OBJECT DebugObject,
IN NTSTATUS MsgStatus,
IN PETHREAD LastThread
);
==以下函数代码来自WRK,非IDA逆出==
首先判断传入MsgStatus,这个值是DbgkpPostFakeProcessCreateMessages
函数的返回值,标志这个函数是不是执行成功。
复制代码 隐藏代码
if (!NT_SUCCESS (MsgStatus)) { //这个是前面插入DebugObject List时候是否成功
LastThread = NULL;
Status = MsgStatus;
} else {
Status = STATUS_SUCCESS;
}
设置被调试进程的DebugPort
复制代码 隐藏代码
if (Process->DebugPort != NULL) {
Status = STATUS_PORT_ALREADY_SET;
break;
}
//
// Assign the debug port to the process to pick up any new threads
//
Process->DebugPort = DebugObject;//设置调试对象
判断是否被调试进程新建线程,双重保险防止遗漏
复制代码 隐藏代码
while (1) {
//
// Acquire the debug port mutex so we know that any new threads will
// have to wait to behind us.
//
GlobalHeld = TRUE;
ExAcquireFastMutex (&DbgkpProcessDebugPortMutex);//获取
//
// If the port has been set then exit now.
//
if (Process->DebugPort != NULL) {
Status = STATUS_PORT_ALREADY_SET;
break;
}
//
// Assign the debug port to the process to pick up any new threads
//
Process->DebugPort = DebugObject;//设置
//
// Reference the last thread so we can deref outside the lock
//
ObReferenceObject (LastThread);
//
// Search forward for new threads
//
Thread = PsGetNextProcessThread (Process, LastThread);//判断一下是否有新的线程,有的话再发假线程消息
if (Thread != NULL) {
//
// Remove the debug port from the process as we are
// about to drop the lock
//
Process->DebugPort = NULL;
ExReleaseFastMutex (&DbgkpProcessDebugPortMutex);
GlobalHeld = FALSE;
ObDereferenceObject (LastThread);
//
// Queue any new thread messages and repeat.
//
Status = DbgkpPostFakeThreadMessages (Process,
DebugObject,
Thread,
&FirstThread,
&LastThread);
if (!NT_SUCCESS (Status)) {
LastThread = NULL;
break;
}
ObDereferenceObject (FirstThread);
} else {
break;
}
}
遍历DebugObject->EventList链表,如果有值则KeSetEvent (&DebugObject->EventsPresent, 0, FALSE);
复制代码 隐藏代码
for (Entry = DebugObject->EventList.Flink;//遍历DebugObject链表
Entry != &DebugObject->EventList;
) {
DebugEvent = CONTAINING_RECORD (Entry, DEBUG_EVENT, EventList);
Entry = Entry->Flink;
if ((DebugEvent->Flags&DEBUG_EVENT_INACTIVE) != 0 && DebugEvent->BackoutThread == ThisThread) {
Thread = DebugEvent->Thread;
//
// If the thread has not been inserted by CreateThread yet then don't
// create a handle. We skip system threads here also
//
if (NT_SUCCESS (Status) && Thread->GrantedAccess != 0 && !IS_SYSTEM_THREAD (Thread)) {
//
// If we could not acquire rundown protection on this
// thread then we need to suppress its exit message.
//
if ((DebugEvent->Flags&DEBUG_EVENT_PROTECT_FAILED) != 0) {
PS_SET_BITS (&Thread->CrossThreadFlags,
PS_CROSS_THREAD_FLAGS_SKIP_TERMINATION_MSG);
RemoveEntryList (&DebugEvent->EventList);
InsertTailList (&TempList, &DebugEvent->EventList);
} else {
if (First) {//只有第一次进入才设置
DebugEvent->Flags &= ~DEBUG_EVENT_INACTIVE;
KeSetEvent (&DebugObject->EventsPresent, 0, FALSE);//设置DebugObject->Event,调试器的KeWait可以等待成功。
First = FALSE;
}
最后释放资源,清理无效DebugEvent
复制代码 隐藏代码
if (GlobalHeld) {
ExReleaseFastMutex (&DbgkpProcessDebugPortMutex);//可以用于反调试 占用这个全局变量导致所有调试器无法调试,链接DebugPort
}
if (LastThread != NULL) {
ObDereferenceObject (LastThread);
}
while (!IsListEmpty (&TempList)) {//清空无效DebugEvent
Entry = RemoveHeadList (&TempList);
DebugEvent = CONTAINING_RECORD (Entry, DEBUG_EVENT, EventList);
DbgkpWakeTarget (DebugEvent);
}
return status;
0x2结语
自此,Windows调试体系的创建调试对象,调试对象的链接以及假消息发送告一段落。剩下的编译异常分发,调试器处理了,这些基础知识也是自建调试通道的基础。