ʕ •̀ 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;
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) { case EXCEPTION_BREAKPOINT: bRet = Int3ExceptionProc(&ExceptionInfo); break; case EXCEPTION_ACCESS_VIOLATION: break;
default: break; } return TRUE; }
|
1 软件断点
1.1 软件断点的原理
软件断点,就是我们常说的 INT3
(陷阱类异常),它的本质就是将下断处的机器码修改为0xCC(INT3
对应的机器码),实质就是 Inline Hook 修改一字节。陷阱类断点。
如下在 0x004023E8
处按 F2
下一个断点(此时该地址的数据前4字节为0x0000A164
),OllyDbg 并不会直接显示该地址对应的值为0xCC
。
此时打开CheatEngine来验证一下,可以看到0x64
已经被修改为0xCC
。
需要注意的是:
- 在OllyDbg 里面,
int3
的硬编码为0xCC
,int 3
的硬编码为0xCD 03
。
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; 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
| 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; bRet = ReadProcessMemory(pDebugEvent->u.CreateProcessInfo.hProcess, pDebugEvent->u.CreateProcessInfo.lpStartAddress, \ &g_int3OriginalCode, 1, NULL); bRet = WriteProcessMemory(pDebugEvent->u.CreateProcessInfo.hProcess, pDebugEvent->u.CreateProcessInfo.lpStartAddress, &bInt3, 1, NULL); return bRet; }
|
1.2.2 异常分发(被调试进程)
在被调试进程中,触发软件断点的过程如下:
- CPU 检测到
int3
指令。
- 在中断描述符表中找到 3 号中断处理函数
KiTrap03
(ISR)。
- 中断处理函数内部会调用
CommonDispatchException
。
CommonDispatchException
内部又会调用 KiDispatchException
。
- 在
KiDispatchException
中,由于是模拟用户层的软件断点,所以这里直接进入处理用户层异常的跳转,在处理用户异常时,如果不存在 0 环调试器或者 0 环调试器未处理异常,就会调用 DbgkForwardException
试图发送给 3 环调试器。
DbgkForwardException
内部最终会调用 DbgkpSendApiMessage
,它是将调试事件发送给调试对象的消息队列。
- 进入
DbgkpSendApiMessage
,刚开始会判断第二个参数的值,若为 TRUE
,则调用 DbgkpSuspendProcess
将本进程(被调试进程)内除自己外的其它进程挂起,然后调用DbgkpQueueMessage
将发送消息的线程也挂起,此时被调试进程就被挂起了。
- 调试器得到信号之后将会在循环中取出调试事件,并根据异常调试事件结构体列出相应信息(当前寄存器的值,内存情况等),接下来便交由用户自定义的逻辑代码去处理。
- 调试器处理完事件之后调用
ContinueDebugEvent
来回复消息。
1.2.3 修复软件断点(调试器)
当 OEP 处的软件断点被触发后,消息循环将接收到 EXCEPTION_DEBUG_EVENT
异常消息,此时我们使用一个公共异常处理函数 CommonExceptionHandler
来处理各种异常。
当我们在调试器中接到该异常后,我们会执行自己想要实现的代码,但是最后一定要修复该异常,及将替换成 0xCC 地址处的原来的硬编码恢复回去,然后修改 EIP。
调试器处理int3
的流程一般为:
- 调用自定义
IsSytemInt3()
函数判断当前int3
是否为初始断点,若为初始断点则不需要修复(因为该断点不是我们自己触发的)此时让函数 ContinueDebugEvent
直接返回DBG_CONTINUE
(表示异常已处理)。断点处的地址则可以通过DebugEvent(调试事件)->u.Exception->ExceptionRecord.ExceptionAddress来获取。
- 如果不是初始断点,则我们可以实现我们自己写的代码。
- 修复
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;
if ( FALSE) return TRUE; else WriteProcessMemory(g_hDebuggeeProcess, pExceptionInfo->ExceptionRecord.ExceptionAddress, &g_int3OriginalCode, 1, NULL);
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; GetThreadContext(g_hDebugeeThread, &Context);
Context.Eip--; bRet = SetThreadContext(g_hDebugeeThread, &Context);
if (bRet) MessageBox(NULL, TEXT("SUCCESS!"), TEXT("A1v1n"), 0); return bRet; }
|
下面来梳理一下每一步做的事:
- 调用自定义
IsSytemInt3()
函数判断当前int3
是否为初始断点,若为初始断点则不需要修复(因为该断点不是我们自己触发的)此时让函数 ContinueDebugEvent
直接返回DBG_CONTINUE
(表示异常已处理)。断点处的地址则可以通过 DebugEvent->u.Exception->ExceptionRecord.ExceptionAddress
来获取。然后调用 WriteProcessMemory
恢复原来的数据。
- 显示断点的位置,该值保存在
ExceptionRecord.ExceptionAddress
的地址。
- 获取线程上下文环境,调用
GetThreadContext
获取,获得到线程上下文环境后,就可以获取到当前状态下各个寄存器的值。
- 接下来需要修复EIP,原因是对于不同类型的断点,断下后
EIP
的位置会有所不同,对于软件断点 int3
,断下后 EIP
会位于原先地址+1
字节的位置,因此这里需要将 EIP-1
,修复 EIP
。
- 显示反汇编,对于常规调试器,要能够实时看到程序的反汇编代码,所以断下后,至少要能够显示断点周围的反汇编代码,这个功能后面看情况决定是否加上。
- 等待用户命令,调试器最主要的一个特征就是对代码进行调试,包括但不限于单步,步进,执行等操作。这里通过
while
循环等待用户执行的命令,若用户未执行命令,就一直等下去。这里参考了 KiDispatchException
在调用完 DbgkForwardException
后也会等待处理结果,判断异常是否得到了处理,若未被处理则会分发给 VEH
或 SEH
去处理。而我们这里调试器就会一直等待 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; 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
| switch(DebugEvent.dwDebugEventCode) { 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查表。
2.2.3 修复软件断点(调试器)
当 OEP 处的软件断点被触发后,消息循环将接收到 EXCEPTION_DEBUG_EVENT
异常消息,此时我们使用一个公共异常处理函数 CommonExceptionHandler
来处理各种异常。
由于软件断点,内存断点,都是通过异常分发流程执行而来的,所以调试器在收到异常调试事件后,需要判断出是哪种类型的异常(断点)。可以通过 EXCEPTION_RECORD.ExceptionCode
来判断属于哪一种异常断点。
在事件循环中收到 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;
|
此时的需要根据 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 #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
|
这里面可以看到我们比较熟悉的 0xC0000005
,访问违例,这也正是内存断点引发的异常情况。从而我们可以通过 switch…case
语句完成对不同类型异常的分别处理。
需要内存断点过滤:由于内存断点是对整个物理页下断,因此断下的地方可能并不是我们下断的地方,所以这里取地址判断是否为下断处,若不是则直接放过执行,若是则进行处理。
在函数 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;
dwAccessFlag = pExceptionInfo->ExceptionRecord.ExceptionInformation[0]; dwAccessAddr = pExceptionInfo->ExceptionRecord.ExceptionInformation[1]; printf("内存断点 %x 0x%p\n", dwAccessFlag, dwAccessAddr); VirtualProtectEx(hDebugeeProcess, (PVOID)dwAccessAddr, 1, dwOriginalProtect, &UselessTemp); Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; GetThreadContext(hDebugeeThread,&Context);
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。
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的属性。
L0/G0~L3/G3:控制DR0~DR3的有效标志(Lx/Gx为1说明DRx有一个断点地址)
- Lx == 1:局部的,表示该断点仅对当前线程有用,每次异常后,Lx都会被清零(clear)。
- Gx == 1:全局的,表示该断点在所有线程都有用,每次异常后,Gx不会被清零。
LENx:DR0~DR3的断点长度,给几个字节数据下断点,执行断点较特殊只有一字节。00(1字节) 、01(2字节)、11(4字节),10未定义。
R/Wx:DR0~DR3的断点类型:
- CR4.DE = 1 时:00(执行断点),01(数据写入时断点),10(I/O 读写),11(数据读取或写入,但不包括读取指令 instruction fetches)。
- CR4.DE = 0 时:00(执行断点),01(数据写入时断点),10(未定义),11(数据读取或写入,但不包括读取指令 instruction fetches)。
LE/GE:这个在 P6 系列处理器、更高版本的 IA-32 处理器和 Intel 64 处理器不支持此功能。如果被置位,那么CPU将会追踪精确的数据断点。LE 是局部的,GE 是全局的。
GD:如果置位,追踪下一条指令是否会访问调试寄存器。如果是,产生异常。GD 置位后,下面的 DR6 的 BD 才有效。
当断点对应的 DR7.R/Wx = 00
时,使用执行断点,在断点寄存器 DR0-DR3 里设置断点的地址。当 DR7.GD = 1
时,使用 DR 寄存器访问触发方式。在后续的指令中访问任何一个 DR 寄存器将产生 #DB 异常。
三、DR6
调试状态寄存器,这个寄存器主要是在调试异常产生后,报告产生调试异常的相关信息。
设置硬件断点时需要同时修改DR0/1/2/3及DR7,当设置的断点被命中时,DR6寄存器才会有值,里面的值才有效。
注意:
但是还有一种情况也会产生单步异常 :
当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
| switch(DebugEvent.dwDebugEventCode) { 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; Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; bRet = GetThreadContext(pDebugEvent->dwThreadId, &Context); Context.Dr0 = (DWORD)((CHAR*)pExceptionRecord->ExceptionAddress + 1); Context.Dr7 |= 1; Context.Dr7 &= 0xfff0ffff; bRet = SetThreadContext(pDebugEvent->dwThreadId, &Context); return bRet; }
|
3.2.2 异常分发(被调试进程)
硬件断点仍然是通过触发异常来实现调试,所以调试的本质就是异常的分发。硬件断点的执行流程也可以完全参考软件断点的执行流程,仅有开始的异常处理函数不同:
3.3.3 修复硬件断点(调试器)
由于单步异常有由于硬件断点产生的也有 Eflags
的 TF
位置 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; Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; GetThreadContext(hDebugeeThread,&Context); if(Context.Dr6&0xF) { 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; } while(bRet == FALSE) { bRet = WaitForUserCommand(); } return bRet; }
|
4 单步步入
4.1 单步步入原理
单步步入原理:CPU在执行完一条指令后,如果检测到此时 Eflags
的 TF = 1
,则产生单步步入中断(中断类型码为1,#DB),引发中断异常,执行 1 号中断处理程序nt!KiTrap01
。陷阱类断点。
注意:单步步入异常和硬件断点都是单步异常(都是EXCEPTION_RECORD.ExceptionCode == EXCEPTION_SINGLE_STEP
),但是有以下区别:
- 硬件断点:断点地址保存在
DR0~4
,异常触发后 DR6
的B0~3
至少有一位为 1
,错误类异常。
- 单步步入:
Eflags.TF = 1
且 DR6
的B0~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
记录的 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
| switch(DebugEvent.dwDebugEventCode) { 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 }; Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; bRet = GetThreadContext(g_hThread,&Context); Context.EFlags |= 0x100; bRet = SetThreadContext(g_hThread, &Context); return bRet; }
|
设置单步步入断点后,然后返回到主函数中,主函数调用 ContinueDebugEvent
返回 DBG_CONTINUE
,在内核中的 KiDispatchException
便会返回 TRUE
,让程序继续执行,执行每一条指令之后判断 Eflags.TF == 1
,就又会产生单步异常,又分发给调试器,如此循环往复直到我们修复 Eflags.TF = 0
为止。
4.2.2 异常分发(被调试进程)
当单步步入的异常产生后,与硬件断点的执行流程是一样的,查找 1
号中断处理函数。并最终将该类型调试事件发送给调试器。
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; Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; GetThreadContext(hDebugeeThread,&Context); if(Context.Dr6&0xF) { 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; } while(bRet == FALSE) { bRet = WaitForUserCommand(); } return bRet; }
|
5 单步步过
5.1 单步步过原理
原理:实现的原理是遇到 call
指令(好几种)后,在返回地址下断,然后执行CPU,便可以实现单步步过。
F8
是在下一条地址下断点,不是执行到下一条。
原理很简单,但是实现起来稍微有一些麻烦,需要借助单步步入的 Eflags.TF
位。
5.2 设置单步步过
假设给定一个地址,需要从该地址开始执行单步步过操作。可以按照以下方法来实现:
- 在目标地址下一个
int3
断点,该断点被触发后,将 0xCC
修改为原来的数据,然后 Context.Eip--
。
- 然后判断当前的地址是否是
CALL
(E8/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
| switch(DebugEvent.dwDebugEventCode) { 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) { case EXCEPTION_BREAKPOINT: bRet = Int3ExceptionProc(&ExceptionInfo); break; default: break; } return TRUE; }
BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO* pExceptionInfo) { g_bRet = FALSE; CONTEXT Context = { 0 }; DWORD dwBuffer = 0; if ( FALSE) return TRUE; else WriteProcessMemory(g_hDebuggeeProcess, pExceptionInfo->ExceptionRecord.ExceptionAddress,\ &g_int3OriginalCode, 1, NULL); Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; GetThreadContext(g_hDebugeeThread, &Context); Context.Eip--; while (g_bRet == TRUE) { switch(g_szchar) { case 'p': ReadProcessMemory(g_hDebuggeeProcess, (LPVOID)Context.Eip, &dwBuffer, 2, NULL); if((wBuffer & 0xFF) == 0xE8) { SetInt3BreakPoint((LPVOID)(Context.Eip+5)); } else if(dwBuffer == 0x15FF) { SetInt3BreakPoint((LPVOID)(Context.Eip+6)); } else { Context.EFlags |= 0x100; } break; } } 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
| 00401000 68 30704000 push 20220902.00407030 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 00401035 C3 retn
00401040 E8 EBFFFFFF call 20220902.00401030 00401045 C3 retn
00401050 E8 EBFFFFFF call 20220902.00401040 00401055 C3 retn
00401060 A0 E8984000 mov al,byte ptr ds:[0x4098E8] 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
|
这行代码很必要:
程序在执行到 FakeRetAddress
函数时的堆栈如下:
如果没有如下代码:
则程序会将 0x0019FF24
里的值修改为 0x00401060
,然后执行 main
函数。 main
函数将会继续 0x00401070
的 ret
指令,此时的 esp == 0x0019FF28
,然后依次执行堆栈上的返回地址,就达不到 Fake F8
的目的。
将 esp
提升到正确的位置后,如下图,按 F8
后直接跟丢了,不会断在 0x00401055
。
当有很多 call
时,调试人员如果选择不 F7
跟进去的话,就达到了反调试目的了(不 F7
跟进去破解不了这种方法)。
6 硬件硬件Hook过检测
利用硬件断点不会修改机器码的特性,以及异常处理函数的机制实现。
可以使用硬件断点+VEH、硬件断点+顶层异常处理函数。本文将使用前一种方法来进行HOOK,可以绕过 CRC 检测。
本例场景:在 Win32 窗口程序中加载一个自己写的 DLL,在 DLL 中 HOOK MessageBox 函数,修改该函数的第二个参数。但是这种 HOOK 只是当前进程的,没有修改 ntdll.dll
的导出地址,也就是局部 HOOK。
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; VirtualProtect(text, length, PAGE_EXECUTE_READWRITE, &oldprotect); _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; 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); 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++; 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 调试符号匹配下载:
- Clash梯子搭好
- Profiles:到GlaDOS复制链接粘贴后,点击Download,然后选择刚下载的配置。
- General:勾选“System proxy”、“Start with Windows”,并记好“Port”是多少。
- Edge浏览器直接测试Google能否访问。
- Windows环境变量,在用户变量下添加
- 在本地先随便建立一个空文件夹,如“C:\Symbols”。
- _NT_SYMBOL_PATH:C:\Symbols。
- _NT_SYMBOL_PROXY:127.0.0.1:Port。
- 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四个文件夹及其对应文件。
- 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; DWORD dwThreadId; 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; UNLOAD_DLL_DEBUG_INFO UnloadDll; 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;
|