Windows消息钩子

已经4个月没有写过博客了,笔者如今已经回到学校,终于可以继续更了。

消息钩子

Windows操纵系统向用户提供GUI,它以事件驱动方式工作。在Windows操作系统中借助鼠标、键盘,选择菜单、按钮,以及移动鼠标、改变窗口大小与位置都是事件。发生一个事件时,OS会把事先定义好的消息发送给相应的应用程序,应用程序分析收到的消息后执行相应的动作。敲击键盘时,消息会从OS发送到应用程序。而消息钩子就是在这中间偷看信息。

SetWindowsHookEx()

使用SetWindowsHookEx() API可以轻松实现钩子,SetWindowsHookEx()定义如下

1
2
3
4
5
6
HHOOK SetWindowsHookEx(){
int idHook, // hook type
HOOKPROC lpfn, // hook procedure
HINSTANCE hMod, // hook procedure所属的DLL句柄
DWORD dwThreadId // 想要挂在的线程ID
}

hook procedure 是由操作系统调用的回调函数。安装消息钩子时,hook procedure需要存在于某个dll内部,且该dll的instance handle即是hMod。若dwThreadId参数设置为0,则安装的钩子为全局钩子,会影响到运行中的所有进程。
使用SetWindowsHookEx()设置钩子之后,在某个进程中生成指定消息时,操作系统会将相关的dll强制注入相应进程,然后调用注册钩子的过程。注入进程时用户几乎不需要做什么,非常方便。

一个键盘消息钩子的demo

编写两个pe文件:HookMain.exe与KeyHook.dll,当在notepad输入时,拦截消息。HookMain是最先加载KeyHook.dll并安装键盘钩子的程序。HookMain.exe加载KeyHook.dll文件后使用SetWindowsHookEx()安装键盘钩子(KeyboardProc)。若其他进程中发生键盘输入事件,OS就会强制将KeyHook.dll加载到相应进程内存,然后调用KeyboardProc()函数。由于OS会将KeyHook.dll强制加载到发生键盘输入事件的所有进程,所以消息钩子技术常常被用作一种dll注入技术。

HookMain.cpp源码

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
#include "windows.h"
#include <stdio.h>
#define DLL_NAME "KeyHook.dll"
#define HOOKSTART "HookStart"
#define HOOKSTOP "HookStop"

using Func_HOOK = void(*)();

int main()
{
HMODULE hDLL = NULL;
Func_HOOK HookStart = NULL;
Func_HOOK HookStop = NULL;
char ch = 0;

// 加载KeyHook.dll
hDLL = LoadLibraryA(DLL_NAME);
// 获取导出函数地址
HookStart = (Func_HOOK)GetProcAddress(hDLL, HOOKSTART);
HookStop = (Func_HOOK)GetProcAddress(hDLL, HOOKSTOP);

// 开始钩取
HookStart();

printf("press q to quit\n");;
while (getchar() != 'q');

// 终止钩取
HookStop();

// 卸载KeyHook.dll
FreeLibrary(hDLL);
}

KeyHook.cpp源码

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
#include "windows.h"
#include <stdio.h>
#define PROCESS_NAME "notepad.exe"

HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_hInstance = hModule;
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
char szPath[MAX_PATH] = { 0, };
char* p = NULL;
if (nCode >= 0)
{
if (!(lParam & 0x80000000)) // 释放键盘按键
{
GetModuleFileNameA(NULL, szPath, MAX_PATH);
p = strrchr(szPath, '\\');
// 比较当前进程名,若为notepad.exe,则不传递消息给下一个钩子
if (!_stricmp(p + 1, PROCESS_NAME))
{
return 1;
}
}
}
// 若非notepad.exe,则调用CallNextHookEx()函数,将消息传递给下一个钩子
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
extern "C"
{
__declspec(dllexport) void HookStart()
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
}
__declspec(dllexport) void HookStop()
{
if (g_hHook)
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
}
}

笔者使用win10,vs2019测试成功。需要注意两点:

  • vs编译时需使用和notepad.exe相同的位数,也就是如果notepad是32位,就编译32位,notepad是64位就编译64位。
  • 关闭输入法。
文章目录
  1. 1. 消息钩子
  2. 2. SetWindowsHookEx()
  3. 3. 一个键盘消息钩子的demo
    1. 3.1. HookMain.cpp源码
    2. 3.2. KeyHook.cpp源码