Reactos 第 7 章 视窗报文 — 7.4 用户空间的外挂函数

第 7 章 视窗报文 --- 7.4 用户空间的外挂函数

本节深入剖析 Windows/ReactOS 中的"外挂函数"(Hook)机制,包括 Windows 钩子(WH_*)、事件钩子(WinEvent Hook)、子类化(Subclassing)等用户态可注册的拦截机制。

概述

外挂函数(Hook)是 Windows 中一种强大的扩展机制,允许应用程序拦截和监视系统中的关键事件。通过 Hook,应用程序可以:

  • 截获所有键盘/鼠标输入(实现全局快捷键、键位记录器);
  • 监视窗口创建/销毁(实现 UI 自动化、辅助功能);
  • 过滤消息流(实现调试器、消息分析工具);
  • 拦截 DLL 加载(实现 API Hook 框架);
  • 接收系统级事件通知(实现屏幕阅读器、自动化测试)。

外挂函数的本质是什么?

外挂函数是一种"分层拦截"机制:系统在事件处理路径上设置了若干"钩子点",应用程序可以注册回调函数挂载到这些钩子点。当事件流经钩子点时,系统会依次调用所有已注册的回调函数,每个回调都可以观察、修改甚至阻断事件。

想象一个快递配送中心:

  • Hook 链:快递中心各环节的安检点(验视、扫描、签收);
  • Hook 函数:每个安检点的安检员(可以开箱检查、盖章、扣件);
  • WH_KEYBOARD:在"签收前"的安检点(监听键盘按键);
  • WH_GETMESSAGE:在"分发前"的安检点(监听消息队列);
  • 全局 Hook:影响所有用户的"国家安全检查"(需要 DLL 注入);
  • 线程 Hook:只影响当前线程的"部门内部检查"。

本节内容概览

  1. 7.4.0 框架图:Hook 机制完整架构;
  2. 7.4.1 Hook 类型总览:13 种 WH_* 钩子;
  3. 7.4.2 PHOOK 数据结构:内核态钩子对象;
  4. 7.4.3 NtUserSetWindowsHookEx 安装钩子
  5. 7.4.4 NtUserUnhookWindowsHookEx 卸载钩子
  6. 7.4.5 co_HOOK_CallHooks 触发钩子链
  7. 7.4.6 co_IntCallHookProc 钩子回调核心
  8. 7.4.7 钩子调用下一个 CallNextHookEx
  9. 7.4.8 事件钩子 WinEvent Hook
  10. 7.4.9 WH_DEBUG 调试钩子
  11. 7.4.10 WndProc 子类化
  12. 7.4.11 设计哲学问答:5 个关键设计问题解答。

学习目标

读完本节后,读者应当能够:

  • 理解 Windows 13 种 WH_* 钩子的分类与用途;
  • 掌握 PHOOK 内核态对象的字段含义;
  • 分析 SetWindowsHookEx / UnhookWindowsHookEx 完整流程;
  • 理解全局钩子与线程钩子的区别(涉及 DLL 注入);
  • 掌握 CallNextHookEx 链式调用的实现;
  • 理解事件钩子(Accessibility)与传统钩子的区别;
  • 解释 WndProc 子类化的实现机制。

涉及的内核子系统

子系统 职责
win32k.sys/ntuser 钩子链表管理、调用分发
win32k.sys/ntuser 钩子对象分配、线程/全局钩子挂载
user32.dll 钩子入口函数、SetWindowsHookEx 用户态包装
ntdll.dll DLL 注入机制(全局钩子)

