Windows消息钩子

API介绍

SetWindowsHookEx 是 Windows API 中用于安装挂钩(Hook)的函数,它允许一个应用程序在特定事件发生时拦截系统消息或输入(例如键盘、鼠标输入),并对其进行处理。通常用于捕获、监控或修改应用程序中的消息流。

1
2
3
4
5
6
HHOOK SetWindowsHookEx(
int idHook, // 挂钩类型
HOOKPROC lpfn, // 挂钩回调函数
HINSTANCE hMod, // 应用程序或DLL实例句柄
DWORD dwThreadId // 线程标识符
);

参数解释:

  1. idHook (挂钩类型): 表示要安装的挂钩类型,它决定了系统在什么事件发生时调用钩子函数。常见的挂钩类型有:
    • WH_KEYBOARD: 捕获键盘消息(不区分按键的目标窗口)。
    • WH_KEYBOARD_LL: 捕获低级别的全局键盘消息,可以用于记录按键等。
    • WH_MOUSE: 捕获鼠标消息。
    • WH_MOUSE_LL: 捕获低级别的全局鼠标消息。
    • WH_CALLWNDPROC: 监控发送给目标窗口的消息。
    • WH_GETMESSAGE: 监控从消息队列中获取消息的操作。
    • WH_CBT: 监控系统中一些特定的事件(如窗口的创建、销毁、最小化、最大化等)。

lpfn (挂钩回调函数): 挂钩处理程序的指针,定义了挂钩触发时要执行的逻辑。这个回调函数将由系统在挂钩事件发生时调用。其签名通常是这样的:

1
2
3
4
5
LRESULT CALLBACK HookProc(
int nCode, // 挂钩代码,决定如何处理消息
WPARAM wParam, // 附加的消息信息,依挂钩类型不同而不同
LPARAM lParam // 附加的消息信息,依挂钩类型不同而不同
);

挂钩函数可以对系统消息进行修改或传递给下一个挂钩函数。

hMod (模块句柄): 指向包含 lpfn 钩子函数的模块句柄(即DLL或EXE的实例句柄)。如果挂钩是在一个动态链接库(DLL)中实现的,就需要提供这个 DLL 的 HINSTANCE,否则可以传入 NULL 表示当前进程的实例句柄。

  • 如果 dwThreadId 参数指定了 0,则 hMod 必须为挂钩所在的 DLL 模块句柄。
  • 如果是线程挂钩,则可以传递 NULL

dwThreadId (线程标识符): 指定要挂钩的线程的标识符(Thread ID)。有以下两种用法:

  • 如果指定一个线程 ID,则挂钩只在该线程的上下文中生效,这是一个线程级挂钩
  • 如果设置为 0,则挂钩将在全局范围内生效,处理系统中的所有消息。

返回值:

成功时,返回挂钩的句柄(HHOOK);失败时,返回 NULL,可以使用 GetLastError 获取错误信息。

GUI线程

主线程(GUI线程):主线程通常用于处理用户界面相关的逻辑,例如创建和显示窗体、处理用户输入、更新UI等操作。主线程是一个单线程的执行环境,所有的UI控件都必须在主线程上创建和更新,否则会引发跨线程访问的异常。因此,我们应该尽可能地将长时间运行的任务和耗时的操作移动到其他线程上执行,以避免阻塞主线程。

GUI线程与设置Windows消息钩子的关系

如果SetWindowHookEx的线程ThreadId参数不是GUI线程的话,将无法完成钩子

原因也非常简单

SetWindowsHookEx 的实现原理与 消息队列 密切相关。它的核心机制是通过拦截和监视系统消息特定线程的消息,并在消息到达目标窗口过程之前或之后执行自定义的钩子处理函数。

而不是GUI线程的普通线程根本没有消息队列,所以想要利用Windows消息钩子完成注入是不可能的

代码demo:

设置Hook的Exe代码:

首先输入一个线程的ID号,因为如果SetWindowsHookEx的最后一个参数不是0(全局挂钩),那么就是挂钩指定的线程,在这里我们加载我们写的Dll,获取dll里面导出的函数

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <Windows.h>
#include<TlHelp32.h>
#include<iostream>

using namespace std;

