第21章 动态链接库和钩子(2)
21.5 Windows钩子
21.5.1 Windows钩子
钩子是Windows消息处理机制中的一个监视点,应用程序可以在这里安装一个监视子程序,这样就可以在系统中的消息流到达目的窗口过程前监控它们。也就是说,钩子是用来截获系统中的消息流,并送给其他应用程序的处理的。
21.5.2 钩子的类型
(1)按作用范围分类
钩子类型 |
说明 |
局部钩子 |
仅钩挂属于自身进程的事件消息 ★钩子模块不需要单独的dll,可以放在同一个可执行文件中 |
远程钩子 |
又分为两种: 线程钩子(LocalHook)——捕获其它进程中的某一线程的事件 系统钩子(RemoteHook)——捕获所有进程的消息,也叫全局钩子 ★钩子回调函数必须放在DLL中:因为只有dll是可以被映射到其他进程的地址空间。这样,HOOK代码才能被系统注入到每个进程里面。在安装这样的钩子后,系统会自动地把该DLL注入到所有的进程空间,这样每个进程发生的特定事件就会被注入进来的Hook代码给拦截。否则,系统是不能从其他进程的地址空间中调用钩子函数,因为两个进程的地址空间是隔离的。 |
(2)按事件分类
① 键盘钩子和低级键盘钩子可以监视各种键盘消息。
② 鼠标钩子和低级鼠标钩子可以监视各种鼠标消息。
③ 外壳钩子可以监视各种Shell事件消息。比如启动和关闭应用程序。
④ 日志钩子可以记录从系统消息队列中取出的各种事件消息。
⑤ 窗口过程钩子监视所有从系统消息队列发往目标窗口的消息。
(3)常见的钩子
钩子类型 |
说明 |
WH_CALLWNDPROC WH_CALLWNDPROCRET |
①WH_CALLWNDPROC:在SendMessage时,当消息到达目标窗口之前,调用钩子函数 ②WH_CALLWNDPROCRET:在SendMessage时,在消息到达目标窗口之后,调用钩子函数。 ③WM_CALLWNDPROCRET会收到CWPRETSTRUCT结构体,包含了来自处理消息的窗口过程的返回值,及与这个消息关联的消息参数。 |
WH_CBT |
①激活、建立、销毁、最小化、最大化,移动、改变尺寸等窗口事件 ②设置输入焦点事件、系统菜单消息 ③同步系统消息队列、完成系统指令 ④来自系统消息队列中的移动鼠标,键盘事件 |
WH_GETMESSAGE |
当GetMessage或 PeekMessage函数获取消息后,调用钩子函数,可以用来监视鼠标和键盘输入及其他消息。 |
WH_KEYBOARD |
当GetMessage或PeekMessage,如果得到的是WM_KEYUP或WM_KEYDOWN时,则调用钩子函数 |
WH_MOUSE |
当GetMessage或PeekMessage时,如果得到的是鼠标消息,则调用钩子函数。 |
WH_HARDWARE |
当GetMessage或PeekMessage时,如果得到的是非鼠标和键盘消息,则调用调子函数 |
WH_MSGFILTER |
当用户对滚动条、菜单或对话框所做所有操作时,系统在发送相应的消息之前调用钩子函数(这种钩子只能是局部的) |
WH_SYSMSGFILTER |
同WH_MSGFILTER,但是系统钩子。 |
WH_SHELL |
当Shell程序准备接收一些通知事件前,调用钩子函数。外壳应用程序是不接受WH_SHELL消息的,要用应用程序能够接收WH_SHELL,必须调用SystemParametersInfo函数注册该消息。WM_SHELL共有5种情况: ①TaskBar需要重绘某个按钮时 ②当系统地要显示关于Taskbar的一个程序的最小化形式 ③当目前的键盘布局状态改变。 ④当按Ctrl+Esc去执行TaskManager(或相等级别的程序)时。 ⑤只要有个top-level、unowned窗口被创建、起作用或被摧毁 |
WH_DEBUG |
用来给其他钩子函数除错的 |
WH_JOURNALRECORD |
用来记录发送给系统消息队列的所有消息,被称为日志记录钩子 |
WH_JOURNALPLAYBACK |
用来回放日志记录钩子的系统事件,被称为日志回放钩子。 |
WH_FOREGROUNDIDLE |
应用程序的前台线程处于空闲状态时,调用钩子函数,可以在这里安排一些优先级很低的任务 |
21.5.3 远程钩子的安装和使用
(1)钩子程序的结构——3个功能模块
功能模块 |
说明 |
①主程序 |
用来实现界面或其他功能 |
②钩子回调函数 |
用来接收系统发过来的消息。 对于局部钩子来说,这模块可以处在同一可执行文件中。 对于远程钩子,该模块必须放在一个DLL库中。 |
③钩子安装和卸载模块 |
没有特别要求,一般也放在动态链接库中。 |
(2)钩子的安装:SetWindowsHookEx
参数 |
含义 |
int idHook |
见前面表格《常见的钩子类型》 |
HOOKPROC lpfn |
钩子函数的指针 ,也即拦截到指定系统消息后的预处理过程,一般定义在DLL中 |
HINSTANCE hMod |
应用程序实例的句柄。如果是全局钩子, hInstance是DLL句柄(DllMain中给的模块地址,就是包含HookProc的动态库加载地址。如果该值为0则只勾自己) |
DWORD dwThreadId |
要安装钩子的线程ID,指定被监视的线程,如果明确指定了某个线程的ID就只监视该线程,此时的钩子即为线程钩子;如果该参数被设置为0,则表示此钩子为监视系统所有线程的全局钩子。 |
(2)钩子函数及钩子链
①LRESULT CALLBACK HookProc(int nCode,WPARAM wParam,LPARAM lParam);
②对于不同的钩子类型,钩子函数的参数的含义是不同的,可参照MSDN
③Windows系统中可能同时存在多个同类型的钩子,多个程序同时安装同一种钩子的时候,就会将这些钩子组成一个钩子链,最近加入的钩子放在链表的头部,Windows负责为每一种钩子维护一个钩子链。当一个事件发生时,Windows调用最后安装的钩子,然后由当前钩子的回调函数发起调用下一个钩子的动作,这样就可以将消息传递下来。因为,程序中于不感兴趣的消息应通过CallNextHookEx(hKeyHook,nCode,wParam,lParam)函数传递给下一个钩子。
(3)钩子的卸载:UnhookWindowsHookEx(HHOOK) 函数卸载钩子,其参数为钩子句柄。
(5)键盘钩子的实例程序
【HookTest程序】
①利用全局鼠标钩子来实现窗口的悬停效果(类似于QQ,当窗口靠左边时,则隐入。当鼠标靠屏幕左边时,渐显出来。
②实现全局键盘钩子实现键盘消息的拦截。
/*-------------------------------------------- 键盘钩子 供动态链接库及主程序使用的头文件 (c)浅墨浓香,2015.6.11 --------------------------------------------*/ #pragma once #include <Windows.h> #ifdef _cplusplus #ifdef API_EXPORT #define EXPORT extern "C" __declspec(dllexport) //当头文件供动态库本身使用时 #else #define EXPORT extern "C" __declspec(dllimport) //当头文件供调用库的程序使用时 #endif #else #ifdef API_EXPORT #define EXPORT __declspec(dllexport) //当头文件供动态库本身使用时 #else #define EXPORT __declspec(dllimport) //当头文件供调用库的程序使用时 #endif #endif //安装鼠标钩子 EXPORT HHOOK InstallMouseHook(HWND hwnd); //御载鼠标钩子 EXPORT void UnInstallMouseHook(); //鼠标钩子回调函数 LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam); //安装鼠标钩子 EXPORT HHOOK InstallKeyBoardHook(HWND hwnd); //御载鼠标钩子 EXPORT void UnInstallKeyBoardHook(); //键盘钩子回调函数 LRESULT CALLBACK KeyBoardProc(int nCode, WPARAM wParam, LPARAM lParam);
//HookLib.c
#define API_EXPORT #include "HookLib.h" #define WM_USER_MOUSEHOOK (WM_USER + 1000) #define WM_USER_KEYBOARDHOOK (WM_USER + 1001) //以下两个变量只供当前进程使用,不需要进程间共享. //g_hInst只供安装钩子的进程使用。 //g_wAscII只调用钩子函数的进程使用。 WORD g_wAscII; //保存每次击键时产生的扫描码转化成的ASCII码 HINSTANCE g_hInst; //动态链接库的句柄 //设置自定义数据段(用来做数据在不同进程中的共享区) //以下三个变量需要在不同进程共享。 #pragma data_seg(".myshared") HWND g_hWnd = NULL; HHOOK g_hMouseHook = NULL; HHOOK g_hKeyBoardHook = NULL; #pragma data_seg() //设置自定义数据段的属性(可读、可写、可共享) #pragma comment (linker,"/SECTION:.myshared,RWS") //设为可读写、可共享属性 //入口和退出点 int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { g_hInst = hInstance; return TRUE; } //安装鼠标钩子 HHOOK InstallMouseHook(HWND hwnd) { //因为安装钩子的进程与钩子函数的进程可能是不同进程,这个变量将在钩子函数里被使用, //所以g_hWnd、g_hMouseHook设为进程间共享的变量 g_hMouseHook = (HHOOK)SetWindowsHookEx(WH_MOUSE, MouseProc, g_hInst, 0);//最后一个参数为NULL,表示全局钩子 g_hWnd = hwnd; return g_hMouseHook; } //御载鼠标钩子 void UnInstallMouseHook() { UnhookWindowsHookEx(g_hMouseHook); } //鼠标钩子函数 LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) { MOUSEHOOKSTRUCT* ms = (MOUSEHOOKSTRUCT*)lParam; CallNextHookEx(g_hMouseHook, nCode, wParam, lParam); static POINT ptOld; if (wParam ==WM_MOUSEMOVE ) { if (ptOld.x != ms->pt.x && ptOld.y != ms->pt.y) { ptOld.x = ms->pt.x; ptOld.y = ms->pt.y; PostMessage(g_hWnd, WM_USER_MOUSEHOOK, wParam, MAKELPARAM(ms->pt.x, ms->pt.y)); } } return 0; } //安装键盘钩子 HHOOK InstallKeyBoardHook(HWND hwnd) { //因为安装钩子的进程与钩子函数的进程可能是不同进程,这个变量将在钩子函数里被使用, //所以g_hWnd、g_hKeyBoardHook设为进程间共享的变量 g_hKeyBoardHook = (HHOOK)SetWindowsHookEx(WH_KEYBOARD, KeyBoardProc, g_hInst, 0);//最后一个参数为NULL,表示全局钩子 g_hWnd = hwnd; return g_hKeyBoardHook; } //御载键盘钩子 void UnInstallKeyBoardHook() { UnhookWindowsHookEx(g_hKeyBoardHook); } /* 键盘钩子回调函数:每个按键该函数会被调用两次,即按下或释放时被调用。 lParam与键盘消息含义一致 0-15表示按键的重复次数 16-23按键的扫描码 位24:是否是扩展键(如F1F2等Fx键或小键盘的数字键),如果是为1 25-28未定义 29:Alt,按下为1,否则为0 30按键原来的状态,发送消息前按键如果是按下的为1,否则为0 31位:按键的当前动作,如果按下为0,释放为1.*/ LRESULT CALLBACK KeyBoardProc(int nCode, WPARAM wParam, LPARAM lParam) { BYTE byKeyState[256]; UINT uScanCode; //传给键盘钩子链中的下一个钩子处理 CallNextHookEx(g_hKeyBoardHook, nCode, wParam, lParam); //安插个后门程序,将当前用户对键盘的操作偷偷发给我们的程序 //以下代码实现将用户的WM_KEYDOWN传为相应的ASCII,并发给我们的程序 if (lParam & 0x80000000) //只处理按下时的情况,即第31位为0时 { GetKeyboardState(byKeyState); //获取当前键盘状态 uScanCode = ((lParam &0x000F0000)>> 16); //取出16-23位的扫描码 //根据当前的键盘布局,键盘状态,将扫描码转为ASCII码 ToAscii(wParam, uScanCode, byKeyState, (LPWORD)&g_wAscII, 0); //将转换 PostMessage(g_hWnd, WM_USER_KEYBOARDHOOK, wParam, (LPARAM)g_wAscII); } return 0; } //LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) //{ // BYTE byKeyState[256]; // UINT uScanCode; // // CallNextHookEx(g_hHook, nCode, wParam, lParam); // //}
//HookTest.c——测试程序
#include <Windows.h> #include "resource.h" #include "..\HookDll\HookLib.h" #pragma comment(lib,"..\..\Debug\HookDll.lib") #define WM_USER_MOUSEHOOK (WM_USER + 1000) #define WM_USER_KEYBOARDHOOK (WM_USER + 1001) BOOL CALLBACK DlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { TCHAR szBuffer[128]; static TCHAR szKeyBuffer[128]; static POINT pt; HDC hdc; PAINTSTRUCT ps; RECT rc; int i; static int nWidth, nHeight; switch (uMsg) { case WM_INITDIALOG: InstallMouseHook(hwndDlg); //安装全局鼠标钩子 InstallKeyBoardHook(hwndDlg); //安装全局键盘钩子 GetWindowRect(hwndDlg, &rc); nWidth = rc.right - rc.left; nHeight = rc.bottom - rc.top; return TRUE; case WM_USER_MOUSEHOOK: //自定义的鼠标钩子消息 pt.x = LOWORD(lParam); pt.y = HIWORD(lParam); GetWindowRect(hwndDlg, &rc); //实现悬挂窗口的效果 if (rc.left<=0) //窗口左边界己经在屏幕左边。 { if (PtInRect(&rc, pt)) //如果鼠标在窗口内,则正常显示窗口 { if (IsWindowVisible(hwndDlg)) { SetWindowPos(hwndDlg, HWND_TOP, 0, rc.top, nWidth, nHeight, SWP_SHOWWINDOW); } } else //如果鼠标不在窗口上,则隐入 { if (IsWindowVisible(hwndDlg)) { for (i = 0; i < nWidth; i++) { SetWindowPos(hwndDlg, HWND_TOP, 0, rc.top, nWidth - i, nHeight, SWP_SHOWWINDOW); } SetWindowPos(hwndDlg, HWND_TOP, 0, rc.top, nWidth - i, nHeight, SWP_HIDEWINDOW); //隐藏起来(含任务栏图标也消失了) } } } if (pt.x<=0) //当鼠标靠屏幕右侧时,如果窗口原来隐藏的,则渐显出来。如果之前己经正常显示的,则不做任何处理。 { if (!IsWindowVisible(hwndDlg)) { for (i = 0; i < nWidth; i += 1) { SetWindowPos(hwndDlg, HWND_TOP, 0, rc.top, i, nHeight, SWP_SHOWWINDOW); } } } InvalidateRect(hwndDlg, NULL, TRUE); return 0; case WM_USER_KEYBOARDHOOK: //自定义的键盘钩子消息 wsprintf(szKeyBuffer, TEXT("您当前按下了键盘上的:%c"), lParam); InvalidateRect(hwndDlg, NULL, TRUE); return TRUE; case WM_PAINT: hdc = BeginPaint(hwndDlg, &ps); SetBkMode(hdc, TRANSPARENT); TextOut(hdc, 10, 40, szBuffer, wsprintf(szBuffer, TEXT("当前坐标:x = %d,y = %d"), pt.x, pt.y)); TextOut(hdc, 10, 60, szKeyBuffer, lstrlen(szKeyBuffer)); EndPaint(hwndDlg, &ps); return TRUE; case WM_CLOSE: UnInstallMouseHook(); //卸载鼠标钩子 UnInstallKeyBoardHook(); //卸载键盘钩子 EndDialog(hwndDlg, 0); return TRUE; } return FALSE; } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { DialogBoxParam(hInstance, TEXT("HOOKTEST"), NULL, DlgProc,0); }
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 HookTest.rc 使用 // #define IDC_TEXT 1001 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1002 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//HookTest.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h " END 2 TEXTINCLUDE BEGIN "#include ""winres.h"" " "