本文为 Maldev Academy 中的 Module62 小节的笔记,通过 Windows 自带的 API 函数 实现 API Hook。(完整学习内容请自行前往跳转链接查看)。
SetWindowsHookEx WinAPI 调用是另一种可用于 API Hooking 的方法。它主要用于监听特定类型的系统事件,这与之前模块所介绍的技术不同,因为 SetWindowsHookExW/A 并不会修改某个函数的功能;相反,它会在某类事件被触发时执行一个回调函数。可被监听的事件类型仅限于 Windows 所提供的那些预定义事件类型。
SetWindowsHookExW 函数定义:
1
2
3
4
5
6
|
HHOOK SetWindowsHookExW(
[in] int idHook, // The type of hook procedure to be installed
[in] HOOKPROC lpfn, // A pointer to the hook procedure (function to execute)
[in] HINSTANCE hmod, // Handle to the DLL containing the hook procedure (this is kept as NULL)
[in] DWORD dwThreadId // A thread Id with which the hook procedure is to be associated with (this is kept as NULL)
);
|
idHook —— 指定要监控的事件类型。例如,WH_KEYBOARD_LL 标志用于监控按键消息,可用作键盘记录器。不过,利用 SetWindowsHookEx 做键盘记录是一种非常古老的技巧。在本模块中,将使用 WH_MOUSE_LL 标志来监控鼠标点击事件。
lpfn —— 指向回调函数的指针,当指定的事件发生时会被调用。在本例中,只要有鼠标点击事件,该回调函数就会被执行。
WH_KEYBOARD_LL 是 Windows Low-Level Keyboard Hook(低级键盘钩子),允许应用程序在系统接收键盘事件时,以用户态方式拦截这些事件。WH_MOUSE_LL 是 Windows 提供的 Low-Level Mouse Hook(低级鼠标钩子),允许用户态程序在系统处理鼠标事件之前拦截这些事件。
WH_MOUSE_LL 特点:
- 全局钩子(不需要 DLL 注入)。
- 捕获所有应用的鼠标事件。
- 能获取鼠标键按下、抬起、移动、滚轮等信息。
- 钩子回调在调用线程的上下文中执行。
1
2
3
4
5
6
|
HHOOK hMouseHook = SetWindowsHookExW(
WH_MOUSE_LL, // 鼠标低级钩子
HookCallback, // 回调函数
NULL, // 低级钩子允许为 NULL
0 // 全局钩子
);
|
WH_MOUSE_LL 钩子回调函数签名:
1
2
3
4
5
|
LRESULT CALLBACK HookCallback(
int nCode, // nCode == HC_ACTION:表示事件有效。
WPARAM wParam, // wParam:表示鼠标事件类型(如 WM_LBUTTONDOWN)。
LPARAM lParam // lParam:指向 MSLLHOOKSTRUCT 结构体。
);
|
WH_MOUSE_LL 会捕获这些事件类型:
| wParam |
意义 |
| WM_MOUSEMOVE |
鼠标移动 |
| WM_LBUTTONDOWN |
左键按下 |
| WM_LBUTTONUP |
左键抬起 |
| WM_RBUTTONDOWN |
右键按下 |
| WM_RBUTTONUP |
右键抬起 |
| WM_MBUTTONDOWN |
中键按下 |
| WM_MBUTTONUP |
中键抬起 |
| WM_MOUSEWHEEL |
滚轮滚动 |
| WM_MOUSEHWHEEL |
横向滚轮 |
| WM_XBUTTONDOWN |
侧键按下 |
| WM_XBUTTONUP |
侧键抬起 |
MSLLHOOKSTRUCT 结构体:
1
2
3
4
5
6
7
|
typedef struct tagMSLLHOOKSTRUCT {
POINT pt; // 鼠标坐标 (屏幕坐标),例如:pt.x = 480, pt.y = 720
DWORD mouseData; // 中键/滚轮等附加信息
DWORD flags; // LLKHF_INJECTED = 是否为注入事件
DWORD time; // 时间戳
ULONG_PTR dwExtraInfo;
} MSLLHOOKSTRUCT, *PMSLLHOOKSTRUCT;
|
回调函数必须是 HOOKPROC 类型,其定义如下:
1
|
typedef LRESULT (CALLBACK* HOOKPROC)(int nCode, WPARAM wParam, LPARAM lParam);
|
因此,一个合法的回调函数应当按下面的形式进行定义:
1
2
3
|
LRESULT HookCallbackFunc(int nCode, WPARAM wParam, LPARAM lParam){
// function's code
}
|
回调函数还需要调用 WinAPI CallNextHookEx 并返回其输出。CallNextHookEx 的作用是将当前钩子的消息传递给钩子链中的下一个钩子过程。换句话说,它会让下一次执行回调的钩子也能够收到本次事件的信息。
加入 CallNextHookEx 后,回调函数应如下所示:
1
2
3
4
5
|
LRESULT HookCallbackFunc(int nCode, WPARAM wParam, LPARAM lParam){
// Function's code
return CallNextHookEx(NULL, nCode, wParam, lParam)
}
|
根据微软文档中的 Remark 说明,调用 CallNextHookEx 虽然是可选的,但强烈建议调用。否则,如果其他应用程序也安装了同类型的钩子,那么它们将无法收到钩子通知,可能导致程序行为异常。
最后,需要编写回调函数的具体逻辑。同本例一致,代码需要监控某些行为,因此以下部分用于检查点击了哪个鼠标按键。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
LRESULT HookCallbackFunc(int nCode, WPARAM wParam, LPARAM lParam){
if (wParam == WM_LBUTTONDOWN){
printf("[ # ] Left Mouse Click \n");
}
if (wParam == WM_RBUTTONDOWN) {
printf("[ # ] Right Mouse Click \n");
}
if (wParam == WM_MBUTTONDOWN) {
printf("[ # ] Middle Mouse Click \n");
}
return CallNextHookEx(NULL, nCode, wParam, lParam)
}
|
回调函数 HookCallbackFunc 是 由 Windows 内核(Win32k.sys) 和 user32.dll 负责调用的,每当有鼠标/键盘事件发生时,系统会自动调用这个函数,并自动传入参数。
换句话说:我们只是把 回调函数地址 注册给 Windows,之后由 Windows 负责调用该函数。
1
|
SetWindowsHookExW(WH_MOUSE_LL, HookCallbackFunc, NULL, 0);
|
Windows 做了三件事:
首先,把 回调函数地址 保存到系统的 Hook 链表(Hook Chain)。
系统会把:回调函数地址、钩子类型(WH_MOUSE_LL)、当前线程或全局钩子都存进一个全局 Hook 链中。
接着,当系统检测到鼠标事件,会遍历 Hook 链条。
例如发生一个“左键按下”事件:系统捕获事件(来自驱动)、把事件包装成 hook message、调用 HookCallbackFunc
最后,系统负责传入参数。
系统会给 回调函数 传入:nCode:钩子状态(HC_ACTION 等)、wParam:事件类型(WM_LBUTTONDOWN)、lParam:指向 MSLLHOOKSTRUCT 的指针。
也就是说:参数由系统准备,不是我们手动准备的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
用户点击鼠标
↓
Windows 内核 (Win32k.sys) 捕获事件
↓
通过 user32.dll 分发事件
↓
检测到有 WH_MOUSE_LL 钩子
↓
遍历 Hook 链条
↓
调用注册的回调函数 HookCallbackFunc
↓
系统自动传入 (nCode, wParam, lParam)
↓
自定义的代码(执行printf/逻辑处理)
↓
调用 CallNextHookEx → 继续传递事件
↓
系统最终把事件交给目标窗口或应用
|
在获得用于监控用户鼠标点击的代码之后,下一步是确保 Hook 能持续保持有效。为此,需要让监控代码在一段特定的时间内持续运行。
实现方式是:在一个线程中调用 SetWindowsHookExW 安装钩子,然后通过 WinAPI WaitForSingleObject 让该线程保持存活,从而维持钩子的活动状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
// The callback function that will be executed whenever the user clicks a mouse button
LRESULT HookCallback(int nCode, WPARAM wParam, LPARAM lParam){
if (wParam == WM_LBUTTONDOWN){
printf("[ # ] Left Mouse Click \n");
}
if (wParam == WM_RBUTTONDOWN) {
printf("[ # ] Right Mouse Click \n");
}
if (wParam == WM_MBUTTONDOWN) {
printf("[ # ] Middle Mouse Click \n");
}
// moving to the next hook in the hook chain
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
BOOL MouseClicksLogger(){
// Installing hook
HHOOK hMouseHook = SetWindowsHookExW(
WH_MOUSE_LL,
(HOOKPROC)HookCallback,
NULL,
NULL
);
if (!hMouseHook) {
printf("[!] SetWindowsHookExW Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Keeping the thread running
while(1){
}
return TRUE;
}
int main() {
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MouseClicksLogger, NULL, NULL, NULL);
if (hThread)
WaitForSingleObject(hThread, 10000); // Monitor mouse clicks for 10 seconds
return 0;
}
|
回顾上面的代码,有几个问题需要被解决:
因为我们使用的是 低级鼠标钩子 WH_MOUSE_LL,它依赖 系统的消息泵(Message Pump) 来正确分发钩子事件。但当线程创建完钩子后:
这个死循环完全不处理消息队列,因此:
- 钩子产生的事件无法被正常分发
- 消息在队列里堆积,导致主系统线程卡顿
- 鼠标移动会变得延迟、卡顿、滞后
需要注意的是回调函数中包含 CallNextHookEx(),但CallNextHookEx() ≠ 消息循环,它不能解决遇到的“鼠标卡顿”“事件堆积”问题。
CallNextHookEx() 的作用是:将同一个钩子事件继续传给下一层 hook(hook chain)、保证钩子的链条不会被阻塞、让其他应用也能收到该钩子的事件。
换句话说:CallNextHookEx 处理的是 “钩子链(Hook Chain)”,它不处理:消息循环、系统消息队列、鼠标事件的默认行为、系统内部光标更新、事件派发到 UI 线程等。
消息循环(Message Loop)的功能是:保持线程活着(不退出)、从系统消息队列取出事件、把事件投递给系统,从而触发钩子回调、让系统继续执行默认处理(光标、滚轮、点击等)。
1
2
3
|
if (wParam == WM_LBUTTONDOWN)
printf("Left Mouse Click");
...
|
原有的代码只处理了鼠标点击消息、但还有大量其他消息未被处理,例如移动、滚轮、系统内部同步消息等。
如果不处理消息队列中的其他消息,系统就无法继续将事件传递到:定义的 HookCallback、其他 hook chain、系统内部鼠标处理逻辑(这就导致整个鼠标卡顿)
1
2
3
4
5
6
|
MSG Msg = { 0 };
while (GetMessageW(&Msg, NULL, NULL, NULL)) {
TranslateMessage(&Msg);
DispatchMessageW(&Msg);
}
|
这段代码的作用是:让线程一直存在、让 Windows 能把低级鼠标钩子的事件传给线程、避免事件堆积造成鼠标卡顿。
MSG 结构体是 Windows 用来保存“某一条消息”的结构体,承载 Windows 投递给线程的消息(包括鼠标钩子事件):
1
2
3
4
5
6
7
8
9
|
typedef struct tagMSG {
HWND hwnd; // 消息属于哪个窗口(没有窗口时也可能为 NULL)
UINT message; // 消息类型,例如 WM_MOUSEMOVE、WM_LBUTTONDOWN
WPARAM wParam; // 参数1
LPARAM lParam; // 参数2
DWORD time; // 消息发生时间
POINT pt; // 鼠标位置(屏幕坐标)
DWORD lPrivate; //
} MSG, *PMSG, *NPMSG, *LPMSG;
|
GetMessageW() 是整个消息循环的核心。它会从当前线程的消息队列取出一条消息,填充到 msg 结构里。也就是说,GetMessage() 让线程保持存活并持续处理来自系统的消息。
1
2
3
4
5
6
|
BOOL GetMessageW(
[out] LPMSG lpMsg,
[in, optional] HWND hWnd,
[in] UINT wMsgFilterMin,
[in] UINT wMsgFilterMax
);
|
TranslateMessage() 将键盘消息转换成字符消息(WM_CHAR),它只处理键盘消息,对鼠标消息几乎没作用。但放这里是 Windows 标准写法,保持消息循环一致。
1
2
3
|
BOOL TranslateMessage(
[in] const MSG *lpMsg
);
|
DispatchMessageW() 把消息投递给目标窗口的窗口过程(WndProc)。
1
2
3
|
LRESULT DispatchMessageW(
[in] const MSG *lpMsg
);
|
如果线程没有窗口(上面的例子就是没有窗口), DispatchMessageW() 会无法调用 WndProc(因为 hwnd=0 或没有注册窗口类)。但 DispatchMessageW() 不会因为 hwnd=NULL 崩溃,DispatchMessageW() 会执行一次空派发,实际上不处理任何东西。
因为我们写的程序没有图形化窗口,所以也可以简单使用DefWindowProcW来进行处理。反正不是窗口程序,那所有消息都直接交给默认窗口处理即可。它只需要维持消息泵运行,让 WH_MOUSE_LL 正常工作,而不是处理 UI 事件。
1
2
3
4
5
|
MSG Msg = { 0 };
while (GetMessageW(&Msg, NULL, NULL, NULL)) {
DefWindowProcW(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
}
|
DefWindowProcW 定义:
1
2
3
4
5
6
|
LRESULT DefWindowProcW(
[in] HWND hWnd,
[in] UINT Msg,
[in] WPARAM wParam,
[in] LPARAM lParam
);
|
所有这些操作都应放在一个循环中执行,以确保每一个未处理的消息都能被手动处理。
要移除由 SetWindowsHookEx 安装的任何钩子,必须调用 WinAPI UnhookWindowsHookEx。该函数只需要传入要被移除的钩子句柄即可。
最终代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
#include <Windows.h>
#include <stdio.h>
#define MONITOR_TIME 20000 // monitor mouse clicks for 20 seconds
// global hook handle variable
HHOOK g_hMouseHook = NULL;
LRESULT HookCallBack(int nCode, WPARAM wParam, LPARAM lParam) {
if (wParam == WM_LBUTTONDOWN) {
printf("[#] Left Mouse Click \n");
}
if (wParam == WM_RBUTTONDOWN) {
printf("[#] Right Mouse Click \n");
}
if (wParam == WM_MBUTTONDOWN) {
printf("[#] Middle Mouse Click \n");
}
// moving to the next hook in the hook chain
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
BOOL MouseClicksLogger() {
MSG Msg = { 0 };
// installing hook
g_hMouseHook = SetWindowsHookExW(WH_MOUSE_LL, (HOOKPROC)HookCallBack, NULL, NULL);
if (!g_hMouseHook) {
printf("[!] SetWindowsHookExW Failed With Error : %d \n", GetLastError());
return FALSE;
}
// process unhandle events
while (GetMessageW(&Msg, NULL, NULL, NULL)) {
TranslateMessage(&Msg);
DispatchMessageW(&Msg);
}
/* other process unhandle events
while (GetMessageW(&Msg, NULL, NULL, NULL)) {
DefWindowProcW(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
}
*/
return TRUE;
}
int main() {
HANDLE hThread = NULL;
DWORD dwThreadId = NULL;
hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MouseClicksLogger, NULL, NULL, &dwThreadId);
if (hThread) {
printf("[+] Thread %d Is Created To Monitor Mouse Clicks For %d Seconds \n\n", dwThreadId, (MONITOR_TIME / 1000));
WaitForSingleObject(hThread, MONITOR_TIME);
}
if (g_hMouseHook && !UnhookWindowsHookEx(g_hMouseHook)) {
printf("[!] UnhookWindowsHookEx Failed With Error : %d\n", GetLastError());
return -1;
}
return 0;
}
|
运行效果:
