API Hooking-Windows APIs

本文为 Maldev Academy 中的 Module62 小节的笔记,通过 Windows 自带的 API 函数 实现 API Hook。(完整学习内容请自行前往跳转链接查看)。

SetWindowsHookEx WinAPI 调用是另一种可用于 API Hooking 的方法。它主要用于监听特定类型的系统事件,这与之前模块所介绍的技术不同,因为 SetWindowsHookExW/A 并不会修改某个函数的功能;相反,它会在某类事件被触发时执行一个回调函数。可被监听的事件类型仅限于 Windows 所提供的那些预定义事件类型。

1、使用 SetWindowsHookEx

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 —— 指向回调函数的指针,当指定的事件发生时会被调用。在本例中,只要有鼠标点击事件,该回调函数就会被执行。

(1)WH_KEYBOARD_LL 与 WH_MOUSE_LL

WH_KEYBOARD_LLWindows 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;

(2)Callback 函数

回调函数必须是 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)
}

(2-1)回调函数 HookCallbackFunc 的调用

回调函数 HookCallbackFunc 是 由 Windows 内核(Win32k.sys)user32.dll 负责调用的,每当有鼠标/键盘事件发生时,系统会自动调用这个函数,并自动传入参数。

换句话说:我们只是把 回调函数地址 注册给 Windows,之后由 Windows 负责调用该函数。

(2-2)SetWindowsHookEx 把回调函数“注册”给系统

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 的指针。

也就是说:参数由系统准备,不是我们手动准备的。

(2-3)完整工作流程

 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  继续传递事件
      
系统最终把事件交给目标窗口或应用

2、处理消息

在获得用于监控用户鼠标点击的代码之后,下一步是确保 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;
}

回顾上面的代码,有几个问题需要被解决:

(1)代码没有没有消息循环(Message Loop)

因为我们使用的是 低级鼠标钩子 WH_MOUSE_LL,它依赖 系统的消息泵(Message Pump) 来正确分发钩子事件。但当线程创建完钩子后:

1
while (1) {}

这个死循环完全不处理消息队列,因此:

  • 钩子产生的事件无法被正常分发
  • 消息在队列里堆积,导致主系统线程卡顿
  • 鼠标移动会变得延迟、卡顿、滞后

需要注意的是回调函数中包含 CallNextHookEx(),但CallNextHookEx() ≠ 消息循环,它不能解决遇到的“鼠标卡顿”“事件堆积”问题。

CallNextHookEx() 的作用是:将同一个钩子事件继续传给下一层 hook(hook chain)、保证钩子的链条不会被阻塞、让其他应用也能收到该钩子的事件。

换句话说:CallNextHookEx 处理的是 “钩子链(Hook Chain)”,它不处理:消息循环、系统消息队列、鼠标事件的默认行为、系统内部光标更新、事件派发到 UI 线程等。

消息循环(Message Loop)的功能是:保持线程活着(不退出)、从系统消息队列取出事件、把事件投递给系统,从而触发钩子回调、让系统继续执行默认处理(光标、滚轮、点击等)。

(2)原有回调函数代码对消息的处理动作

1
2
3
if (wParam == WM_LBUTTONDOWN)
    printf("Left Mouse Click");
...

原有的代码只处理了鼠标点击消息、但还有大量其他消息未被处理,例如移动、滚轮、系统内部同步消息等。

如果不处理消息队列中的其他消息,系统就无法继续将事件传递到:定义的 HookCallback、其他 hook chain、系统内部鼠标处理逻辑(这就导致整个鼠标卡顿)

(3)完善消息循环(Message Loop)代码

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
);

所有这些操作都应放在一个循环中执行,以确保每一个未处理的消息都能被手动处理。

3、移除 Hook

要移除由 SetWindowsHookEx 安装的任何钩子,必须调用 WinAPI UnhookWindowsHookEx。该函数只需要传入要被移除的钩子句柄即可。

4、完整代码

最终代码如下:

 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;

}

运行效果:

image-20251211225018190

updatedupdated2025-12-122025-12-12