前言
Windows
提供了可以附着指定进程,获取进程的寄存器状态与数值。在调试模式下,当遇到INT 3
指令时会进入软件断点,因此可以在任意地址写入INT 3
指令达到函数调试的效果。
调试钩取
调试钩取则是在需要钩取的函数中写入INT 3
,中断函数执行流程,此时在调试模式下,可以任意修改寄存器或堆栈的值,达到修改参数或读取参数的效果,从而实现钩取的目的。
首先是将附着到指定进程上,进入调试模式,这里是通过DebugActiveProcess
函数,该函数很简单,只需要传入需要附着的进程号即可。
1 2 3
| BOOL DebugActiveProcess( [in] DWORD dwProcessId );
|
附着完毕之后,则需要获取进程的状态,进程可能会遇到各种各样的事件,我们需要捕获这些事件做出相应的处理。这些事件则是通过DEBUG_EVENT
进行存储。可以看到事件有非常多,这里我们只需要关注异常调试事件,这是因为我们需要通过INT 3
中断去完成钩取操作。除此之外还需要关注CREATE_PROCESS_DEBUG_INFO
事件,则是因为,我们需要一个契机,能够修改希望钩取函数的首条指令为INT 3
指令,这样当运行到指定函数时就会触发软件中断,我们就可以完成响应的钩取操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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;
|
那么为了等待事件的到来,在附着了进程之后,还需要配合WaitForDebugEvent
函数,该函数接受两个参数,一个是用于存储事件的结构体,一个是等待事件。
1 2 3 4
| BOOL WaitForDebugEvent( [out] LPDEBUG_EVENT lpDebugEvent, [in] DWORD dwMilliseconds );
|
那么在等待事件之后就需要处理相应的事件,首先需要处理的事件就是CREATE_PROCESS_DEBUG_INFO
事件,为了在程序执行时可以钩取指定函数。这里需要判断状态码是否为CREATE_PROCESS_DEBUG_EVENT
,紧接着就可以进入到钩取的处理,修改指定函数的指令。
1 2 3 4 5 6 7
| .... if (de.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) { HookFunction(&de); } ....
|
由于我们需要读写进程内存空间,需要配合ReadProcessMemory
与WriteProcessMemory
函数。首先我们需要获取需要钩取函数的地址,这里以CreateProcessW
为例子,通过进程的调试结构体,获取进程句柄,然后在读取CreateProcessW
函数的首个字节并存储,方便后续的还原操作,最后就是将INT3
指令写入,为了后续的中断处理。
1 2 3 4 5 6 7 8 9 10 11 12 13
| ... HMODULE hMoudle = GetModuleHandleW(L"kernel32.dll"); if (hMoudle == NULL) { std::cerr << "Get Module ERROR" << std::endl; exit(-1); } g_pfCreateProcess = GetProcAddress(hMoudle, "CreateProcessW"); memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO)); ReadProcessMemory(g_cpdi.hProcess, g_pfCreateProcess,&g_orInfo, sizeof(BYTE), NULL); WriteProcessMemory(g_cpdi.hProcess, g_pfCreateProcess, &g_chINT3, sizeof(BYTE), NULL); ...
|
在完成了指令写入操作之后,就需要等到中断异常的到来并处理。那么首先就是等待异常事件的到来。
1 2 3 4 5 6 7 8
| ... if (de.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) { HandleFunction(&de); } ...
|
在进入异常处理时,需要判断该异常是否为断点异常并且判断断点的地址是否为我们需要钩取的函数地址。
1 2 3 4 5 6
| if (per->ExceptionCode == EXCEPTION_BREAKPOINT) { if (per->ExceptionAddress == g_pfCreateProcess) { ... }
|
由于函数的参数是通过寄存器或者堆栈进行传递的,因此我们需要获取中断时进程的寄存器的值,通过获取上下文的操作,获取此刻进程的寄存器的值。
ContextFlags
有如下取值:
CONTEXT_CONTROL:
程序计数器 (EIP/RIP)
、堆栈指针 (ESP/RSP)
、段寄存器 (SS)
以及标志寄存器 (EFLAGS/RFLAGS)
CONTEXT_INTEGER:
通用寄存器 (EAX/RAX, EBX/RBX, ECX/RCX, EDX/RDX, ESI/RSI, EDI/RDI)
CONTEXT_SEGMENTS:
段选择器 (CS, DS, ES, FS, GS)
CONTEXT_FLOATING_POINT:
浮点状态 (FPU)
与浮点寄存器
CONTEXT_DEBUG_REGISTERS:
调试寄存器 (DR0, DR1, DR2, DR3, DR6, DR7)
CONTEXT_EXTENDED_REGISTERS:
获取或设置扩展寄存器上下文,主要用于获取或设置 SSE/AVX
寄存器。
具体需要哪个值则根据实际需求判断需要哪些寄存器的值而决定。
1 2 3 4 5
| ... ctx.ContextFlags = CONTEXT_INTEGER; GetThreadContext(g_cpdi.hThread, &ctx); ...
|
紧接着通过ReadProcessMemory
函数读取寄存器中的值。
1 2 3 4 5 6 7 8 9 10
| ... LPCSTR appName = (LPCSTR)malloc(MAX_PATH); ReadProcessMemory( g_cpdi.hProcess, (LPVOID)ctx.Rcx, (LPVOID)appName, MAX_PATH, NULL ); ...
|
若需要修改函数的参数则通过WriteProcessMemory
函数。
1 2 3 4 5 6 7 8 9
| ... WriteProcessMemory( g_cpdi.hProcess, (LPVOID)ctx.Rdx, (LPVOID)newCommandLine.c_str(), MAX_PATH, NULL ); ...
|
那么最后一步就是使得程序正常执行,在完成钩取操作之后则需要让程序执行原本的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13
| ... WriteProcessMemory(g_cpdi.hProcess, g_pfCreateProcess, &g_orInfo, sizeof(BYTE), NULL); ... ctx.ContextFlags = CONTEXT_CONTROL; GetThreadContext(g_cpdi.hThread, &ctx); ctx.Rip = (DWORD64)g_pfCreateProcess; SetThreadContext(g_cpdi.hThread, &ctx);
|
总体流程如下图,利用调试模式捕获事件,然后做事件处理实现钩取。
完整项目:https://github.com/h0pe-ay/HookTechnology/tree/main/Hook-Debug
总结
使用调试钩取方法便捷简单,由于目标进程处于调试模式下,因此可以获取相当多的信息,但是利用该方法则无法配合其它调试器进行调试,并且当目标进程存在反调试的措施时,则需要首先绕过反调试才能使用该方法进行钩取。
参考链接
《逆向工程核心原理》