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_KEYBOARD
lpfn
是钩子函数的地址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