7.4.0 框架图

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────┐
│                    外挂函数(Hook)机制完整架构                                          │
├──────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                      │
│   钩子类型分类                                                                          │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  线程钩子:影响指定线程  ── pti->aphkStart[HookId]                        │     │
│   │  全局钩子:影响所有线程  ── pDesk->pGlobalHooks[HookId]                   │     │
│   │                                                                            │     │
│   │  按用途分类:                                                               │     │
│   │  ├─► 输入拦截:WH_KEYBOARD / WH_MOUSE / WH_KEYBOARD_LL / WH_MOUSE_LL     │     │
│   │  ├─► 消息过滤:WH_GETMESSAGE / WH_CALLWNDPROC / WH_CALLWNDPROCRET        │     │
│   │  ├─► 窗口事件:WH_CBT                                                     │     │
│   │  ├─► Shell 事件:WH_SHELL                                                 │     │
│   │  ├─► 调试支持:WH_DEBUG / WH_FOREGROUNDIDLE                              │     │
│   │  └─► 日志回放:WH_JOURNALRECORD / WH_JOURNALPLAYBACK                     │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                                                                                      │
│   钩子触发点                                                                            │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  内核态事件                              触发的钩子                        │     │
│   │  ─────────────────────────────────────────────────────────────────         │     │
│   │  键盘/鼠标输入                       ── WH_KEYBOARD_LL / WH_MOUSE_LL     │     │
│   │  窗口创建/销毁                       ── WH_CBT                           │     │
│   │  GetMessage / PeekMessage 取出消息    ── WH_GETMESSAGE                    │     │
│   │  消息发送到 WndProc 之前             ── WH_CALLWNDPROC                   │     │
│   │  WndProc 返回消息之后                 ── WH_CALLWNDPROCRET                │     │
│   │  消息对话框/菜单处理                  ── WH_MSGFILTER / WH_SYSMSGFILTER   │     │
│   │  前台进程切换                        ── WH_FOREGROUNDIDLE / WH_SHELL     │     │
│   │  Shell 事件                          ── WH_SHELL                         │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                                                                                      │
│   钩子链与执行                                                                          │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  PHOOK Chain:                                                               │     │
│   │  ┌─────┐    ┌─────┐    ┌─────┐    ┌─────┐                                 │     │
│   │  │Hook1├─►│Hook2├─►│Hook3├─►│HookN├─► (原处理函数)                       │     │
│   │  │Proc1│    │Proc2│    │Proc3│    │ProcN│                                 │     │
│   │  └─────┘    └─────┘    └─────┘    └─────┘                                 │     │
│   │     │           │           │           │                                  │     │
│   │     ▼           ▼           ▼           ▼                                  │     │
│   │  CallNextHookEx ──► CallNextHookEx ──► CallNextHookEx ──► 原始处理       │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                                                                                      │
└──────────────────────────────────────────────────────────────────────────────────────┘

7.4.1 Hook 类型总览

7.4.1.1 13 种 WH_* 钩子

