《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核心编程》谈谈内存映射文件。
#pragma data_seg("shared")
HWND hWnd=NULL;
HHOOK hHook=NULL;
#pragma data_seg()
#pragma comment(linker,"/SECTION:shared,RWS")
怎么多了个hHook,hHook是创建的钩子的句柄。因为在钩子函数中会调用CallNextHookEx将消息传给钩子链的下一结点。二者都是在其它进程调用的,因此我们也必须把钩子的句柄设为共享。
DLL内创建钩子的代码:
KEYHOOKDLL_API bool SetHook(
bool IsInstall,//true表示安装钩子,false表示卸载钩子。
HWND hWnd, //主程序窗体句柄,用于在主程序内传入设置。
int ThreadId)//要安装钩子的线程。
{
::hWnd=hWnd;//将当前窗体句柄赋给DLL共线段内的窗体句柄。
if(IsInstall)
{
hHook=SetWindowsHookEx( WH_KEYBOARD,KeyHookProc,GetModuleHandle
("keyhookdll"),ThreadId);
return true;
}
else
{
UnhookWindowsHookEx(hHook);
return true;
}
}
创建的钩子类型为WH_KEYBOARD,他能够拦截WM_KEYDOWN 和WM_KEYUP 消息。详细请參考MSDN.
创建钩子函数功能非常easy,只安装钩子和设置共享段内的数据。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日下午实现。
总结:以上程序花了近三个小时实现,此程序看似easy但一旦自己动手实现各种问题接踵而至。所以以后要常常动手实现一些看似easy的程序,不要眼高手低。打这些字的时候键盘监控程序仍在工作,显示着我按下的每个键。有明显的电脑感觉速度比寻常慢了不少,看来使用钩子,尤其是系统范围内的钩子会导致非常大的overhead。
windows核心编程中谈到注入dll的几种方式。当中介绍了使用windows钩子,可是介绍的非常easy。以上内容參考自《windows核心编程》第五版,第四部分和《windows程序设计》第二版,王艳平著。如有错误,请指正。