Windows钩子函数的概念和实现方法
首先我们必须大致了解Windows的基本运作机理,Windows作为一个多任务操作系统,它是分有层次概念的,运行在最底下的称为Ring 0层,在这一层里基本上都是一些硬件驱动程序和Windows的总内核,一般的应用程序极少极少运行在这层,当然也有例外,例如调试软件SoftICE(不过基本上这个软件的作用是Crack软件而不是调试)、还原精灵还有分区魔法大师,就是运行在Ring 0层的,另外就是著名的CIH病毒。运行在Ring 0级的程序能够对所有硬件进行直接地址级访问,所受到的限制也最小。 消息(Message)传递是Windows独有的一种机制,因为Windows规定运行在Ring 0以上的程序是没有权利知道究竟硬件发生了怎样的中断变化的,Windows统一将这些中断变化封装成一系列的消息(黑箱作业,也就是常说的Black Box),比如鼠标移动,系统产生一个OnMouseMove消息(但这条消息从何而来,相关的硬件中断向量是什么,程序无从得知),OnMouseMove这条消息最后送达每一个窗口程序以供处理。在更高层次的地方,比如说控件级,所有的消息还被封装成一系列“事件”,比如TextBox控件有KeyPress事件,实际上,这些事件都是林林种种的消息映射。事件的概念使得程序员能够更加傻瓜化地进行编程,但是从另一个角度来说,这种黑箱作业也使得程序员过分依赖系统的安排,限制了程序员的思维,举个例子,Windows为按钮控件封装了大部分常用的属性和事件,完成一般的常规妈作是没有问题的,但是很遗憾,或许是Windows的疏忽,按钮控件的字体颜色永远默认是黑色,而且Windows没有为此提供一个专门的接口来修改,碰到这种情况,程序员就会非常头疼。 钩子函数(Hook Function),就像一把钩子,它的作用是将消息在抵达窗口程序之前先钩到一个地方以便程序员进行分析,这个地方称为挂接函数链,消息在这里先被一系列的函数处理然后由程序员决定是否交还给Windows系统,在这里,你可以“吞噬”(Lickup)一些你不希望发生的消息,比如说你吞掉所有的键盘消息而不交还给系统,那么键盘将会失灵。当然,经过了这道周折,系统效率将会有极其微小的降低,但是,由于Windows规定所有不运行在Ring 0层的程序都不能直接访问硬件中断,因此作为一种中断驱动程序的补充,钩子函数在很多场合下是非常有用的,有时候甚至是唯一的方法。 下面就以键盘拦截为例讲述钩子函数的使用方法: 首先定义以下API: Public Declare Function SetWindowsHookEx Lib "user32" _ Alias "SetWindowsHookExA" (ByVal idHook As Long, _ ByVal lpfn As Long, _ ByVal hmod As Long, _ ByVal dwThreadId As Long) As Long SetWindowsHookEx,这个函数是一切钩子函数的根本,其作用是通知Windows进行钩子妈作并定义钩子函数。 参数1,idHook为定义需要进行的拦截类型,是键盘拦截?鼠标拦截?还是别的。如 WH_KEYBOARD捕捉键盘 消息,而WH_MOUSE捕捉鼠标消息。 参数2,lpfn为该挂接函数链的首地址指针,因为VB是没有指针这种数据类型的所以用Long 代替。lpfn为 钩 子 函 数 , 在 VB中 可 以 使 用 AddressOf获 得 钩 子 函 数 的 地 址 。 这 个 函 数 因 为 钩 子 类 型不 同 而 有 所 不 同 。 如 键 盘 钩 子 为 : Public Function KeyboardProc(ByVal nCode As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long 如 果 Code不 为 0, 钩 子 函 数 必 须 调 用 CallNextHookEx, 将 消 息 传 递 给 下 面 的 钩 子 。 wParam和 lParam不 是 按 键 。 参数3,hmod为创建钩子函数那个实体的句柄,即你的程序本身的句柄(handle),hmod用于全局钩子,VB要实 现钩子,必须设为0。 (关于句柄:每一个程序都有一个ID号称为进程,句柄则分得更细,每一个进程里的每一个控件的ID号称为句柄。
比如一个程序既有输入框又有下拉条,那么下拉条和输入框都有自己的ID号,这个称为句柄。)
参数4,dwThreadId ,为监控代码,0表示全局监控,dwThreadId用 于 线 程 钩 子 VB中 可 以 设 置 为 App.ThreadID。 UnhookWindowsHookEx 函数为释放钩子,将钩子函数所占用的资源交还给系统,起一个简单的清道夫功能。 Private Declare Sub CopyMemory Lib "kernel32" _ Alias "RtlMoveMemory" _ (pDest As Any, _ pSource As Any, _ ByVal cb As Long) CopyMemory 作用是将内存里的某一块数据pSource拷贝到另一个地址pDest。最后一个参数cb表示拷贝内容的字节大小。 Private Declare Function GetAsyncKeyState Lib "user32" _ (ByVal vKey As Long) As Integer GetAsyncKeyState作用是获得各种辅助功能键的状态(如CTRL,SHIFT什么的)。 Private Declare Function CallNextHookEx Lib "user32" _ (ByVal hHook As Long, _ ByVal nCode As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long CallNextHookEx 挂钩函数拦截了某条消息后,由CallNextHookEx决定是否将这些消息送还给Windows系统。 Private Type KBDLLHOOKSTRUCT vkCode As Long scanCode As Long flags As Long time As Long dwExtraInfo As Long End Type KBDLLHOOKSTRUCT为键盘钩子的结构体定义,关于该结构的成员,我没有从API函数帮助库里找到任何资源,不过大概猜也能猜出来。 成员1:vkCode为虚拟键码 成员2:scanCode为扫描码 成员3:flags为功能键状态 成员4:扩展信息? 实际上本例中我们只是需要简单的知道vkCode然后用chr函数置换成字符即可,所谓的vkCode实际上和ASCIIC码是一一对应的。 Private Const HC_ACTION = 0 Private Const LLKHF_EXTENDED = &H1 Private Const LLKHF_INJECTED = &H10 Private Const LLKHF_ALTDOWN = &H20 Private Const LLKHF_UP = &H80 Private Const WH_KEYBOARD_LL = 13& Public Const VK_TAB = &H9 Public Const VK_CONTROL = &H11 Public Const VK_ESCAPE = &H1B Public Const VK_DELETE = &H2E 以上10个定义为常量定义,常量定义没有什么特别好说的,仅仅是帮助记忆而已。你完全可以在使用VK_DELETE的地方使用&H2E,如果你觉得&H2E比VK_DELETE更容易理解的话... Public KeyboardHandle As Long 全局变量KeyboardHandle 为键盘钩子函数句柄,这个变量在开始挂钩的时候产生,结束挂钩的时候需要用它进行清场。 好了,现在我们开始工作: 首先创建一个工程,因为我们需要一个实体来运行我们的钩子函数,所以必须创建一个工程,在窗体部分填写: Private Const WH_KEYBOARD_LL = 13& Windows规定,键盘拦截的ID号为13号拦截。 Public Sub HookKeyboard() KeyboardHandle = SetWindowsHookEx( _ WH_KEYBOARD_LL, AddressOf KeyboardCallback, _ App.hInstance, 0&) End Sub 定义一个不带参数的子程序HookKeyboard, KeyboardHandle 存储 钩子函数所产生的ID号,这个在清场的时候需要用到。 SetWindowsHookEx 的4个参数, 第一个,WH_KEYBOARD_LL, 正如前面定义的常量,它为13 第二个,AddressOf KeyboardCallback,这是自VB5以来的一次革命,VB5之后增加了一个保留字AddressOf,它的作用是获取某一个函数的首地址指针,在VB5之前的版本是没有这个功能的,有了AddressOf,大大扩展了VB的功能,才使得VB能够调用绝大部分API函数。因为VB本身并不存在指针。此行的意思是获得挂接函数链的首地址。 第三个,App.hInstance,App也是VB的保留字,表示本程序本身,如App.Path表示本程序当前目录,App.hInstance表示的是本程序本身的句柄,关于句柄前面已有所描述。 第四个,0,表示全局拦截,意思就是拦截所有窗口下的键盘输入。
定义完钩子函数,下面进入函数的具体实现,这些函数属于全局函数,所以不能在窗体级定义,必须降低到“模块”级别,现创建一个模块,然后开始编写,注:在VB里调用API几乎都是模块级的,很少出现在窗体级。实际不必深究这个问题,只要记住凡是调用API就用模块来编写就没错了。
Public Function KeyboardCallback(ByVal Code As Long, _ ByVal wParam As Long, ByVal lParam As Long) As Long Static Hookstruct As KBDLLHOOKSTRUCT If (Code = HC_ACTION) Then Call CopyMemory(Hookstruct, ByVal lParam, Len(Hookstruct)) If (IsHooked(Hookstruct)) Then KeyboardCallback = 1 Exit Function End If End If KeyboardCallback = CallNextHookEx(KeyboardHandle, _ Code, wParam, lParam) End Function 函数KeyboardCallback,是整个钩子函数的核心, 参数1:Code,表示拦截层次,之前我们已经说过,如果Code为0,则拦截所有窗口的键盘输入。 参数2:wParam表示是何种Windows消息 参数3:lParam表示某条Windows消息的具体内容的指针,它实际指向存储那个内容的内存地址。 下面详细叙述关于wParam和lParam: wParam和lParam是Windows消息机制的两个最重要参数,整个Windows依靠这两个参数传递各种各样的消息,首先是wParam,它表示此次的消息类型是什么?是键盘?是鼠标?键盘里又分按下还是抬起,鼠标里又分是单击还是双击,等等。lParam是一个指针,它指向本条消息所存储的信息的内存区域的首地址,很显然,这个地址存放的东西是很灵活的,比如鼠标消息,那么这里可能存放的是各键的状态或者光标的X,Y座标。换成键盘消息,则是键码等等。总之,wParam区分了类别,lParam存放了该类别所存储的信息。因为VB没有指针,好在这里并不需要更多的指针妈作,只是记录一个首地址,所以可以用Long来代替。 注:在本程序里,wParam始终为256和257,257表示抬起键盘,256表示按下键盘,lParam每次运行都不一定一样,因为每次系统重启Windows都会重新定义消息指针,但是就一次进入而言,这个值是不会改变的,退出之后再进就有可能发生变化了。 Static Hookstruct As KBDLLHOOKSTRUCT 定义一个局部静态结构体实例,结构体为KBDLLHOOKSTRUCT。 If (Code = HC_ACTION) Then Call CopyMemory(Hookstruct, ByVal lParam, Len(Hookstruct)) If (IsHooked(Hookstruct)) Then KeyboardCallback = 1 Exit Function End If End If If Code=HC_ACTION,这条鉴别Windows的消息来源,实际上我们之前已经将HC_ACTION定义为0了,所以所有键盘消息都将从此通过,那么,这个鉴别的意义何在呢?因为有可能还有别的键盘拦截程序等待着这些消息,我们不应该将这些消息据为己有,而是该交还给系统。 Call CopyMemory(Hookstruct, ByVal lParam, Len(Hookstruct)) 这里用到了CopyMemory,作用是将lParam地址的内容复制到本地变量Hookstruct里来,这里又是VB的一个弊端,因为没有指针,所以必须动用到CopyMemory来完成这个使命。 If (IsHooked(Hookstruct)) Then KeyboardCallback = 1 Exit Function End If IsHooked是我们自己定义的一个函数,该函数过滤了我们不希望出现的键盘码,比如说我们现在就是要不允许用户输入A,那么就可以在IsHooked里将A吞噬掉,那么键盘就永远打不出A来了。该函数返回0或者1,1表示此键确为我们不想要的字符,那么吃掉这条消息,不将它交还系统,Exit Function直接退出函数。如果不是,则略过。 可以看到,在If语句的最后是: KeyboardCallback = CallNextHookEx(KeyboardHandle, _ Code, wParam, lParam) 表示:如果前面的条件都不成立,那么说明这条消息并非我们需要的消息,此时我们将此消息释放,用CallNextHookEx交还给系统。 下面我们进行前面提到的IsHooked函数的定义,这个函数主要是过滤我们不想要的那些键或者键盘组合,这里我们屏蔽了Alt+TAB组合: Public Function IsHooked(ByRef Hookstruct As KBDLLHOOKSTRUCT) _ As Boolean If (KeyboardHook Is Nothing) Then IsHooked = False Exit Function End If 有时候CopyMemory也会发生意想不到的事情,所以,当KeyboardHook = Nothing (无值)的情况下,退出,略过该函数,以防不可预知的错误。 If (Hookstruct.vkCode = VK_TAB) And _ CBool(Hookstruct.flags And _ LLKHF_ALTDOWN) Then IsHooked = True Exit Function End If 以上拦截了Alt+Tab的键盘组合,并将IsHooked返回True(就是1),表示本次按键确实符合了过滤原则,应该吞吃掉。 End Function 最后一步,释放钩子函数: Public Declare Function UnhookWindowsHookEx Lib "user32" _ (ByVal hHook As Long) As Long Public Sub UnhookKeyboard() If (Hooked) Then Call UnhookWindowsHookEx(KeyboardHandle) End If End Sub 这个函数很简单,使用UnhookWindowsHookEx这个API释放之前由SetWindowsHookEx所定制的钩子函数。 一般而言,我们将钩子函数的创建放在窗体的Load部分,表示一进入程序立即启动钩子,而释放钩子则放在UnLoad部分,表示一旦程序关闭,我们需要立刻释放钩子所占用的资源。 结束语: 事实上,该例子演示了整个键盘拦截和屏蔽的功能,如果仅仅是需要知道用户按下了什么键,那么IsHooked函数是不需要的,并且我们也没有必要吞噬任何消息,只是让消息从我们的钩子函数里过一遍,知道它的值,然后再将此消息交还给系统即可,那样的话,程序将会更加简单。另外,在调试这类超越了VB编译环境本身的API程序时必须注意存盘,不断地存盘,因为VB环境和这些API函数同属系统级,因此它无法管理这些API,一旦出现问题,整个VB环境会在毫无预知的情况下造成全线崩溃的局面(比如VB编译环境的界面不提示任何内容自己就莫名其妙地突然消失等等),所以在按下RUN之前一定要记得存盘!! |