Hook-Debug

前言

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; //发生调试事件的进程的 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;

那么为了等待事件的到来,在附着了进程之后,还需要配合WaitForDebugEvent函数,该函数接受两个参数,一个是用于存储事件的结构体,一个是等待事件。

1
2
3
4
BOOL WaitForDebugEvent(
[out] LPDEBUG_EVENT lpDebugEvent, //指向 DEBUG_EVENT 结构体的指针,该结构体会被填充为描述发生的调试事件的信息
[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);
}
....

由于我们需要读写进程内存空间,需要配合ReadProcessMemoryWriteProcessMemory函数。首先我们需要获取需要钩取函数的地址,这里以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);
}
//获取需要HOOK的函数
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)
{
//printf("HandleFunction\n");
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
...
//获取上下文,这里由于需要RCX、RDX、R8、R9寄存器
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);
...
//修改RIP指针指向需要调用的函数
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);
ctx.Rip = (DWORD64)g_pfCreateProcess;
SetThreadContext(g_cpdi.hThread, &ctx);

总体流程如下图,利用调试模式捕获事件,然后做事件处理实现钩取。

image-20240518120902632

完整项目:https://github.com/h0pe-ay/HookTechnology/tree/main/Hook-Debug

总结

使用调试钩取方法便捷简单,由于目标进程处于调试模式下,因此可以获取相当多的信息,但是利用该方法则无法配合其它调试器进行调试,并且当目标进程存在反调试的措施时,则需要首先绕过反调试才能使用该方法进行钩取。

参考链接

《逆向工程核心原理》


Hook-Debug
https://h0pe-ay.github.io/Hook-Debug/
作者
hope
发布于
2024年7月15日
许可协议