Windows XP 用户态调试(三)断点处理-创建进程

ʕ •̀ o •́ ʔ

调试的本质,就是在被调试进程中触发异常,并由调试器接管异常的过程

其中有3种触发异常的方式:

  • 软件断点
  • 内存断点
  • 硬件断点

调试处理异常的框架一般为:

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
int main(int argc, char* argv[])
{
DEBUG_EVENT debugEvent = { 0 };
BOOL bRet = FALSE;

while (WaitForDebugEvent(&DebugEvent, INFINITE))
{
switch (DebugEvent.dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT:
bRet = CommonExceptionHandler(&DebugEvent);
break;
case CREATE_PROCESS_DEBUG_EVENT:
bRet = SetInt3BreakPoint(&DebugEvent);
break;
/*case ...:
*
*/
default:
break;
}
if (bRet)
{
bRet = ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_CONTINUE);
}
}

return 0;
}

// 调试器异常分发器
BOOL CommonExceptionHandler(DEBUG_EVENT* pDebugEvent)
{
BOOL bRet = TRUE;
EXCEPTION_DEBUG_INFO ExceptionInfo = pDebugEvent->u.Exception;

switch (ExceptionInfo.ExceptionRecord.ExceptionCode)
{
//INT3异常
case EXCEPTION_BREAKPOINT:
bRet = Int3ExceptionProc(&ExceptionInfo);
break;
case EXCEPTION_ACCESS_VIOLATION:
break;
/*case ...:
*
*/
default:
break;
}
return TRUE;
}

1 软件断点

1.1 软件断点的原理

软件断点,就是我们常说的 INT3(陷阱类异常),它的本质就是将下断处的机器码修改为0xCCINT3对应的机器码),实质就是 Inline Hook 修改一字节。陷阱类断点。

如下在 0x004023E8 处按 F2 下一个断点(此时该地址的数据前4字节为0x0000A164),OllyDbg 并不会直接显示该地址对应的值为0xCC

11.png

此时打开CheatEngine来验证一下,可以看到0x64已经被修改为0xCC

12.png

需要注意的是:

  1. 在OllyDbg 里面,int3的硬编码为0xCCint 3的硬编码为0xCD 03
  2. int3 断点为陷阱类异常,发生异常时EXCEPTION_RECORD结构记录的ExceptionAddress为产生异常的地址,但是此时的EIP为产生异常的下一条要执行的地址(本例为0x004023E9)。

1.2 软件断点分发和处理

触发软件断点的过程,实际上就是CPU异常分发的过程,所以说了解异常是学习调试的基础。

1.2.1 设置异常(调试器)

当我们以打开方式调试一个进程时,可以接收到 DEBUG_EVENT.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT 调试信息。此时设置内存断点的位置可以为 DEBUG_EVENT.u.CREATE_PROCESS_DEBUG_INFO.lpStartAddress(OEP)。注意,接收到创建进程消息时初始断点这些都还没设置。(只有在第一次进入 3 环时在 ntdll!LdrpInitializeProcess APC 函数中,设置好初始断点之后,返回 0 环,然后再次返回 3 环才会执行 kernel32!BaseProcessStartThunk 函数)。

所以我们应该设置一个即将要执行到的地址,让其触发断点,然后让调试器接收。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _CREATE_PROCESS_DEBUG_INFO {
HANDLE hFile;
HANDLE hProcess;
HANDLE hThread;
LPVOID lpBaseOfImage;
DWORD dwDebugInfoFileOffset;
DWORD nDebugInfoSize;
LPVOID lpThreadLocalBase;
LPTHREAD_START_ROUTINE lpStartAddress; // OEP
LPVOID lpImageName;
WORD fUnicode;
} CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO;

所以设置内存断点如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// while(WaitForDebugEvent(&debugEvent, INFINITE))
switch(DebugEvent.dwDebugEventCode)
{
case CREATE_PROCESS_DEBUG_EVENT:
SetInt3BreakPoint(&DebugEvent);
break;
case EXCEPTION_DEBUG_EVENT:
CommonExceptionHandler(&DebugEvent);
break;
}

BOOL SetInt3BreakPoint(DEBUG_EVENT* pDebugEvent) {
BOOL bRet = FALSE;
BYTE bInt3 = 0xCC;
g_hDebuggeeProcess = pDebugEvent->u.CreateProcessInfo.hProcess;
g_hDebugeeThread = pDebugEvent->u.CreateProcessInfo.hThread;
//1.备份
bRet = ReadProcessMemory(pDebugEvent->u.CreateProcessInfo.hProcess, pDebugEvent->u.CreateProcessInfo.lpStartAddress, \
&g_int3OriginalCode/*全局变量*/, 1, NULL);
//2.修改
//需要 VirtualProtectEx(hProcess,pStartAddredd,dwSize,NewProtect,&OldProtect) 来设置可写
bRet = WriteProcessMemory(pDebugEvent->u.CreateProcessInfo.hProcess, pDebugEvent->u.CreateProcessInfo.lpStartAddress, &bInt3, 1, NULL);
return bRet;
}

1.2.2 异常分发(被调试进程)

在被调试进程中,触发软件断点的过程如下:

  1. CPU 检测到int3指令。
  2. 在中断描述符表中找到 3 号中断处理函数 KiTrap03(ISR)。
  3. 中断处理函数内部会调用 CommonDispatchException
  4. CommonDispatchException 内部又会调用 KiDispatchException
  5. KiDispatchException中,由于是模拟用户层的软件断点,所以这里直接进入处理用户层异常的跳转,在处理用户异常时,如果不存在 0 环调试器或者 0 环调试器未处理异常,就会调用 DbgkForwardException 试图发送给 3 环调试器。
  6. DbgkForwardException 内部最终会调用 DbgkpSendApiMessage,它是将调试事件发送给调试对象的消息队列。
  7. 进入 DbgkpSendApiMessage,刚开始会判断第二个参数的值,若为 TRUE,则调用 DbgkpSuspendProcess 将本进程(被调试进程)内除自己外的其它进程挂起,然后调用DbgkpQueueMessage将发送消息的线程也挂起,此时被调试进程就被挂起了。
  8. 调试器得到信号之后将会在循环中取出调试事件,并根据异常调试事件结构体列出相应信息(当前寄存器的值,内存情况等),接下来便交由用户自定义的逻辑代码去处理。
  9. 调试器处理完事件之后调用ContinueDebugEvent来回复消息。

flowpath_4.png

1.2.3 修复软件断点(调试器)