win32ss/user/ntuser/hook.h(file:///d:/reactos/win32ss/user/ntuser/hook.h) 定义了所有标准钩子类型:

HookId 名称 触发时机 用途
WH_MINHOOK (0) 内部保留 内部使用 内核钩子
WH_MSGFILTER (1) 消息过滤 对话框/菜单消息 监视对话框输入
WH_JOURNALRECORD (2) 日志记录 输入到系统消息队列 记录输入事件
WH_JOURNALPLAYBACK (3) 日志回放 消息分发 回放记录的事件
WH_KEYBOARD (4) 键盘 WM_KEYDOWN 等 全局键盘监视
WH_GETMESSAGE (5) 消息取出 GetMessage/PeekMessage 监视消息队列
WH_CALLWNDPROC (6) 窗口过程调用 消息到 WndProc 之前 拦截消息
WH_CALLWNDPROCRET (7) 窗口过程返回 WndProc 返回后 观察返回值
WH_MOUSE (8) 鼠标 鼠标消息 全局鼠标监视
WH_HARDWARE (9) 硬件 硬件消息 硬件输入(罕见)
WH_CBT (10) CBT 窗口/系统事件 训练课程,UI 自动化
WH_SYSMSGFILTER (11) 系统消息过滤 系统级消息 监视全局消息
WH_SHELL (12) Shell Shell 事件 外壳钩子
WH_FOREGROUNDIDLE (13) 前台空闲 前台线程空闲 空闲处理
WH_KEYBOARD_LL (14) 低级键盘 原始键盘输入 全局热键(不需要 DLL 注入)
WH_MOUSE_LL (15) 低级鼠标 原始鼠标输入 全局鼠标拦截

7.4.1.2 线程钩子 vs 全局钩子

类型 范围 实现机制
线程钩子 仅当前线程 通过 pthread->aphkStart[HookId] 链表
全局钩子 所有线程(桌面内) 通过 pDesktop->pGlobalHooks[HookId] 链表,需要 DLL 注入(系统将含钩子函数的 DLL 注入到所有 GUI 进程)

特殊例外WH_KEYBOARD_LLWH_MOUSE_LL全局钩子但不需要 DLL 注入,因为它们在内核态实现。


7.4.2 PHOOK 数据结构

7.4.2.1 HOOK 结构定义

win32ss/user/ntuser/hook.h(file:///d:/reactos/win32ss/user/ntuser/hook.h):

c 复制代码
typedef struct _HOOK
{
    HEAD head;                  // 通用对象头部
    PTHREADINFO pti;            // 拥有此钩子的线程
    PDESKTOP pDesk;             // 所属桌面(全局钩子)
    PHOOK phkNext;              // 链表中下一个钩子
    INT HookId;                 // 钩子类型
    HOOKPROC Proc;              // 钩子函数指针
    INT ihmod;                  // 模块索引
    ULONG_PTR offPfn;           // 函数偏移
    BOOLEAN Ansi;               // 是否为 ANSI 钩子
    UNICODE_STRING ModuleName;  // 模块名
} HOOK, *PHOOK;

7.4.2.2 HOOKPACK 结构

钩子分发时使用的"钩子包":

c 复制代码
typedef struct _HOOKPACK
{
    PHOOK pHk;                  // 钩子对象
    LPARAM lParam;              // 钩子的 lParam
    PVOID pHookStructs;         // 钩子结构(MSLLHOOKSTRUCT 等)
} HOOKPACK, *PHOOKPACK;

7.4.2.3 钩子链表

c 复制代码
// 每线程钩子链表
PHOOK pti->aphkStart[NB_HOOKS];  // NB_HOOKS = 16

// 全局钩子链表
PHOOK pDesk->pGlobalHooks[NB_HOOKS];

NB_HOOKS 是钩子类型数量(包含 LL 钩子)。


7.4.3 NtUserSetWindowsHookEx 安装钩子

7.4.3.1 函数签名

c 复制代码
HHOOK
APIENTRY
NtUserSetWindowsHookEx(
    HINSTANCE Mod,        // 钩子函数所在模块的句柄
    PUNICODE_STRING UnsafeModuleName,  // 模块名
    DWORD ThreadId,       // 目标线程 ID(0 表示全局)
    int HookId,           // 钩子类型
    HOOKPROC HookProc,    // 钩子函数指针
    BOOL Ansi             // 是否为 ANSI 钩子
);

7.4.3.2 关键实现步骤

win32ss/user/ntuser/hook.c:1439(file:///d:/reactos/win32ss/user/ntuser/hook.c#L1439):

  1. 验证参数:检查 HookId 范围、ThreadId 有效性等;
  2. 创建 HOOK 对象:分配 PHOOK 并初始化;
  3. 加载模块 (全局钩子):调用 IntLoadHookModule 将模块加载到所有相关进程;
  4. 挂载到链表
    • 线程钩子:插入 pti->aphkStart[HookId] 链表;
    • 全局钩子:插入 pDesk->pGlobalHooks[HookId] 链表;
  5. 更新 pti->fsHooks:标记该线程启用了哪些钩子类型;
  6. 返回 HHOOK 句柄

7.4.3.3 fsHooks 标志

PCLIENTINFO->fsHooks 是位图,标识线程已启用的钩子类型:

c 复制代码
#define WH_MINHOOK_MASK        0x0000FFFF
#define WH_VALID_HOOKS (WH_MINHOOK_MASK & ~((1<<WH_MOUSE_LL) | (1<<WH_KEYBOARD_LL)))

用于快速检查(避免遍历链表)。


7.4.4 NtUserUnhookWindowsHookEx 卸载钩子

7.4.4.1 函数实现

win32ss/user/ntuser/hook.c:1691(file:///d:/reactos/win32ss/user/ntuser/hook.c#L1691):

c 复制代码
BOOL APIENTRY
NtUserUnhookWindowsHookEx(HHOOK Hook)
{
    PHOOK pHook;
    if (!(pHook = IntGetHookObject(Hook)))
    {
        EngSetLastError(ERROR_INVALID_HOOK_HANDLE);
        return FALSE;
    }
    UserReferenceObject(pHook);
    IntRemoveHook(pHook);
    UserDereferenceObject(pHook);
    return TRUE;
}

7.4.4.2 IntRemoveHook

c 复制代码
BOOL FASTCALL
IntRemoveHook(PVOID Object)
{
    PHOOK Hook = Object;
    UserDeleteObject(Hook, &Hook->head);
    return TRUE;
}

钩子删除会触发引用计数减少,最终调用 IntFreeHook 释放资源。

7.4.4.3 IntFreeHook

win32ss/user/ntuser/hook.c:1022(file:///d:/reactos/win32ss/user/ntuser/hook.c#L1022):

c 复制代码
VOID FASTCALL
IntFreeHook(PHOOK Hook)
{
    /* Free the module name */
    if (Hook->ModuleName.Buffer)
        ExFreePoolWithTag(Hook->ModuleName.Buffer, USERTAG_HOOK);

    /* Unlink from the hook chain */
    if (Hook->phkNext)
        Hook->phkNext->phkNext = NULL;
    // ... 实际是链表遍历移除 ...

    ExFreePoolWithTag(Hook, USERTAG_HOOK);
}

7.4.5 co_HOOK_CallHooks 触发钩子链

7.4.5.1 函数签名

c 复制代码
LRESULT
APIENTRY
co_HOOK_CallHooks(
    INT HookId,    // 钩子类型
    INT Code,      // 钩子代码
    WPARAM wParam,
    LPARAM lParam);

7.4.5.2 实现位置

win32ss/user/ntuser/hook.c:1102(file:///d:/reactos/win32ss/user/ntuser/hook.c#L1102):

co_HOOK_CallHooks 是触发钩子的总入口:

  • 遍历 pDesk->pGlobalHooks[HookId](全局钩子);
  • 遍历 pti->aphkStart[HookId](线程钩子);
  • 依次调用每个钩子。

7.4.5.3 钩子调用伪代码

c 复制代码
LRESULT co_HOOK_CallHooks(INT HookId, INT Code, WPARAM wParam, LPARAM lParam)
{
    PHOOK Hook;
    LRESULT Result = 0;
    BOOLEAN Called = FALSE;

    /* 1. 全局钩子 */
    if (pDesk->pGlobalHooks[HookId])
    {
        for (Hook = pDesk->pGlobalHooks[HookId]; Hook != NULL; Hook = Hook->phkNext)
        {
            Result = co_IntCallHookProc(HookId, Code, wParam, lParam, ...);
            Called = TRUE;
        }
    }

    /* 2. 线程钩子 */
    if (pti->aphkStart[HookId])
    {
        for (Hook = pti->aphkStart[HookId]; Hook != NULL; Hook = Hook->phkNext)
        {
            Result = co_IntCallHookProc(HookId, Code, wParam, lParam, ...);
            Called = TRUE;
        }
    }

    return Called ? Result : 0;
}

7.4.6 co_IntCallHookProc 钩子回调核心

7.4.6.1 函数签名

win32ss/user/ntuser/callback.h:27(file:///d:/reactos/win32ss/user/ntuser/callback.h#L27):

c 复制代码
LRESULT APIENTRY
co_IntCallHookProc(
    INT HookId,
    INT Code,
    WPARAM wParam,
    LPARAM lParam,
    HOOKPROC Proc,
    INT Mod,
    ULONG_PTR offPfn,
    BOOLEAN Ansi,
    PUNICODE_STRING ModuleName);

7.4.6.2 实现(简化)

c 复制代码
LRESULT
co_IntCallHookProc(INT HookId, INT Code, WPARAM wParam, LPARAM lParam,
                   HOOKPROC Proc, INT Mod, ULONG_PTR offPfn, BOOLEAN Ansi,
                   PUNICODE_STRING ModuleName)
{
    HOOKPROC_CALLBACK_ARGUMENTS StackArguments = { 0 };
    PHOOKPROC_CALLBACK_ARGUMENTS Arguments;
    NTSTATUS Status;
    PVOID ResultPointer, pActCtx;
    ULONG ResultLength, ArgumentLength;
    LRESULT Result;
    PWND pWnd = NULL;
    HWND hWnd = NULL;

    /* 桌面线程不允许回调到用户态 */
    ASSERT(PsGetCurrentThreadWin32Thread() != gptiDesktopThread);

    Arguments = &StackArguments;
    ArgumentLength = sizeof(HOOKPROC_CALLBACK_ARGUMENTS);
    Arguments->HookId = HookId;
    Arguments->Code = Code;
    Arguments->wParam = wParam;
    Arguments->lParam = lParam;
    Arguments->Proc = Proc;
    Arguments->Mod = Mod;
    Arguments->offPfn = offPfn;
    Arguments->Ansi = Ansi;
    if (ModuleName)
        Arguments->ModuleName = *ModuleName;

    /* 切换 TEB 窗口缓存 */
    IntSetTebWndCallback(&hWnd, &pWnd, &pActCtx);

    /* 释放用户态锁 */
    UserLeaveCo();

    /* 调用 user32 钩子入口 */
    Status = KeUserModeCallback(USER32_CALLBACK_HOOKPROC,
                                Arguments,
                                ArgumentLength,
                                &ResultPointer,
                                &ResultLength);

    /* 重新获取用户态锁 */
    UserEnterCo();

    /* 恢复 TEB 窗口缓存 */
    IntRestoreTebWndCallback(hWnd, pWnd, pActCtx);

    /* 复制结果 */
    if (NT_SUCCESS(Status))
    {
        _SEH2_TRY
        {
            RtlMoveMemory(Arguments, ResultPointer, ArgumentLength);
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            Status = _SEH2_GetExceptionCode();
        }
        _SEH2_END;
    }

    if (!NT_SUCCESS(Status))
    {
        ERR("Call to user mode failed! 0x%08lx\n", Status);
        return 0;
    }
    return Arguments->Result;
}

co_IntCallWindowProc 类似 ,但使用 USER32_CALLBACK_HOOKPROC 作为回调编号。

7.4.6.3 HOOKPROC_CALLBACK_ARGUMENTS

c 复制代码
typedef struct _HOOKPROC_CALLBACK_ARGUMENTS
{
    INT HookId;                // 钩子类型
    INT Code;                  // 钩子代码
    WPARAM wParam;
    LPARAM lParam;
    HOOKPROC Proc;             // 钩子函数指针
    INT Mod;                   // 模块索引
    ULONG_PTR offPfn;          // 函数偏移
    BOOLEAN Ansi;              // 是否为 ANSI
    UNICODE_STRING ModuleName; // 模块名
    LRESULT Result;            // 钩子返回值
} HOOKPROC_CALLBACK_ARGUMENTS;

7.4.7 钩子调用下一个 CallNextHookEx

7.4.7.1 CallNextHookEx 语义

CallNextHookEx 让当前钩子调用链中的下一个钩子(或原处理函数)。在 Windows 钩子编程中,每个钩子函数必须决定:

  • 消费消息:返回非 0,不再调用后续钩子;
  • 传递消息 :返回 CallNextHookEx 的结果,继续调用后续钩子。

7.4.7.2 NtUserCallNextHookEx

win32ss/user/ntuser/hook.c:1370(file:///d:/reactos/win32ss/user/ntuser/hook.c#L1370):

c 复制代码
DWORD APIENTRY
NtUserCallNextHookEx(int Code, WPARAM wParam, LPARAM lParam, HHOOK Hook)
{
    PHOOK NextHook;
    LRESULT Result;

    /* 在钩子链中查找调用点之后的下一个钩子 */
    NextHook = IntGetNextHook(Hook);
    if (!NextHook) return 0;

    /* 调用下一个钩子 */
    Result = co_HOOK_CallHookNext(NextHook, Code, wParam, lParam);
    return (DWORD)Result;
}

7.4.7.3 co_HOOK_CallHookNext

c 复制代码
static
LRESULT
APIENTRY
co_HOOK_CallHookNext(PHOOK Hook, INT Code, WPARAM wParam, LPARAM lParam)
{
    TRACE("Calling Next HOOK %d\n", Hook->HookId);
    return co_IntCallHookProc(Hook->HookId,
                              Code, wParam, lParam,
                              Hook->Proc,
                              Hook->ihmod,
                              Hook->offPfn,
                              Hook->Ansi,
                              &Hook->ModuleName);
}

IntGetNextHook 在钩子链中查找当前 Hook 的下一个:

c 复制代码
PHOOK IntGetNextHook(PHOOK Hook)
{
    // ... 遍历链表查找 Hook 之后的下一个 ...
}

7.4.8 事件钩子 WinEvent Hook

7.4.8.1 事件钩子 vs 传统钩子

维度 传统钩子(WH_*) 事件钩子(WinEvent)
API SetWindowsHookEx SetWinEventHook
事件 系统级"事件点" UI 元素状态变化
典型用途 全局热键、消息过滤 UI 自动化、屏幕阅读器
DLL 注入 全局钩子需要 不需要(基于 LPC)
性能 影响所有消息 按需调用
代表 WH_KEYBOARD_LL EVENT_OBJECT_FOCUS

7.4.8.2 SetWinEventHook 实现

事件钩子使用 LPC 通信而非 DLL 注入,事件源进程通过 LPC 将事件发送到订阅者进程。这避免了 DLL 注入带来的稳定性和安全性问题。

7.4.8.3 co_IntCallEventProc

win32ss/user/ntuser/callback.h:37(file:///d:/reactos/win32ss/user/ntuser/callback.h#L37):

c 复制代码
LRESULT APIENTRY
co_IntCallEventProc(
    HWINEVENTHOOK hook,
    DWORD event,
    HWND hwnd,
    LONG idObject,
    LONG idChild,
    DWORD dwEventThread,
    DWORD dwmsEventTime,
    WINEVENTPROC Proc,
    INT Mod,
    ULONG_PTR offPfn);

实现与 co_IntCallHookProc 类似,但使用 USER32_CALLBACK_EVENTPROC

7.4.8.4 EVENTPROC_CALLBACK_ARGUMENTS

c 复制代码
typedef struct _EVENTPROC_CALLBACK_ARGUMENTS
{
    HWINEVENTHOOK hook;
    DWORD event;
    HWND hwnd;
    LONG idObject;
    LONG idChild;
    DWORD dwEventThread;
    DWORD dwmsEventTime;
    WINEVENTPROC Proc;
    INT Mod;
    ULONG_PTR offPfn;
    BOOL Ansi;
    LRESULT Result;
} EVENTPROC_CALLBACK_ARGUMENTS;

7.4.9 WH_DEBUG 调试钩子

7.4.9.1 调试钩子的特殊用途

WH_DEBUG 钩子不直接处理事件,而是观察其他钩子的行为:

  • 监视 WH_KEYBOARD_LL、WH_MOUSE_LL 的真实 lParam;
  • 调试钩子链中的数据流;
  • 实现消息拦截的"X-Ray"。

7.4.9.2 co_IntCallDebugHook

win32ss/user/ntuser/hook.c:385(file:///d:/reactos/win32ss/user/ntuser/hook.c#L385):

c 复制代码
static LRESULT FASTCALL
co_IntCallDebugHook(PHOOK Hook, int Code, WPARAM wParam, LPARAM lParam, BOOL Ansi)
{
    DEBUGHOOKINFO Debug;
    PVOID HooklParam = NULL;
    ULONG Size;
    BOOL BadChk = FALSE;
    LRESULT lResult = 0;

    /* 读取 lParam 指向的 DEBUGHOOKINFO */
    if (lParam)
    {
        ProbeForRead((PVOID)lParam, sizeof(DEBUGHOOKINFO), 1);
        RtlCopyMemory(&Debug, (PVOID)lParam, sizeof(DEBUGHOOKINFO));
    }
    else
        return lResult;

    /* 根据 wParam (钩子类型) 确定调试数据大小 */
    switch (wParam)
    {
        case WH_CBT:
            switch (Debug.code)
            {
                case HCBT_CLICKSKIPPED: Size = sizeof(MOUSEHOOKSTRUCTEX); break;
                case HCBT_MOVESIZE:    Size = sizeof(RECT); break;
                case HCBT_ACTIVATE:    Size = sizeof(CBTACTIVATESTRUCT); break;
                case HCBT_CREATEWND:   Size = sizeof(CBT_CREATEWND); break;
                default:               Size = sizeof(LPARAM);
            }
            break;

        case WH_MOUSE_LL:     Size = sizeof(MSLLHOOKSTRUCT); break;
        case WH_KEYBOARD_LL:  Size = sizeof(KBDLLHOOKSTRUCT); break;
        case WH_GETMESSAGE:   Size = sizeof(MSG); break;
        case WH_JOURNALPLAYBACK:
        case WH_JOURNALRECORD: Size = sizeof(EVENTMSG); break;
        // ...
    }

    /* 分配并复制调试数据 */
    if (Size > sizeof(LPARAM))
        HooklParam = ExAllocatePoolWithTag(NonPagedPool, Size, TAG_HOOK);

    if (HooklParam)
    {
        ProbeForRead((PVOID)Debug.lParam, Size, 1);
        RtlCopyMemory(HooklParam, (PVOID)Debug.lParam, Size);
        Debug.lParam = (LPARAM)HooklParam;
    }

    /* 调用下一个钩子(传递调试信息) */
    lResult = co_HOOK_CallHookNext(Hook, Code, wParam, (LPARAM)&Debug);
    if (HooklParam) ExFreePoolWithTag(HooklParam, TAG_HOOK);

    return lResult;
}

7.4.10 WndProc 子类化

7.4.10.1 子类化(Subclassing)概念

子类化是一种"伪 Hook"机制:通过替换窗口过程函数来拦截特定窗口的消息,而不需要全局钩子。

c 复制代码
// 原 WndProc
WNDPROC OldWndProc = NULL;

LRESULT CALLBACK NewWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    if (msg == WM_CLOSE)
    {
        if (MessageBox(hwnd, L"确定关闭?", L"提示", MB_YESNO) == IDNO)
            return 0;  // 拦截
    }
    return CallWindowProc(OldWndProc, hwnd, msg, wParam, lParam);
}

// 安装子类化
OldWndProc = (WNDPROC)SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)NewWndProc);

7.4.10.2 实现机制

子类化通过修改 PWND->lpfnWndProc 实现:

c 复制代码
// 伪代码
LONG_PTR SetWindowLongPtr(HWND hwnd, int nIndex, LONG_PTR dwNewLong)
{
    PWND Window = UserGetWindowObject(hwnd);
    LONG_PTR OldValue;

    UserEnterExclusive();
    if (nIndex == GWLP_WNDPROC)
    {
        OldValue = (LONG_PTR)Window->lpfnWndProc;
        Window->lpfnWndProc = (WNDPROC)dwNewLong;
    }
    UserLeave();
    return OldValue;
}

7.4.10.3 CallWindowProc

CallWindowProcW/A 是 user32 的函数,用于调用旧的 WndProc:

c 复制代码
LRESULT CallWindowProcW(WNDPROC lpPrevWndFunc, HWND hWnd, UINT Msg,
                         WPARAM wParam, LPARAM lParam)
{
    // ...
    return co_IntCallWindowProc(lpPrevWndFunc, !Unicode, hWnd, Msg, wParam, lParam, -1);
}

调用 co_IntCallWindowProc(详见 7.3 节)实际触发内核 → 用户态回调。

7.4.10.4 子类化的局限性

  • 仅影响单个窗口,无法跨窗口;
  • 无法拦截消息产生阶段,只能在 WndProc 收到消息后处理;
  • 多个子类叠加时需要手动维护调用链;
  • 子类化深度过深会导致栈溢出和性能问题。

7.4.11 设计哲学问答

Q1:为什么 WH_KEYBOARD_LL 和 WH_MOUSE_LL 不需要 DLL 注入?

A在内核态实现 + 内核态回调

WH_KEYBOARD_LLWH_MOUSE_LL 由 RIT(Raw Input Thread)在内核态直接处理:

  • 原始输入数据由键盘/鼠标驱动产生;
  • RIT 在 win32k 内核态处理这些数据;
  • 钩子回调通过 co_IntCallLowLevelHook 切换到用户态;
  • 不需要 DLL 注入,因为钩子函数指针在内核态维护。

这降低了安全风险(恶意软件无法通过注入 DLL 执行任意代码)和稳定性问题(避免 DLL 兼容性问题)。

Q2:全局钩子为什么要 DLL 注入?

A钩子函数必须在目标进程上下文中执行

全局钩子影响所有线程,钩子函数必须在每个目标进程的用户态地址空间中可访问:

  • 不同进程的地址空间是隔离的;
  • 钩子函数指针(如 0x00401234)只在调用进程有效;
  • 为了让其他进程的线程能调用该函数,必须将包含钩子函数的 DLL 映射到目标进程。

DLL 注入机制(基于 NtCreateThreadEx + LoadLibrary)由 win32k 在安装全局钩子时自动执行。

Q3:为什么事件钩子(WinEvent)不需要 DLL 注入?

A事件钩子使用 LPC 跨进程通信

事件钩子的工作流程:

  1. 订阅者进程调用 SetWinEventHook 注册回调;
  2. 事件源进程通过 NtUserEventHook 触发事件;
  3. 事件数据通过 LPC 端口从事件源发送到订阅者;
  4. 订阅者进程在收到 LPC 消息后调用回调函数。

这种"事件驱动 + LPC"模型既避免了 DLL 注入的复杂性,又支持跨进程的事件分发(适合辅助功能、UI 自动化等场景)。

Q4:为什么 CoCreateInstance(COM)中的 CoMarshalInterface 不使用 Hook?

AHook 适用于消息级拦截,COM 适用于对象级调用

Hook 机制的优势在于"消息级别"拦截(截获所有消息),但无法:

  • 截获直接的函数调用(如 C++ 虚函数);
  • 跨进程透明地调用接口;
  • 维护对象引用计数和生命周期。

COM 通过接口指针 + 代理/存根机制实现跨进程、跨语言的调用拦截,更适合"对象级别"的拦截需求。

Q5:为什么 WndProc 子类化可以替代部分钩子功能?

A子类化更轻量级、更安全

维度 钩子 子类化
性能 影响所有消息 仅影响特定窗口
安全 全局钩子可截获所有输入 仅影响目标窗口
复杂度 需要 DLL 注入 直接替换函数指针
维护 钩子链需手动管理 简单的调用链

子类化适用于单窗口或少数窗口 的拦截需求;当需要拦截所有窗口或系统级事件时,仍需使用钩子。


总结

外挂函数(Hook)机制是 Windows 系统最强大、最复杂的扩展点之一。本节介绍了:

  1. 13 种 WH_ 钩子*:键盘、鼠标、消息、窗口、Shell、调试等;
  2. PHOOK 内核态对象:钩子的元数据与链表管理;
  3. SetWindowsHookEx / UnhookWindowsHookEx:钩子安装与卸载;
  4. co_HOOK_CallHooks:钩子触发点;
  5. co_IntCallHookProc:钩子回调核心(KeUserModeCallback);
  6. CallNextHookEx:钩子链调用;
  7. 事件钩子:基于 LPC 的无 DLL 注入机制;
  8. WH_DEBUG:观察其他钩子的调试钩子;
  9. WndProc 子类化:轻量级的消息拦截。

核心要点回顾

  1. 线程钩子影响范围小,全局钩子需要 DLL 注入;
  2. LL 钩子(WH_KEYBOARD_LL/WH_MOUSE_LL)在内核态实现,无需 DLL 注入;
  3. 事件钩子使用 LPC 跨进程通信,比传统钩子更安全;
  4. CallNextHookEx 是钩子链调用的关键;
  5. 子类化是替代全局钩子的轻量级方案。

本章代码索引

文件 内容
win32ss/user/ntuser/hook.c(file:///d:/reactos/win32ss/user/ntuser/hook.c) NtUserSetWindowsHookEx、NtUserUnhookWindowsHookEx、co_HOOK_CallHooks、co_IntCallHookProc、co_HOOK_CallHookNext、co_IntCallDebugHook
win32ss/user/ntuser/hook.h(file:///d:/reactos/win32ss/user/ntuser/hook.h) HOOK 结构、HOOKPACK、钩子类型定义
win32ss/user/ntuser/callback.c(file:///d:/reactos/win32ss/user/ntuser/callback.c) co_IntCallHookProc、co_IntCallEventProc(详见 7.3)
win32ss/user/ntuser/callback.h(file:///d:/reactos/win32ss/user/ntuser/callback.h) HOOKPROC_CALLBACK_ARGUMENTS、EVENTPROC_CALLBACK_ARGUMENTS
win32ss/user/ntuser/main.c(file:///d:/reactos/win32ss/user/ntuser/main.c) InitThreadCallback 初始化 aphkStart 链表
win32ss/user/ntuser/dde.c(file:///d:/reactos/win32ss/user/ntuser/dde.c) DDE 消息处理(与 DDE 钩子相关)
win32ss/user/ntuser/object.c(file:///d:/reactos/win32ss/user/ntuser/object.c) 钩子对象管理(详见 7.1)
相关推荐
辣香牛肉面2 小时前
Windows发票工具大全
windows·发票助手
caimouse2 小时前
Reactos 第 9 章 设备驱动 — 9.3 DPC函数及其执行
windows
caimouse3 小时前
Reactos 第 9 章 设备驱动 — 9.8 设备驱动模块的装载
windows
caimouse3 小时前
Reactos 第 9 章 设备驱动 — 9.2 一个“老式“驱动模块的实例
windows
caimouse3 小时前
Reactos 第 9 章 设备驱动 — 9.4 内核劳务线程
开发语言·windows
星栈独行3 小时前
Rust + Makepad 应用怎么打包发布:Windows、macOS、Linux 全平台交付
windows·程序人生·macos·ui·rust
辣香牛肉面3 小时前
Windows PDF转换工具箱
windows·pdf
daly5203 小时前
PyCharm怎么下载?2026最新版PyCharm安装教程(Windows/macOS/Linux)
windows·macos·pycharm
西凉的悲伤3 小时前
redis-windows 安装 redis 到 windows 电脑
java·windows·redis·redis-windows