钩子编程

钩子编程（hooking），也称作“挂钩”，是计算机程序设计术语，指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码，被称为钩子（hook）。 钩子编程有多种用途，如调试、扩展功能。例如在键盘或鼠标事件到达应用程序之前拦截这些事件；拦截应用程序或其他模块的操作系统调用以监督行为、修改功能。也广泛用于benchmarking程序，如度量3D游戏的帧率。 钩子编程也被用于恶意代码，如rootkit是各种通过假扮系统API调用输出来隐藏自身进程的可见性的工具与技术；游戏外挂是另一类例子。 在应用程序执行之前，物理修改可执行程序，这典型通过找到函数调用入口点，修改入口点使之在函数被执行前先执行其他的代码。另一种挂钩的方法是修改可执行程序的输入表（import table）。还有一种挂钩方法是采用包装库，使得应用程序不需修改即可调用该包装库完成其功能，而在包装库中插入钩子然后再调用原有的库。 操作系统与软件可提供方法，在运行时插入事件钩子。Microsoft Windows允许插入钩子以处理或修改对话框、滚动条、菜单等的系统事件、应用程序事件；插入、删除、处理或修改键盘鼠标事件。Linux允许类似的钩子通过NetFilter以处理网络事件。 如果没有上述机制或权限，也可拦截进程的库函数调用，在函数调用开始处注入代码。这可通过修改内存中的进程的中断向量表（英语：Interrupt vector table）或输入表（import table）实现。 C++使用虚函数，因此可在运行时直接修改虚函数表的内容来挂钩。 Window上很多软件库以COM方式提供的（如DirectX）， 所以有需求拦截COM调用的COM Hook。COM里的接口是C++虚表的形式提供的，所以COM的Hook其实就是虚表（vtable）的Hook。ATL就是用接口替代的方式来调试和记录COM接口引用计数的次数 class VirtualTable { // example class public: virtual void VirtualFunction01( ticket ); }; void VirtualTable::VirtualFunction01( ticket ) { printf("VirtualFunction01 called"); } typedef void ( __thiscall* VirtualFunction01_t )( ticket* thisptr ); VirtualFunction01_t g_org_VirtualFunction01; //our detour function void __fastcall hk_VirtualFunction01( ticket* thisptr, int edx ) { printf("Custom function called"); //call the original function g_org_VirtualFunction01(thisptr); } int _tmain(int argc, _TCHAR* argv[]) { DWORD oldProtection; VirtualTable* myTable = new VirtualTable(); void** base = *(void***)myTable; VirtualProtect( &base[0], 4, PAGE_EXECUTE_READWRITE, &oldProtection ); //save the original function g_org_VirtualFunction01 = (VirtualFunction01_t)base[0]; //overwrite base[0] = &hk_VirtualFunction01; VirtualProtect( &base[0], 4, oldProtection, 0 ); //call the virtual function (now hooked) from our class instance myTable->VirtualFunction01(); return 0; } using System.Runtime.InteropServices; namespace Hooks { public class KeyHook { /* Member variables */ protected static int Hook; protected static LowLevelKeyboardDelegate Delegate; protected static readonly object Lock = new object(); protected static bool IsRegistered = false; /* DLL imports */ [DllImport("user32")] private static extern int SetWindowsHookEx(int idHook, LowLevelKeyboardDelegate lpfn, int hmod, int dwThreadId); [DllImport("user32")] private static extern int CallNextHookEx(int hHook, int nCode, int wParam, KBDLLHOOKSTRUCT lParam); [DllImport("user32")] private static extern int UnhookWindowsHookEx(int hHook); /* Types & constants */ protected delegate int LowLevelKeyboardDelegate(int nCode, int wParam, ref KBDLLHOOKSTRUCT lParam); private const int HC_ACTION = 0; private const int WM_KEYDOWN = 0x0100; private const int WM_KEYUP = 0x0101; private const int WH_KEYBOARD_LL = 13; [StructLayout(LayoutKind.Sequential)] public struct KBDLLHOOKSTRUCT { public int vkCode; public int scanCode; public int flags; public int time; public int dwExtraInfo; } /* Methods */ private static int LowLevelKeyboardHandler(int nCode, int wParam, ref KBDLLHOOKSTRUCT lParam) { if (nCode == HC_ACTION) { if (wParam == WM_KEYDOWN) System.Console.Out.WriteLine("Key Down: " + lParam.vkCode); else if (wParam == WM_KEYUP) System.Console.Out.WriteLine("Key Up: " + lParam.vkCode); } return CallNextHookEx(Hook, nCode, wParam, lParam); } public static bool RegisterHook() { lock (Lock) { if (IsRegistered) return true; Delegate = LowLevelKeyboardHandler; Hook = SetWindowsHookEx( WH_KEYBOARD_LL, Delegate, Marshal.GetHINSTANCE( System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0] ).ToInt32(), 0 ); if (Hook != 0) return IsRegistered = true; Delegate = null; return false; } } public static bool UnregisterHook() { lock (Lock) { return IsRegistered = (UnhookWindowsHookEx(Hook) != 0); } } } } 下述例子使用JMP（英语：JMP (x86 instruction)）指令，修改windows API中的MessageBoxW函数的前6个字节执行其他代码。这段代码编译为DLL文件，采用DLL注入技术让应用程序使用。微软提供了封装好的Detours库用于此目的。 /* This idea is based on chrom-lib approach, Distributed under GNU LGPL License. Source chrom-lib: https://github.com/linuxexp/chrom-lib Copyright (C) 2011 Raja Jamwal */ #include <windows.h> #define SIZE 6 typedef int (WINAPI *pMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT); // Messagebox prototype int WINAPI MyMessageBoxW(HWND, LPCWSTR, LPCWSTR, UINT); // Our detour void BeginRedirect(LPVOID); pMessageBoxW pOrigMBAddress = NULL; // address of original BYTE oldBytes[SIZE] = {0}; // backup BYTE JMP[SIZE] = {0}; // 6 byte JMP instruction DWORD oldProtect, myProtect = PAGE_EXECUTE_READWRITE; INT APIENTRY DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved) { switch(Reason) { case DLL_PROCESS_ATTACH: // if attached pOrigMBAddress = (pMessageBoxW) GetProcAddress(GetModuleHandle("user32.dll"), // get address of original "MessageBoxW"); if(pOrigMBAddress != NULL) BeginRedirect(MyMessageBoxW); // start detouring break; case DLL_PROCESS_DETACH: memcpy(pOrigMBAddress, oldBytes, SIZE); // restore backup case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break; } return TRUE; } void BeginRedirect(LPVOID newFunction) { BYTE tempJMP[SIZE] = {0xE9, 0x90, 0x90, 0x90, 0x90, 0xC3}; // 0xE9 = JMP 0x90 = NOP 0xC3 = RET memcpy(JMP, tempJMP, SIZE); // store jmp instruction to JMP DWORD JMPSize = ((DWORD)newFunction - (DWORD)pOrigMBAddress - 5); // calculate jump distance VirtualProtect((LPVOID)pOrigMBAddress, SIZE, // assign read write protection PAGE_EXECUTE_READWRITE, &oldProtect); memcpy(oldBytes, pOrigMBAddress, SIZE); // make backup memcpy(&JMP[1], &JMPSize, 4); // fill the nop's with the jump distance (JMP,distance(4bytes),RET) memcpy(pOrigMBAddress, JMP, SIZE); // set jump instruction at the beginning of the original function VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL); // reset protection } int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uiType) { VirtualProtect((LPVOID)pOrigMBAddress, SIZE, myProtect, NULL); // assign read write protection memcpy(pOrigMBAddress, oldBytes, SIZE); // restore backup int retValue = MessageBoxW(hWnd, lpText, lpCaption, uiType); // get return value of original function memcpy(pOrigMBAddress, JMP, SIZE); // set the jump instruction again VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL); // reset protection return retValue; // return original return value } 使用Netfilter钩子修改Linux内核的网络交通。 #include <linux/module.h> #include <linux/kernel.h> #include <linux/skbuff.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/in.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> /* Port we want to drop packets on */ static const uint16_t port = 25; /* This is the hook function itself */ static unsigned int hook_func(unsigned int hooknum, struct sk_buff **pskb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { struct iphdr *iph = ip_hdr(*pskb); struct tcphdr *tcph, tcpbuf; if (iph->protocol != IPPROTO_TCP) return NF_ACCEPT; tcph = skb_header_pointer(*pskb, ip_hdrlen(*pskb), sizeof(*tcph), &tcpbuf); if (tcph == NULL) return NF_ACCEPT; return (tcph->dest == port) ? NF_DROP : NF_ACCEPT; } /* Used to register our hook function */ static struct nf_hook_ops nfho = { .hook = hook_func, .hooknum = NF_IP_PRE_ROUTING, .pf = NFPROTO_IPV4, .priority = NF_IP_PRI_FIRST, }; static __init int my_init(void) { return nf_register_hook(&nfho); } static __exit void my_exit(void) { nf_unregister_hook(&nfho); } module_init(my_init); module_exit(my_exit); 下例展示通过修改可执行程序的输入地址表（IAT），把钩子函数替代原函数。 #include <windows.h> typedef int(__stdcall *pMessageBoxA) (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType); //This is the 'type' of the MessageBoxA call. pMessageBoxA RealMessageBoxA; //This will store a pointer to the original function. void DetourIATptr(const char* function, void* newfunction, HMODULE module); int __stdcall NewMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) { //Our fake function printf("The String Sent to MessageBoxA Was : %s\n", lpText); return RealMessageBoxA(hWnd, lpText, lpCaption, uType); //Call the real function } int main(int argc, CHAR *argv[]) { DetourIATptr("MessageBoxA",(void*)NewMessageBoxA,0); //Hook the function MessageBoxA(NULL, "Just A MessageBox", "Just A MessageBox", 0); //Call the function -- this will invoke our fake hook. return 0; } void **IATfind(const char *function, HMODULE module) { //Find the IAT (Import Address Table) entry specific to the given function. int ip = 0; if (module == 0) module = GetModuleHandle(0); PIMAGE_DOS_HEADER pImgDosHeaders = (PIMAGE_DOS_HEADER)module; PIMAGE_NT_HEADERS pImgNTHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)pImgDosHeaders + pImgDosHeaders->e_lfanew); PIMAGE_IMPORT_DESCRIPTOR pImgImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE)pImgDosHeaders + pImgNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); if (pImgDosHeaders->e_magic != IMAGE_DOS_SIGNATURE) printf("libPE Error : e_magic is no valid DOS signature\n"); for (IMAGE_IMPORT_DESCRIPTOR *iid = pImgImportDesc; iid->Name != NULL; iid++) { for (int funcIdx = 0; *(funcIdx + (LPVOID*)(iid->FirstThunk + (SIZE_T)module)) != NULL; funcIdx++) { char *modFuncName = (char*)(*(funcIdx + (SIZE_T*)(iid->OriginalFirstThunk + (SIZE_T)module)) + (SIZE_T)module + 2); const uintptr_t nModFuncName = (uintptr_t)modFuncName; bool isString = !(nModFuncName & (sizeof(nModFuncName) == 4 ? 0x80000000 : 0x8000000000000000)); if (isString) { if (!_stricmp(function, modFuncName)) return funcIdx + (LPVOID*)(iid->FirstThunk + (SIZE_T)module); } } } return 0; } void DetourIATptr(const char *function, void *newfunction, HMODULE module) { void **funcptr = IATfind(function, module); if (*funcptr == newfunction) return; DWORD oldrights, newrights = PAGE_READWRITE; //Update the protection to READWRITE VirtualProtect(funcptr, sizeof(LPVOID), newrights, &oldrights); RealMessageBoxA = (pMessageBoxA)*funcptr; //Some compilers require the cast like "MinGW" not sure about MSVC *funcptr = newfunction; //Restore the old memory protection flags. VirtualProtect(funcptr, sizeof(LPVOID), oldrights, &newrights); } SetWinEventHook：基本没有权限问题，也就是说这个API可以Hook到高权限程序的事件，同时支持进程内(WINEVENT_INCONTEXT)和进程外(WINEVENT_OUTOFCONTEXT)2种Hook方式，可以进程外的方式Hook到64位程序的事件。 SetWindowsHookEx：64位编程情形，32位DLL没法直接注入到64位的应用程序里面， 因为地址空间完全不一样。UAC打开的情况下低权限程序没法Hook高权限程序。对于64位问题，解决方法是提供2个DLL，分别可以Hook32和64位程序。对于权限问题，通过注册系统服务，由服务进程创建工作进程。因为Windows Vista开始有了Session隔离机制，服务进程运行在Session 0，用户程序运行在Session 1, Session 2等，如果直接在服务程序里干活，我们就只能在Session 0里工作。通过创建进程，可以在DuplicateTokenEx后将Token的SessionID设置成目标Session，并且在CreateProcessAsUser时指定目标WinStation和Desktop， 这样就获得了System权限，并且也可以和当前桌面进程交互。 回调函数 委托 (编程) 终止并驻留（英语：Terminate and Stay Resident#Using TSR） User exit（英语：User exit） WinAPIOverride32（英语：WinAPIOverride32） For more information, see http://ntvalk.blogspot.nl/2013/11/hooking-explained-detouring-library.html （页面存档备份，存于互联网档案馆） Jonathan Daniel. Hooking explained: detouring library calls and vtable patching in Windows/Linux/MAC-OSX. 2013-11-27 [2014-01-01]. （原始内容存档于2018-05-03）.  Binh Nguyen. Hacking-Lexicon / Linux Dictionary V 0.16. 2004-08-16 [2008-02-23]. （原始内容存档于2021-03-22）. Hook  Author: Holy Father. Hooking Windows API - Technics of hooking API functions on Windows 1.1 english (PDF). 2002-06-10 [2008-02-21]. （原始内容存档 (PDF)于2020-10-28）.  Information on Import Address Table function hooking. （页面存档备份，存于互联网档案馆） Information from Microsoft on hooking （页面存档备份，存于互联网档案馆） Information and various techniques regarding x86 hooking. （页面存档备份，存于互联网档案馆） APISpy32 （页面存档备份，存于互联网档案馆） is an application used to hook win32 API. Detours （页面存档备份，存于互联网档案馆） is a general purpose function hooking library created by Microsoft Research which works in C / C++. winspy （页面存档备份，存于互联网档案馆） Three ways to inject code into another process. HookTool SDK (ACF SDK) （页面存档备份，存于互联网档案馆） Provides a comprehensive overview on API hooking and code injection. A commercial product available too. madCodeHook （页面存档备份，存于互联网档案馆） is a commercial x86 and x64 API hooking and DLL injection library for C++ and Delphi. EasyHook （页面存档备份，存于互联网档案馆） is an open source hooking engine supporting x86 and x64 in Windows in both user and kernel land. SpyStudio Application Trace （页面存档备份，存于互联网档案馆） SpyStudio is an Application tracer which hook calls, displaying the results in a structured way. rohitab.com API Monitor （页面存档备份，存于互联网档案馆） is a freeware application that can hook and display 10,000+ Windows APIs and COM Interfaces in 32-bit and 64-bit applications and services. Deviare API Hook （页面存档备份，存于互联网档案馆） Deviare is a freeware inter-process hook framework that can be used to intercept other processes' API calls and show full-parameter information or create API monitors. WinAPIOverride （页面存档备份，存于互联网档案馆） WinAPIOverride is a freeware for non commercial use. It can hook win32 API, COM, OLE, ActiveX, .NET in 32-bit and 64-bit processes. It includes monitoring post analysis tools. urmem （页面存档备份，存于互联网档案馆） C++11 cross-platform library (x86) for working with memory (hooks, patches, pointer's wrapper, signature scanner etc.) [1] A student research project that utilizes hooking. [2] Functionality that allows a piece of software to observe and control the execution of another process. [3] （页面存档备份，存于互联网档案馆） Use of LD_PRELOAD to hook shared library calls. Emacs Hooks （页面存档备份，存于互联网档案馆） Hooks are an important mechanism for customization of Emacs. A hook is a Lisp variable which holds a list of functions, to be called on some well-defined occasion. (This is called running the hook.) Cydia Substrate （页面存档备份，存于互联网档案馆） is a framework for jailbroken iOS devices allowing developers to hook into any other framework or application. harpoon （页面存档备份，存于互联网档案馆） is an OS X library for runtime function hooking. x86 API Hooking Demystified （页面存档备份，存于互联网档案馆） Article on various API Hooking methods, for the x86 architecture.