首页 服务器 编程 必备知识 搜索引擎 圩日手册
站内搜索
最近浏览
推荐文章
热文排行

基于Visual C++的钩子编程技巧


摘要: 本文主要对钩子这种特殊的Windows编程机制进行了讨论并给出了鼠标钩子和键盘钩子的具体实现方法。

  关键词: 钩子;鼠标钩子;Visual C++

  钩子概述

  钩子(Hook)是Windows消息处理机制的一个要点(Point)。应用程序可以通过钩子机制截获处理Window消息或是其他一些特定事件。同DOS中断截获处理机制类似,应用程序可以在钩子上设置多个钩子函数,由其组成一个与钩子相关联的指向钩子函数的指针列表(钩子链表)。当钩子所监视的消息出现时,Windows首先将其送到调用链表中所指向的第一个钩子函数中,钩子函数将根据其各自的功能对消息进行监视、修改和控制,并在处理完成后把消息传递给下一钩子函数直至到达钩子链表的末尾。在钩子函数交出控制权后,被拦截的消息最终仍将交还给窗口处理函数。虽然钩子函数对消息的过滤将会略加影响系统的运行效率,但在很多场合下通过钩子对消息的过滤处理可以完成一些其他方法所不能完成的特殊功能。

  可以看出,钩子的本质是一段用以处理系统消息或特定事件的函数,通过系统调用将其挂入到系统。钩子的种类有很多,每一种钩子负责截获并处理相应的消息。钩子机制允许应用程序截获并处理发往指定窗口的消息或特定事件,其监视的窗口即可以是本进程内的也可以是由其他进程所创建的。在特定的消息发出后、达目的窗口前,钩子程序拥有对其控制权,此时的钩子函数除了可以对截获的消息进行各种处理外,甚至还可以强行终止消息的继续传递。

  对于多个钩子的安装,最近安装的钩子将被放置于钩子链的开始,最早安装的钩子则放在最后,在钩子监视的消息出现时,操作系统调用链表开始处的第一个钩子函数进行处理,也就是说最后加入的钩子优先获得控制权。这里提到的钩子函数必须是一个回调函数,而且不能定义为类成员函数,只能是普通的C函数,如:

  LRESULT CALLBACK HookProc(int nCode ,WPARAM wParam,LPARAM lParam);

  线程局部钩子与系统全局钩子

  钩子根据其对消息监视范围的不同而分为系统全局钩子和线程局部钩子两大类,其中线程局部钩子只能监视本进程中某个指定的线程,而全局钩子则可对在当前系统下运行的所有线程进行监视。显然,线程钩子可以看作是全局钩子的一个子集,全局钩子虽然功能强大但同时实现起来也比较烦琐:其钩子函数的实现必须封装在独立的动态链接库中才可以被各种相关联的应用程序所使用。

  虽然对于线程局部钩子并不要求其象系统全局钩子一样必须放置于动态链接库中,但是推荐的做法仍是将其放到动态链接库中去实现。这样的处理不仅能使钩子为系统内的多个进程所访问,同时也可以在系统中被直接调用。对于一个只供单进程访问的钩子,还可以将其钩子处理过程放在安装钩子的同一个线程内。
钩子的安装与卸载

  系统是通过调用位于钩子链表最开始处的钩子函数而进行消息拦截处理的,因此在设置钩子时要把回调函数放置于钩子链表的链首,操作系统会使其首先被调用。由函数SetWindowsHookEx()负责将回调函数放置于钩子链表的开始位置。SetWindowsHookEx()函数原型声明为:

HHOOK SetWindowsHookEx(int idHook;HOOKPROC lpfn;HINSTANCE hMod;DWORD dwThreadId);

  其中,参数idHook 指定了钩子的类型,可以使用的类型有以下13种:

WH_CALLWNDPROC 系统将消息发送到指定窗口之前的“钩子”
WH_CALLWNDPROCRET 消息已经在窗口中处理的“钩子”
WH_CBT 基于计算机培训的“钩子”
WH_DEBUG 差错“钩子”
WH_FOREGROUNDIDLE 前台空闲窗口“钩子”
WH_GETMESSAGE 接收消息投递的“钩子”
WH_JOURNALPLAYBACK 回放以前通过WH_JOURNALRECORD“钩子”记录的输入消息
WH_JOURNALRECORD 输入消息记录“钩子”
WH_KEYBOARD 键盘消息“钩子”
WH_MOUSE 鼠标消息“钩子”
WH_MSGFILTER 对话框、消息框、菜单或滚动条输入消息“钩子”
WH_SHELL 外壳“钩子”
WH_SYSMSGFILTER 系统消息“钩子”

  参数lpfn为指向钩子函数的指针,也即回调函数的首地址;参数hMod标识了钩子处理函数所处模块的句柄;参数dwThreadId 指定被监视的线程,如果明确指定了某个线程的ID就只监视该线程,此时的钩子即为线程钩子;如果该参数被设置为0,则表示此钩子为监视系统所有线程的全局钩子。此函数在执行完后将返回一个钩子句柄。

  在SetWindowsHookEx()函数完成对钩子的安装后,如果被监视的事件发生,系统会立即调用位于相应钩子链表开始处的钩子函数进行处理,每一个钩子函数在进行处理时都要考虑是否需要把事件传递给下一个钩子处理函数。如果需要传递,就要调用函数CallNestHookEx()。尽管在理论上不调用CallNestHookEx()也并不算错,但在实际使用时还是强烈建议无论是否需要进行事件传递都要在过程的最后调用一次CallNextHookEx( ),否则将会引起一些无法预知的系统行为或是系统锁定。该函数将返回位于钩子链表中的下一个钩子处理过程的地址,至于具体的返回值类型则要视所设置的钩子类型而定。CallNextHookEx( )的函数原型为:

LRESULT CallNextHookEx(HHOOK hhk;int nCode;WPARAM wParam;LPARAM lParam);

  其中,参数hhk为由SetWindowsHookEx()函数返回的当前钩子句柄;参数nCode为传给钩子过程的事件代码;参数wParam和lParam 则为传给钩子处理函数的参数值,其具体含义同设置的钩子类型有关。

  由于安装钩子对系统的性能有一定的影响,所以在钩子使用完毕后应及时将其卸载以释放其所占资源。释放钩子的函数为UnhookWindowsHookEx(),该函数比较简单只有一个参数用于指定此前由SetWindowsHookEx()函数所返回的钩子句柄,原型声明如下:

BOOL UnhookWindowsHookEx(HHOOK hhk);

  使用鼠标钩子

  由于系统全局钩子在功能上完全覆盖了线程局部钩子,因此其实际使用范围要远比线程局部钩子广泛的多。本节也由此着重对系统全局钩子的使用进行介绍。

  鼠标钩子是钩子中比较常用也是使用比较简单的一类钩子。下面给出的应用示例将通过安装鼠标全局钩子来捕获鼠标当前所处窗口的窗口标题。由于本例程使用了全局钩子,因此首先构造全局钩子的载体——动态链接库。考虑到 Win32 DLL与Win16 DLL存在的差别,在Win32环境下要在多个进程间共享数据,就必须采取一些措施将待共享的数据提取到一个独立的数据段,并通过def文件将其属性设置为读写共享:

#pragma data_seg("mydata")
HWND glhPrevTarWnd = NULL; // 上次鼠标所指的窗口句柄
HWND glhDisplayWnd = NULL; // 显示目标窗口标题编辑框的句柄
HWND glhHook = NULL; // 安装的鼠标钩子句柄
HINSTANCE glhInstance = NULL; // DLL实例句柄
#pragma data_seg()
……
SECTIONS // def文件中将数据段TestData设置为读写共享
TestData READ WRITE SHARED

  在完成上述准备工作后,在动态库输出函数StartHook()中调用SetWindowsHookEx()函数完成对全局鼠标钩子的安装,设定鼠标钩子函数为MouseProc(),安装函数返回的钩子句柄保存于变量glhHook中:

BOOL CMouseHook::StartHook(HWND hWnd)
{
 BOOL result = FALSE;
 // 安装钩子
 glhHook = (HWND)SetWindowsHookEx(WH_MOUSE, MouseProc, glhInstance, 0);
 if (glhHook != NULL)
  result = TRUE;
 glhDisplayWnd = hWnd; // 设置显示目标窗口标题编辑框的句柄
 return result;
}

  在鼠标钩子安装完毕后,系统内的任何鼠标动作所发出的鼠标消息均要经过钩子函数MouseProc()的拦截过滤处理。在此进行的处理是通过获取当前鼠标所在位置下的窗口句柄,并以此进一步得到窗口标题。在处理完成后,调用CallNextHookEx()函数将本事件传递到钩子链表中的下一个钩子函数:

LRESULT WINAPI MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
 LPMOUSEHOOKSTRUCT pMouseHook = (MOUSEHOOKSTRUCT FAR *) lParam;
 if (nCode >= 0) {
  HWND glhTargetWnd = pMouseHook->hwnd; // 取目标窗口句柄
  HWND ParentWnd = glhTargetWnd;
  while (ParentWnd != NULL){
   glhTargetWnd = ParentWnd;
   ParentWnd = GetParent(glhTargetWnd); // 取应用程序主窗口句柄
  }
  if (glhTargetWnd != glhPrevTarWnd) {
   char szCaption[100];
   GetWindowText(glhTargetWnd, szCaption, 100); // 取目标窗口标题
   if (IsWindow(glhDisplayWnd))
    SendMessage(glhDisplayWnd, WM_SETTEXT, 0, (LPARAM)(LPCTSTR)szCaption);
   glhPrevTarWnd = glhTargetWnd; // 保存目标窗口
  }
 }
 // 继续传递消息
 return CallNextHookEx((HHOOK)glhHook, nCode, wParam, lParam);
}

  此动态链接库还提供有输出函数StopHook(),调用程序通过对此函数的调用完成对先前加载钩子的卸载。在此输出函数内则是通过UnhookWindowsHookEx()函数来卸载指定钩子的:

BOOL CMouseHook::StopHook()
{
 BOOL result = FALSE;
 if (glhHook){
  result = UnhookWindowsHookEx((HHOOK)glhHook); // 卸载钩子
  if (result)
   glhDisplayWnd = glhPrevTarWnd = glhHook = NULL;
 }
 return result;
}

  通过编译、链接可以得到有关鼠标全局钩子的动态链接库,在经过调用程序对其的调用后,可以实现对在当前系统所有线程中的鼠标消息的拦截处理。在钩子动态链接库加载到进程后,只需调用输出函数StartHook()安装好全局钩子即可对鼠标消息进行拦截过滤,在调用程序退出前调用输出函数StopHook()卸载钩子。
使用键盘钩子

  键盘钩子同鼠标钩子一样,也是一类较常用的钩子。而且总的来说键盘钩子的使用与鼠标钩子的使用也是比较类似的,下面通过一个程序实例来对键盘钩子的使用进行介绍,在此例程中通过设置键盘全局钩子来记录当前系统输入的字符,并将拦截到的字符按日期保存到文件。

  由于本例程也使用的系统全局钩子,因此由调用程序和动态链接库程序两部分组成。动态链接库程序提供了两个输出函数InstallLaunchEv()和UnInstall(),分别用于键盘钩子的安装与卸载:

DllExport void WINAPI InstallLaunchEv()
{
 glhHook = (HHOOK)SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)LauncherHook, theApp.m_hInstance, 0); // 安装键盘钩子
}
DllExport void WINAPI UnInstall()
{
 if (glhHook) {
  BOOL result = UnhookWindowsHookEx((HHOOK)glhHook); // 卸载钩子
  if (result)
   glhHook = NULL;
 }
}

  在钩子安装函数中,通过SetWindowsHookEx()设置了键盘钩子函数LauncherHook()。在键盘事件发生后此钩子函数将被调用,通过SaveLog()函数将捕获到的字符保存到文件:

LRESULT CALLBACK LauncherHook(int nCode, WPARAM wParam, LPARAM lParam)
{
 // 将键盘事件传递给下一钩子函数
 LRESULT Result = CallNextHookEx(glhHook, nCode, wParam, lParam);
 if (nCode == HC_ACTION) // 处理键盘动作
 {
  if (lParam & 0x80000000) // 保存键盘动作到文件
  {
   char c[1];
   c[0] = wParam;
   SaveLog(c);
   if (c[0] == 13) {
    c[0] = 10;
    SaveLog(c);
   }
  }
 }
 return Result;
}

  至于调用程序则非常简单,只是在程序开始运行后和在程序退出前分别调用动态链接库提供的钩子设置函数和钩子卸载函数即可。

  钩子的种类有很多,从前面给出的鼠标钩子和键盘钩子也可以看出其编写过程是非常类似的。通常情况下作为钩子载体的动态链接库要提供有两个必要的输出函数——钩子安装函数和钩子卸载函数。在钩子安装函数中通过SetWindowsHookEx()为不同类型的钩子设置相应的钩子函数,实质性的工作也主要是在钩子函数中完成的,具体情况应视钩子的类型和要完成的功能而定。在钩子卸载函数中则主要是对UnhookWindowsHookEx()的调用,并辅以必要的保护性代码。
[wangjy17908]
添加时间:2010-06-29
版权所有(C)2005-2015