当 OEP 处的软件断点被触发后,消息循环将接收到 EXCEPTION_DEBUG_EVENT 异常消息,此时我们使用一个公共异常处理函数 CommonExceptionHandler 来处理各种异常。

当我们在调试器中接到该异常后,我们会执行自己想要实现的代码,但是最后一定要修复该异常,及将替换成 0xCC 地址处的原来的硬编码恢复回去,然后修改 EIP

调试器处理int3的流程一般为:

  1. 调用自定义 IsSytemInt3() 函数判断当前int3是否为初始断点,若为初始断点则不需要修复(因为该断点不是我们自己触发的)此时让函数 ContinueDebugEvent 直接返回DBG_CONTINUE(表示异常已处理)。断点处的地址则可以通过DebugEvent(调试事件)->u.Exception->ExceptionRecord.ExceptionAddress来获取。
  2. 如果不是初始断点,则我们可以实现我们自己写的代码。
  3. 修复EIP。我们之前人为的将触发软件断点的地方修改为0xCC,软件断点是陷阱类异常,此时的EIP 为产生异常的下一条要执行的地址,需要修改为EIP++Windows XP 异常处理(一)异常采集1.3查表。

软件断点的处理函数:

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
BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO* pExceptionInfo)
{
BOOL bRet = FALSE;
CONTEXT Context;

//1.将INT3修复为原来的数据(如果是系统断点,不用修复)
if ( /*IsSystemInt3(pExceptionInfo)*/FALSE)
return TRUE;
else
WriteProcessMemory(g_hDebuggeeProcess, pExceptionInfo->ExceptionRecord.ExceptionAddress, &g_int3OriginalCode, 1, NULL);

//2.显示断点位置
//printf("int 3断点: 0x%p \n", pExceptionInfo->ExceptionRecord.ExceptionAddress);

//3.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(g_hDebugeeThread, &Context);

//4.修复EIP
Context.Eip--;
bRet = SetThreadContext(g_hDebugeeThread, &Context);

//5.显示反汇编

//6.等待用户命令
/*
while (bRet == FALSE)
{
bRet = WaitForUserCommand();
}
*/
if (bRet) MessageBox(NULL, TEXT("SUCCESS!"), TEXT("A1v1n"), 0);
return bRet;
}

下面来梳理一下每一步做的事:

  1. 调用自定义 IsSytemInt3() 函数判断当前int3是否为初始断点,若为初始断点则不需要修复(因为该断点不是我们自己触发的)此时让函数 ContinueDebugEvent 直接返回DBG_CONTINUE(表示异常已处理)。断点处的地址则可以通过 DebugEvent->u.Exception->ExceptionRecord.ExceptionAddress 来获取。然后调用 WriteProcessMemory 恢复原来的数据。
  2. 显示断点的位置,该值保存在 ExceptionRecord.ExceptionAddress 的地址。
  3. 获取线程上下文环境,调用 GetThreadContext获取,获得到线程上下文环境后,就可以获取到当前状态下各个寄存器的值。
  4. 接下来需要修复EIP,原因是对于不同类型的断点,断下后 EIP 的位置会有所不同,对于软件断点 int3,断下后 EIP 会位于原先地址+1字节的位置,因此这里需要将 EIP-1,修复 EIP
  5. 显示反汇编,对于常规调试器,要能够实时看到程序的反汇编代码,所以断下后,至少要能够显示断点周围的反汇编代码,这个功能后面看情况决定是否加上。
  6. 等待用户命令,调试器最主要的一个特征就是对代码进行调试,包括但不限于单步,步进,执行等操作。这里通过 while 循环等待用户执行的命令,若用户未执行命令,就一直等下去。这里参考了 KiDispatchException 在调用完 DbgkForwardException 后也会等待处理结果,判断异常是否得到了处理,若未被处理则会分发给 VEHSEH 去处理。而我们这里调试器就会一直等待 WaitUserForCommand 传回来的结果,该函数将手动实现,后面涉及到单步操作时会学习到。

2 内存断点

调试的本质就是异常处理,要调试一个程序,就让它出异常。

2.1 内存断点原理

内存断点的原理是:修改目标地址所在物理页属性。修改的是一整块 4kb 内存页属性。错误类断点。

在 OllyDbg 中我们可以设置内存访问、写断点。

在用户层,调试器调用的是 VirtualProtectEx (Ex:跨进程)来修改被调试进程的物理页属性来达到实现内存断点的目的。以下为 VirtualProtectEx 函数原型:

1
2
3
4
5
6
7
8
//改变内存地址内存页的属性
BOOL VirtualProtectEx(
IN HANDLE hProcess, // 要修改内存的进程句柄
IN LPVOID lpAddress, // 要修改内存的起始地址
IN SIZE_T dwSize, // 页区域大小
IN DWORD flNewProtect, // 新内存页属性
OUT PDWORD lpflOldProtect //原内存页属性 用于保存改变前的属性
)

注意:该函数一定要指定一个地址来接收最后一个参数的输出值,否则该函数会调用失败。

针对内存访问、写断点,对应该函数第四个参数 flNewProtect取值,修改它的值,达到修改所指内存所在物理页的 PTE 属性:

  • PAGE_NOACCESS:不可访问(PTE.P位 = 1)
  • PAGE_EXECUTE_READ:可读可执行,不可写(PTE.P位 = 1, PTE.R/W = 0)

如果不考虑最小权限原则,在设置内存页不可读写访问时,可均将内存页属性设置为 PAGE_NOACCESS

2.2 内存断点分发和处理

2.2.1 设置异常(调试器)

当我们以打开方式调试一个进程时,可以接收到 DEBUG_EVENT.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT 调试信息。此时设置内存断点的位置可以为 DEBUG_EVENT.u.CREATE_PROCESS_DEBUG_INFO.lpStartAddress(OEP)。注意,接收到创建进程消息时初始断点这些都还没设置。(只有在第一次进入 3 环时在 ntdll!LdrpInitializeProcess APC 函数中,设置好初始断点之后,返回 0 环,然后再次返回 3 环才会执行 kernel32!BaseProcessStartThunk 函数)。

所以我们应该设置一个即将要执行到的地址,让其触发断点,然后让调试器接收。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _CREATE_PROCESS_DEBUG_INFO {
HANDLE hFile;
HANDLE hProcess;
HANDLE hThread;
LPVOID lpBaseOfImage;
DWORD dwDebugInfoFileOffset;
DWORD nDebugInfoSize;
LPVOID lpThreadLocalBase;
LPTHREAD_START_ROUTINE lpStartAddress; // OEP
LPVOID lpImageName;
WORD fUnicode;
} CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO;