DWORD getThreadID(ULONG32 ulTargetProcessID);
BOOL InjectDllBySetWindowsHook(ULONG32 ulTargetProcessID);
bool IsGUIThread(DWORD dwThreadID);
DWORD getThreadID(ULONG32 ulTargetProcessID);

BOOL InjectDllBySetWindowsHook(ULONG32 ulTargetProcessID)
{
HANDLE TargetProcessHandle = NULL;
TargetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ulTargetProcessID);

if (NULL == TargetProcessHandle)
{
printf("Couldn't get Target Process Handle\r\n");
return FALSE;
}

HMODULE DllModule = LoadLibrary("Dll.dll");

if (DllModule == NULL)
{
printf("cannt find dll\r\n");
return FALSE;
}

//获取Dll中导出的函数的地址
HOOKPROC Sub_1Address = NULL;
Sub_1Address = (HOOKPROC)GetProcAddress(DllModule, "MyMessageProcess");
if (Sub_1Address == NULL)
{
printf("cannt found MyMessageProcess");
return FALSE;
}

DWORD ThreadID = getThreadID(ulTargetProcessID);

HHOOK Handle = SetWindowsHookEx(WH_KEYBOARD,
Sub_1Address, DllModule, ThreadID);

//MessageBox(0, 0, 0, 0);


if (Handle == NULL)
{
printf("cannt hook\r\n");
return FALSE;
}
printf("hook success\r\n");
getchar();
getchar();
getchar();
UnhookWindowsHookEx(Handle);

FreeLibrary(DllModule);
}


// 检查线程是否是 GUI 线程
bool IsGUIThread(DWORD dwThreadID) {
GUITHREADINFO guiInfo;
guiInfo.cbSize = sizeof(GUITHREADINFO);
if (GetGUIThreadInfo(dwThreadID, &guiInfo)) {
return true; // 线程有消息队列,是 GUI 线程
}
return false; // 线程没有消息队列,不是 GUI 线程
}

DWORD getThreadID(ULONG32 ulTargetProcessID)
{
HANDLE Handle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (Handle != INVALID_HANDLE_VALUE)
{
THREADENTRY32 te;
te.dwSize = sizeof(te);
if (Thread32First(Handle, &te))
{
do
{
if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID))
{
if (te.th32OwnerProcessID == ulTargetProcessID)
{
if (IsGUIThread(te.th32ThreadID))
{
CloseHandle(Handle);
cout << "已找到GUI线程" << endl;
return te.th32ThreadID;
}
}
}
} while (Thread32Next(Handle, &te));
}
}
CloseHandle(Handle);
cout << "没找到GUI线程" << endl;
return (DWORD)0;
}


int main()
{
ULONG32 ulTargetProcessID;
cout << "请输入目标进程ID:";
cin >> ulTargetProcessID;

if (!InjectDllBySetWindowsHook(ulTargetProcessID))
{
cout << "Set Hook Unsuccess!\r\n" << endl;
return 0;
}
cout << "Inject Success!\r\n" << endl;
return 0;

}

Dll的代码:

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
#include <windows.h>

#pragma data_seg(SHARD_SEG_NAME)
static HHOOK g_hHook;
#pragma data_seg()

BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
MessageBox(NULL, L"Inject Success!", L"1", 0);

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{

}

case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

extern "C"

__declspec(dllexport)LRESULT MyMessageProcess(int Code, WPARAM wParam, LPARAM lParam)
{
//
//你自己对消息的处理
//
MessageBox(NULL, L"GetMessage!", L"Message", 0);
return CallNextHookEx(g_hHook, Code, wParam, lParam);
}

需要注意的点:

来自微软官方文档:

Windows 应用商店应用开发 如果 dwThreadId 为零,则 Windows 应用商店应用进程和 Windows 运行时代理进程不会在进程内加载窗口挂钩 DLL,除非它们由 UIAccess 进程(辅助功能工具)安装。 通知在安装程序的线程上传送这些挂钩:

例如

  • Microsoft Edge
    • 进程名:MicrosoftEdge.exe
  • 照片
    • 进程名:Microsoft.Photos.exe
  • 计算器
    • 进程名:Calculator.exe
  • 邮件和日历
    • 进程名:HxOutlook.exe