-
-
[原创]利用调试器原理实现 API Hook(WriteFile示例)
-
发表于: 2025-8-30 16:37 729
-
Hook是什么
Hook,中文直译就是”钩子“。在软件开发和安全领域,它是一种用来监控或修改程序执行流程的技术。
具体来说,当程序调用某个函数的时候,我们在中间”挂一个钩子“,这样执行流会先经过我们的代码,再决定是否
继续运行原本的逻辑,还是插入、修改、甚至完全阻止这次调用。
举个例子,如果对CreateProcess()函数进行Hook,就能够截获所有进程创建操作。这样我们不仅能看到目标进程
的启动参数,还能选择是否拦截或者篡改它,从而达到监控或修改的目的。
Hook的分类
以下出自《加密与解密》,个人觉得写的非常好
程序的本质是数据+指令,这两样东西只要被篡改,程序的指向结果就发生变化。相应地,出现了两大类Hook,
一类是通过修改数据(通常是引用的函数)进行Hook(IAT/EAT Hook),另一类则直接修改函数内的指令进行Hook。
这两大类也就是Address Hook与Inline Hook
类似于C语言中的指针,第1种Hook就是修改指针的值,第2种Hook就是修改指针指向的内容的值。
下面给到一个简单的程序示例,PrintChar函数本来要输出字符"A",但为了能让其输出“B”,可以采取两种方法
void PrintChar(char* pch)
{
printf("%c\n", *pch);
}
int main()
{
char ch = 'A';
char ch2 = 'B';
char* pChar;
pChar = &ch;
PrintChar(pChar); //打印 A
pChar = &ch2;
PrinChar(pChar); //打印 B (修改了"指针的值" 也就是修改指向)
pChar = &ch1;
*pChar = 'B';
PrinChar(pChar); //打印 B (修改了"指针指向的值")
}而本文所讲的利用调试器进行Hook属于一种特殊的InlineHook
调试器的工作原理
上面我们了解了Hook是什么,接下来我们学习利用调试器进行Hook之前还需要学习调试器的工作原理
调试器是一类特殊的程序,当它加载或附加某个进程之后,这个被调试的进程发生调试事件(Debug Event)时,
OS就会暂停其运行,并且向调试器报告相应事件。调试器接管被调试进程的执行流