所以设置内存断点如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// while(WaitForDebugEvent(&debugEvent, INFINITE))
switch(DebugEvent.dwDebugEventCode)
{
//进程创建的时候,在OEP处设置内存访问断点
case CREATE_PROCESS_DEBUG_EVENT:
SetMemoryBreakPoint(&DebugEvent);
break;
case EXCEPTION_DEBUG_EVENT:
CommonExceptionHandler(&DebugEvent);
break;
}

BOOL SetMemoryBreakPoint(DEBUG_EVENT* pDebugEvent)
{
BOOL bRet = FALSE;
DWORD dwOldProtect = 0;

bRet = VirtualProtectEx(pDebugEvent->u.hProcess,pDebugEvent->u.lpThreadLocalBase,sizeof(WORD),PAGE_NOACCESS,&dwOldProtect);

return bRet;
}

2.2.2 异常分发(被调试进程)

当被调试进程访问到OEP时,将会触发相应的页异常,进入异常处理流程,并最终将该调试事件发送到调试对象,接下来交由调试器接管,所以本质上,还是异常处理流程。

内存断点的执行流程可以完全参考软件断点的执行流程,仅有开始的异常处理函数不一样(0xE号中断属错误类异常)。Windows XP 异常处理(一)异常采集1.3查表。

14.png

2.2.3 修复软件断点(调试器)

当 OEP 处的软件断点被触发后,消息循环将接收到 EXCEPTION_DEBUG_EVENT 异常消息,此时我们使用一个公共异常处理函数 CommonExceptionHandler 来处理各种异常。

由于软件断点,内存断点,都是通过异常分发流程执行而来的,所以调试器在收到异常调试事件后,需要判断出是哪种类型的异常(断点)。可以通过 EXCEPTION_RECORD.ExceptionCode 来判断属于哪一种异常断点。

  1. 在事件循环中收到 DebugEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT,此时 u 对应结构为 EXCEPTION_DEBUG_INFO

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    typedef struct _EXCEPTION_DEBUG_INFO {
    EXCEPTION_RECORD ExceptionRecord;
    DWORD dwFirstChance;
    } EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;

    typedef struct _EXCEPTION_RECORD {
    NTSTATUS ExceptionCode;
    ULONG ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    ULONG NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    } EXCEPTION_RECORD;
  2. 此时的需要根据 EXCEPTION_RECORD.ExceptionCode 判断是哪一种异常,这里我们关心的是内存访问异常。

    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
    #define WAIT_IO_COMPLETION                  STATUS_USER_APC
    #define STILL_ACTIVE STATUS_PENDING
    #define EXCEPTION_ACCESS_VIOLATION STATUS_ACCESS_VIOLATION // 内存访问
    #define EXCEPTION_DATATYPE_MISALIGNMENT STATUS_DATATYPE_MISALIGNMENT
    #define EXCEPTION_BREAKPOINT STATUS_BREAKPOINT // int3
    #define EXCEPTION_SINGLE_STEP STATUS_SINGLE_STEP
    #define EXCEPTION_ARRAY_BOUNDS_EXCEEDED STATUS_ARRAY_BOUNDS_EXCEEDED
    #define EXCEPTION_FLT_DENORMAL_OPERAND STATUS_FLOAT_DENORMAL_OPERAND
    #define EXCEPTION_FLT_DIVIDE_BY_ZERO STATUS_FLOAT_DIVIDE_BY_ZERO
    #define EXCEPTION_FLT_INEXACT_RESULT STATUS_FLOAT_INEXACT_RESULT
    #define EXCEPTION_FLT_INVALID_OPERATION STATUS_FLOAT_INVALID_OPERATION
    #define EXCEPTION_FLT_OVERFLOW STATUS_FLOAT_OVERFLOW
    #define EXCEPTION_FLT_STACK_CHECK STATUS_FLOAT_STACK_CHECK
    #define EXCEPTION_FLT_UNDERFLOW STATUS_FLOAT_UNDERFLOW
    #define EXCEPTION_INT_DIVIDE_BY_ZERO STATUS_INTEGER_DIVIDE_BY_ZERO
    #define EXCEPTION_INT_OVERFLOW STATUS_INTEGER_OVERFLOW
    #define EXCEPTION_PRIV_INSTRUCTION STATUS_PRIVILEGED_INSTRUCTION
    #define EXCEPTION_IN_PAGE_ERROR STATUS_IN_PAGE_ERROR
    #define EXCEPTION_ILLEGAL_INSTRUCTION STATUS_ILLEGAL_INSTRUCTION
    #define EXCEPTION_NONCONTINUABLE_EXCEPTION STATUS_NONCONTINUABLE_EXCEPTION
    #define EXCEPTION_STACK_OVERFLOW STATUS_STACK_OVERFLOW
    #define EXCEPTION_INVALID_DISPOSITION STATUS_INVALID_DISPOSITION
    #define EXCEPTION_GUARD_PAGE STATUS_GUARD_PAGE_VIOLATION
    #define EXCEPTION_INVALID_HANDLE STATUS_INVALID_HANDLE
    #define CONTROL_C_EXIT STATUS_CONTROL_C_EXIT

    #define STATUS_ACCESS_VIOLATION 0xc0000005 // 内存访问
    #define STATUS_ARRAY_BOUNDS_EXCEEDED 0xc000008c
    #define STATUS_BAD_COMPRESSION_BUFFER 0xc0000242
    #define STATUS_BREAKPOINT 0x80000003
    #define STATUS_DATATYPE_MISALIGNMENT 0x80000002
    #define STATUS_FLOAT_DENORMAL_OPERAND 0xc000008d
    #define STATUS_FLOAT_DIVIDE_BY_ZERO 0xc000008e
    #define STATUS_FLOAT_INEXACT_RESULT 0xc000008f
    #define STATUS_FLOAT_INVALID_OPERATION 0xc0000090
    #define STATUS_FLOAT_OVERFLOW 0xc0000091
    #define STATUS_FLOAT_STACK_CHECK 0xc0000092
    #define STATUS_FLOAT_UNDERFLOW 0xc0000093
    #define STATUS_FLOAT_MULTIPLE_FAULTS 0xc00002b4
    #define STATUS_FLOAT_MULTIPLE_TRAPS 0xc00002b5
    #define STATUS_GUARD_PAGE_VIOLATION 0x80000001
    #define STATUS_ILLEGAL_FLOAT_CONTEXT 0xc000014a
    #define STATUS_ILLEGAL_INSTRUCTION 0xc000001d
    #define STATUS_INSTRUCTION_MISALIGNMENT 0xc00000aa
    #define STATUS_INVALID_HANDLE 0xc0000008
    #define STATUS_INVALID_LOCK_SEQUENCE 0xc000001e
    #define STATUS_INVALID_OWNER 0xc000005a
    #define STATUS_INVALID_PARAMETER_1 0xc00000ef
    #define STATUS_INVALID_SYSTEM_SERVICE 0xc000001c
    #define STATUS_INTEGER_DIVIDE_BY_ZERO 0xc0000094
    #define STATUS_INTEGER_OVERFLOW 0xc0000095
    #define STATUS_IN_PAGE_ERROR 0xc0000006
    #define STATUS_KERNEL_APC 0x100
    #define STATUS_LONGJUMP 0x80000026
    #define STATUS_NO_CALLBACK_ACTIVE 0xc0000258
    #define STATUS_NO_EVENT_PAIR 0xc000014e
    #define STATUS_PRIVILEGED_INSTRUCTION 0xc0000096
    #define STATUS_SINGLE_STEP 0x80000004
    #define STATUS_STACK_OVERFLOW 0xc00000fd
    #define STATUS_SUCCESS 0x0
    #define STATUS_THREAD_IS_TERMINATING 0xc000004b
    #define STATUS_TIMEOUT 0x102
    #define STATUS_UNWIND 0xc0000027
    #define STATUS_WAKE_SYSTEM_DEBUGGER 0x80000007
  3. 这里面可以看到我们比较熟悉的 0xC0000005,访问违例,这也正是内存断点引发的异常情况。从而我们可以通过 switch…case 语句完成对不同类型异常的分别处理。

  4. 需要内存断点过滤由于内存断点是对整个物理页下断,因此断下的地方可能并不是我们下断的地方,所以这里取地址判断是否为下断处,若不是则直接放过执行,若是则进行处理。

