65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (43投票s)

2009 年 12 月 28 日

CPOL

5分钟阅读

viewsIcon

487690

downloadIcon

8358

本文介绍了一种简便的方法来设置系统范围的全局 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
(REG_SZ)

要加载的 DLL 的空格或逗号分隔列表。应使用短文件名指定 DLL 的完整路径。 C:\PROGRA~1\Test\Test.dll
RequireSignedAppInit_DLLs
(REG_DWORD)
要求代码签名的 DLL。 0x0 – 加载任何 DLL。
0x1 – 仅加载代码签名的 DLL。

表 1 - AppInit_DLLs 基础结构注册表值。

3. Mhook 库

有几个用于 API 挂钩的库。它们通常执行以下操作:

  1. 用自定义代码覆盖目标函数的开头(所谓的 **跳板**)。当函数执行时,它将跳转到挂钩处理程序。
  2. 将目标函数已覆盖的原始代码存储在某处。这对于目标函数的正确运行是必需的。
  3. 恢复目标函数已覆盖的部分。

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 的根目录下。

api-hooks/diskc.PNG

图 1 - 挂钩 DLL 已放入磁盘 C 的根目录。

打开注册表编辑器,找到 AppInit_DLLs 注册表项(该项是 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT \CurrentVersion\Windows)。然后指定挂钩 DLL 的路径(在本例中为 C:\AppInitHook.dll)。

api-hooks/registry.PNG

图 2 – 修改注册表。

修改注册表后,挂钩开始工作。让我们运行几个 calc.exe 实例。然后打开 Windows 任务管理器,查看进程选项卡。根本没有 calc.exe!

api-hooks/taskmgr.png

图 3 - Windows 任务管理器进程选项卡。

让我们看看 Mark Russinovich 编写的另一个流行工具 - Process Explorer - 显示了什么。

api-hooks/procexp.png

图 4 - Process Explorer 显示没有 calc.exe。

所有 calc.exe 实例都已成功隐藏。最后,运行命令行工具 tasklist.exe。

api-hooks/tasklist.png

图 5 - Tasklist.exe 运行进程列表。

挂钩正在工作!

6. 局限性

此挂钩技术有一些您应该了解的局限性:

  1. 如前所述,此挂钩仅应用于那些链接到 User32.dll 的进程。
  2. 由于挂钩是在 User32.dllDllMain 中执行的,因此您只能调用 Kernel32.dllNtdll.dll 中的函数(其他库尚未初始化)。
  3. Windows 7/Windows 2008 R2 引入了一项新的安全功能 - AppInit DLL 必须经过数字签名(但是有一个注册表项可以关闭此功能)。
  4. AppInit DLL 的文件路径不应包含空格。

有用参考

  1. Apriorit 网站上的 这篇文章
  2. 使用 AppInit_DLLs 注册表值
  3. Windows 7 和 Windows Server 2008 R2 中的 AppInit DLL
  4. API Hooking揭秘
  5. Mhook,一个 API 挂钩库,v2.2
  6. Microsoft Research 的 Detours
  7. DllMain 回调函数
© . All rights reserved.