-
-
[原创]Windows内核学习笔记之调试(下)
-
2022-1-3 21:10 8025
-
四.处理调试事件
1.调试事件在用户层的处理
调试器是在用户层读取和处理调试事件的,在用户层是使用DEBUG_EVENT结构来表示调试事件的,该结构定义如下:
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) | 值 | 说明 | 详细信息所使用的结构 |
---|---|---|---|
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 |
Windows系统提供了WaitForDebugEvent来供调试器等待和接收调试事件,函数定义如下:
1 2 3 4 | BOOL WINAPI WaitForDebugEvent( __out LPDEBUG_EVENT lpDebugEvent, __in DWORD dwMilliseconds ); |
参数 | 含义 |
---|---|
lpDebugEvent | 指向DEBUG_EVENT结构的指针,用来保存收到的调试事件 |
dwMilliseconds | 指定要等待的毫秒数,或者使用常量INFINITE(0xFFFFFFFF),意思是无限等待 |
调用该函数会导致线程阻塞,直到调试事件发生,或等待时间已到或发生错误才返回。这也是大多数调试器使用多线程的原因,可使用其他线程处理UI更新和用户对话。
调试器在处理好调试事件以后,应该调用ContinueDebugEvent函数来向调试子系统回复处理结果,函数定义如下:
1 2 3 4 5 | BOOL WINAPI ContinueDebugEvent( __in DWORD dwProcessId, __in DWORD dwThreadId, __in DWORD dwContinueStatus ); |
参数 | 含义 |
---|---|
dwProcessId | 接收到的调试事件(DEBUG_EVENT)中包含的进程ID |
dwThreadId | 接收到的调试事件(DEBUG_EVENT)中包含的线程ID |
dwContinueStatus | 可以为DBG_CONTINUE(0x001002L)和DBG_EXCEPTION_NOT_HANDLED(0x0001001L)两个常量之一,对于异常事件(EXCEPTION_DEBUG_EVENT)之外的其他所有事件,这两个常量没有差异,调试子系统收到后,都会恢复运行被调试进程(调用DbgkpResumeProcess) |
对于异常事件,dwContinueStatus的两个常量的差异如下:
DBG_CONTINUE表示调试器处理了该异常。DbgkForwardException函数收到此返回值后会向它的调用者(KiDispatchException)返回真
DBG_EXCEPTION_NOT_HANDLED表示调试器不处理该异常。DbgkForwardException会返回假给KiDispatchException
以下代码是上述内容的一个例子:
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 | #include <cstdio> #include <Windows.h> #define PID 5180 // 要调试的进程PID void ShowError(char * msg); int main() { DEBUG_EVENT DbgEvt; / / 用来读取调试事件的数据结构 DWORD dwContinueStatus = DBG_CONTINUE; / / 恢复继续执行用的状态代码 bool bContinue = true; / / 是否继续 / / 附加到被调试进程 bContinue = DebugActiveProcess(PID); if (!bContinue) ShowError( "DebugActiveProcess" ); while (bContinue) { memset(&DbgEvt, 0 , sizeof(DbgEvt)); / / 等待调试事件发生 bContinue = WaitForDebugEvent(&DbgEvt, INFINITE); if (!bContinue) { ShowError( "WaitForDebugEvent" ); break ; } switch (DbgEvt.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT: { printf( "EXCEPTION_DEBUG_EVENT\n" ); break ; } case CREATE_THREAD_DEBUG_EVENT: { printf( "CREATE_THREAD_DEBUG_EVENT\n" ); break ; } case CREATE_PROCESS_DEBUG_EVENT: { printf( "CREATE_PROCESS_DEBUG_EVENT\n" ); break ; } case EXIT_THREAD_DEBUG_EVENT: { printf( "EXIT_THREAD_DEBUG_EVENT\n" ); break ; } case EXIT_PROCESS_DEBUG_EVENT: { printf( "EXIT_PROCESS_DEBUG_EVENT\n" ); break ; } case LOAD_DLL_DEBUG_EVENT: { printf( "LOAD_DLL_DEBUG_EVENT\n" ); break ; } case UNLOAD_DLL_DEBUG_EVENT: { printf( "UNLOAD_DLL_DEBUG_EVENT\n" ); break ; } case OUTPUT_DEBUG_STRING_EVENT: { printf( "OUTPUT_DEBUG_STRING_EVENT\n" ); break ; } case RIP_EVENT: { printf( "RIP_EVENT\n" ); break ; } } / / 恢复被调试进程继续运行 bContinue = ContinueDebugEvent(DbgEvt.dwProcessId, DbgEvt.dwThreadId, dwContinueStatus); if (!bContinue) ShowError( "ContinueDebugEvent" ); } return 0 ; } void ShowError(char * msg) { printf( "%s Error %d\n" , msg, GetLastError()); } |
2.接收调试事件
上面说到了WaitForDebugEvent被调试器用来等待和接收调试事件,该函数是kernel32.dll中的一个函数,从反汇编结果可以看到,函数会调用DbgUiWaitStateChange函数
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 | .text: 7C85B458 ; BOOL __stdcall WaitForDebugEvent(LPDEBUG_EVENT lpDebugEvent, DWORD dwMilliseconds) .text: 7C85B458 public _WaitForDebugEvent@ 8 .text: 7C85B458 _WaitForDebugEvent@ 8 proc near ; DATA XREF: .text:off_7C802654↑o .text: 7C85B458 .text: 7C85B458 DbgUiWaitStateCange = _DBGUI_WAIT_STATE_CHANGE ptr - 68h .text: 7C85B458 var_8 = byte ptr - 8 .text: 7C85B458 lpDebugEvent = dword ptr 8 .text: 7C85B458 dwMilliseconds = dword ptr 0Ch .text: 7C85B458 .text: 7C85B458 mov edi, edi .text: 7C85B45A push ebp .text: 7C85B45B mov ebp, esp .text: 7C85B45D sub esp, 68h .text: 7C85B460 push esi .text: 7C85B461 push [ebp + dwMilliseconds] .text: 7C85B464 lea eax, [ebp + var_8] .text: 7C85B467 push eax .text: 7C85B468 call _BaseFormatTimeOut@ 8 ; BaseFormatTimeOut(x,x) .text: 7C85B46D mov esi, eax .text: 7C85B46F .text: 7C85B46F loc_7C85B46F: ; CODE XREF: WaitForDebugEvent(x,x) + 26 ↓j .text: 7C85B46F ; WaitForDebugEvent(x,x) + 2D ↓j .text: 7C85B46F push esi ; TimeOut .text: 7C85B470 lea eax, [ebp + DbgUiWaitStateCange] .text: 7C85B473 push eax ; DbgUiWaitStateCange .text: 7C85B474 call _DbgUiWaitStateChange@ 8 |
DbgUiWaitStateChange函数是ntdll.dll中的一个函数,该函数会将在内核中表示调试事件的结构转换为DBGUI_WAIT_STATE_CHANGE结构,第一个参数就是用来接收转换以后的结构的地址,而DBGUI_WAIT_STATE_CHA NGE结构的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; |
根据DbgUiWaitStateChange函数的返回值来判断是否转换成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | .text: 7C85B479 cmp eax, STATUS_ALERTED .text: 7C85B47E jz short loc_7C85B46F .text: 7C85B480 cmp eax, STATUS_USER_APC .text: 7C85B485 jz short loc_7C85B46F .text: 7C85B487 test eax, eax .text: 7C85B489 jge short loc_7C85B492 .text: 7C85B48B cmp eax, DBG_UNABLE_TO_PROVIDE_HANDLE .text: 7C85B490 jnz short loc_7C85B4B8 .text: 7C85B492 .text: 7C85B492 loc_7C85B492: ; CODE XREF: WaitForDebugEvent(x,x) + 31 ↑j .text: 7C85B492 cmp eax, STATUS_TIMEOUT .text: 7C85B497 jnz short loc_7C85B4A7 .text: 7C85B499 push 79h ; dwErrCode .text: 7C85B49B call _SetLastError@ 4 ; SetLastError(x) .text: 7C85B4A0 .text: 7C85B4A0 loc_7C85B4A0: ; CODE XREF: WaitForDebugEvent(x,x) + 66 ↓j .text: 7C85B4A0 ; WaitForDebugEvent(x,x) + 6E ↓j .text: 7C85B4A0 xor eax, eax ; jumptable 7C85B4C8 default case .text: 7C85B4A2 .text: 7C85B4A2 loc_7C85B4A2: ; CODE XREF: WaitForDebugEvent(x,x) + BA↓j .text: 7C85B4A2 pop esi .text: 7C85B4A3 leave .text: 7C85B4A4 retn 8 |
如果转换成功,就会调用ntdll.dll中的DbgUiConvertStateChangeStruct函数将转换后的结构DBGUI_WAIT_STATE_CHANGE再转换为在用户层使用的调试事件的DEBUG_EVENT结构
1 2 3 4 5 6 7 8 | .text: 7C85B4A7 loc_7C85B4A7: ; CODE XREF: WaitForDebugEvent(x,x) + 3F ↑j .text: 7C85B4A7 mov esi, [ebp + lpDebugEvent] .text: 7C85B4AA push esi ; DebugEvent .text: 7C85B4AB lea eax, [ebp + DbgUiWaitStateCange] .text: 7C85B4AE push eax ; WaitStateChange .text: 7C85B4AF call _DbgUiConvertStateChangeStructure@ 8 ; DbgUiConvertStateChangeStructure(x,x) .text: 7C85B4B4 test eax, eax .text: 7C85B4B6 jge short loc_7C85B4C0 |
ntdll.dll中的DbgUiWaitStateChange则是通过内核函数ZwWaitForDebugEvent来完成转换的,此时入栈的第一个参数是调试对象的句柄,第四个参数是要填充的结构体DBGUI_WAIT_STATE_CHANGE的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | .text: 7C96FF75 public DbgUiWaitStateChange .text: 7C96FF75 DbgUiWaitStateChange proc near ; DATA XREF: .text:off_7C923428↑o .text: 7C96FF75 .text: 7C96FF75 arg_0 = dword ptr 8 .text: 7C96FF75 arg_4 = dword ptr 0Ch .text: 7C96FF75 .text: 7C96FF75 mov edi, edi .text: 7C96FF77 push ebp .text: 7C96FF78 mov ebp, esp .text: 7C96FF7A mov eax, large fs: 18h ; 获取TEB的地址 .text: 7C96FF80 push [ebp + arg_0] .text: 7C96FF83 push [ebp + arg_4] .text: 7C96FF86 push 1 .text: 7C96FF88 push dword ptr [eax + 0F24h ] ; 压入调试对象句柄 .text: 7C96FF8E call ZwWaitForDebugEvent .text: 7C96FF93 pop ebp .text: 7C96FF94 retn 8 .text: 7C96FF94 DbgUiWaitStateChange endp |
在内核函数中会通过调用ObReferenceObjectByHandle函数来获取调试对象
1 2 3 4 5 6 7 8 9 10 11 | PAGE: 0058BC8D push esi ; HandleInformation PAGE: 0058BC8E lea eax, [ebp + Object ] PAGE: 0058BC91 push eax ; Object PAGE: 0058BC92 push dword ptr [ebp + AccessMode] ; AccessMode PAGE: 0058BC95 push _DbgkDebugObjectType ; ObjectType PAGE: 0058BC9B push 1 ; DesiredAccess PAGE: 0058BC9D push [ebp + DebugObject] ; Handle PAGE: 0058BCA0 call _ObReferenceObjectByHandle@ 24 ; ObReferenceObjectByHandle(x,x,x,x,x,x) PAGE: 0058BCA5 mov edi, [ebp + Object ] ; 将调试对象赋给edi PAGE: 0058BCA8 cmp eax, esi PAGE: 0058BCAA jl loc_58BE92 |
调用KeWaitForSingleObject对调试对象进行等待
1 2 3 4 5 6 7 8 9 | PAGE: 0058BCB6 push ebx ; Timeout PAGE: 0058BCB7 push dword ptr [ebp + Alertable] ; Alertable PAGE: 0058BCBA push dword ptr [ebp + AccessMode] ; WaitMode PAGE: 0058BCBD push esi ; WaitReason PAGE: 0058BCBE push edi ; Object PAGE: 0058BCBF call _KeWaitForSingleObject@ 20 ; KeWaitForSingleObject(x,x,x,x,x) PAGE: 0058BCC4 mov ebx, eax PAGE: 0058BCC6 cmp ebx, esi PAGE: 0058BCC8 jge short loc_58BCD3 |
判断等待的情况
1 2 3 4 5 6 7 8 | PAGE: 0058BCD3 loc_58BCD3: ; CODE XREF: NtWaitForDebugEvent(x,x,x,x) + D8↑j PAGE: 0058BCD3 ; NtWaitForDebugEvent(x,x,x,x) + DF↑j PAGE: 0058BCD3 cmp ebx, STATUS_TIMEOUT PAGE: 0058BCD9 jz loc_58BE28 PAGE: 0058BCDF cmp ebx, STATUS_ALERTED PAGE: 0058BCE5 jz loc_58BE28 PAGE: 0058BCEB cmp ebx, STATUS_USER_APC PAGE: 0058BCF1 jz loc_58BE28 |
如果符合条件,就会从调试对象的调试消息队列中获取调试消息
1 2 3 4 5 6 7 8 | PAGE: 0058BCF7 mov [ebp + var_Count], 0 PAGE: 0058BCFB lea ecx, [edi + _DEBUG_OBJECT.Mutex] ; FastMutex PAGE: 0058BCFE call ds:__imp_@ExAcquireFastMutex@ 4 ; ExAcquireFastMutex(x) PAGE: 0058BD04 test byte ptr [edi + _DEBUG_OBJECT.Flags], 1 PAGE: 0058BD08 jnz short loc_58BD85 PAGE: 0058BD0A lea eax, [edi + _DEBUG_OBJECT.EventList] ; 获取调试对象的调试消息队列 PAGE: 0058BD0D mov ecx, [eax + LIST_ENTRY.Flink] ; 获取调试对象中保存的调试消息节点 PAGE: 0058BD0F jmp short loc_58BD48 |
随后会对获取的调试消息进行验证;通过DbgkpConvertKernelToUserStateChange来将内核调试消息转换为DBGUI_WAIT_STATE_CHANGE结构,此时第一个参数是栈中的地址空间,用来保存DBGUI_WAIT_STATE_CHANGE结构,esi则是符合条件的内核调试消息结构
1 2 3 4 5 6 | PAGE: 0058BD6A push esi PAGE: 0058BD6B lea eax, [ebp + var_StateChange] PAGE: 0058BD71 push eax PAGE: 0058BD72 call _DbgkpConvertKernelToUserStateChange@ 8 ; DbgkpConvertKernelToUserStateChange(x,x) PAGE: 0058BD77 or [esi + _DBGKM_DEBUG_EVENT.Flags], 1 PAGE: 0058BD7B jmp short loc_58BD81 |
在DbgkpConvertKernelToUserStateChange首先对进程与线程ID进行赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | PAGE: 0058BB1B ; __stdcall DbgkpConvertKernelToUserStateChange(x, x) PAGE: 0058BB1B _DbgkpConvertKernelToUserStateChange@ 8 proc near PAGE: 0058BB1B ; CODE XREF: NtWaitForDebugEvent(x,x,x,x) + 182 ↓p PAGE: 0058BB1B PAGE: 0058BB1B arg_StateChange = dword ptr 8 PAGE: 0058BB1B arg_DbgkmDebugEvent = dword ptr 0Ch PAGE: 0058BB1B PAGE: 0058BB1B mov edi, edi PAGE: 0058BB1D push ebp PAGE: 0058BB1E mov ebp, esp PAGE: 0058BB20 mov edx, [ebp + arg_DbgkmDebugEvent] ; 将内核调试消息赋给edx PAGE: 0058BB23 mov ecx, [edx + _DBGKM_DEBUG_EVENT.ClientId.UniqueProcess] PAGE: 0058BB26 mov eax, [ebp + arg_StateChange] ; 将StateChange地址赋给eax PAGE: 0058BB29 mov [eax + DBGUI_WAIT_STATE_CHANGE.AppClientId.UniqueProcess], ecx PAGE: 0058BB2C mov ecx, [edx + _DBGKM_DEBUG_EVENT.ClientId.UniqueThread] PAGE: 0058BB2F mov [eax + DBGUI_WAIT_STATE_CHANGE.AppClientId.UniqueThread], ecx |
由于结构中存在联合体,所以需要根据内核调试消息的ReturnedStatus来选择如何为剩下成员赋值并设置NewState的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | PAGE: 0058BB32 mov ecx, [edx + _DBGKM_DEBUG_EVENT.ApiMsg.ReturnedStatus] PAGE: 0058BB35 sub ecx, 0 PAGE: 0058BB38 push esi PAGE: 0058BB39 push edi PAGE: 0058BB3A jz short loc_58BBB2 PAGE: 0058BB3C dec ecx PAGE: 0058BB3D jz short loc_58BB9E PAGE: 0058BB3F dec ecx PAGE: 0058BB40 jz short loc_58BB87 PAGE: 0058BB42 dec ecx PAGE: 0058BB43 jz short loc_58BB79 PAGE: 0058BB45 dec ecx PAGE: 0058BB46 jz short loc_58BB71 PAGE: 0058BB48 dec ecx PAGE: 0058BB49 jz short loc_58BB5A PAGE: 0058BB4B dec ecx PAGE: 0058BB4C jnz loc_58BBE0 PAGE: 0058BB52 mov [eax + DBGUI_WAIT_STATE_CHANGE.NewState], 0Ah PAGE: 0058BB58 jmp short loc_58BB7F |
当函数返回的时候,此时局部变量var_StateChange中就保存了转换以后的DBGUI_WAIT_STATE_CHANGE结构,接下来就需要将其赋值到参数StateChange指定的区域中,该区域是用户层DBGUI_WAIT_STATE_CHANGE结构的地址
1 2 3 | PAGE: 0058BE39 lea esi, [ebp + var_StateChange] PAGE: 0058BE3F mov edi, [ebp + StateChange] PAGE: 0058BE42 rep movsd |
当函数返回到用户层的时候,也就是DbgUiWaitStateChange函数返回的时候,此时已经成功从内核中将调试对象的调试消息队列中保存的消息转换成DBGUI_WAIT_STATE_CHANGE结构。
DbgUiConvertStateChangeStructure就可以根据DBGUI_WAIT_STATE_CHANGE结构来生成用户层使用的DEBUG_EVENT结构,由于这两个结构都在用户层,并不需要进入内核。所以转换过程直接就在ntdll.dll中的DbgUiConvertStateChangeStructure中完成。
3.回复调试事件
ContinueDebugEvent函数被用来向调试子系统回复调试器处理结果,该函数通过调用ntdll中的DbgUiContinue来实现
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 | .text: 7C85B53D ; BOOL __stdcall ContinueDebugEvent(DWORD dwProcessId, DWORD dwThreadId, DWORD dwContinueStatus) .text: 7C85B53D public _ContinueDebugEvent@ 12 .text: 7C85B53D _ContinueDebugEvent@ 12 proc near ; DATA XREF: .text:off_7C802654↑o .text: 7C85B53D .text: 7C85B53D ClientId = _CLIENT_ID ptr - 8 .text: 7C85B53D dwProcessId = dword ptr 8 .text: 7C85B53D dwThreadId = dword ptr 0Ch .text: 7C85B53D dwContinueStatus = dword ptr 10h .text: 7C85B53D .text: 7C85B53D mov edi, edi .text: 7C85B53F push ebp .text: 7C85B540 mov ebp, esp .text: 7C85B542 push ecx .text: 7C85B543 push ecx .text: 7C85B544 push esi .text: 7C85B545 mov esi, [ebp + dwProcessId] .text: 7C85B548 push edi .text: 7C85B549 push [ebp + dwContinueStatus] ; ContinueStatus .text: 7C85B54C mov edi, [ebp + dwThreadId] .text: 7C85B54F lea eax, [ebp + ClientId] .text: 7C85B552 push eax ; ClientId .text: 7C85B553 mov [ebp + ClientId.UniqueProcess], esi .text: 7C85B556 mov [ebp + ClientId.UniqueThread], edi .text: 7C85B559 call _DbgUiContinue@ 8 ; DbgUiContinue(x,x) .text: 7C85B55E test eax, eax .text: 7C85B560 jge short loc_7C85B56C |
而DbgUiContinue则是通过调用内核函数ZwDebugContinue来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | .text: 7C96FF9C public DbgUiContinue .text: 7C96FF9C DbgUiContinue proc near ; DATA XREF: .text:off_7C923428↑o .text: 7C96FF9C .text: 7C96FF9C arg_0 = dword ptr 8 .text: 7C96FF9C arg_4 = dword ptr 0Ch .text: 7C96FF9C .text: 7C96FF9C mov edi, edi .text: 7C96FF9E push ebp .text: 7C96FF9F mov ebp, esp .text: 7C96FFA1 mov eax, large fs: 18h ; 获取TEB地址 .text: 7C96FFA7 push [ebp + arg_4] .text: 7C96FFAA push [ebp + arg_0] .text: 7C96FFAD push dword ptr [eax + 0F24h ] ; 将调试对象句柄入栈 .text: 7C96FFB3 call ZwDebugContinue .text: 7C96FFB8 pop ebp .text: 7C96FFB9 retn 8 .text: 7C96FFB9 DbgUiContinue endp |
在内核函数中,会根据ContinueStatus来选择相应的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | PAGE: 0058C5CC mov eax, [ebp + ContinueStatus] PAGE: 0058C5CF cmp eax, DBG_EXCEPTION_NOT_HANDLED PAGE: 0058C5D4 jz short loc_58C5FC PAGE: 0058C5D6 cmp eax, 10000h PAGE: 0058C5DB jle short loc_58C5F2 PAGE: 0058C5DD cmp eax, DBG_CONTINUE PAGE: 0058C5E2 jle short loc_58C5FC PAGE: 0058C5E4 cmp eax, DBG_UNABLE_TO_PROVIDE_HANDLE PAGE: 0058C5E9 jle short loc_58C5F2 PAGE: 0058C5EB cmp eax, DBG_TERMINATE_PROCESS PAGE: 0058C5F0 jle short loc_58C5FC PAGE: 0058C5F2 PAGE: 0058C5F2 loc_58C5F2: ; CODE XREF: NtDebugContinue(x,x,x) + 50 ↑j PAGE: 0058C5F2 ; NtDebugContinue(x,x,x) + 5E ↑j PAGE: 0058C5F2 mov eax, STATUS_INVALID_PARAMETER PAGE: 0058C5F7 jmp loc_58C6D2 |
如果ContinueStatus是DBG_CONTINUE,则会调用函数获取调试对象
1 2 3 4 5 6 7 8 9 10 11 12 13 | PAGE: 0058C5FC loc_58C5FC: ; CODE XREF: NtDebugContinue(x,x,x) + 49 ↑j PAGE: 0058C5FC ; NtDebugContinue(x,x,x) + 57 ↑j ... PAGE: 0058C5FC push ebx ; HandleInformation PAGE: 0058C5FD lea eax, [ebp + Object ] PAGE: 0058C600 push eax ; Object PAGE: 0058C601 push dword ptr [ebp + AccessMode] ; AccessMode PAGE: 0058C604 push _DbgkDebugObjectType ; ObjectType PAGE: 0058C60A push 1 ; DesiredAccess PAGE: 0058C60C push [ebp + DebugObject] ; Handle PAGE: 0058C60F call _ObReferenceObjectByHandle@ 24 ; ObReferenceObjectByHandle(x,x,x,x,x,x) PAGE: 0058C614 mov [ebp + var_20], eax PAGE: 0058C617 cmp eax, ebx PAGE: 0058C619 jl loc_58C6D2 |
取出调试对象中的调试消息队列,遍历这个消息队列,根据传入的线程ID和从消息队列中的调试消息节点的线程ID进行比较,如果相等,则说明找到符合条件的调试消息节点,会将这个消息节点的地址保存到ebx并将这个消息节点删除,由于这部分代码比较乱就不贴出来,感兴趣的可以自己分析一下
1 2 3 4 5 | PAGE: 0058C626 lea ecx, [edi + _DEBUG_OBJECT.Mutex] ; FastMutex PAGE: 0058C629 call ds:__imp_@ExAcquireFastMutex@ 4 ; ExAcquireFastMutex(x) PAGE: 0058C62F lea esi, [edi + _DEBUG_OBJECT.EventList] PAGE: 0058C632 mov eax, [esi + LIST_ENTRY.Flink] PAGE: 0058C634 jmp short loc_58C664 |
如果找到了,调用SetEvent函数,此时的edi执行的是调试对象DEBUG_OBJECT,也就是对DEBUG_OBJECT的EventsPresent进行调用,通知调试器来读取调试消息队列中的调试消息
1 2 3 4 5 | PAGE: 0058C66C and dword ptr [eax + 2Ch ], 0FFFFFFFBh PAGE: 0058C670 push 0 ; Wait PAGE: 0058C672 push 0 ; Increment PAGE: 0058C674 push edi ; Event PAGE: 0058C675 call _KeSetEvent@ 12 |
随后对调试消息进行赋值,调用DbgkpWakeTarget。此时ebx指向的就是从调试消息队列中获取的符合条件,也就是和传入的线程ID相等的调试消息对象
1 2 3 4 5 6 | PAGE: 0058C690 mov eax, [ebp + ContinueStatus] PAGE: 0058C693 mov [ebx + 54h ], eax ; 为调试消息的DBGKM_APIMSG的ReturnedStatus进行赋值 PAGE: 0058C696 and [ebx + _DBGKM_DEBUG_EVENT.Status], 0 PAGE: 0058C69A push ebx ; P PAGE: 0058C69B call _DbgkpWakeTarget@ 4 ; DbgkpWakeTarget(x) PAGE: 0058C6A0 jmp short loc_58C6A9 |
在DbgkpWakeTarget中首先会将调试线程唤醒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | PAGE: 0058BFDA ; int __stdcall DbgkpWakeTarget(PVOID P) PAGE: 0058BFDA _DbgkpWakeTarget@ 4 proc near ; CODE XREF: DbgkpCloseObject(x,x,x,x,x) + CB↓p PAGE: 0058BFDA ; DbgkClearProcessDebugObject(x,x) + C1↓p ... PAGE: 0058BFDA PAGE: 0058BFDA P = dword ptr 8 PAGE: 0058BFDA PAGE: 0058BFDA mov edi, edi PAGE: 0058BFDC push ebp PAGE: 0058BFDD mov ebp, esp PAGE: 0058BFDF push esi PAGE: 0058BFE0 mov esi, [ebp + P] PAGE: 0058BFE3 test byte ptr [esi + _DBGKM_DEBUG_EVENT.Flags], 20h PAGE: 0058BFE7 push edi PAGE: 0058BFE8 mov edi, [esi + _DBGKM_DEBUG_EVENT.Thread] PAGE: 0058BFEB jz short loc_58BFF5 PAGE: 0058BFED push 0 PAGE: 0058BFEF push edi PAGE: 0058BFF0 call _PsResumeThread@ 8 |
根据标志释放线程锁
1 2 3 4 5 | PAGE: 0058BFF5 loc_58BFF5: ; CODE XREF: DbgkpWakeTarget(x) + 11 ↑j PAGE: 0058BFF5 test byte ptr [esi + _DBGKM_DEBUG_EVENT.Flags], 8 PAGE: 0058BFF9 jz short loc_58C006 PAGE: 0058BFFB lea ecx, [edi + _ETHREAD.RundownProtect] PAGE: 0058C001 call @ExReleaseRundownProtection@ 4 |
根据标志调用SetEvent,此时esi指向的是消息队列中的消息节点,将它的地址加上8,得到的就是消息节点中的ContinueEvent,这样处于等待状态的被调试器进程就会被唤醒
1 2 3 4 5 6 7 8 9 | PAGE: 0058C006 loc_58C006: ; CODE XREF: DbgkpWakeTarget(x) + 1F ↑j PAGE: 0058C006 test byte ptr [esi + _DBGKM_DEBUG_EVENT.Flags], 2 PAGE: 0058C00A jnz short loc_58C01B PAGE: 0058C00C push 0 ; Wait PAGE: 0058C00E push 0 ; Increment PAGE: 0058C010 add esi, 8 PAGE: 0058C013 push esi ; 指向ContinueEvent PAGE: 0058C014 call _KeSetEvent@ 12 PAGE: 0058C019 jmp short loc_58C021 |
五.断点和单步执行
1.调试原理
调试器是基于异常的机制来对其他进程进行调试的,因为当进程产生了异常,就会进入异常分发函数KiDispatchException,该函数会判断是否存在调试器,如果存在调试器,就会通过DbgkForwardException将异常消息采集起来发送给调试子系统。调试子系统会将异常消息封装起来,挂入调试对象的调试消息队列中。调试器通过WaitForDebugEvent从调试消息队列中将调试消息取出,就可以获取此时被调试进程的各种信息,对其进行操作以后,在通过ContinueDebugEvent让被调试进程继续运行。
因此,调试器若想要接管被调试进程,对被调试的进程进行各种操作的话,就需要通过各种方法在被调试进程中触发异常。这样,被调试进程就会在异常分发的时候,通知调试器来接管被调试进程。
2.软件断点
软件断点是通过特殊的”int 3“指令来触发异常,当CPU执行这条指令的时候,会触发CPU异常,这样就会进入到异常分发函数中,最后让调试进程来接管被调试进程。
因此,软件断点的实现就是调试器将需要调试的地方的指令修改为int 3(0xCC)来实现的。由于int 3指令是软件调试中最经常用到的指令,所以它又被称为调试指令。
3.内存断点
内存断点的实现是通过修改内存属性实现的,可以通过VirtualProtectEx函数来实现
1 2 3 4 5 6 7 | BOOL WINAPI VirtualProtectEx( __in HANDLE hProcess, __in LPVOID lpAddress, __in SIZE_T dwSize, __in DWORD flNewProtect, __out PDWORD lpflOldProtect ); |
其中第4个参数则指定了新的内存属性,当它为PAGE_NOACCESS(0x1)的时候,此时会将PTE的有效位,也就是第一位(P位)修改为0。这样,当程序访问这段地址的时候,就会触发异常,调试进程就可以接管被调试进程。如果第四个参数为PAGE_EXECUTE_READ(0x20)的时候,此时PTE的P依然为1,但是决定是否可写的R/W位将被改成0。此时,如果程序对这段地址进行写操作就会触发异常。
4.单步异常
A.概念
调试器有时需要按顺序一行一行查看被调试进程的代码,观察其运行。如果使用软件断点或内存断点来实现的话,就需要频繁对被调试进程的内存或内存属性进行更改。为了方便调试器进行调试,处理器提供了两种方法来触发单步异常,分别是通过设置EFLAGS寄存器的TF位和硬件断点的方式。
IA-32架构专门分配了两个中断向量来支持软件调试,即向量1和向量3。向量3用于int 3指令产生的断点异常(#BP)。向量1用于其他情况的调试异常,简称调试异常(#DB)。而单步异常使用的就是向量1所对应的例程
B.标志寄存器的TF位
下图是标志寄存器(EFLAGS寄存器)的各个位,其中第9位,也就是TF位为单步标志位。当该位为1的时候,程序执行一条指令以后就会触发单步异常,调试器就可以接管被调试进程。只要让TF位一直为1,就可以一直触发单步异常,这样就可以做到一行一行地查看被调试程序的执行。
C.硬件断点
硬件断点是通过调试寄存器来实现的,IA-32处理器定义了8个调试寄存器,分别称为DR0~DR7。在32位模式下,它们都是32位的;在64位模式下,它们都是64位的。下图是32位的调试寄存器的内容:
首先,DR4和DR5是保留的,当调试扩展功能被启用(CR4寄存器的DE位设为1)时,任何对DR4和DR5的引用都会产生一个非法指令异常(#UD),当此功能被禁止时,DR4和DR5分别是DR6和DR7的别名寄存器,即等价于访问后者。
其他6个寄存器分别如下:
4个32位的调试地址寄存器(DR0~DR3),64位下是64位的
1个32位调试控制寄存器(DR7),64位时,高32位保留未用
1个32位的调试状态寄存器(DR6),64位时,高32位保留未用
通过以上内容可知,硬件断点最多只能设置4个,因为只有DR0~DR3四个寄存器可以用来指定断点的内存(线性地址)或I/O地址。对于设置在内存空间中的断点,这个地址应该是断点的线性地址而不是物理地址,因为CPU是在线性地址被翻译为物理地址之前来做断点工作的。这意味着,在保护模式下,不能使用调试寄存器来针对一个物理内存地址设置断点。
在调试控制寄存器DR7中,有24位是被划分成4组分别与4个调试地址寄存器想对应的,比如L0, G0, R/W0和LEN0这6位是与DR0相对应的,L1, G1, R/W1和LEN1这6位是与DR1相对应的,其余的依次类推。下图列出了DR7中各个位域的具体函数:
由于EFLAGS寄存器的TF位和硬件断点都是触发单步异常,所以,调试器需要使用调试状态寄存器(DR6)来判断到底是什么原因触发的单步异常。
调试状态寄存器(DR6)的作用是当CPU检测到匹配断点条件的断点或其他调试事件发生时,用来向调试寄存器的断点异常处理程序传递断点异常的详细信息,以便使调试器可以很容易地识别除发生地是什么调试事件。例如,如果B0被置为1,那么就说明DR0,LEN0和R/W0所定义条件地断点发生了。下图列出了DR6各个标志位的具体含义:
下图则总结了各种导致调试异常的情况及该情况所产生异常的类型
对于错误类调试异常,因为恢复执行后断点条件仍然存在,所以,为了避免重复发生异常,调试软件必须在使用iretd指令返回重新执行触发异常的指令前将标志寄存器EFLAGS的RF位设为1,告诉CPU,不要再执行返回后的第一条指令时产生调试异常,CPU执行完该指令就会自动清除RF标志。
六.参考资料
《软件调试》(第二版)卷1
《软件调试》(第二版)卷2
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!