在函数 CommonExceptionHandler 中判断异常类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL CommonExceptionHandler(DEBUG_EVENT* pDebugEvent)
{
BOOL bRet = FALSE;
PEXCEPTION_DEBUG_INFO pExceptionInfo = (PEXCEPTION_DEBUG_INFO)pDebugEvent->u;
NTSTATUS ExceptionCode = pExceptionInfo->ExceptionRecord.ExceptionCode;

switch(ExceptionCode)
{
case EXCEPTION_BREAKPOINT:
bRet = Int3ExceptionProc(pExceptionInfo);
break;
case EXCEPTION_ACCESS_VIOLATION:
bRet = MemoryAccessExceptionProc(pExceptionInfo);
break;
}

return bRet;
}

这里之所以用了 EXCEPTION_BREAKPOINT 替代 STATUS_BREAKPOINT 是为了看着更清晰,本质上是一样的,只是 Windows 又定义了一遍它的宏。

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
BOOL MemoryAccessExceptionProc(EXCEPTION_DEBUG_INFO* pExceptionInfo)
{
BOOL bRet = FALSE;
CONTEXT Context;
DWORD dwAccessFlag;
DWORD dwAccessAddr;
DWORD UselessTemp;

//1.获取异常信息,修改内存属性
dwAccessFlag = pExceptionInfo->ExceptionRecord.ExceptionInformation[0];
dwAccessAddr = pExceptionInfo->ExceptionRecord.ExceptionInformation[1];
printf("内存断点 %x 0x%p\n", dwAccessFlag, dwAccessAddr);
VirtualProtectEx(hDebugeeProcess, (PVOID)dwAccessAddr, 1, dwOriginalProtect, &UselessTemp);

//2.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebugeeThread,&Context);

//3.修复EIP,内存断点不需要修复EIP,软件断点需要

//4.显示反汇编

//5.等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}
return bRet;
}

需要注意EXCEPTION_RECORD结构 的最后一个成员 ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS] 对于其中两类异常有特殊用途:

ExceptionCode ExceptionInformation[0] ExceptionInformation[1] ExceptionInformation[2]
EXCEPTION_ACCESS_VIOLATION(0xC0000005)
该线程试图对没有权限的虚拟地址进行读取或写入。
指明导致异常的原因:
0:访问不可读内存产生的异常。
1:向不可写内存写数据产生的异常。
8:DEP异常,执行不可执行的内存页。
给出无法访问的虚拟地址,不是产生异常的地址
EXCEPTION_RECORD.ExceptionAddress 是产生异常的位置。
EXCEPTION_IN_PAGE_ERROR(0xC0000006)
这个异常是在读写映射文件而不是页文件的时候导致。例如,如果在网络上运行程序时网络连接丢失,可能会发生此异常。
指明导致异常的原因:
0:访问不可读内存产生的异常。
1:向不可写内存写数据产生的异常。
8:DEP异常,执行不可执行的内存页。
给出无法访问的虚拟地址,不是产生异常的地址
EXCEPTION_RECORD.ExceptionAddress 是产生异常的位置。
指定导致异常的底层NTSTATUS代码。

也可以参考汉化版SDK API文档:Win32API参考手册

3 硬件断点

3.1 硬件断点原理

前两篇介绍了软件断点与内存断点,这两种类型的断点都会留下明显的痕迹,也有相应的应对措施,对于软件断点,可以用CRC校验来检测;对于内存断点,可以起一个线程不断刷新PTE的属性,防止其被修改。

硬件断点不依赖于内存和数据,依赖于 CPU 提供的 DR0~DR7 寄存器。

原理:检测到 CPU 正在读的地址和 DR0~DR3 寄存器的值相等时,就会触发硬件断点异常。错误类断点。

如下图为与硬件断点相关的调试寄存器,x86 上是 32 bits(x86-64 处理的每个寄存器都是 64 bits)。具体见 卷 3A—Chapter 17 Debug, Branch Profile, TSC, and Intel® Resource Director Technology. (Intel® RDT) Features—17.2 Debug Registers

15.png

DR寄存器一共有 8 个,其中DR4、DR5保留未使用,DR0~DR3存放硬件断点线性地址,DR6控制硬件断点类型,DR7控制DR0~DR3寄存器的属性。

一、DR0~DR3 寄存器

这四个寄存器用于设置硬件断点,用来保存产生硬件断点的32位线性地址。由于只有4个断点寄存器,所以最多只能设置4个硬件调试断点

