给一个老系统做外挂式日志采集,想在程序弹框时顺手记一条。先试消息钩子,没反应;后来改成 inline hook,改了函数开头几个字节才成。方案选错就是白折腾。

hook 在拦什么

本质是在目标代码的执行路径上插一段自己的逻辑:拦消息、拦函数调用、拦系统调用。拦截点越靠底层,拿到的信息越多,稳定性和兼容性风险也越大。

四种常见做法

  • 消息钩子:SetWindowsHookEx 装 WH_KEYBOARD_LL 之类的全局钩子,回调在安装钩子的线程里执行,必须有消息循环,否则收不到任何东西。
  • IAT hook:改导入地址表,把 MessageBoxA 的地址换成自己的函数。只对静态导入有效,程序加壳或者动态取地址就直接失效。
  • inline hook:在函数开头写一条跳转,跳到自己的处理函数,执行完再把被覆盖的指令补回来。要处理指令长度、多线程写入、内存保护属性。
  • 驱动层:内核里改系统调用表或挂回调,权限最高,但 64 位系统有 PatchGuard 和签名强制,成本和风险都不低。

一段最小的键盘钩子

HHOOK h = SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(NULL), 0);

装完还要跑 GetMessage 循环,退出前 UnhookWindowsHookEx。忘了卸载,钩子链会残留,调试时表现为输入延迟越来越明显。

失败多数出在这三处

位数不匹配,32 位进程 hook 不了 64 位目标,得写一个同位的代理进程;权限不够,跨会话操作会被 UIPI 拦,要提权或换到用户自己的会话;时序不对,模块还没加载就去取函数地址,拿到空指针。另外改代码段要先 VirtualProtect 改成可写,写完再改回去,不然直接访问冲突。

移动端思路差不多

Android 上用 Frida 挂 Java 方法或 native 函数,Xposed 改方法入口,都算 hook。限制主要来自签名校验、反调试和完整性检查,企业壳还会校验方法体哈希。iOS 上越狱环境用 Cydia Substrate,非越狱基本只能做网络层拦截。

调试、兼容层、监控这类自用场景,hook 很划算;要做成对外产品,先算清被杀软误杀和被目标程序检测的成本。