WIN32钩子

摘译自Win32 Hooks
作者:Kyle Marsh
        本文描述钩子及其子MS Win32应用编程接口中的使用,讨论钩子函数、
过滤函数、以及以下类型的钩子:

  • WH_CALLWNDPROC
  • WH_CBT
  • WH_DEBUG
  • WH_FOREGROUNDIDLE
  • WH_GETMESSAGE
  • WH_JOURNALPLAYBACK
  • WH_JOURNALRECORD
  • WH_KEYBOARD
  • WH_MOUSE
  • WH_MSGFILTER
  • WH_SHELL
  • WH_SYSMSGFILTER

简介
        在Windows操作系统中,钩子是在事件(包括消息、鼠标动作、击键)到达应用程序之前在函数中将其截获的一种机制。截获的事件后函数就可以修改、甚至抛弃事件。这里,将接收事件的函数称为过滤函数(filter functions),过滤函数可以根据截获的事件类型进行分类,例如,接收所有键盘和鼠标事件的过滤函数。窗口在调用一个过滤函数之前,必须先将过滤函数安装为一个窗口钩子。将一个或多个过滤函数安装到一个钩子的过程,称为配置钩子。如果一个钩子有不止一个过滤函数,则Windows维护一个过滤函数链,最近安装的过滤函数在链的开端。
        当在钩子上安装了一个或多个过滤函数且触发钩子的事件发生了,Windows会调用过滤函数链中的第一个过滤函数,这个过程称为调用钩子。
        过滤函数的维护和访问,需要使用SetWindowsHookEx和UnhookWindowsHookEx函数。钩子为基于窗口的应用程序提供了强大的能力,包括:
(1)处理或修改应用程序中用于对话框、消息框、滚动条、菜单的所有消息(WH_MSGFILTER)
(2)处理或修改系统中用于对话框、消息框、滚动条、菜单的所有消息(WH_SYSMSGFILTER)
(3)在调用GetMessage或PeekMessage函数时处理或修改系统的所有消息(WH_GETMESSAGE)
(4)在调用SendMessage函数时处理或修改所有消息(WH_CALLWNDPROC)
(5)记录或重复键盘和鼠标事件(WH_JOURNALRECORD, WH_JOURNALPLAYBACK)
(6)处理、修改或移除键盘事件(WH_KEYBOARD)
(7)处理、修改或抛弃鼠标事件(WH_MOUSE)
(8)对特定的系统行为作出响应,这使得开发基于计算机的训练应用程序成为可能(WH_CBT).
(9)阻止将要调用的另一个过滤函数(WH_DEBUG)
        应用程序使用钩子可以:
(1)可以为菜单、对话框、消息框提供F1帮助支持(WH_MSGFILTER)
(2)提供鼠标和键盘记录和回放特性,经常成为宏录制功能。例如,Windows录音机附件程序使用钩子提供录音和回放功能(WH_JOURNALRECORD, WH_JOURNALPLAYBACK)
(3)监视消息以决定哪些消息将被发送到特定窗口或决定该消息将会产生哪些行为(WH_GETMESSAGE, WH_CALLWNDPROC)。Win32 SDK中的Spy工具就是使用钩子来执行这些任务的。
(4)模拟鼠标或键盘输入(WH_JOURNALPLAYBACK)。钩子提供唯一可靠的方式来模拟这些活动,如果你试图通过发送或寄送消息来模拟这些事件,Windows内核不会更新键盘或鼠标的状态,这可能导致不可预期的行为。如果钩子被用来回放键盘或鼠标事件,则这些事件就像真实的鼠标或键盘事件那样确确实实地被处理。微软Excel使用钩子来实现其SEND.KEYS宏函数。
(5)为Windows环境下的应用提供CBT支持(WH_CBT)。

如何使用钩子

要了解如何使用钩子,需要知道:
(1)如何使用Windows钩子函数将过滤函数加入一个钩子的过滤函数链中,以及如何从链中移除过滤函数;
(2)安装的过滤函数需要执行什么样的行为;
(3)钩子有哪些类型,能干什么,以及会给过滤函数传递什么样的信息/参数。
Windows钩子函数
        基于窗口的应用程序使用SetWindowsHookEx、UnhookWindowsHookEx、以及CallNextHookEx函数来管理钩子的过滤函数链。
        SetWindowsHookEx函数:向钩子增加一个过滤函数,四个参数:
HHOOK SetWindowsHookEx(
int idHook,        // type of hook to install
HOOKPROC lpfn,     // address of hook procedure
HINSTANCE hMod,    // handle to application instance
DWORD dwThreadId   // identity of thread to install hook for
);
(1)一个整数码:描述过滤函数将要安装到的钩子类型。
(2)过滤函数地址:过滤函数必须在应用程序或DLL的模块定义文件中被包含在EXPORTS语句中。
(3)包含过滤函数的模块的实例句柄。在Win32中,如果要安装线程特定的钩子时该值应为NULL;当安装系统范围或在另一个进程中安装线程特定的钩子时,必须使用过滤函数所在的DLL的实例句柄。
(4)过滤函数被安装的线程ID。如果线程ID非零,则过滤函数只有在线程特定的环境下会被调用;如果线程ID为零,则过滤函数具有系统范围的作用域,可以在系统的任意线程环境中调用。应用程序或库可以使用GetCurrentThreadId函数获得线程句柄来勾住当前线程。
        一些钩子只能设置为系统范围,而另一些钩子则只能设置为线程特定,其他的钩子则既可以设置为系统范围,也可以设置为线程范围。具体见下表:

Hook
Scope

WH_CALLWNDPROC
Thread or System

WH_CBT
Thread or System

WH_DEBUG
Thread or System

WH_GETMESSAGE
Thread or System

WH_JOURNALRECORD
System Only

WH_JOURNALPLAYBACK
System Only

WH_FOREGROUNDIDLE
Thread or System

WH_SHELL
Thread or System

WH_KEYBOARD
Thread or System

WH_MOUSE
Thread or System

WH_MSGFILTER
Thread or System

WH_SYSMSGFILTER
System Only

         对于特定类型的钩子,线程范围的钩子首先被调用,然后是系统范围的钩子。通常如果使用线程范围的钩子能够满足要求,则尽量使用线程钩子,原因如下:
(1)应用程序不会导致系统范围的开销;
(2)不会引起所有的事件被序列化;例如,如何应用程序安装了一个系统范围的键盘钩子,则发生到所有应用程序的所有的键盘消息将会流经该应用程序的键盘过滤函数,导致系统的多个输入队列毫无用处。如果过滤函数停止处理键盘事件,则对用户而言系统似乎停滞了,然而实际上没有停。这时候,用户似乎需要使用CTRL+ALT+DEL组合键来解决问题喽。
(3)不需要将过滤函数打包为一个独立的DLL。因为所有的系统级钩子必须置于DLL中。
(4)不需要做DLL中共享数据。系统级过滤函数必须打包为一个DLL,还必须共享其它进程所需的数据。由于共享数据不是DLL的缺省行为,所以必须仔细的设计系统级过滤函数。如果一个过滤函数不能正确地实现数据共享,会导致进程的崩溃。
         SetWindowsHookEx函数返回一个到所安装钩子的句柄(HHOOK),应用程序或库在调用UnhookWindowsHookEx函数时必须使用该句柄来标识钩子。如果过滤函数不能安装到钩子,则SetWindowsHookEx函数返回NULL。此外,SetWindowsHookEx函数会将最后的错误设置为如下值之一,以说明函数调用失败的原因:
ERROR_INVALID_HOOK_FILTER:钩子码无效
ERROR_INVALID_FILTER_PROC:过滤函数无效
ERROR_HOOK_NEEDS_HMOD:一个全局钩子却使用NULL实例参数,或者线程级钩子设置为可用于其它应用程序的线程
ERROR_GLOBAL_ONLY_HOOK:一个系统钩子被安装到特定线程
ERROR_INVALID_PARAMETER:线程ID无效
ERROR_JOURNAL_HOOK_SET:已经存在一个journal类型的钩子。此外,如果在屏保运行时应用程序尝试设置journal钩子,也会设置该代码。
ERROR_MOD_NOT_FOUND:用于全局钩子的hInstance参数不是一个库,该代码说明用户在其模块列表中不能定位模块句柄。
其它值:安全不允许设置钩子,或系统内存溢出
        Windows在内部保存过滤函数链,且不依赖于过滤函数来存储下一个过滤函数的地址。此外,由于内部存储过滤函数链,使得性能得以很大提升。

WIN32钩子


         UnhookWindowsHookEx函数:从钩子链中移除过滤函数,返回值表示是否钩子被移除。
过滤函数(Filter Functions)
        过滤函数是安装到钩子上的函数。由于过滤函数是被Windows而非应用程序调用的,所以通常认为过滤函数是回调函数(callback functions)。所有的过滤函数具有如下形式:
LRESULT CALLBACK FilterFunc( nCode, wParam, lParam )int nCode;
WORD wParam;
DWORD lParam;
        所有的过滤函数均应返回LONG型值,FilterFunc是实际过滤函数名的占位符。
