《windows核心编程系列》十八谈谈windows钩子
windows应用程序是基于消息驱动的。各种应用程序对各种消息作出响应从而实现各种功能。
windows钩子是windows消息处理机制的一个监视点,通过安装钩子可以达到监视指定窗口某种类型的消息的功能。所谓的指定窗口并不局限于当前进程的窗口,也可以是其他进程的窗口。当监视的某一消息到达指定的窗口时,在指定的窗口处理消息之前,钩子函数将截获此消息,钩子函数既可以加工处理该消息,也可以不作任何处理继续传递该消息。使用钩子是实现dll注入的方法之一。其他常用的方法有:注册表注入,远程线程注入。
钩子函数是一个处理消息的程序段。是在安装钩子的时候向系统注册的。
关于windows钩子要清楚以下三点:
1:钩子是用来截获系统中的消息流的。利用钩子可以处理任何我们感兴趣的消息,当然包括其他进程的消息。
2:截获该消息后,用于处理该消息的程序叫做钩子函数。它是自定义的函数,在安装钩子时将此函数的地址告诉windows。
3:系统同一时间可能有多个进程安装钩子,多个钩子构成钩子链。所以截获消息并处理后,应该将此消息继续传递下去,以便其他钩子处理这一消息。
注意:使用钩子会使系统变慢,因为它增加了系统对每个消息的处理量。所以要仅在必要的时候才安装钩子。不需要时要及时卸载。
安装钩子:
1:
- SetWindowsHookEx(
- int idHook, //要安装的钩子的类型。
- HOOKPROC lpfn, //钩子函数的地址。
- HINSTANCE hMode, //钩子函数所在DLL在进程内的地址。
- DWORD dwThread, //要安装钩子的线程。如为0,则为所有线程安装钩子。
- );
idHook指定要安装钩子的类型,他可以是下面的值:
WH_CALLWNDPROC //目标线程调用SendMessage发送消息时,钩子函数被调用。
WH_CALLWNDPROCRET //当SendMessage返回时,钩子函数被调用。
WH_KEYBOARD //从消息队列中查询WM_KEYUP或WM_KEYDOWN时。
WH_GETMESSAGE //目标线程调用GetMessage或PeekMessage时
WH_MOUSE //查询消息队列中鼠标事件消息时。
WH_MSGFILTER //以下请参考MSDN。
WH_SYSMSGFILTER
WH_JORNALRECORD
WHJORNALPLAYBACK
WH_SHELL
WH_CBT
WH_FOREGROUNDIDLE
WH_DEBUG
2 :
lpfn是钩子函数的地址。钩子安装后如果有相应的消息发生,windows将调用此参数指向的函数。一般钩子函数都是位于一个DLL中。当为其他进程内的线程安装钩子时,如果钩子函数在DLL中,系统会把DLL映射到那个进程内,使他能在该进程内被调用。
注意:钩子函数多是被其他进程内的线程调用,而不一定是安装钩子的线程。
钩子函数被调用的过程:
当进程A一个线程准备向一个窗口发送一个消息,系统检查该线程是否被安装了钩子,如果该线程被安装了钩子且该消息与钩子要截获的消息类型一致,此消息将被截获。系统检查该钩子的钩子函数所在的DLL是否已经被映射进程A的地址空间中。如果尚未映射,系统会强制将该DLL映射到进程A的地址空间。然后获得钩子函数在进程A的虚拟地址,并调用钩子函数。我们可以在钩子函数内定义我们对该消息处理的过程。
注意:当系统把钩子函数所在的DLL映射到某个进程地址空间时,会映射整个DLL,而不仅仅是钩子函数,这也就说我们可以使用该DLL中的所有导出函数。
3:hmod参数是钩子函数所在dll的实例句柄,也是该dll在进程内的虚拟地址。如果钩子函数在当前进程中,此参数应被指定为NULL.
4:dwThreadid指定要被安装钩子的线程的ID号。如果被设为0,就会为系统内的所有GUI线程安装钩子。
5:钩子函数
钩子被安装后,如果有相应的消息发生,windows将调用钩子函数。以下为钩子函数的原型:
- LRESULT CALLBACK HookProc(int nCode,WPARAM wParam,LPARAM lParam)
- {
- //处理消息的代码。
- return CallNextHookEx(hHook,nCode,wParam,lParam);
- }
HookProc为钩子函数的名称。
nCode指定是否必须处理该消息。如果它为HC_ACTION,那么钩子函数就必须处理该消息。如果小于0,钩子函数就必须将该消息传递给CallNextHookEx,不对该消息进行处理,并返回CallNextHookEx的返回值。
CallNextHookEx用于把消息传递到钩子链中下一个钩子函数。
wParam和lParam的值依赖于具体的钩子类型。请参考MSDN。
卸载钩子。
BOOL UnhookWindowsHookEx(HHOOK hhk);
hhk为要卸载的钩子句柄。
下面将要实现一个例子,实现对键盘按键的监控。一旦有键盘被按下,就在主程序窗口显示一条信息指示哪一个键被按下。
程序外观:
首先要实现DLL:
在dll内实现钩子函数这是毫无疑问的。而安装钩子和卸载钩子的函数既可以写在主程序内,也可以写在DLL内。写在主程序内时只可以在主程序内安装钩子。而在dll内实现则可以让所有载入该dll的程序安装钩子。如当某进程将该DLL载入的时候,可以在DllMain中创建一个线程,让他调用安装钩子的函数,实现为此进程内的线程安装钩子的目的。为了拓展程序的功能,实现代码重用,最好是将钩子函数写在DLL内。另外这也可以实现模块化。一旦需求发生更改可以只修改DLL内的代码,而不需要改变主程序。
当钩子函数被调用的时候,也就是我们被拦截的消息已被触发,如何让主程序得到这个通知呢 ?
我们可以在其他进程内的钩子函数内给主程序的窗口发送消息。但如何发送呢?
PostMessage可以实现这个功能。
看原型:
- BOOL WINAPI PostMessage(HWND hWnd,UINT Msg,WPARAM wparam,LPARAM lParam);
hWnd即为要接受消息的窗口句柄。
Msg为要发送的消息。
wParam和lParam为消息的附加参数。
虽然可以使用PostMessage实现向主程序的窗口发送消息,但是我们如何获得主程序的窗口句柄呢?我们知道钩子函数是在DLL内实现的,而DLL会被加载到各个进程内。在其他进程要想得到主程序的窗口句柄这是一个问题。
在《windows核心编程系列》谈谈内存映射文件中,我们谈到了在可执行文件内使用共享段,可以实现同一个可执行文件的多个实例共享共享段内的数据的目的。那么在DLL使用共享段呢?哈哈,或许你已经猜出来了,由于DLL被映射到了各个进程,将数据放在DLL的共享段,可以实现在各个进程内共享DLL内共享段数据的目的。
我们的解决方法就是:在DLL内建立共享段,将主程序的窗口句柄放在共享段中。在主程序调用安装钩子的函数时可以将共享段内的窗口句柄赋为主程序的窗口句柄。从而达到在各个进程内共享数据的目的。到此,我们又学习一种在进程间共享数据的方法,另一种方法是利用内存映射文件。
建立和设置共享段的代码:可以参考《windows核心编程》谈谈内存映射文件。
- <span style="font-size:18px;"> #pragma data_seg("shared")
- HWND hWnd=NULL;
- HHOOK hHook=NULL;
- #pragma data_seg()
- #pragma comment(linker,"/SECTION:shared,RWS")
- </span>
怎么多了个hHook,hHook是创建的钩子的句柄。由于在钩子函数中会调用CallNextHookEx将消息传给钩子链的下一结点。二者都是在其他进程调用的,因此我们也必须把钩子的句柄设为共享。
DLL内创建钩子的代码:
- <span style="font-size:18px;"> KEYHOOKDLL_API bool SetHook(</span>
- <span style="font-size:18px;"> bool IsInstall,//true表示安装钩子,false表示卸载钩子。</span>
- <span style="font-size:18px;"> HWND hWnd, //主程序窗口句柄,用于在主程序内传入设置。</span>
- <span style="font-size:18px;"> int ThreadId)//要安装钩子的线程。
- {
- ::hWnd=hWnd;//将当前窗口句柄赋给DLL共线段内的窗口句柄。
- if(IsInstall)
- {
- hHook=SetWindowsHookEx( WH_KEYBOARD,KeyHookProc,GetModuleHandle </span>
- <span style="font-size:18px;"> ("keyhookdll"),ThreadId);
- return true;
- }
- else
- {
- UnhookWindowsHookEx(hHook);
- return true;
- }
- }</span>
创建的钩子类型为WH_KEYBOARD,他可以拦截WM_KEYDOWN 和WM_KEYUP 消息。具体请参考MSDN.
创建钩子函数功能很简单,仅仅安装钩子和设置共享段内的数据。Thread为要安装钩子的线程。主程序在调用时传入0,表示为所有线程安装钩子。
再看钩子函数:
- LRESULT CALLBACK KeyHookProc(int nCode ,WPARAM wParam,LPARAM lParam)
- {
- if(nCode<0||nCode==HC_NOREMOVE)
- {
- return CallNextHookEx(hHook,nCode,wParam,lParam);
- }
- if(lParam&0x40000000)//只对WM_DOWN进行响应。
- {
- PostMessage(hWnd,WM_KEYDOWN,wParam,lParam);
- }
- return CallNextHookEx(hHook,nCode,wParam,lParam);
在钩子函数中首先判断nCode的值,当他小于零时应该直调用CallNextHookEx,除此之外它也可以有以下取值:
ACTION:说明wParam和lParam包含按键消息的信息,可以处理。
HC_NOREMOVE:说明wParam和lParam包含按键消息的信息,但该消息没有被从消息队列中移除。即程序是调用PeekMessage来查询消息队列内的消息的。
( 与GetMessage的区别与联系:他们都从消息队列内查询消息,有消息时将此消息发送出去,GetMessage在消息队列没有消息时会一直等待,直到有消息到达时才返回。而PeekMessage无论消息队列中是否有消息都立即返回。)
因此当检测到nCode小于0或者为WH_NOREMOVE时不能对消息进行处理而要直接调用CallNextHookEx。lParam的第30位为1时说明此时键被按下,为零时说明键被弹起。此处进行了判断,仅在键被按下时向窗口发送消息。防止消息每次击键发送两次消息。
当某消息到达时我们给主程序窗口发送的消息为用户自定义消息:WM_KEY
他被定义为#define WM_KEY WM_USER+1
在主程序内我们必须自己实现相应此消息的消息处理函数。
原型为:
- afx_msg LRESULT OnKey(WPARAM wParam,LPARAM lParam);
实现:
- char keyname[100];
- ::GetKeyNameText(lParam,keyname,100);//获得按键的键名。
- CString a;
- a.Format("用户按键:%s\r\n",keyname);
- m_output+=a;
- UpdateData(false);
- ::MessageBeep(MB_OK);
- CEdit *edit=(CEdit*)GetDlgItem(IDC_EDIT_OUTPUT);
- edit->LineScroll(edit->GetLineCount());
- return 0;
到此为止各主要函数都介绍完毕,剩下都是如何创建dll。此处不再介绍。例子程序2011年12月2日下午实现。
总结:以上程序花了近三个小时实现,此程序看似容易但一旦自己动手实现各种问题接踵而至。所以以后要经常动手实现一些看似容易的程序,不要眼高手低。打这些字的时候键盘监控程序仍在工作,显示着我按下的每一个键。有明显的电脑感觉速度比平常慢了不少,看来使用钩子,尤其是系统范围内的钩子会导致很大的overhead。
windows核心编程中谈到注入dll的几种方式。其中介绍了使用windows钩子,但是介绍的很简单。以上内容参考自《windows核心编程》第五版,第四部分和《windows程序设计》第二版,王艳平著。如有错误,请指正。