在调试相关的事件里,最重要的一类是异常事件(EXCEPTION_DEBUG_EVENT)
其中,调试器最常用的就是EXCEPTION_BREAKPOINT,也就是”断点异常“,断点对应的机器指令是INT3(在IA-32架构下它的机器码是0xCC)
当CPU执行到INT3指令时,程序会立刻中断运行,操作系统把这个异常事件交给调试器,调试器此时就能检查、修改寄存器堆栈等
调试器实现断点的方法非常简单,找到要设置断点的代码再内存种的起始地址,把第一个字节修改为0xCC就可以了
想继续调试时,再将其恢复原值。通过调试Hook的技术也就是利用了断点的这种特性
利用调试技术Hook记事本的WriteFile()函数的具体流程
首先,我们需要附加到记事本的进程上。找到WriteFile()函数的首地址,将其第一个字节保存下来,然后修改为0xCC
这样当记事本执行到WriteFile()函数时,就会触发断点异常,此时控制权转移到我们,首先判断异常发生的位置是不是WriteFile()函数
如果是的话,就说明记事本在调用WriteFile()函数。接着,我们获取线程上下文(寄存器、栈等信息)就可以进行参数的监控或篡改
然后我们要让程序恢复执行:把WriteFile()函数的第一个字节改回去,再把EIP改成WriteFile()的起始位置(因为执行了0xCC执行,所以当前的EIP往后加了一个字节)
最后恢复被调试进程,然后再次将WriteFile()函数的首地址修改为0xCC(继续挂钩)
具体实现代码
说了这么多理论,下面是具体对WriteFile()函数Hook的代码(32位记事本)
达到的效果是无论在记事本中写入什么内容保存成txt后都是kanxue.com
#include <Windows.h>
#include <stdio.h>
#include <strsafe.h>
#include <tchar.h>
#include <iostream>
LPVOID g_pfWriteFile;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chOrgByte = 0;
BYTE g_chINT3 = 0xCC;
CHAR Buffer[] = "kanxue.com";
SIZE_T size = strlen(Buffer);
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
//获取WriteFile() API地址
g_pfWriteFile = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "WriteFile");
//先通过pde拿到进程相关信息
memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
//读取原先的字节
ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
//写入0xCC
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
return true;
}
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
CONTEXT ctx;
PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;
DWORD dwNumberOfBytesToWrite, dwAddrOfBuffer, i;
PBYTE lpBuffer = NULL;
//是断点异常(INT 3)时
if (EXCEPTION_BREAKPOINT == per->ExceptionCode)
{
//判断断点地址是否是WriteFile
if (g_pfWriteFile == per->ExceptionAddress)
{
// UnHook
//将0xCC恢复为original byte
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
// 获取线程上下文
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);
// 获取WriteFile()的param 2、3值
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8), &dwAddrOfBuffer, sizeof(DWORD), NULL);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC), &dwNumberOfBytesToWrite, sizeof(DWORD), NULL);
LPVOID lpOrgBuffer = (LPVOID)malloc(dwNumberOfBytesToWrite+1);
memset(lpOrgBuffer, 0, dwNumberOfBytesToWrite + 1);
//输出原来的字符串和大小
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpOrgBuffer, dwNumberOfBytesToWrite, NULL);
printf("\n### original string : %s size : %d\n", lpOrgBuffer,dwNumberOfBytesToWrite);
//申请存放kanxue.com的空间
LPVOID lpBuffer = VirtualAllocEx(g_cpdi.hProcess, NULL, size+1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(g_cpdi.hProcess, lpBuffer, Buffer, size+1, NULL);
//改写参数
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8), &lpBuffer, sizeof(DWORD), NULL);
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC), &size, sizeof(DWORD), NULL);
printf("\n### converted string : %s size : %d\n", Buffer, size);
// 将线程上下文的EIP更改为WriteFile()首地址
ctx.Eip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);
// 运行被调试进程
ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
//立刻切换线程 防止同时操作WriteFile函数那块内存
Sleep(0);
// 继续挂钩
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
return TRUE;
}
}
return FALSE;
}
void DebugLoop()
{
DEBUG_EVENT de;
DWORD dwContinueStatus;
// 等待调试事件发生
while (WaitForDebugEvent(&de, INFINITE))
{
dwContinueStatus = DBG_CONTINUE;
//被调试进程生成或附加事件
if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)
{
OnCreateProcessDebugEvent(&de);
}
//异常事件
else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode)
{
if (OnExceptionDebugEvent(&de))
{
continue;
}
}
//被调试进程终止事件
else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)
{
break;
}
//再次执行被调试进程
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}
bool EnableDebugPrivilege() {
HANDLE hToken;
TOKEN_PRIVILEGES tp;
LUID luid;
// 打开当前进程的访问令牌
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
std::cerr << "OpenProcessToken failed" << std::endl;
return false;
}
// 获取 SeDebugPrivilege 的 LUID
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {
std::cerr << "LookupPrivilegeValue failed" << std::endl;
CloseHandle(hToken);
return false;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
// 启用 SeDebugPrivilege 权限
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, 0, NULL, NULL)) {
std::cerr << "AdjustTokenPrivileges failed" << std::endl;
CloseHandle(hToken);
return false;
}
// 检查是否成功启用权限
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
std::cerr << "The privilege is not assigned" << std::endl;
CloseHandle(hToken);
return false;
}
CloseHandle(hToken);
return true;
}
int main(int argc, char* argv[])
{
DWORD dwPID = 1;
EnableDebugPrivilege();
if (argc != 2)
{
printf("\nUSAGE : hookdbg.exe <pid>\n");
return 1;
}
//附加调试进程
dwPID = atoi(argv[1]);
if (!DebugActiveProcess(dwPID))
{
printf("DebugActiveProcess(%lu) failed!!!\n"
"Error Code = %lu\n", dwPID, GetLastError());
return 1;
}
//调试器循环
DebugLoop();
return 0;
}程序运行效果

总结与思考
本技术将调试器原理与Hook相结合,非常巧妙,充分展示了调试器如何通过操作进程内存与控制流,影响目标程序的行为。
通过本文章可以学习到基本的调试器原理与Hook技术
参考文献
《加密与解密》
《逆向工程核心原理》