消息钩子
Windows操作系统向用户提供GUI(图形用户界面),以事件驱动(Event Driven)方式工作。在操作系统中借助键盘、鼠标、选择菜单、按钮、鼠标移动、改变窗口大小与位置等都是事件(Event)发生事件时,OS会把事先定义好的消息发送给相应的应用程序,应用程序分析收到的信息后执行相应操作。以键盘消息为例。
常规Windows消息流。
#发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS message queue]。
#OS判断哪个应用程序中发生了事件,然后从[OS message queue]取出消息,添加到相应应用程序的 [application message queue]中。
#应用程序监视自身的[application message queue],发现新添加的WM_KEYDOWN消息后,调用相应的事件 处理程序处理。
如图所示,OS消息队列与应用程序消息队列之间存在一条“钩链”(Hook Chain),设置好键盘消息钩子之后,处于“钩链”中的键盘消息钩子会比应用程序先看到相应信息。在键盘消息钩子函数内部,除了可以查看消息之外,还可以修改消息本身,而且还能对消息实施拦截,组织消息传递。
MS Visual Studio中提供的SPY++是一个十分强大的消息钩取程序,能够查看操作系统中来往的所有消息。
SetWindowsHookEx()
使用SetWindowsHookEx()API可轻松实现消息钩子,SetWindowsHookEx()API的定义如下所示。
HHOOK SetWindowsHookEx(
int idHook, //钩子类型
HOOKPROC lpfn, //钩子函数
HINSTANCE hMod, //包含钩子函数模块的句柄
DWORD dwThreadId //想要挂钩的进程ID
);
钩子过程(hook procedure)是由操作系统调用的回调函数。安装消息“钩子”时,“钩子”过程需要存在于某个DLL内部,且该DLL的示例句柄(instance handle)即是hMod。
提示:若dwThreadID参数被设置为0,则安装的钩子为“全局钩子”(Global Hook),它会影响到运行中的(以及以后要运行的)所有进程。
使用SetWindowsHookEx()设置好钩子后,在某个进程中生成指定消息时,操作系统会将相关的DLL文件强制注入(injection)相应进程,然后调用注册的“钩子”过程。注入进程时用户几乎不需要做什么,十分方便。
键盘消息钩取练习
KeyHook.dll文件是一个含有钩子过程(KeyboardProc)的DLL文件。HookMain.exe是最先加载SetWindowsHookEx()安装键盘钩子(KeyboardProc)。若其他进程(explore.exe、iexplorr.exe、notepad.exe等)中发生键盘输入事件,OS就会强制将KeyHook.dll加载到相应进程的内存,然后调用KeyboardProc()函数。
需注意,OS会将KeyHook.dll强制加载到发生键盘输入事件的所有进程。换言之,消息钩取技术常常被用作一种DLL注入技术。
分析源码
WinXP/7中可用。
HookMain.cpp
//HookMain
#include "stdio.h"
#include "windows.h"
//Console Input/Output,定义了通过控制台进行数据输入和数据输出的函数
//主要是一些用户通过按键盘产生的对应操作,比如getch()函数等等
#include "conio.h"
//定义一些常量
#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"
//定义两个参数为空、返回值为void即没有的函数指针
typedef void (*PFN_HOOKSTART)();
typedef void (*PFN_HOOKSTOP)();
void main(){
//定义及初始化句柄变量和函数指针
HMODULE hDll = NULL;
PFN_HOOKSTART HookStart = NULL;
PFN_HOOKSTOP HookStop = NULL;
//加载KeyHook.dll
hDll = LoadLibraryA(DEF_DLL_NAME);
//若加载不成功,则输出错误信息
if( hDll == NULL ){
printf("[-]无法加载%s [%d]\n", DEF_DLL_NAME, GetLastError());
return;
}
//获取导出函数地址
HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);
//开始钩取
HookStart();
//直至用户输入“q”退出钩取
printf("[*]等待输入 'q' 来停止钩取...\n");
while( _getch() != 'q' );
//终止钩取
HookStop();
//卸载KeyHook.dll
FreeLibrary(hDll);
}
源码非常简单。先加载KeyHook.dll文件,然后调用HookStart()函数开始钩取,用户输入“q”时,调用HookStop()函数终止钩取。
KeyHook
//KeyHook.cpp
#include "stdio.h"
#include "windows.h"
//定义目标进程名为notepad.exe
#define DEF_PROCESS_NAME "notepad.exe"
//定义全局变量
HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
//DllMain()函数在DLL被加载到进程后会自动执行
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved){
switch( dwReason ){
case DLL_PROCESS_ATTACH:
g_hInstance = hinstDLL;
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 ){
//释放键盘按键时,bit 31 : 0 => press, 1 => release
if( !(lParam & 0x80000000) ){
GetModuleFileNameA(NULL, szPath, MAX_PATH);
p = strrchr(szPath, '\\');
//比较当前进程名称,若为notepad.exe,则消息不会传递给应用程序或下一个钩子函数
//_stricmp()函数用于比较字符串,i表示不区分大小写,若两个值相等则返回0
if( !_stricmp(p + 1, DEF_PROCESS_NAME) ){
return 1;
}
}
}
//比较当前进程名称,若非notepad.exe,则消息传递给应用程序或下一个钩子函数
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
//在C++中调用C的库文件,用extern "C"告知编译器,因为C++支持函数重载而C不支持,两者的编译规则不同
#ifdef __cplusplus
extern "C"{
#endif
//__declspec,针对编译器的关键字,用于指出导出函数
//当调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链
__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;
}
}
#ifdef __cplusplus
}
#endif
调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链。