首页
社区
课程
招聘
[原创]Windows内核学习笔记之调试(下)
2022-1-3 21:10 8025

[原创]Windows内核学习笔记之调试(下)

2022-1-3 21:10
8025

上篇:Windows内核学习笔记之调试(上)

四.处理调试事件

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());
}

avatar

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,就可以一直触发单步异常,这样就可以做到一行一行地查看被调试程序的执行。

 

avatar

C.硬件断点

硬件断点是通过调试寄存器来实现的,IA-32处理器定义了8个调试寄存器,分别称为DR0~DR7。在32位模式下,它们都是32位的;在64位模式下,它们都是64位的。下图是32位的调试寄存器的内容:

 

avatar

 

首先,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中各个位域的具体函数:

 

avatar

 

由于EFLAGS寄存器的TF位和硬件断点都是触发单步异常,所以,调试器需要使用调试状态寄存器(DR6)来判断到底是什么原因触发的单步异常。

 

调试状态寄存器(DR6)的作用是当CPU检测到匹配断点条件的断点或其他调试事件发生时,用来向调试寄存器的断点异常处理程序传递断点异常的详细信息,以便使调试器可以很容易地识别除发生地是什么调试事件。例如,如果B0被置为1,那么就说明DR0,LEN0和R/W0所定义条件地断点发生了。下图列出了DR6各个标志位的具体含义:

 

avatar

 

下图则总结了各种导致调试异常的情况及该情况所产生异常的类型

 

avatar

 

对于错误类调试异常,因为恢复执行后断点条件仍然存在,所以,为了避免重复发生异常,调试软件必须在使用iretd指令返回重新执行触发异常的指令前将标志寄存器EFLAGS的RF位设为1,告诉CPU,不要再执行返回后的第一条指令时产生调试异常,CPU执行完该指令就会自动清除RF标志。

六.参考资料

  • 《软件调试》(第二版)卷1

  • 《软件调试》(第二版)卷2


阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

最后于 2022-1-3 21:14 被1900编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回