二、DR7

DR7也叫调试控制寄存器,是最重要的寄存器,它控制着断点(DR0~DR3)的各类属性,其L0/G0~L3/G3、R/W0~R/W3、LEN0~LEN3分别用来控制DR0~DR3的属性。

  1. L0/G0~L3/G3:控制DR0~DR3的有效标志(Lx/Gx为1说明DRx有一个断点地址)

    • Lx == 1:局部的,表示该断点仅对当前线程有用,每次异常后,Lx都会被清零(clear)。
    • Gx == 1:全局的,表示该断点在所有线程都有用,每次异常后,Gx不会被清零。
  2. LENx:DR0~DR3的断点长度,给几个字节数据下断点,执行断点较特殊只有一字节。00(1字节) 、01(2字节)、11(4字节),10未定义。

  3. R/Wx:DR0~DR3的断点类型:

    • CR4.DE = 1 时:00(执行断点),01(数据写入时断点),10(I/O 读写),11(数据读取或写入,但不包括读取指令 instruction fetches)。
    • CR4.DE = 0 时:00(执行断点),01(数据写入时断点),10(未定义),11(数据读取或写入,但不包括读取指令 instruction fetches)。
  4. LE/GE:这个在 P6 系列处理器、更高版本的 IA-32 处理器和 Intel 64 处理器不支持此功能。如果被置位,那么CPU将会追踪精确的数据断点。LE 是局部的,GE 是全局的。

  5. GD:如果置位,追踪下一条指令是否会访问调试寄存器。如果是,产生异常。GD 置位后,下面的 DR6 的 BD 才有效。

    当断点对应的 DR7.R/Wx = 00 时,使用执行断点,在断点寄存器 DR0-DR3 里设置断点的地址。当 DR7.GD = 1 时,使用 DR 寄存器访问触发方式。在后续的指令中访问任何一个 DR 寄存器将产生 #DB 异常。

三、DR6

调试状态寄存器,这个寄存器主要是在调试异常产生后,报告产生调试异常的相关信息。

设置硬件断点时需要同时修改DR0/1/2/3及DR7,当设置的断点被命中时,DR6寄存器才会有值,里面的值才有效。

注意:

  • 硬件调试断点产生的异常是 STATUS_SINGLE_STEP(单步异常)。

  • B0~B3:标识DR0~DR3中哪个寄存器触发的异常(也用来区别是DR寄存器单步异常还是Eflags的TF单步异常)。

但是还有一种情况也会产生单步异常 :

16.png

当Eflags的TF位置1时,产生的异常也是单步异常。DR6的作用就是用来确定产生的是哪一种单步异常

  • 方法一:看 DR6 bit 14 的 BS 位,调试异常是由单步执行模式触发的(通过 EFLAGS 寄存器中的 TF 标志启用)。 单步模式是最高优先级的调试异常。 当 BS 标志被设置时,任何其他调试状态位 bit 也可能被设置。
  • 方法二:当B0-B3中有值时,则可以确定是某一个硬件断点触发产生的单步异常。若B0-B3的值均为空,说明是Eflags的TF值置1产生的单步异常。

可参考:调试寄存器 原理与使用:DR0-DR7

3.2 硬件断点分发和处理

3.2.1 设置异常(调试器)

下断时将需要下断的线性地址写入 Dr0~Dr3 中任意一个寄存器中,同时修改 DR7 寄存器,当CPU执行到该线性地址时,发现与调试寄存器中的值相同,便会断下,触发异常。注意一点,这里下断是修改当前线程Context中记录的调试寄存器的值,线程间是隔离的,因为设置硬件断点不会影响到别的线程。

在上面的软件断点和内存断点下断位置是调试器接收到 CREATE_PROCESS_DEBUG_EVENT 进程创建消息将时,将相应的断点下在 OEP 处。但是硬件断点不能在接收到进程创建消息时将断点下在 OEP 处,因为硬件断点的是通过修改 Context.DRx 来下断的,当调试器收到被调试进程的进程创建消息时,被调试进程还在 0 环,还没有进去 3 环,此时被调试的线程在 3 环还不能用。

所以:可以在调试器接收到 CREATE_PROCESS_DEBUG_EVENT 进程创建消息时,在被调试进程的 OEP处下一个软件断点/内存断点,当执行到 0EP 的软件断点/内存断点被触发时,在EXCEPTION_DEBUG_EVENT 的条件下调用硬件断点下断函数,将断点地址设置为 OEP+1 即可。

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
// while(WaitForDebugEvent(&debugEvent, INFINITE))
switch(DebugEvent.dwDebugEventCode)
{
//进程创建的时候,在OEP处设置内存访问断点
case CREATE_PROCESS_DEBUG_EVENT:
SetMemoryBreakPoint(&DebugEvent);
break;
case EXCEPTION_DEBUG_EVENT:
SetHardBreakPoint(&DebugEvent);
break;
}

BOOL SetHardBreakPoint(DEBUG_EVENT* pDebugEvent)
{
BOOL bRet = FALSE;
CONTEXT Context = { 0 };
PEXCEPTION_DEBUG_INFO pExceptionInfo = (PEXCEPTION_DEBUG_INFO)pDebugEvent->u;
EXCEPTION_RECORD pExceptionRecord = (PEXCEPTION_RECORD)pExceptionInfo->ExceptionRecord;

//1.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
bRet = GetThreadContext(pDebugEvent->dwThreadId, &Context);

//2.设置断点位置(这里选DR0作为断点寄存器)
Context.Dr0 = (DWORD)((CHAR*)pExceptionRecord->ExceptionAddress + 1);

//3.设置内存访问断点
Context.Dr7 |= 1;

//4.设置断点长度(内存访问断点长度必须为1)R/W0 = 00,LEN0 = 00
Context.Dr7 &= 0xfff0ffff;

//5.设置线程上下文
bRet = SetThreadContext(pDebugEvent->dwThreadId, &Context);

return bRet;
}

3.2.2 异常分发(被调试进程)

硬件断点仍然是通过触发异常来实现调试,所以调试的本质就是异常的分发。硬件断点的执行流程也可以完全参考软件断点的执行流程,仅有开始的异常处理函数不同:

17.png

3.3.3 修复硬件断点(调试器)

