前言
钩子和注入是逆向的灵魂所在,本篇先介绍基本的Windows编程,然后介绍如何通过Windows编程实现钩子和注入。之后的工具篇会以这一篇为基础。
Windows编程
这里的Windows编程并不涉及消息或者窗口。我们仅使用WindowsDLL提供的导出函数来使用WindowsAPI。因此,这里的Windows编程和普通编程唯一的区别就是使用了Windows定义的宏类型和WindowsAPI。为什么说是宏类型,是因为其中的各个类型都是预定义的宏,和C自带的类型没有任何本质上的区别,只是起了一个更好听的名字。
现在来写一个Hello World吧
HelloWorld
#include<Windows.h>
int main(void) { MessageBoxA(NULL, "Hello, World!", "Greetings", MB_OK); MessageBoxW(NULL, L"Hello, World!", L"Greetings", MB_OK); return 0;}执行以上代码,将会生成两个框,且可点击OK。
可以发现第一个函数的后缀为A,第二个函数的后缀为W。这是Windows为了Unicode兼容性提供的两个版本的函数。A后缀表示这个函数接受的字符串为ascii字符串。W后缀表示这个函数接受的字符串为宽字符串(Wide)。对于字面量来说,直接写L前缀就可以表示宽字符。
可以直接使用不带后缀的宏函数,来自动使用当前的系统设置,一般来说,此时的设置都是wide。
下面是另一个稍微复杂的例子:
#include <Windows.h>
int main(void) { // 申请一个1024大小的只读内存页 LPVOID pBuffer = VirtualAlloc(NULL, 1024, MEM_COMMIT | MEM_RESERVE, PAGE_READONLY); if (pBuffer == NULL) { WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), L"VirtualAlloc failed\r\n", 19, NULL, NULL); return 1; }
DWORD dwData = 0x12345678; DWORD dwOldProtect = 0;
// 修改内存页的保护属性为可读写 if (!VirtualProtect(pBuffer, 1024, PAGE_READWRITE, &dwOldProtect)) { WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), L"change protect failed\r\n", 23, NULL, NULL); VirtualFree(pBuffer, 0, MEM_RELEASE); return 1; }
// 写入数据 *(LPDWORD)pBuffer = dwData;
// 读数据 WCHAR buf[64]; // 先格式化 wsprintfW(buf, L"pBuffer value = 0x%08X\r\n", *(LPDWORD)pBuffer);
// 恢复内存页的保护属性 if (!VirtualProtect(pBuffer, 1024, dwOldProtect, &dwOldProtect)) { WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), L"restore protect failed\r\n", 24, NULL, NULL); // 仍然继续尝试输出并释放内存 }
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); if (hOut == INVALID_HANDLE_VALUE || hOut == NULL) { VirtualFree(pBuffer, 0, MEM_RELEASE); return 1; }
DWORD ft = GetFileType(hOut); if (ft == FILE_TYPE_CHAR) { // 直接写控制台 DWORD written = 0; WriteConsoleW(hOut, buf, lstrlenW(buf), &written, NULL); }
// 释放内存页 VirtualFree(pBuffer, 0, MEM_RELEASE); return 0;}一些内容
这里有一些重要的类型和函数需要了解:
PVOID,PBYTE,PWORD,PDWORD,PDWORD64// 表示指针类型,去掉P表示对应的长度类型,前面加L表示长指针即64位,由于本身就是在64位环境,因此不需要加L,有的前面会加C表示constCHAR,WCHAR// 字符和长字符WSTR//长字符串HANDLE,HMODULE,HWND//句柄,类似于指针,后面两个一个是模块句柄,一个是窗口句柄
// e.g.LPCWSTR // LP C WSTR 指向const的长字符串指针
VirtualAlloc()// 申请内存VirtualProtect()// 修改内存属性VirtualFree()// 释放内存
LoadLibrary() //加载DLLFreeLibrary()// 卸载DLL
CreateThread()//创建线程CreateRemoteThread()//创建远程线程CreateProcess()//创建进程函数可以遇到了再学,上面的函数如何使用就不多说了,直接在官网查看即可。
Hook
钩子是指在执行目标代码片段前后获取其参数或返回值。Hook有很多层级,这里用一个Python代码演示:
def hook(): import secret
# 保存原始的encodePath1 originalencode = secret.encodePathFinal original_encodePath1 = secret.encodePath1
# 创建包装函数 def wrap(func, name): def wrapper(*args, **kwargs): print(f"Entering {name} with args: {args}") result = func(*args, **kwargs) print(f"Exiting {name} with result: {result}") return result return wrapper
# 替换函数 secret.encodePathFinal = wrap(originalencode, "encodePathFinal") secret.encodePath1 = wrap(original_encodePath1, "encodePath1")
print("Hooks installed successfully for encodePathFinal and encodePath1")执行以上代码后,你可以尝试自行实现secret中的encode函数,令其调用encodePath1和encodePathFinal,观察区别。改代码全是了hook的作用,不只是打印内容,还可以修改,这让调试和逆向更为方便。
本段直接搬运了老博客的内容
Inline Hook
本节主要介绍的是汇编(C/CPP)上的hook,以下为inline Hook的原理图:

具体实现方法是:
int main(){ hooker(MessageBoxW, mybox, 5); MessageBoxW(0, L"Hello", 0, 0); return 0;}main函数先调用hooker函数,修改messagebox的内容,然后执行被修改的messagebox函数
那么hooker函数就要传入messagebox函数的地址,和我们想要修改成的函数的地址,这里的长度可以是固定的,可以不写5
void hooker(void* src, void* dst, int len) { DWORD old; VirtualProtect(src, len, 0x40, &old); memcpy(back, src, len); *(BYTE*)src = 0xE9; uintptr_t ra = (uintptr_t)dst - (uintptr_t)src - 5; *(DWORD*)((BYTE*)src + 1) = ra; VirtualProtect(src, len, old, &old);
}在函数外面创建一个备份BYTE back[5];用来保存原始数据,然后用VirtualProtect修改函数前5个字节变成可写属性(0x40),然后备份原始属性到old并修改,核心公式是:
*(BYTE*)src = 0xE9;uintptr_t ra = (uintptr_t)dst - (uintptr_t)src - 5;然后改回去
最后就是实现自己的函数,一定要注意函数的传入参数必须要和原函数相同
int WINAPI mybox(_In_opt_ HWND hWnd, _In_opt_ LPCWSTR lpText, _In_opt_ LPCWSTR lpCaption, _In_ UINT uType){ lpText = L"hooked";//想要修改的效果 unhook(MessageBoxW, back, 5); MessageBoxW(hWnd, lpText, lpCaption, uType); hooker(MessageBoxW, mybox, 5); return 0;}这里通过去官网直接抄传入参数就可以直接得到参数名 同时注意要先解除钩子再调用原函数不然会陷入循环递归
void unhook(void* src, void* back, int len) { DWORD old; VirtualProtect(src, len, 0x40, &old); memcpy(src, back, 5); VirtualProtect(src, len, old, &old);}在调用完目标函数后,还要再次钩住函数,等待下一次调用
Trampoline Hook
由于刚刚的hook函数,每次必须要先hook,然后再unhook,十分麻烦。同时,每次调用messagebox的效果都是一样的,如果想要实现显示其它东西,需要另一种方法。
对于第一个问题:如果不对hook过的函数unhook,那么就会陷入死循环,无限调用自己,如果现在有一个和messagebox的函数效果相同的函数,在hook后,不调用messagebox,而是调用这个函数,就不会内存溢出
对于第二个问题:如果有一个和messagebox函数相同的函数,那么直接调用那个函数并修改参数就可以显示其它东西。
因此,只要能构造出一个和我们想hook的函数功能相同的函数就可以解决上面两个问题。
图示:

那么先构造trampoline函数
void* trampoline(void* src, void* dst, int len){ BYTE* boxin = (BYTE*)VirtualAlloc(0, len + 5, 0x00001000, 0x40); memcpy(boxin, src, len); *(boxin + len) = 0xe9; *(DWORD*)(boxin + len + 1) = (BYTE*)src - boxin - 5; //以下为原hooker的内容 DWORD old; VirtualProtect(src, len, 0x40, &old); *(BYTE*)src = 0xE9; uintptr_t ra = (uintptr_t)dst - (uintptr_t)src - 5; *(DWORD*)((BYTE*)src + 1) = ra; VirtualProtect(src, len, old, &old); return boxin;}前4句都是在创建功能相同函数,直接申请内存把messagebox前5个字节复制过来,然后jmp到messagebox里(偷懒)
第5句到10句的hooker和之前的hooker一样
void hooker(void* src, void* dst, int len) { DWORD old; VirtualProtect(src, len, 0x40, &old); *(BYTE*)src = 0xE9; uintptr_t ra = (uintptr_t)dst - (uintptr_t)src - 5; *(DWORD*)((BYTE*)src + 1) = ra; VirtualProtect(src, len, old, &old);}最后一句return是为了之后使用不被hook的函数
然后先把相同功能函数的调用写出来
using PFUN = int (WINAPI*)( _In_opt_ HWND hWnd, _In_opt_ LPCWSTR lpText, _In_opt_ LPCWSTR lpCaption, _In_ UINT uType);PFUN pfun = nullptr;int main(){ pfun = (PFUN)trampoline(MessageBoxW, mybox, 5); MessageBoxW(0, L"Hello", 0, 0); MessageBoxW(0, L"Hello", 0, 0); MessageBoxW(0, L"Hello", 0, 0); pfun(0, L"123123", 0, 0); return 0;}最后添上hook函数
int WINAPI mybox(_In_opt_ HWND hWnd, _In_opt_ LPCWSTR lpText, _In_opt_ LPCWSTR lpCaption, _In_ UINT uType){ lpText = L"hooked"; pfun(hWnd, lpText, lpCaption, uType); return 0;}这样调用时,会先显示3个hook,在最后显示123123.
Inject
好了,上面的hook代码都属于进程内Hook,也就是说如果想要实现hook,目标代码片段必须和Hook安装/卸载的代码在同一个内存空间中。那么,如何将自己写的Hook代码安装到其它进程中呢?这就是注入的作用,将外部代码写入目标进程,然后执行的一种技术。
通过以上内容,我们可以发现,注入分为写入和执行。这两种都有多种方法且可以任意组合。例如执行可以是通过CreateRemoteThread创建远程线程或者pNtQueueApcThreadEx挂载APC,写入可以是LoadLibrary或者ManualMap。同时需要注意:如果是注入DLL,那么注入器,DLL,目标进程的架构需要相同,都是x64或者x86才可能成功。
CreateRemoteThread+LoadLibrary
这里就以最基本的CreateRemoteThread+LoadLibrary来演示。
具体流程为:
-
将DLL路径写入目标进程
-
获取目标进程的LoadLibrary地址
-
以LoadLibrary为目标函数创建远程线程执行,参数为DLL路径
有几个需要注意的地方:
- 目标进程地址空间和注入器的地址空间不同,需要将DLL路径写入目标进程,否则目标进程无法获取路径地址。
- 同时也需要获取目标进程的LoadLibrary函数地址,但是由于kernel32.dll在各个进程映射路径相同,可以直接获取本地地址,该地址同时也是目标进程的LoadLibrary函数地址。
- DLL注入如果失败提示没有权限,需要先获取SeDebugPrivilege权限或提升至管理员
int inject(DWORD dwPID, LPCTSTR szDllPath) { HANDLE hProcess = 0, hThread = 0; if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))//取得对应PID句柄 { _tprintf(_T("open %d failed\n"), dwPID); return FALSE; } DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR); LPVOID pBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);//申请内存 if (pBuf == 0) { _tprintf(_T("memory alloc failed\n")); return FALSE; }
WriteProcessMemory(hProcess, pBuf, (LPVOID)szDllPath, dwBufSize, NULL);//写入地址 HMODULE kernel = GetModuleHandle(L"kernel32.dll"); if (kernel == NULL) return FALSE; LPTHREAD_START_ROUTINE pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(kernel, "LoadLibraryW");//获取LodaLibrary地址 hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc,//线程回调函数 pBuf,//传参 0, NULL); if (hThread == NULL) { _tprintf(_T("create hThread failed\n")); return FALSE; } WaitForSingleObject(hThread, INFINITE);//等待线程结束 CloseHandle(hThread); CloseHandle(hProcess); return TRUE;}直接同上编写即可。
总结
通过以上内容,可以实现将自己的Hook注入至目标进程,实现自定义效果。
部分信息可能已经过时









