消息钩子

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),设置好键盘消息钩子之后,处于“钩链”中的键盘消息钩子会比应用程序先看到相应信息。在键盘消息钩子函数内部,除了可以查看消息之外,还可以修改消息本身,而且还能对消息实施拦截,组织消息传递。
DC7C9ZUO}PX}F4J8Y4RW84I.png
MS Visual Studio中提供的SPY++是一个十分强大的消息钩取程序,能够查看操作系统中来往的所有消息。

SetWindowsHookEx()

使用SetWindowsHookEx()API可轻松实现消息钩子,SetWindowsHookEx()API的定义如下所示。

  1. HHOOK SetWindowsHookEx(
  2. int idHook, //钩子类型
  3. HOOKPROC lpfn, //钩子函数
  4. HINSTANCE hMod, //包含钩子函数模块的句柄
  5. DWORD dwThreadId //想要挂钩的进程ID
  6. );

钩子过程(hook procedure)是由操作系统调用的回调函数。安装消息“钩子”时,“钩子”过程需要存在于某个DLL内部,且该DLL的示例句柄(instance handle)即是hMod。
提示:若dwThreadID参数被设置为0,则安装的钩子为“全局钩子”(Global Hook),它会影响到运行中的(以及以后要运行的)所有进程。
使用SetWindowsHookEx()设置好钩子后,在某个进程中生成指定消息时,操作系统会将相关的DLL文件强制注入(injection)相应进程,然后调用注册的“钩子”过程。注入进程时用户几乎不需要做什么,十分方便。

键盘消息钩取练习

UC31(4MV9O~H~J$1$((]D9E.png
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

  1. //HookMain
  2. #include "stdio.h"
  3. #include "windows.h"
  4. //Console Input/Output,定义了通过控制台进行数据输入和数据输出的函数
  5. //主要是一些用户通过按键盘产生的对应操作,比如getch()函数等等
  6. #include "conio.h"
  7. //定义一些常量
  8. #define DEF_DLL_NAME "KeyHook.dll"
  9. #define DEF_HOOKSTART "HookStart"
  10. #define DEF_HOOKSTOP "HookStop"
  11. //定义两个参数为空、返回值为void即没有的函数指针
  12. typedef void (*PFN_HOOKSTART)();
  13. typedef void (*PFN_HOOKSTOP)();
  14. void main(){
  15. //定义及初始化句柄变量和函数指针
  16. HMODULE hDll = NULL;
  17. PFN_HOOKSTART HookStart = NULL;
  18. PFN_HOOKSTOP HookStop = NULL;
  19. //加载KeyHook.dll
  20. hDll = LoadLibraryA(DEF_DLL_NAME);
  21. //若加载不成功,则输出错误信息
  22. if( hDll == NULL ){
  23. printf("[-]无法加载%s [%d]\n", DEF_DLL_NAME, GetLastError());
  24. return;
  25. }
  26. //获取导出函数地址
  27. HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
  28. HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);
  29. //开始钩取
  30. HookStart();
  31. //直至用户输入“q”退出钩取
  32. printf("[*]等待输入 'q' 来停止钩取...\n");
  33. while( _getch() != 'q' );
  34. //终止钩取
  35. HookStop();
  36. //卸载KeyHook.dll
  37. FreeLibrary(hDll);
  38. }

源码非常简单。先加载KeyHook.dll文件,然后调用HookStart()函数开始钩取,用户输入“q”时,调用HookStop()函数终止钩取。
KeyHook

  1. //KeyHook.cpp
  2. #include "stdio.h"
  3. #include "windows.h"
  4. //定义目标进程名为notepad.exe
  5. #define DEF_PROCESS_NAME "notepad.exe"
  6. //定义全局变量
  7. HINSTANCE g_hInstance = NULL;
  8. HHOOK g_hHook = NULL;
  9. //DllMain()函数在DLL被加载到进程后会自动执行
  10. BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved){
  11. switch( dwReason ){
  12. case DLL_PROCESS_ATTACH:
  13. g_hInstance = hinstDLL;
  14. break;
  15. case DLL_PROCESS_DETACH:
  16. break;
  17. }
  18. return TRUE;
  19. }
  20. //
  21. LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam){
  22. char szPath[MAX_PATH] = {0,};
  23. char *p = NULL;
  24. if( nCode >= 0 ){
  25. //释放键盘按键时,bit 31 : 0 => press, 1 => release
  26. if( !(lParam & 0x80000000) ){
  27. GetModuleFileNameA(NULL, szPath, MAX_PATH);
  28. p = strrchr(szPath, '\\');
  29. //比较当前进程名称,若为notepad.exe,则消息不会传递给应用程序或下一个钩子函数
  30. //_stricmp()函数用于比较字符串,i表示不区分大小写,若两个值相等则返回0
  31. if( !_stricmp(p + 1, DEF_PROCESS_NAME) ){
  32. return 1;
  33. }
  34. }
  35. }
  36. //比较当前进程名称,若非notepad.exe,则消息传递给应用程序或下一个钩子函数
  37. return CallNextHookEx(g_hHook, nCode, wParam, lParam);
  38. }
  39. //在C++中调用C的库文件,用extern "C"告知编译器,因为C++支持函数重载而C不支持,两者的编译规则不同
  40. #ifdef __cplusplus
  41. extern "C"{
  42. #endif
  43. //__declspec,针对编译器的关键字,用于指出导出函数
  44. //当调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链
  45. __declspec(dllexport) void HookStart(){
  46. g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
  47. }
  48. __declspec(dllexport) void HookStop(){
  49. if(g_hHook){
  50. UnhookWindowsHookEx(g_hHook);
  51. g_hHook = NULL;
  52. }
  53. }
  54. #ifdef __cplusplus
  55. }
  56. #endif

调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链。