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 获取错误信息。

代码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
#include "Main.h"
#include "iostream"
DWORD TestHook(DWORD Thread_Id)
{
HMODULE hMessageDll = LoadLibraryA("MessageDll.dll");
if (!hMessageDll)
{
printf("MessageDll模块Load失败\n");
return -1;
}
typedef void(*PSETHOOK)(DWORD Thread_Id);
PSETHOOK SetHook = (PSETHOOK)GetProcAddress(hMessageDll, "SetHook");
if (!SetHook)
{
printf("SetHook函数获取失败\n");
return -1;
}
typedef void(*PUNHOOK)();
PUNHOOK UnHook = (PUNHOOK)GetProcAddress(hMessageDll, "UnHook");
if (!UnHook)
{
printf("UnHook函数获取失败\n");
return -1;
}


HWND hwnd = FindWindow(NULL, TEXT("test1"));
if (!hwnd)
{
printf("窗口获取失败\n");
return -1;
}

SetHook(Thread_Id);
// 发送指定消息到窗口
SendMessage(hwnd, WM_USER + 0x1, (WPARAM)100, (LPARAM)200);

UnHook();
return 0;
}

int main()
{
DWORD Thread_Id;
std::cin >> Thread_Id;
TestHook(Thread_Id);
getchar();
}

Dll的代码:

SetWindowsHookExUnhookWindowsHookMessageHookProc必须写在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
#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <stdio.h>


HHOOK g_HookProc;

LRESULT CALLBACK MessageHookProc(int nCode, WPARAM wParam, LPARAM lParam);
extern "C" void __declspec(dllexport) SetHook();
extern "C" void __declspec(dllexport) UnHook();


BOOL APIENTRY DllMain(
HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
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" void __declspec(dllexport) SetHook(DWORD Thread_Id)
{
g_HookProc = SetWindowsHookExA(WH_CALLWNDPROC, MessageHookProc, GetModuleHandle(TEXT("MessageDll.dll")), Thread_Id);
}

extern "C" void __declspec(dllexport) UnHook()
{
if (g_HookProc) UnhookWindowsHookEx(g_HookProc);
}

LRESULT CALLBACK MessageHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION)
{
PCWPSTRUCT pcw = (PCWPSTRUCT)lParam;
if (pcw->message == WM_USER + 0x1)
{
char szBuf[200] = { 0 };
sprintf(szBuf, "wParam: %d\n,lParam: %d\nPID: %d\n", pcw->wParam, pcw->lParam, GetCurrentProcessId());
MessageBoxA(NULL, szBuf, "[contorl]", MB_OK);
}
}
return CallNextHookEx(g_HookProc, nCode, wParam, lParam);
}

如何调试设置的HookProc

当在 Visual Studio 中启动调试,或附加到一个进程时,调试器会查找与加载的 DLL 文件匹配的调试符号(PDB 文件)。PDB 文件包含了源码的位置信息、函数名称、变量等调试信息。

调试器会检查目标程序加载的所有模块(EXE 和 DLL),并将这些模块与相应的 PDB 文件进行匹配。

如果是无模块注入,那就不太懂了,感觉vs不能识别到

例如想要在test1.exe下钩子

1728566268361

附加到test1.exe上

1728566652250

但是我发现,挂完钩子,但是断点好像没有马上被断下来

而是等我用SendMessage给test1.exe发完消息之后,才会断下来,这就有点奇怪了,但是总之是断下来了

1728566411020