从一个误会说起
王端明《深入浅出 Windows API 程序设计》里对 Hook 的阐述是:Hook 是 Windows 消息处理机制中的一个监视点,应用程序可以在这里安装钩子函数(一个回调),监视系统消息,并在消息到达目标窗口过程之前先由钩子函数处理。
第一次读容易被”目标窗口”带偏:Hook 是不是窗口程序专属的技术?不是。它监视的是”别人的消息流”,你自己的程序有没有窗口都行。这篇笔记按书上的分类把消息钩子理清,再用书上的 HookDll + HookApp 工程实战一次。
书上的分类:局部钩子与远程钩子
Windows 用 SetWindowsHookEx 安装的钩子,书上按作用范围分两类:
- 局部钩子:只挂钩自身进程的事件
- 远程钩子:挂钩其他进程的事件,又分两种——基于其他进程中某个线程的线程钩子,和系统范围的系统钩子
对照到 API 参数上:
| 分类 | dwThreadId 参数 |
钩子函数位置 |
|---|---|---|
| 局部钩子 | 自己进程的线程 ID | 可以在 exe 里,不用 DLL |
| 远程钩子-线程钩子 | 别的进程的线程 ID | 必须在 DLL 里(系统把 DLL 注入那个进程) |
| 远程钩子-系统钩子 | 0 | 必须在 DLL 里(注入所有 GUI 进程) |
一条规律贯穿:远程钩子的钩子函数必须位于 DLL 中。原因很直接:远程钩子的回调要在别的进程上下文里执行,代码总得有个载体过去——系统的做法就是把装着回调的 DLL 映射进目标进程地址空间。局部钩子不跨进程,exe 里直接写回调就行。
书里 InstallHook(int idHook, DWORD dwThreadId, HWND hwnd) 保留了 dwThreadId 参数,正是兼容这几种用法——传 0 是系统钩子,传具体线程 ID 是线程钩子。
实战:远程钩子之系统钩子(HookDll + HookApp)
书上这个演示工程是远程钩子中的系统钩子:dwThreadId = 0,监视全系统的键盘消息。
工程结构
两个工程配合:
- HookDll:钩子 DLL,导出
InstallHook/UninstallHook,内部含钩子回调KeyboardProc - HookApp:对话框程序,两个按钮负责装/卸钩子,界面上实时显示全系统敲下的按键
运行效果:点”安装钩子”后,在任何程序里打字——记事本、浏览器地址栏、别的对话框——按键都会同步出现在 HookApp 的编辑框里。这就是”系统范围”的含义。
为什么是 DLL?这是远程钩子的硬性要求
SetWindowsHookEx 的签名决定了架构:
1 | HHOOK SetWindowsHookEx( |
钩子回调要在别的进程的上下文里执行(键盘事件发生在别人那里),系统实现这一点的方式很粗暴:把装着回调的 DLL 映射进每一个 GUI 进程的地址空间。所以系统钩子的回调必须放在 DLL 里——这不是作者的设计选择,是系统机制的要求。
HookDll 核心代码
1 | // 钩子回调: 每次系统里有键盘消息, 先经过这里. |
调用时传 InstallHook(WH_KEYBOARD, 0, hwndDlg):WH_KEYBOARD 是钩子类型,0 表示系统钩子,hwndDlg 是 HookApp 的窗口句柄——也就是钩子回调里 SendMessage 的第一个参数,用于把按键字符回传给 HookApp 窗口。
HookApp 一侧就简单了
1 | case IDC_BTN_INSTALLHOOK: |
装/卸钩子调 DLL 导出的两个函数;收到 WM_COPYDATA 就把字符追加到编辑框。
整条数据流
1 | 用户在任何程序里敲键 |
三个要点:
- 先经过你,再放行:钩子函数先于目标窗口过程被调用,看完可以放行(
CallNextHookEx),也可以吞掉(不调用即可),这就是”监视点”的含义。 - 钩子构成链:系统里可以装多个同类钩子,
CallNextHookEx把消息传给链上的下一个,谁不放行谁就截断了后面的所有环节。 - 回调跑在别人家里:
KeyboardProc的执行上下文是目标进程,这也是为什么回传结果要用WM_COPYDATA(系统支持的跨进程传数据方式),而不能直接读写 HookApp 的内存。
消息钩子的常见类型
SetWindowsHookEx 的第一个参数决定监视什么,常用的:
| 类型 | 监视内容 | 是否需要 DLL |
|---|---|---|
WH_KEYBOARD |
键盘消息 | 做远程钩子时需要 |
WH_KEYBOARD_LL |
低级键盘输入(在自己进程上下文执行) | 不需要,系统钩子也不用 |
WH_MOUSE / WH_MOUSE_LL |
鼠标,同上规律 | 视范围而定 |
WH_GETMESSAGE |
投递到消息队列的所有消息 | 做远程钩子时需要 |
WH_CBT |
窗口创建/激活/关闭等事件 | 做远程钩子时需要 |
顺带一提:如果只是做全局键盘记录,WH_KEYBOARD_LL 是更简单的选择——它在自己进程里执行,不用 DLL 注入。书里用 WH_KEYBOARD + DLL 的写法,教学价值更高:它把”DLL 注入”这个后面 API Hook、内核开发都要用的机制提前演示了一遍。
更大视角:Hook 不止消息钩子
消息钩子只是 Hook 技术的一个分支——有官方设施、最安全的那一支。把视野放大,凡是”在执行路径上插一个点,先经过你,你再决定放行、修改还是替换”的技术都叫 Hook:
| 类别 | 拦截的是什么 | 典型手段 | 是否有官方支持 |
|---|---|---|---|
| 消息钩子 | 窗口消息(键盘、鼠标、窗口事件) | SetWindowsHookEx(本文实战) |
有,系统原生设施 |
| API 钩子 | 函数调用 | IAT 改写、inline hook(改函数开头跳转)、Detours | 无,自己动手 |
| 内核钩子 | 系统调用、内核事件 | SSDT hook、回调注册(ObRegisterCallbacks 等) | 部分有(回调注册类) |
API 钩子应用面更广(安全软件、注入、调试器都靠它),但没有官方支持,涉及内存改写,坑也多,是下一步的学习内容。
安全边界
全局键盘钩子就是键盘记录器(keylogger)的技术原型,攻防同源。学习时在自己的虚拟机和测试程序里跑,搞清楚机制;不要用在任何他人机器或生产环境。
小结
1 | 消息钩子(SetWindowsHookEx) 按作用范围分(书上分类) |
下一篇计划讲 API 钩子:IAT 改写和 inline hook,看看”没有官方设施时,怎么自己造一个钩子”。