由于单步异常有由于硬件断点产生的也有 EflagsTF 位置 1 产生的,因此在处理函数内部需要使用 DR6 判断一下是否为硬件断点导致的异常。

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
BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO * pExceptionInfo)
{
CONTEXT Context;
BOOL bRet = FALSE;

//1.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebugeeThread,&Context);

//2.判断是否是硬件断点导致的异常
if(Context.Dr6&0xF) //B0~B3不为空
{
//显示断点信息
printf("硬件断点 %d 0x%p\n",Context.Dr7 & 0x00030000,Context.Dr0);

//将断点去除
Context.Dr0 = 0;
Context.Dr7 &= 0xfffffffe;
}
else
{
//显示断点信息
printf("单步异常 0x%p\n",Context.Eip);

//去除单步异常
Context.EFlags &= 0xfffffeff;
}

//3.等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}
return bRet;
}

4 单步步入

4.1 单步步入原理

单步步入原理:CPU在执行完一条指令后,如果检测到此时 EflagsTF = 1,则产生单步步入中断(中断类型码为1,#DB),引发中断异常,执行 1 号中断处理程序nt!KiTrap01陷阱类断点。

注意:单步步入异常和硬件断点都是单步异常(都是EXCEPTION_RECORD.ExceptionCode == EXCEPTION_SINGLE_STEP),但是有以下区别:

  • 硬件断点:断点地址保存在 DR0~4,异常触发后 DR6B0~3 至少有一位为 1错误类异常。
  • 单步步入:Eflags.TF = 1DR6B0~3 = 0陷阱类异常。
  • 相同点:寄存器的值都是和线程相关的。

想要实现单步步入,有很多手段,可以通过不断的下软件/硬件断点,每执行一行,就下一个 INT3,然后恢复再重新执行。虽然这是可行的办法,但是过于复杂,因此 Intel 在设计 CPU 时考虑到了这一点,调试程序是必不可少的手段,因而在 Eflags 里设置了一个 TF 位。

4.2 单步步入分发和处理

4.2.1 设置异常(调试器)

单步步入有着特殊性,只要 Eflags.TF = 1,CPU 就会单步走,所以如果要从某个特定地址下一个单步步入断点,就要先在该地址设置一个软件/内存/硬件断点,当该软件/内存/硬件断点触发后再设置单步步入断点。

所以:可以在调试器接收到 CREATE_PROCESS_DEBUG_EVENT 进程创建消息时,在被调试进程的 OEP处下一个软件/内存/硬件断点,当执行到 0EP 的软件/内存/硬件断点被触发时,在EXCEPTION_DEBUG_EVENT 的条件下调用单步步入断点下断函数,将断点地址设置为 OEP 即可。

单步步入的实现,就是将 Context 记录的 EflagsTF1

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
// while(WaitForDebugEvent(&debugEvent, INFINITE))
switch(DebugEvent.dwDebugEventCode)
{
//进程创建的时候,在OEP处设置内存访问断点
case CREATE_PROCESS_DEBUG_EVENT:
SetMemoryBreakPoint(&DebugEvent);
break;
case EXCEPTION_DEBUG_EVENT:
SetSingleStepBreakPoint(&DebugEvent);
break;
}

BOOL SetSingleStepBreakPoint(DEBUG_EVENT* pDebugEvent);
{
BOOL bRet = FALSE;
CONTEXT Context = { 0 };
//1.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
bRet = GetThreadContext(g_hThread,&Context);

//2.设置单步执行
Context.EFlags |= 0x100;

//3.设置线程上下文
bRet = SetThreadContext(g_hThread, &Context);

return bRet;
}

设置单步步入断点后,然后返回到主函数中,主函数调用 ContinueDebugEvent 返回 DBG_CONTINUE ,在内核中的 KiDispatchException 便会返回 TRUE,让程序继续执行,执行每一条指令之后判断 Eflags.TF == 1,就又会产生单步异常,又分发给调试器,如此循环往复直到我们修复 Eflags.TF = 0 为止。

4.2.2 异常分发(被调试进程)

当单步步入的异常产生后,与硬件断点的执行流程是一样的,查找 1 号中断处理函数。并最终将该类型调试事件发送给调试器。

17.png

4.2.3 修复单步步入(调试器)

单步异常产生时,EXCEPTION_RECORD.ExceptionCode 都等于 EXCEPTION_SINGLE_STEP

由于单步异常有由于硬件断点产生的也有 Eflags == 1 产生的,因此在处理函数内部需要使用 DR6 判断一下是否为硬件断点导致的异常,然后用 Eflags.TF == 1 判断一下是否是单步步入异常。

如果是单步步入异常,我们就要将 Eflags.TF = 0 进行修复。

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
BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO * pExceptionInfo)
{
CONTEXT Context;
BOOL bRet = FALSE;

//1.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebugeeThread,&Context);

//2.判断是否是硬件断点导致的异常
if(Context.Dr6&0xF) //B0~B3不为空
{
//显示断点信息
printf("硬件断点 %d 0x%p\n",Context.Dr7 & 0x00030000,Context.Dr0);

//将断点去除
Context.Dr0 = 0;
Context.Dr7 &= 0xfffffffe;
}
else
{
//显示断点信息
printf("单步异常 0x%p\n",Context.Eip);

//去除单步异常
Context.EFlags &= 0xfffffeff;
}

//3.等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}
return bRet;
}

5 单步步过

5.1 单步步过原理

原理:实现的原理是遇到 call 指令(好几种)后,在返回地址下断,然后执行CPU,便可以实现单步步过。

F8 是在下一条地址下断点,不是执行到下一条。

原理很简单,但是实现起来稍微有一些麻烦,需要借助单步步入Eflags.TF 位。

5.2 设置单步步过

假设给定一个地址,需要从该地址开始执行单步步过操作。可以按照以下方法来实现:

  1. 在目标地址下一个 int3 断点,该断点被触发后,将 0xCC 修改为原来的数据,然后 Context.Eip--
  2. 然后判断当前的地址是否是 CALLE8/FF15),如果是的话计算返回地址,并在返回地址处再下一个 int3 断点,如果不是的话就下 Eflags.TF = 1 断点。
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
// while(WaitForDebugEvent(&debugEvent, INFINITE))
switch(DebugEvent.dwDebugEventCode)
{
// int3 断点被触发
case EXCEPTION_DEBUG_EVENT:
CommonExceptionHandler(&DebugEvent);
break;
}

// 调试器异常分发器
BOOL CommonExceptionHandler(DEBUG_EVENT* pDebugEvent)
{
BOOL bRet = TRUE;
EXCEPTION_DEBUG_INFO ExceptionInfo = pDebugEvent->u.Exception;

switch (ExceptionInfo.ExceptionRecord.ExceptionCode)
{
//int3异常
case EXCEPTION_BREAKPOINT:
bRet = Int3ExceptionProc(&ExceptionInfo);
break;
default:
break;
}
return TRUE;
}

