1.简介
Windows消息传递机制,当在应用程序进行相关操作,例如点击鼠标、按下键盘,操作窗口等,操作系统能够感知这一事件,接着把此消息放到应用程序的消息序列中,应用程序通过Getmessage函数取出消息,然后调用DispatchMessage函数将这条消息调度给操作系统,操作系统会调用在设计窗口类时指定的应用程序窗口对这一消息进行处理,处理过程如图所示:
然而在实际应用中,可能需要对某些特殊消息进行屏蔽,为实现这一功能可以安装一个HOOK过程,即钩子过程,在《VC深入详解》一书将钩子过程比喻为警察为了抓逃犯而设置的检查站,基本原理也确实与此类似。就是在应用程序将信息传递给操作系统时,对消息进行捕获和过滤,从而阻止消息发送到指定的窗口过程,最终完成对某些消息的屏蔽功能。
2.基本框架
钩子过程在代码中基本实现过程如下图所示:
其中安装的Windows的钩子过程,主要通过SetWindowsHookEx函数来实现的,该函数声明如下:
该函数各个参数的含义如下;
- idHook
指定将要安装的钩子过程类型,其入参如下表所示:
后面主要以WH_KEYBOARD、WH_CBT、WH_SHELL为例描述钩子的使用方式。
- lpfn 钩到消息时响应的回调函数。
- hMod 存放钩子回调函数的模块句柄,如果是线程钩子可以设为NULL,如果是系统钩子则需要将回调函数写在动态链接库中,将此句柄设为对应的动态链接库句柄。
- dwThreadId 钩子存在两种类型,线程钩子和系统钩子,线程钩子需要传入对应的线程ID,系统钩子则将该参数设为0。
调用UnhookWindowsHookEx(HHook hook)可以卸载对应的钩子过程,入参是安装钩子函数的返回值。
3.键盘钩子(线程钩子)
下面以键盘钩子(WH_KEYBOARD)为例,描述线程钩子的使用方式及键盘钩子的作用,调用键盘钩子函数,
g_Hook1 = SetWindowsHookEx(WH_KEYBOARD, KeyProc, NULL, GetCurrentThreadId());
通过以上语句可以看出,设置钩子类型为键盘钩子,设置钩子回调函数为KeyProc,将钩子设为线程钩子,线程为当前线程,此时KeyProc回调函数可以写在当前项目中,调研过程中函数定义如下:
/**
*键盘钩子回调函数
*@param[in] nCode 响应类型
*@param[in] wParam 消息类型
*@param[in] lParam 特殊结构体句柄
*@return LRESULT
*-1 截取消息不再传递
*-0 表示不作处理,消息继续传递
*/
LRESULT CALLBACK CHookDeal::keyProc(int nCode,WPARAM wParam,LPARAM lParam )
{
//在WH_KEYBOARD_LL模式下lParam 是指向KBDLLHOOKSTRUCT类型地址
BOOL bDiscardFlag = FALSE;
if (nCode == HC_ACTION)
{
switch (wParam)
{
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
{
PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT) lParam;
if ( (( VK_TAB== p->vkCode) && ((p->flags & LLKHF_ALTDOWN) != 0)) //Tab + Alt
|| ((VK_ESCAPE == p->vkCode) && ((p->flags & LLKHF_ALTDOWN) != 0))//Esc + Alt
|| ((VK_ESCAPE == p->vkCode) && ((GetKeyState(VK_CONTROL) & 0x8000) != 0)) //Esc + Ctrl
|| (((GetKeyState(VK_CONTROL) & 0x8000) != 0) && (p->vkCode == VK_SPACE)) //Ctrl + Space
||(VK_LWIN == p->vkCode) //左系统键
||(VK_RWIN == p->vkCode) // 右系统键
||((p->vkCode > VK_DIVIDE) && (p->vkCode < VK_F24)) // 屏蔽F1-F24键
||((p->vkCode == VK_LCONTROL) || ((GetKeyState(VK_CONTROL)) & 0x8000) != 0) // 屏蔽ctrl
)
{
bDiscardFlag = TRUE;
}
break;
}
}
}
return(bDiscardFlag? 1 : CallNextHookEx(NULL, nCode, wParam, lParam));
//返回1表示截取消息不再传递,返回0表示不作处理,消息继续传
}
说明:
【1】入参ncode是响应类型,不同类型的钩子程序对应着不同的响应类型,查看类型定义主要有以下响应类型:
其中键盘钩子类型为HC_ACTION类型。
【2】参数wParam是触发回调函数的键盘操作,主要是键盘按下和弹起等事件。
【3】参数lParam是面显对应键盘消息结构体,PKBDLLHOOKSTRUCT,其定义如下:
通过结构体定义注释可以看出是键盘钩子传入的结构体类型,可以预想windows定义了许多其他类型的钩子结构体,例如鼠标钩子结构体LPMOUSEHOOKSTRUCT,DEBUG钩子结构体DEBUGHOOKINFO等,根据钩子的类型不同在函数中将第三个入参lpara转为对应的结构体指针,从而获得对应的信息,键盘结构体含义如下:
- vkCode代表引发回调函数时操作的键盘码。
- falgs扩展键标识、事件注入标识等,通过该标志可以查看哪个键盘同时按下。
系统通过vkCode获得当前按下的键盘,同时通过flags获得同时按下的键盘(通过与ctrl等键盘标识进行按位与),最终获得当前消息对应的键盘,屏蔽了Tab+Alt、Win_L、WIN_R等一系列的热键,注意此时只屏蔽当前线程下的键盘消息。如果想要屏蔽全部键盘消息,则需要将安装钩子函数改为:
SetWindowsHookEx(WH_KEYBOARD_LL, KeyProc, NULL,NULL);
其中WH_KEYBOARD_LL与WH_MOUSE_LL类型钩子与一般钩子不同,设为全局钩子时,可以将钩子响应函数写在本项目中,而不用新建一个动态链接库项目,将钩子函数写在动态链接库中。
4.界面消息钩子(系统全局钩子)
在第三章以线程钩子为例讲述了键盘钩子的使用方法,那么窗口消息钩子则以系统钩子为例进行描述。全局钩子首先要新建一个动态链接库项目,将钩子回调函数写到该链接库中,以便于所有的进程调用,根据《Windows核心编程(第5版)》描述过程,将全局钩子的过程绘制如下所示:
首先我们在进程A中加载了全局钩子的动态链接库,同时安装了全局钩子,然后操作系统会进行如下处理:
- 进程B中的某个线程准备派送一个全局钩子对应的消息。
- 操作系统检查该线程是否安装了对应消息的钩子过程。
- 查看全局钩子所在的DLL是否已经映射到进程B中,未映射则自动加载该DLL,并调用对应的钩子函数,进行相关消息屏蔽等操作。
- 实现过程:
首先创建一个动态链接库工程,GlobalHook,在该工程中导出钩子安装函数,其代码如下图所示:
其中第三个入参是存放钩子函数的动态链接库句柄,第四个入参设为0说明当前是系统全局钩子,窗口钩子的回调函数声明与键盘钩子相同:
LRESULT CALLBACK wndProc(int nCode,WPARAM wParam,LPARAM lParam )
说明:
【1】nCode的类型有10种,定义如下,具体含义不在赘述:
【2】wParam是对应的窗口句柄(HWND)
【3】lParam根据nCode的不同会对应不同的结构体指针,例如HCBT_ACTIVE:
或者HCBT_CREATEWND:
对于WH_CBT钩子来说,只有nCode为以上两个类型时,lParam的值才有意义。
以下是截获创建顶级窗口的钩子回调函数:
LRESULT CALLBACK wndProc(int nCode,WPARAM wParam,LPARAM lParam )
{
switch (nCode)
{
case HCBT_CREATEWND:
{
HWND hWnd = (HWND)wParam;
LPCBT_CREATEWND pcbt = (LPCBT_CREATEWND)lParam;
LPCREATESTRUCT pcs = pcbt->lpcs;
//WS_EX_TOOLWINDOW
if ((DWORD)(pcs->dwExStyle) == WS_EX_TOPMOST)
{
MessageBox(NULL, NULL, L"顶级窗口创建!", 0);
}
break;
}
default:
break;
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
//return 1;//返回1表示截取消息不再传递,返回0表示不作处理,消息继续传
}
5.调试钩子(系统钩子WH_DEBUG)
在钩子过程中,有一种特殊的钩子即WH_DEBUG类型,该类型钩子主要用于调试已经安装的钩子,对当前系统安装的钩子进行处理,操作系统在调用其他钩子过程之前首先进入WH_DEBUG钩子过程,通过该钩子过程可以决定操作系统是否调用相关钩子过程。其中WH_DEBUG钩子回调函数声明与其他钩子类型一致:
LRESULT CALLBACK DebugProc(int nCode, WPARAM wParam, LPARAM lParam)
说明:
【1】nCode钩子响应代码。
【2】wParam指示当前即将被调用的钩子的类型,如WH_MOUSE,WH_KEYBOARD 参数。
【3】lParam 指向DEBUGHOOKINFO 结构。
- idThread 安装WH_DEBUG钩子的线程ID。
- idThreadInstaller 当前即将被调用的钩子所在的线程ID。
- lParam 当前即将被调用的钩子的lParam参数。
- wParam 当前即将被调用的钩子的wParam参数。
- Code 当前即将被调用的钩子的nCode参数。
- 返回值:当你已经处理了该钩子并且不希望即将被调用的钩子继续执行,则必须返回非0值 否则请返回CallNextHookEx的值。
下面是屏蔽当前线程中所有钩子的实现函数:
LRESULT CALLBACK DebugProc(int nCode, WPARAM wParam, LPARAM lParam) //WH_DEBUG钩子例程
{
DEBUGHOOKINFO *debug=(DEBUGHOOKINFO *)lParam;
//判断:是当前线程的钩子
if(debug->idThread!=debug->idThreadInstaller)
{
return 1;
}
return ::CallNextHookEx(DEBUG_hhook, nCode, wParam ,lParam);
}
注意:
钩子在操作系统中执行过程是按照后进先出的过程,即后面安装的钩子过程会抢占钩子链顶部,并将原头部钩子过程挤压到第二个位置,也就是说操作系统会首先调用钩子链顶部钩子过程(最后安装钩子过程),然后按照钩子链一次调用相关钩子过程。