过滤函数的参数
        过滤函数接受三个参数:ncode(钩子代码)、wParam、lParam。钩子代码是一个整数,用来通知过滤函数一些附加的信息,例如钩子代码能够说明什么样的行为会引起调用钩子。
        在Windows 3.1版本之前,钩子代码表示过滤函数是否应该处理事件或调用DEFHookProc。如果钩子代码是负数,过滤函数不应处理事件,而应调用DefHookProc,并直接将三个参数传递过去。Windows在应用程序的辅助下使用这些负数代码来维护过滤函数链。
        在Windows 3.1中,如果Windows发送一个负数钩子代码给过滤函数,则过滤函数必须以传递给过滤函数的参数为实参对CallNextHookEx函数进行调用,并返回CallNextHookEx的返回值。
        传递给过滤函数的第二个参数wParam是一个WPARAM,第三个参数lParam是一个LPARAM,这两个参数传递过滤函数需要的信息。每种钩子对wParam和lParam附加了不同的意义。例如,安装到WH_KEYBOARD钩子的过滤函数在wParam中接收一个虚拟键代码,而lParam包含一组位域(bit fields)来描述键盘事件发生时的键盘状态;安装到WH_MSGFILTER钩子的过滤函数在wParam中接收一个NULL值,在lParam中接收一个指向消息结构的指针;具体wParam和lParam的意义依赖于引起钩子调用的事件。完整的参数列表及每种类型钩子下的意义,需要参考Win32 SDK中如下的内容:

Hook
Filter function documentation

WH_CALLWNDPROC
CallWndProc

WH_CBT
CBTProc

WH_DEBUG
DebugProc

WH_GETMESSAGE
GetMsgProc

WH_JOURNALRECORD
JournalRecordProc

WH_JOURNALPLAYBACK
JournalPlaybackProc

WH_SHELL
ShellProc

WH_KEYBOARD
KeyboardProc

WH_MOUSE
MouseProc

WH_MSGFILTER
MessageProc

WH_SYSMSGFILTER
SysMsgProc

调用钩子链中的下一个函数

        钩子设置完成后,Windows调用钩子链中的第一个过滤函数,然后Windows的工作结束,调用钩子链中下一个过滤函数的责任必须由过滤函数来保证。为此,Windows提供了CallNextHookEx来完成该项工作。函数CallNextHookEx有四个参数:
(1)第一个参数是SetWindowsHookEx调用的返回值,即当前钩子的句柄;
(2)其它参数分别是nCode、wParam、lParam

        Windows在内部存储过滤函数,并跟踪哪个过滤函数被调用。因此在调用CallNextHookEx函数时,Windows确定钩子链中的下一个过滤函数,并对其进行调用。

        有时候,过滤函数并不希望将事件传递给钩子链中的其它钩子函数,尤其在钩子允许过滤函数抛弃事件且过滤函数决定这样做时,一定不要调用CallNextHookEx。

        由于过滤函数不会以任何特定的顺序被安装,因此在任何时刻都不能确定一个过滤函数在钩子链的什么地方,当然有例外,在过滤函数安装的时候,它是钩子链的第一个函数。同样,不要绝对地肯定你会获得所有出现的事件,因为在你之后安装的过滤函数可能不会将事件传递给你的过滤函数。

DLL中的过滤函数

        系统级的过滤函数必须存在于一个DLL中。在Win16中,允许但不推荐在一个应用程序中将过滤函数安装到一个系统级钩子上,这在Win32中已不可行。WH_JOURNALRECORD和WH_JOURNALPLAYBACK对该规则例外。

        系统级钩子的过滤函数运行在多个不同的进程中,因此必须准备好共享任何需要的数据。一个DLL被映射到其每个客户进程的地址空间,在DLL中的全局变量将是实例特定的,除非全局变量被放在共享数据段。例如,在钩子的例子程序中HOOKSDLL.DLL库需要共享两个数据项:

  • 显示消息的窗口句柄
  • 窗口中文本线段高度

        要共享这些数据,HOOKSDLL将其放在一个共享数据段中。以下是HOOKSDLL共享数据的步骤:

  • 使用pragmas指令将数据放在一个命名数据段,注意数据必须初始化:
    // Shared DATA #pragma data_seg(".SHARDATA") static HWND   hwndMain = NULL;  // Main hwnd. We will get this from the app. static int    nLineHeight = 0;  // Height of lines in window. #pragma data_seg() 
  • 在DLL的定义文件中增加SECTIONS语句:
    SECTIONS    .SHARDATA   Read Write Shared 
  • 从.DEF文件创建一个.EXP文件:
    hooksdll.exp: hooksdll.obj hooksdll.def    $(implib) -machine:$(CPU)     \    -def:hooks.def      \    hooksdll.obj  \    -out:hooksdll.lib 
  • 链接HOOKSDLL.EXP文件:
    hooksdll.dll: hooksdll.obj hooksdll.def hooksdll.lib hooksdll.exp    $(link) $(linkdebug)     \    -base:0x1C000000  \    -dll       \    -entry:LibMain$(DLLENTRY)     \    -out:hooksdll.dll    \    hooksdll.exp hooksdll.obj hooksdll.rbj \    $(guilibsdll) 

你可能感兴趣的:(Win32)