// int3 断点异常处理
BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO* pExceptionInfo)
{
g_bRet = FALSE;
CONTEXT Context = { 0 };
DWORD dwBuffer = 0;

//1.将INT3修复为原来的数据(如果是系统断点,不用修复)
if ( /*IsSystemInt3(pExceptionInfo)*/FALSE)
return TRUE;
else
WriteProcessMemory(g_hDebuggeeProcess, pExceptionInfo->ExceptionRecord.ExceptionAddress,\
&g_int3OriginalCode, 1, NULL);

//2.显示断点位置

//3.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(g_hDebugeeThread, &Context);

//4.修复EIP
Context.Eip--;

//5.显示反汇编

//6.等待用户命令
while (g_bRet == TRUE)
{
switch(g_szchar)
{
case 'p':
// 1. 读取当前EIP指向的机器码
ReadProcessMemory(g_hDebuggeeProcess, (LPVOID)Context.Eip, &dwBuffer, 2, NULL);
if((wBuffer & 0xFF) == 0xE8)
{
//2. 在当前地址之后的第5个字节设置软件断点(E8指令占5个字节)
SetInt3BreakPoint((LPVOID)(Context.Eip+5));
}
else if(dwBuffer == 0x15FF)
{
//2. 在当前地址之后的第6个字节设置软件断点(FF15指令占6个字节)
SetInt3BreakPoint((LPVOID)(Context.Eip+6));
}
else
{
//3. 不是CALL指令,设置陷阱标志位触发单步异常即可
Context.EFlags |= 0x100;
}
break;
}// end of switch
}// end of while

//4. 设置线程上下文
bRet = SetThreadContext(g_hDebugeeThread, &Context);

return bRet;
}

参考单步步入&单步步过

5.3 Fake F8

在 OllyDbg 中单步步过的快捷键是 F8,可以用调试器下单步步过的特点来反调试,让调试人员跟丢。如下代码:

1
2
0x00421002    FFD6            call esi
0x00421004 E9 61CF0300 jmp ntdll.773A4A04

当我们目前断点断在地址 0x00421002 并执行到该地址,下面我们 F8 单步步过,正常情况下会断在函数返回地址 0x00421004。因为调试器是在返回地址的地方下断的。

但是如果我们在 esi 指向的地址里修改了函数的返回地址,esi 指向的函数就会返回到其他地方。如下示例(生成Release版本):

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
#include "stdafx.h"

int main(int argc, char* argv[]);
bool bExit = false;

void FakeRetAddress()
{
bExit = true;
printf("构造的返回函数执行了!\n");
__asm
{
lea eax, dword ptr ds:[main];
add esp, 0xC
mov dword ptr ds:[esp], eax
}
return;
}

void RetAddress()
{
__asm call FakeRetAddress
return;
}

void TransferAddress2()
{
__asm call RetAddress
return;
}

void TransferAddress1()
{
__asm call TransferAddress2
return;
}

int main(int argc, char* argv[])
{
if(bExit == false)
{
TransferAddress1();
}
return 0;
}

汇编代码如下:

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
// FakeRetAddress()
00401000 68 30704000 push 20220902.00407030 // ASCII "构造的返回函数执行了!\n"
00401005 C605 E8984000>mov byte ptr ds:[0x4098E8],0x1
0040100C E8 6F000000 call 20220902.00401080
00401011 83C4 04 add esp,0x4
00401014 3E:8D05 60104>lea eax,dword ptr ds:[0x401060]
0040101B 83C4 0C add esp,0xC
0040101E 3E:890424 mov dword ptr ds:[esp],eax
00401022 C3 retn

00401030 E8 CBFFFFFF call 20220902.00401000 // RetAddress()
00401035 C3 retn

00401040 E8 EBFFFFFF call 20220902.00401030 // TransferAddress2()
00401045 C3 retn

00401050 E8 EBFFFFFF call 20220902.00401040 // TransferAddress1()
00401055 C3 retn

00401060 A0 E8984000 mov al,byte ptr ds:[0x4098E8] // main()
00401065 84C0 test al,al
00401067 75 05 jnz short 20220902.0040106E
00401069 E8 E2FFFFFF call 20220902.00401050
0040106E 33C0 xor eax,eax
00401070 C3 retn

这行代码很必要:

1
add esp, 0xC

程序在执行到 FakeRetAddress 函数时的堆栈如下:

18.png

如果没有如下代码:

1
add esp, 0xC

则程序会将 0x0019FF24 里的值修改为 0x00401060,然后执行 main 函数。 main 函数将会继续 0x00401070ret 指令,此时的 esp == 0x0019FF28,然后依次执行堆栈上的返回地址,就达不到 Fake F8 的目的。

esp 提升到正确的位置后,如下图,按 F8 后直接跟丢了,不会断在 0x00401055

19.gif

当有很多 call 时,调试人员如果选择不 F7 跟进去的话,就达到了反调试目的了(不 F7 跟进去破解不了这种方法)。

6 硬件硬件Hook过检测

利用硬件断点不会修改机器码的特性,以及异常处理函数的机制实现。

可以使用硬件断点+VEH硬件断点+顶层异常处理函数。本文将使用前一种方法来进行HOOK,可以绕过 CRC 检测。

本例场景:在 Win32 窗口程序中加载一个自己写的 DLL,在 DLL 中 HOOK MessageBox 函数,修改该函数的第二个参数。但是这种 HOOK 只是当前进程的,没有修改 ntdll.dll 的导出地址,也就是局部 HOOK。

20210610205143998.png

DLL 入口函数:

1
2
3
4
5
6
7
8
int APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved)
{
if (reason == DLL_PROCESS_ATTACH)
{
SetSehHook();
}
return TRUE;
}

DLL 核心逻辑:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include "windows.h"
#include "TlHelp32.h"
#include "stdio.h"
#include "limits.h"

typedef HANDLE(WINAPI *OPENTHREAD) (DWORD dwFlag, BOOL bUnknow, DWORD dwThreadId);
OPENTHREAD g_lpfnOpenThread = NULL;

DWORD g_HookAddr;
DWORD g_HookAddrOffset;

