Windows Hook 技术浅析
本文最后更新于:2022年4月7日 晚上
前情提要
众所周知,出于安全性考虑,操作系统的进程间是相互隔离的,窗口也不例外
Windows的通信基于消息机制,操作系统会将各种信息通过消息(Message)的形式传递给各个线程(消息队列是以线程为单位的)
这也意味着一个进程只能获取Windows发送给自身窗体的消息,如:
- 键盘按键信息
- 鼠标滚轮、双击信息
- 窗口最小化/最大化
- …
“两耳不闻窗外事,一心只读圣贤书”了属于是
他想闻也闻不到啊主要是,操作系统给他掐了
Windows:喂,别东张西望
外面的世界
那莫,我要是求知欲特别强,就是想知道别的进程/窗口发生了什么,怎么办呢?
比如:(监听其他窗口的密码输入)
哦,不是不是,误会了,我的老伙计
我是说:帮助一些行动不便的程序,处理一些消息~
咳咳
那就需要用到Windows Hook技术了
Windows Hook
什么是Hook
Hook,直译:钩子,挂钩
非常形象,其作用就是在Windows将消息分发给指定窗口过程中捕获(钩住)消息,从而获得对消息的控制权,以便处理或修改,甚至可以拦截(不继续传递)
比如:在浏览器输入文字,按键消息就会由Windows发布到浏览器窗口,在这个过程中,可以用键盘钩子捕获消息,从而得知用户的输入
Hook类型(这一段可以跳过,查阅用)
有如下的几种常用类型:
- 键盘钩子和低级键盘钩子可以监视各种键盘消息
- 鼠标钩子和低级鼠标钩子可以监视各种鼠标消息
- 外壳钩子可以监视各种
Shell事件消息。比如启动和关闭应用程序 - 日志钩子可以记录从系统消息队列中取出的各种事件消息
- 窗口过程钩子监视所有从系统消息队列发往目标窗口的消息
详细来说:
1.WH_CALLWNDPROC 和 WH_CALLWNDPROCRET
监视发送到窗口过程的消息。系统在消息发送到接收窗口过程之前调用WH_CALLWNDPROC Hook子程,并且在窗口过程处理完消息之后调用WH_CALLWNDPROCRET Hook子程
2.WH_CBT
在以下事件之前,系统都会调用WH_CBT Hook子程,这些事件包括:
- 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件
- 完成系统指令
- 来自系统消息队列中的移动鼠标,键盘事件
- 设置输入焦点事件
- 同步系统消息队列事件
Hook子程的返回值确定系统是否允许或者防止这些操作中的一个
3.WH_DEBUG
在系统调用系统中与其他Hook关联的子程之前,系统会调用WH_DEBUG Hook子程。你可以使用这个Hook来决定是否允许系统调用与其他Hook关联的子程
4.WH_FOREGROUNDIDLE
当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLE Hook执行低优先级的任务。当应用程序的前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程
5.WH_GETMESSAGE
应用程序使用WH_GETMESSAGE Hook来监视从GetMessage or PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息
6.WH_JOURNALPLAYBACK
WH_JOURNALPLAYBACK Hook使应用程序可以插入消息到系统消息队列。可以使用这个Hook回放通过使用WH_JOURNALRECORD Hook记录下来的连续的鼠标和键盘事件。只要WH_JOURNALPLAYBACK Hook已经安装,正常的鼠标和键盘事件就是无效的。WH_JOURNALPLAYBACK Hook是全局Hook,不会注入dll。WH_JOURNALPLAYBACK Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长时间,这就使Hook可以控制实时事件的回放。
7.WH_JOURNALRECORD
WH_JOURNALRECORD Hook用来监视和记录输入事件。可以使用这个Hook记录连续的鼠标和键盘事件,然后通过使用WH_JOURNALPLAYBACK Hook来回放。WH_JOURNALRECORD Hook是全局Hook,不会注入dll
8.WH_KEYBOARD
在应用程序中,WH_KEYBOARD Hook用来监视WM_KEYDOWN and WM_KEYUP消息,这些消息通过GetMessage or PeekMessage 函数返回。可以使用这个Hook来监视输入到消息队列中的键盘消息
9.WH_KEYBOARD_LL
监视即将输入到线程消息队列中的键盘消息
10.WH_MOUSE
监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。使用这个Hook监视输入到消息队列中的鼠标消息
11.WH_MOUSE_LL
监视即将输入到线程消息队列中的鼠标消息
12.WH_MSGFILTER 和 WH_SYSMSGFILTER
可以监视菜单,滚动条,消息框,对话框消息并且发现用户使用ALT+TAB or ALT+ESC 组合键切换窗口
13.WH_SHELL
当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用WH_SHELL Hook子程
不管了,先开始
上面一大坨看得头痛是吧,那就先别看了,不如上手试试
其实大多数人入手Hook的第一个例子就是鼠标和键盘钩子,比如:获取全局键盘输入
那么,问题来了,哪一种类型的Hook可以实现这种需求呢?
小学英语也可以筛选出两个:WH_KEYBOARD && WH_KEYBOARD_LL
Q:那这俩怎么选嘛,都是KeyBoard,诶,那个LL后缀是啥呀
A:Low Level,代表低级键盘钩子
Q:有啥区别呢
A:WH_KEYBOARD_LL会在消息即将发布到线程消息队列时调用,而WH_KEYBOARD会在消息被线程处理前调用,也就是说,WH_KEYBOARD_LL调用时机更早
而且使用起来更简单,为啥呢?因为WH_KEYBOARD_LL调用时,消息还未发布到具体线程,所以不用编写独立dll用以注入。坏处是,正由于消息还未发布到具体线程,所以不知道消息的接收者是谁。
而WH_KEYBOARD是在线程获取消息时调用,所以上下文已经切换至具体线程,想要执行Hook函数就必须编写独立dll并将dll注入指定线程(Windows执行注入)才能执行(同一片地址空间),这样就会削削有点麻烦,但可以实现监控指定线程
那么什么Hook可以不用dll呢,貌似Global Only的可以不要:

写了再说
管不了那么多了,直接莽
编写钩子程序的步骤分为三步:定义钩子函数、安装钩子和卸载钩子
1.定义钩子函数
钩子函数是一种特殊的回调函数,钩子监视的特定事件发生后,系统会调用钩子函数进行处理
1 | |
参数wParam 和 lParam包含所钩消息的信息,比如鼠标位置、状态,键盘按键等,nCode包含有关消息本身的信息,比如是否从消息队列中移出
在钩子函数中实现了具体功能后,还需要返回值
推荐用CallNextHookEx函数把消息传递给后一个对象(下一个钩子函数或特定线程)
1 | |
当然也可以直接return true不继续传递,即拦截消息
(这样非常霸道,可以用来屏蔽键盘)
注:在钩子函数中,不要做耗时操作,否则会阻塞系统,导致性能下降
2.安装钩子
调用函数SetWindowsHookEx安装钩子:
1 | |
idHook表示钩子类型,如:WH_KEYBOARDlpfn是钩子函数的地址hMod是钩子函数所在的实例的句柄dwThreadId指定钩子所监视的线程的ID,若为NULL,则监视所有线程(全局钩子)- 返回值为钩子句柄(唯一标识符)
注:最近安装的钩子放在Hook链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权
注:如果钩子过程超时,系统将消息传递给下一个钩子。但是,在 Windows 7 及更高版本上,该挂钩会在不被调用的情况下被静默删除。应用程序无法知道挂钩是否被移除
Windows 10 版本 1709 及更高版本系统允许的最大超时值为 1000 毫秒(1 秒)。如果LowLevelHooksTimeout值设置为大于 1000 的值,系统将默认使用 1000 毫秒超时。
3.卸载钩子
钩子特别是系统钩子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装钩子,在使用完毕后要及时卸载:
1 | |
不过,实际上呢,由于Windows系统的鲁棒性,在程序退出时,系统会隐式释放所有句柄,所以其实不用太担心
实验表明,如果钩子函数写在dll中,程序直接退出,系统会释放Hook句柄,并卸载在其他线程注入的dll(可能有延迟)
注:调用SetWindowsHookEx的进程获得句柄(载入dll后调用也是(同一进程空间))
实操:低级键盘钩子(无dll)
1 | |
是不是非常简单,这样就实现了屏蔽键盘的功能
但是,这也太low了吧,还不能监控指定线程,换碟!
实操2:鼠标钩子(dll)
一般与特定线程/窗口相关的钩子都需要编写独立dll
那么怎么写呢?
一般用Visual Studio来编写(新建dll工程就有模板)
1 | |
注意:
1 | |
这段用于定义共享数据段
为什么要共享呢?因为dll注入在不同的线程中,数据是不共享的,那可坏了,Hook句柄和服务端窗口句柄都读不到了
为了共享就需要用到编译器指令#pragma data_seg
同时:
1 | |
这句话用于声明dll中要导出的函数(以C风格 为了兼容性),否则外部不能访问
处理函数在注入到其他线程的dll中,那么天高皇帝远,我们怎么才能获取到消息呢?
这就要让注入的dll充当内鬼了,将消息通过PostMessage传递出来
为啥是PostMessage而不是SendMessage呢?
因为前面说了,钩子函数要尽量高效,不能阻塞,而SendMessage是阻塞函数,显然不行
那么问题又来了,由于PostMessage异步执行,所以不能传递指针(所指向的对象早就销毁了)
而lParam通常是一个指向结构体的指针
那咋办呢?
要么用VirtualAlloc在目标进程开空间,然后复制过去,要么,最多只能传递sizeof(LPARAM),64bits 8字节的数据
那发消息总得有暗号吧
可以使用RegisterWindowMessage定义用户消息,接收端就可以识别了
使用例
Okay,dll写完了,总得有人用吧,我们以Qt为例:
1 | |
可以用QLibrary载入dll(Windows API为LoadLibrary)
并根据函数名定位函数地址(有了地址就可以调用了)
然后在消息循环中接收内鬼dll发来的信息
1 | |
Okay,大功告成,这样就实现了监听Chrome标题栏的左键双击并模拟Ctrl+W关闭标签页的效果
问题和注意事项
Win32系统会确保内存中只有一个该DLL的拷贝,这是通过内存映射文件来实现的。不同的进程分别将这份DLL的代码段地址映射到自己的进程空间中,同时不同的进程在自己的进程空间分别有各自的一份该DLL的数据段(全局变量)拷贝
2.既然代码段共享,如果进程A中修改DLL的代码内容,使用同一DLL的其它进程B是否受影响?
NO!
这就涉及到一个内存的COPY ON WRITE机制问题
如果进程A修改了dll代码或数据,则有内容被修改后,会构造副本,重新映射
所以基本上内存中只有一份dll可以节省空间
3.不调用UnhookWindowsHookEx释放钩子会如何?
其实不会如何,由于Windows系统的鲁棒性,在程序退出时,系统会隐式释放所有句柄,所以其实不用太担心
实验表明,如果钩子函数写在dll中,程序直接退出,系统会释放Hook句柄,并卸载在其他线程注入的dll(可能有延迟)
所有dll释放后,共享数据段被清空(我猜的)
Ref
Visual Studio关于hook项目的简单使用_吨吨不打野的博客-CSDN博客
HOOK API (一)——HOOK基础+一个鼠标钩子实例 - …..? - 博客园
SetWindowsHookExA function (winuser.h) - Win32 apps | Microsoft Docs