第 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:只影响当前线程的"部门内部检查"。
本节内容概览
- 7.4.0 框架图:Hook 机制完整架构;
- 7.4.1 Hook 类型总览:13 种 WH_* 钩子;
- 7.4.2 PHOOK 数据结构:内核态钩子对象;
- 7.4.3 NtUserSetWindowsHookEx 安装钩子;
- 7.4.4 NtUserUnhookWindowsHookEx 卸载钩子;
- 7.4.5 co_HOOK_CallHooks 触发钩子链;
- 7.4.6 co_IntCallHookProc 钩子回调核心;
- 7.4.7 钩子调用下一个 CallNextHookEx;
- 7.4.8 事件钩子 WinEvent Hook;
- 7.4.9 WH_DEBUG 调试钩子;
- 7.4.10 WndProc 子类化;
- 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_LL 和 WH_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):
- 验证参数:检查 HookId 范围、ThreadId 有效性等;
- 创建 HOOK 对象:分配 PHOOK 并初始化;
- 加载模块 (全局钩子):调用
IntLoadHookModule将模块加载到所有相关进程; - 挂载到链表 :
- 线程钩子:插入
pti->aphkStart[HookId]链表; - 全局钩子:插入
pDesk->pGlobalHooks[HookId]链表;
- 线程钩子:插入
- 更新 pti->fsHooks:标记该线程启用了哪些钩子类型;
- 返回 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_LL 和 WH_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 跨进程通信。
事件钩子的工作流程:
- 订阅者进程调用
SetWinEventHook注册回调; - 事件源进程通过
NtUserEventHook触发事件; - 事件数据通过 LPC 端口从事件源发送到订阅者;
- 订阅者进程在收到 LPC 消息后调用回调函数。
这种"事件驱动 + LPC"模型既避免了 DLL 注入的复杂性,又支持跨进程的事件分发(适合辅助功能、UI 自动化等场景)。
Q4:为什么 CoCreateInstance(COM)中的 CoMarshalInterface 不使用 Hook?
A :Hook 适用于消息级拦截,COM 适用于对象级调用。
Hook 机制的优势在于"消息级别"拦截(截获所有消息),但无法:
- 截获直接的函数调用(如 C++ 虚函数);
- 跨进程透明地调用接口;
- 维护对象引用计数和生命周期。
COM 通过接口指针 + 代理/存根机制实现跨进程、跨语言的调用拦截,更适合"对象级别"的拦截需求。
Q5:为什么 WndProc 子类化可以替代部分钩子功能?
A :子类化更轻量级、更安全。
| 维度 | 钩子 | 子类化 |
|---|---|---|
| 性能 | 影响所有消息 | 仅影响特定窗口 |
| 安全 | 全局钩子可截获所有输入 | 仅影响目标窗口 |
| 复杂度 | 需要 DLL 注入 | 直接替换函数指针 |
| 维护 | 钩子链需手动管理 | 简单的调用链 |
子类化适用于单窗口或少数窗口 的拦截需求;当需要拦截所有窗口或系统级事件时,仍需使用钩子。
总结
外挂函数(Hook)机制是 Windows 系统最强大、最复杂的扩展点之一。本节介绍了:
- 13 种 WH_ 钩子*:键盘、鼠标、消息、窗口、Shell、调试等;
- PHOOK 内核态对象:钩子的元数据与链表管理;
- SetWindowsHookEx / UnhookWindowsHookEx:钩子安装与卸载;
- co_HOOK_CallHooks:钩子触发点;
- co_IntCallHookProc:钩子回调核心(KeUserModeCallback);
- CallNextHookEx:钩子链调用;
- 事件钩子:基于 LPC 的无 DLL 注入机制;
- WH_DEBUG:观察其他钩子的调试钩子;
- WndProc 子类化:轻量级的消息拦截。
核心要点回顾:
- 线程钩子影响范围小,全局钩子需要 DLL 注入;
- LL 钩子(WH_KEYBOARD_LL/WH_MOUSE_LL)在内核态实现,无需 DLL 注入;
- 事件钩子使用 LPC 跨进程通信,比传统钩子更安全;
- CallNextHookEx 是钩子链调用的关键;
- 子类化是替代全局钩子的轻量级方案。
本章代码索引
| 文件 | 内容 |
|---|---|
| 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) |