void GetInformation(PCONTEXT context)
{
printf("EAX: %X \nEBX: %X\nECX: %X\nEDX: %X\nESP: %X\nEBP: %X\nESI: %X\nEDI: %X\n",
context->Eax,
context->Ebx,
context->Ecx,
context->Edx,
context->Esp,
context->Ebp,
context->Esi,
context->Edi
);

printf("参数 \n"
"参数1: %X\n"
"参数2: %s\n"
"参数3: %s\n"
"参数4: %s\n",
(HWND) (*(DWORD*)(context->Esp + 0x4)),
(char*)(*(DWORD*)(context->Esp + 0x8)),
(char*)(*(DWORD*)(context->Esp + 0xC)),
(UINT) (*(DWORD*)(context->Esp + 0x10))
);

}

void ModifytheText(PCONTEXT debug_context)
{
char* text = (char*)(*(DWORD*)(debug_context->Esp + 0x8));
int length = strlen(text);

DWORD oldprotect = 0;
//修改PTE.P=1 PTE.R/W=0
VirtualProtect(text, length, PAGE_EXECUTE_READWRITE, &oldprotect);
//修改messagebox的信息
_snprintf(text, length, "Hook 成功");
VirtualProtect(text, length, oldprotect, &oldprotect);
}

void __declspec(naked) OriginalFunc(void)
{
__asm
{
mov edi, edi
jmp[g_HookAddrOffset]
}
}

//异常处理函数
LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{

if ((DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress == g_HookAddr)
{
PCONTEXT pcontext = ExceptionInfo->ContextRecord;
//修改messagebox信息
ModifytheText(pcontext);
//获取寄存器,堆栈信息
GetInformation(pcontext);
pcontext->Eip = (DWORD)&OriginalFunc;
return EXCEPTION_CONTINUE_EXECUTION;
}

}
return EXCEPTION_CONTINUE_SEARCH;
}

void SetSehHook()
{

g_lpfnOpenThread = (OPENTHREAD)GetProcAddress(LoadLibrary(L"kernel32.dll"),"OpenThread");
g_HookAddr = (DWORD)GetProcAddress(GetModuleHandle(L"user32.dll"), "MessageBoxA");
g_HookAddrOffset = g_HookAddr + 2;
printf("MessageBoxA:%X\n", g_HookAddr);

//遍历线程 找到要Hook的地址
HANDLE hTool32 = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hTool32 != INVALID_HANDLE_VALUE)
{
THREADENTRY32 thread_entry32;
thread_entry32.dwSize = sizeof(THREADENTRY32);
HANDLE hHookThrad = NULL;
DWORD dwCount = 0;
if (Thread32First(hTool32, &thread_entry32))
{
do
{
if (thread_entry32.th32OwnerProcessID == GetCurrentProcessId())
{
dwCount++;
//Hook第一条线程
if (dwCount == 1)
{

hHookThrad = g_lpfnOpenThread(
THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION,
FALSE,
thread_entry32.th32ThreadID);

}
}
thread_entry32.dwSize = sizeof(THREADENTRY32);

} while (Thread32Next(hTool32,&thread_entry32));


//注册顶层异常处理函数
SetUnhandledExceptionFilter(ExceptionFilter);

//设置硬件断点
CONTEXT thread_context = { CONTEXT_DEBUG_REGISTERS };
thread_context.Dr0 = g_HookAddr ;
thread_context.Dr7 = 1;
SetThreadContext(hHookThrad, &thread_context);
CloseHandle(hHookThrad);

}
CloseHandle(hTool32);
}

}

可参考《无痕 hook - 硬件断点》。

7 反调试、HOOK文章

Windows内核驱动Hook入门InfinityHook在19041小版本间的兼容性问题自己动手制作一个过保护调试器

调试&检测26种对付反调试的方法

使用时间无关调试技术(Timeless Debugging)高效分析混淆代码

Windows RPC 远程过程调用—初理解

3环的HOOK系列可以看海哥基础班PE部分。

8 Windbg+Win10下载调试符号

Windows10 调试符号匹配下载:

  1. Clash梯子搭好
    • Profiles:到GlaDOS复制链接粘贴后,点击Download,然后选择刚下载的配置。
    • General:勾选“System proxy”、“Start with Windows”,并记好“Port”是多少。
    • Edge浏览器直接测试Google能否访问。
  2. Windows环境变量,在用户变量下添加
    • 在本地先随便建立一个空文件夹,如“C:\Symbols”。
    • _NT_SYMBOL_PATH:C:\Symbols。
    • _NT_SYMBOL_PROXY:127.0.0.1:Port。
  3. Windbg
    • 先随便打开任意一个可执行文件进行调试(下面下载的符号是该文件加载的DLL对应的符号)。
    • 设置符号链接:srv*C:\Symbols*https://msdl.microsoft.com/download/symbols(https不是http)。
    • Windbg命令行输入:!sym moisy(查看下载时的日志)。
    • Windbg命令行输入:.reload /f。查看C:\Symbols已经下载好了一些.pdb符号了,但是不全。
    • Windbg命令行输入:.reload /f .reload /f C:\Windows\System32\ntoskrnl.exe(全路径),下载指定符号,中间可能会中断,由于网路原因多试几次就好了。
    • 此时会下载:ntkrnlmp.exe、ntoskrnl.exe、ntkrnlpa.exe、ntkrnlmp.pdb四个文件夹及其对应文件。
  4. IDA连接调试符号
    • 复制IDA提供的远程Server(如win64_remote64.exe)到已经下载好符号的设备,运行。
    • 这里注意,如果IDA要反汇编ntoskrnl.exe,需要使用步骤3下载的该文件及对应的该文件的调试符号(此处不能使用System32下的那个内核文件,否则内核文件与符号文件时间戳对不上,IDA无法识别,此处应该是为啥.reload /f没有下载内核文件的调试符号,因为时间戳没对上,所以直接下载了4个文件夹和文件),《加密与解密第四版 2.4.1、7.3.2》。
    • 如果IDA中是ntoskrnl.exe,则需要在_NT_SYMBOL_PATH中将步骤3中的“C:\Symbols”替换成“C:\Symbols\ntkrnlmp.pdb\DCD0B9772B46C59FF6E45DFBB3D1AE7B1”,保存。(每次使用Windbg双机调试时又需要换成“C:\Symbols”)。
    • 在IDA中:File-Loadfile-PDB file-OK-远程ServerIP:端口。

DEBUG_EVENT

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
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;

typedef struct _EXCEPTION_DEBUG_INFO {
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;

typedef struct _EXCEPTION_RECORD {
NTSTATUS ExceptionCode;
ULONG ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
ULONG NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;