设置全局 API 挂钩的简便方法






4.99/5 (43投票s)
本文介绍了一种简便的方法来设置系统范围的全局 API 挂钩。
目录
1. 引言
1.1. 什么是 API 挂钩?
1.2. 本地挂钩和全局挂钩
2. AppInit_DLLs 基础结构
3. Mhook 库
4. 编写代码
4.1. 原始函数
4.2. 被挂钩的函数
4.3. 设置挂钩
4.4. 取消挂钩
5. 运行示例
6. 局限性
7. 有用参考
1. 引言
本文介绍了一种简便的方法来设置系统范围的全局 API 挂钩。它使用 AppInit_DLLs 注册表项进行 DLL 注入,并使用 Mhook 库进行 API 挂钩。为了演示此技术,我们将展示如何轻松地将 calc.exe 从正在运行的进程列表中隐藏。
1.1 什么是 API 挂钩?
API 挂钩意味着拦截某些 API 函数调用。通过这种方式,您可以改变任何软件的行为。挂钩被广泛应用于杀毒软件、安全应用程序、系统实用程序、编程工具等。
1.2 本地挂钩和全局挂钩
有两种类型的挂钩:本地挂钩和全局挂钩。本地挂钩仅应用于特定应用程序。全局挂钩应用于系统中的所有进程。本文介绍的挂钩技术是全局的,并影响所有会话中的所有进程(与绑定到特定桌面的 SetWindowsHooks
方法不同)。
2. AppInit_DLLs 基础结构
AppInit_DLLs
基础结构是一种机制,用于将任意 DLL 列表加载到所有与 User32.dll 链接的用户模式进程中(实际上,很少有可执行文件不链接它)。DLL 在 User32.dll 初始化时被加载。
AppInit_DLLs
基础结构的行为由存储在注册表中 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows 键下的一组值配置。这些注册表值在表中进行描述
值 | 描述 | 示例值 |
LoadAppInit_DLLs (REG_DWORD) |
全局启用或禁用 AppInit_DLLs 的值。 | 0x0 – AppInit_DLLs 已禁用。 0x1 – AppInit_DLLs 已启用。 |
AppInit_DLLs |
要加载的 DLL 的空格或逗号分隔列表。应使用短文件名指定 DLL 的完整路径。 | C:\PROGRA~1\Test\Test.dll |
RequireSignedAppInit_DLLs (REG_DWORD) |
要求代码签名的 DLL。 | 0x0 – 加载任何 DLL。 0x1 – 仅加载代码签名的 DLL。 |
表 1 - AppInit_DLLs 基础结构注册表值。
3. Mhook 库
有几个用于 API 挂钩的库。它们通常执行以下操作:
- 用自定义代码覆盖目标函数的开头(所谓的 **跳板**)。当函数执行时,它将跳转到挂钩处理程序。
- 将目标函数已覆盖的原始代码存储在某处。这对于目标函数的正确运行是必需的。
- 恢复目标函数已覆盖的部分。
Mhook 是一个免费的开源 API 挂钩库。它支持 x86 和 x64 平台,并且非常易于使用。Mhook 接口简单且自成体系。
BOOL Mhook_SetHook(PVOID *ppSystemFunction, PVOID pHookFunction); BOOL Mhook_Unhook(PVOID *ppHookedFunction);
有关库使用的更多信息,请参见下一段中的代码示例,或访问 Mhook 主页。
4. 编写代码
我们将编写一个用户模式 DLL。首先,您应该下载最新的 Mhook 源代码并将其添加到项目中。如果您使用预编译头文件,请为 Mhook 文件禁用它。
如上所述,我们的示例将隐藏 calc.exe,使其不出现在正在运行的进程列表中。
4.1 原始函数
正在运行的进程列表通过调用 NTAPI 函数 NtQuerySystemInformation
来查询。因此,我们需要在项目中添加一些 NTAPI 内容。不幸的是,winternl.h 头文件不包含完整信息,我们必须自己定义所需的数据类型。
///////////////////////////////////////////////////////////////////////// // Defines and typedefs #define STATUS_SUCCESS ((NTSTATUS)0x00000000L) typedef struct _MY_SYSTEM_PROCESS_INFORMATION { ULONG NextEntryOffset; ULONG NumberOfThreads; LARGE_INTEGER Reserved[3]; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ImageName; ULONG BasePriority; HANDLE ProcessId; HANDLE InheritedFromProcessId; } MY_SYSTEM_PROCESS_INFORMATION, *PMY_SYSTEM_PROCESS_INFORMATION; typedef NTSTATUS (WINAPI *PNT_QUERY_SYSTEM_INFORMATION)( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength );
要存储原始函数地址,请创建一个全局变量并进行初始化。
////////////////////////////////////////////////////////////////////////// // Original function PNT_QUERY_SYSTEM_INFORMATION OriginalNtQuerySystemInformation = (PNT_QUERY_SYSTEM_INFORMATION)::GetProcAddress(::GetModuleHandle(L"ntdll"), "NtQuerySystemInformation");
被挂钩的函数
在被挂钩的函数中,我们首先调用原始函数。然后检查 SystemInformationClass
。如果它是 SystemProcessInformation
,我们将遍历正在运行的进程列表,找到所有 calc.exe 的条目,并将它们从列表中移除。就这样!
注意:此函数必须与原始函数具有相同的签名。
////////////////////////////////////////////////////////////////////////// // Hooked function NTSTATUS WINAPI HookedNtQuerySystemInformation( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength ) { NTSTATUS status = OriginalNtQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); if (SystemProcessInformation == SystemInformationClass && STATUS_SUCCESS == status) { // // Loop through the list of processes // PMY_SYSTEM_PROCESS_INFORMATION pCurrent = NULL; PMY_SYSTEM_PROCESS_INFORMATION pNext = (PMY_SYSTEM_PROCESS_INFORMATION)SystemInformation; do { pCurrent = pNext; pNext = (PMY_SYSTEM_PROCESS_INFORMATION)((PUCHAR)pCurrent + pCurrent->NextEntryOffset); if (!wcsncmp(pNext->ImageName.Buffer, L"calc.exe", pNext->ImageName.Length)) { if (0 == pNext->NextEntryOffset) { pCurrent->NextEntryOffset = 0; } else { pCurrent->NextEntryOffset += pNext->NextEntryOffset; } pNext = pCurrent; } } while(pCurrent->NextEntryOffset != 0); } return status; }
4.3 设置挂钩
设置挂钩非常简单:当 DLL 加载到新进程时,在 DllMain
中调用 Mhook_SetHook
。
////////////////////////////////////////////////////////////////////////// // Entry point BOOL WINAPI DllMain( __in HINSTANCE hInstance, __in DWORD Reason, __in LPVOID Reserved ) { switch (Reason) { case DLL_PROCESS_ATTACH: Mhook_SetHook((PVOID*)&OriginalNtQuerySystemInformation, HookedNtQuerySystemInformation); break;
4.4 取消挂钩
取消挂钩是通过在 DLL 从进程卸载时在 DllMain
中调用 Mhook_Unhook
来执行的。
////////////////////////////////////////////////////////////////////////// // Entry point BOOL WINAPI DllMain( __in HINSTANCE hInstance, __in DWORD Reason, __in LPVOID Reserved ) { switch (Reason) { ... case DLL_PROCESS_DETACH: Mhook_Unhook((PVOID*)&OriginalNtQuerySystemInformation); break; }
5. 运行示例
现在是时候展示描述的挂钩是如何工作的了。构建项目,并将生成的 AppInitHook.dll 放在磁盘 C 的根目录下。
图 1 - 挂钩 DLL 已放入磁盘 C 的根目录。
打开注册表编辑器,找到 AppInit_DLLs
注册表项(该项是 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT \CurrentVersion\Windows)。然后指定挂钩 DLL 的路径(在本例中为 C:\AppInitHook.dll)。
图 2 – 修改注册表。
修改注册表后,挂钩开始工作。让我们运行几个 calc.exe 实例。然后打开 Windows 任务管理器,查看进程选项卡。根本没有 calc.exe!
图 3 - Windows 任务管理器进程选项卡。
让我们看看 Mark Russinovich 编写的另一个流行工具 - Process Explorer - 显示了什么。
图 4 - Process Explorer 显示没有 calc.exe。
所有 calc.exe 实例都已成功隐藏。最后,运行命令行工具 tasklist.exe。
图 5 - Tasklist.exe 运行进程列表。
挂钩正在工作!
6. 局限性
此挂钩技术有一些您应该了解的局限性:
- 如前所述,此挂钩仅应用于那些链接到 User32.dll 的进程。
- 由于挂钩是在 User32.dll 的
DllMain
中执行的,因此您只能调用 Kernel32.dll 和 Ntdll.dll 中的函数(其他库尚未初始化)。 - Windows 7/Windows 2008 R2 引入了一项新的安全功能 - AppInit DLL 必须经过数字签名(但是有一个注册表项可以关闭此功能)。
- AppInit DLL 的文件路径不应包含空格。