InlineHook 是一种计算机安全编程技术,其原理是在计算机程序执行期间进行拦截、修改、增强现有函数功能。它使用钩子函数(也可以称为回调函数)来截获程序执行的各种事件,并在事件发生前或后进行自定义处理,从而控制或增强程序行为。Hook技术常被用于系统加速、功能增强、开发等领域。本章将重点讲解Hook是如何实现的,并手动封装实现自己的Hook挂钩模板。
对于4.1中所提到的Hook方法还是过于复杂,我们可以将上述代码定义为MyHook类,构造函数用来初始化,析构函数用来恢复钩子,在Hook()成员函数中完成了3项工作,首先是获得了被HOOK函数的函数地址,接下来是保存了被HOOK函数的前5字节,最后是用构造好的跳转指令来修改被HOOK函数的前5字节的内容。
如下封装中实现了三个类内函数,其中Hook()用于开始Hook函数,此函数接收三个参数,参数1为需要Hook的动态链接库名,参数2为需要挂钩的函数名,参数3为自定以中转函数地址,其中UnHook()用于恢复函数挂钩,最后的ReHook()用于重新挂钩,以下是该类提供的功能的简要摘要:
// 自己封装的Hook组件class MyHook{ public: PROC m_pfnOrig; // 保存函数地址 BYTE m_bOldBytes[5]; // 保存函数入口代码 BYTE m_bNewBytes[5]; // 保存Inlie Hook代码 public: // 构造函数 MyHook() { m_pfnOrig = NULL; ZeroMemory(m_bOldBytes, 5); ZeroMemory(m_bNewBytes, 5); } // 析构函数清理现场 ~MyHook() { UnHook(); m_pfnOrig = NULL; ZeroMemory(m_bOldBytes, 5); ZeroMemory(m_bNewBytes, 5); } // 开始挂钩 BOOL Hook(LPSTR pszModuleName, LPSTR pszFuncName, PROC pfnHookFunc) { BOOL bRet = FALSE; // 获取指定模块中函数的地址 m_pfnOrig = (PROC)GetProcAddress(GetModuleHandleA(pszModuleName), pszFuncName); if (m_pfnOrig != NULL) { // 保存该地址处 5 字节的内容 DWORD dwNum = 0; ReadProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bOldBytes, 5, &dwNum); // 构造 JMP 指令 m_bNewBytes[0] = '\xe9'; // jmp Opcode // pfnHookFunc 是 HOOK 后的目标地址 // m_pfnOrig 是原来的地址 // 5 是指令长度 *(DWORD*)(m_bNewBytes + 1) = (DWORD)pfnHookFunc - (DWORD)m_pfnOrig - 5; // 将构造好的地址写入该地址处 WriteProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bNewBytes, 5, &dwNum); bRet = TRUE; } return bRet; } // 取消挂钩 BOOL UnHook() { if (m_pfnOrig != 0) { DWORD dwNum = 0; WriteProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bOldBytes, 5, &dwNum); } return TRUE; } // 重新挂钩 BOOL ReHook() { BOOL bRet = FALSE; if (m_pfnOrig != 0) { DWORD dwNum = 0; WriteProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bNewBytes, 5, &dwNum); bRet = TRUE; } return bRet; }};
同样我们以替换自身弹窗为例子具体讲解一下该库如何使用,如下代码中首先我们自定义一个MyMessageBoxA函数,该函数的原型读者可通过微软官方文档获取,以下是MessageBoxA函数的原型(函数声明):
int MessageBoxA( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
其中,参数的含义如下:
有了函数原型声明部分读者则可以自己实现一个MyMessageBoxA函数,需注意参数传递必须与原函数保持一致,在自定以函数内部我们首先通过MsgHook.UnHook();恢复之前的钩子,并调用原函数实现功能替换,当调用结束后记得使用MsgHook.ReHook();重新挂钩恢复钩子。
// 定义全局类MyHook MsgHook;// 定义自定义Hook函数int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType){ // 先来恢复Hook 之所以要恢复是因为我们需要调用原始的MsgBox弹窗 MsgHook.UnHook(); MessageBoxA(hWnd, "hi hook api", lpCaption, uType); // 弹窗完成后重新Hook MsgHook.ReHook(); return 0;}
在主函数中我们通过调用MsgHook.Hook()函数,挂钩住user32.dll模块内的MessageBoxA函数,并将该函数请求转发到MyMessageBoxA上面做处理,当此时调用MessageBoxA时读者可观察弹出提示是否为我们所期望的,最后通过MsgHook.UnHook();用于解除钩子;
// 调用Hook组件int main(int argc, char* argv[]){ // 开始Hook MsgHook.Hook((char *)"user32.dll", (char *)"MessageBoxA", (PROC)MyMessageBoxA); // 调用函数 MessageBoxA(NULL, "hello lyshark", "Msg", MB_OK); // 结束Hook MsgHook.UnHook(); return 0;}
读者可自行运行上述代码,当尝试调用MessageBox函数并传入hello lyshark参数时,输出的结果却变成了hi hook api如下图所示,则说明内联挂钩生效了。
32位钩子的封装实现详细读者已经能够理解了,接着我们来实现64位钩子的封装,64位与32位系统之间无论从寻址方式,还是语法规则都与x32架构有着本质的不同,由于64位编译器无法直接内嵌汇编代码,导致我们只能调用C库函数内嵌机器码来实现Hook的中转。
首先实现去MessageBox弹窗,由于64位编译器无法直接内嵌汇编代码,所以在我们需要Hook时只能将跳转机器码以二进制字节方式写死在程序里,如下代码是一段去弹窗演示案例。
#include <stdio.h>#include <Windows.h>BYTE OldCode[12] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };BYTE HookCode[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 };DWORD_PTR base;// 自己实现中转函数int WINAPI MyMessageBoxW(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType){ return 1;}int main(int argc,char * argv[]){ HMODULE hwnd = GetModuleHandle(TEXT("user32.dll")); DWORD_PTR base = (DWORD_PTR)GetProcAddress(hwnd, "MessageBoxW"); DWORD OldProtect; if (VirtualProtect((LPVOID)base, 12, PAGE_EXECUTE_READWRITE, &OldProtect)) { memcpy(OldCode, (LPVOID)base, 12); // 拷贝原始机器码指令 *(PINT64)(HookCode + 2) = (INT64)&MyMessageBoxW; // 填充90为指定跳转地址 } memcpy((LPVOID)base, &HookCode, sizeof(HookCode)); // 拷贝Hook机器指令 MessageBoxW(NULL, L"hello lyshark", NULL, NULL); return 0;}
接着我们在上面代码的基础上继续进行完善,添加恢复钩子的功能,该功能时必须要有的,因为我们还是需要调用原始的弹窗代码,所以要在调用时进行暂时恢复,调用结束后再继续Hook挂钩。
#include <stdio.h>#include <Windows.h>void Hook();void UnHook();BYTE Ori_Code[12] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };BYTE HookCode[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 };/* Hook 机器码的原理如下所示MOV RAX, 0x9090909090909090JMP RAX*/// 定义函数指针static int (WINAPI *OldMessageBoxW)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) = MessageBoxW;// 执行自己的弹窗int WINAPI MyMessageBoxW(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType){ UnHook(); // 恢复Hook int ret = OldMessageBoxW(hWnd, TEXT("Hook Inject"), lpCaption, uType); // 调用原函数 Hook(); // 继续hook return ret;}// 开始Hookvoid Hook(){ DWORD OldProtect; if (VirtualProtect(OldMessageBoxW, 12, PAGE_EXECUTE_READWRITE, &OldProtect)) { memcpy(Ori_Code, OldMessageBoxW, 12); // 拷贝原始机器码指令 *(PINT64)(HookCode + 2) = (INT64)&MyMessageBoxW; // 填充90为指定跳转地址 } memcpy(OldMessageBoxW, &HookCode, sizeof(HookCode)); // 拷贝Hook机器指令}// 结束Hookvoid UnHook(){ memcpy(OldMessageBoxW, &Ori_Code, sizeof(Ori_Code)); // 恢复hook原始代码}int main(int argc, char *argv []){ Hook(); MessageBoxW(NULL, TEXT("hello lyshark"), TEXT("MsgBox"), MB_OK); UnHook(); MessageBoxW(NULL, TEXT("hello lyshark"), TEXT("MsgBox"), MB_OK); return 0;}
将上面所写的代码进行函数化封装,实现一个完整的钩子处理程序。代码如下所示。
#include <stdio.h>#include <Windows.h>BYTE OldCode[12] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };BYTE HookCode[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 };void Hook(LPCWSTR lpModule, LPCSTR lpFuncName, LPVOID lpFunction){ DWORD_PTR FuncAddress = (UINT64)GetProcAddress(GetModuleHandle(lpModule), lpFuncName); DWORD OldProtect = 0; if (VirtualProtect((LPVOID)FuncAddress, 12, PAGE_EXECUTE_READWRITE, &OldProtect)) { memcpy(OldCode, (LPVOID)FuncAddress, 12); // 拷贝原始机器码指令 *(PINT64)(HookCode + 2) = (UINT64)lpFunction; // 填充90为指定跳转地址 } memcpy((LPVOID)FuncAddress, &HookCode, sizeof(HookCode)); // 拷贝Hook机器指令 VirtualProtect((LPVOID)FuncAddress, 12, OldProtect, &OldProtect);}void UnHook(LPCWSTR lpModule, LPCSTR lpFuncName){ DWORD OldProtect = 0; UINT64 FuncAddress = (UINT64)GetProcAddress(GetModuleHandle(lpModule), lpFuncName); if (VirtualProtect((LPVOID)FuncAddress, 12, PAGE_EXECUTE_READWRITE, &OldProtect)) { memcpy((LPVOID)FuncAddress, OldCode, sizeof(OldCode)); } VirtualProtect((LPVOID)FuncAddress, 12, OldProtect, &OldProtect);}int WINAPI MyMessageBoxW(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType){ // 首先恢复Hook代码 UnHook(L"user32.dll", "MessageBoxW"); int ret = MessageBoxW(0, L"hello lyshark", lpCaption, uType); // 调用结束后,再次挂钩 Hook(L"user32.dll", "MessageBoxW", (PROC)MyMessageBoxW); return ret;}bool APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid){ switch (dword) { case DLL_PROCESS_ATTACH: Hook(L"user32.dll", "MessageBoxW", (PROC)MyMessageBoxW); break; case DLL_PROCESS_DETACH: UnHook(L"user32.dll", "MessageBoxW"); break; } return true;}
上方的代码还是基于过程化的案例,为了能更加通用,我们将其封装成MyHook类,这样后期可以直接引入项目调用了。
#include <iostream>#include <Windows.h>class MyHook{ public: FARPROC m_pfnOrig; // 保存函数地址 BYTE m_bOldBytes[12]; // 保存函数入口代码 BYTE m_bNewBytes[12]; // 保存Inlie Hook代码 public: // 构造函数 MyHook() { BYTE OldCode[12] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; BYTE NewCode[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 }; RtlMoveMemory(MyHook::m_bNewBytes, NewCode, 12); memset(MyHook::m_bOldBytes, 0, 12); m_pfnOrig = NULL; } // 析构函数 ~MyHook() { m_pfnOrig = NULL; ZeroMemory(MyHook::m_bNewBytes, 12); ZeroMemory(MyHook::m_bOldBytes, 12); } // 开始挂钩 BOOL Hook(LPCWSTR lpModule, LPCSTR lpFuncName, LPVOID lpFunction) { DWORD_PTR FuncAddress = (UINT64)GetProcAddress(GetModuleHandle(lpModule), lpFuncName); DWORD OldProtect = 0; if (VirtualProtect((LPVOID)FuncAddress, 12, PAGE_EXECUTE_READWRITE, &OldProtect)) { memcpy(m_bOldBytes, (LPVOID)FuncAddress, 12); // 拷贝原始机器码指令 *(PINT64)(MyHook::m_bNewBytes + 2) = (UINT64)lpFunction; // 填充90为指定跳转地址 } memcpy((LPVOID)FuncAddress, &m_bNewBytes, sizeof(m_bNewBytes)); // 拷贝Hook机器指令 VirtualProtect((LPVOID)FuncAddress, 12, OldProtect, &OldProtect); return TRUE; } // 结束挂钩 BOOL UnHook(LPCWSTR lpModule, LPCSTR lpFuncName) { DWORD OldProtect = 0; UINT64 FuncAddress = (UINT64)GetProcAddress(GetModuleHandle(lpModule), lpFuncName); if (VirtualProtect((LPVOID)FuncAddress, 12, PAGE_EXECUTE_READWRITE, &OldProtect)) { memcpy((LPVOID)FuncAddress, m_bOldBytes, sizeof(m_bOldBytes)); } VirtualProtect((LPVOID)FuncAddress, 12, OldProtect, &OldProtect); return TRUE; }};// 定义全局类MyHook MsgHook;// 自己实现中转函数int WINAPI MyMessageBoxW(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType){ // 首先恢复Hook代码 MsgHook.UnHook(L"user32.dll", "MessageBoxW"); int ret = MessageBoxW(0, L"hi hook api", lpCaption, uType); // 调用结束后,再次挂钩 MsgHook.Hook(L"user32.dll", "MessageBoxW", (PROC)MyMessageBoxW); return ret;}int main(int argc, char* argv[]){ // 开始Hook MsgHook.Hook(L"user32.dll", "MessageBoxW", (PROC)MyMessageBoxW); MessageBoxW(NULL, L"hello lyshark", L"Msg", MB_OK); // 结束Hook MsgHook.UnHook(L"user32.dll", "MessageBoxW"); return 0;}
读者可自行运行上述代码,当尝试调用MessageBox函数并传入hello lyshark参数时,输出的结果却变成了hi hook api如下图所示,则说明内联挂钩生效了。