天天看点

hook

Windows钩子 钩子是Windows的消息处理机制中的一个监视点,应用程序可以在这里安装一个监视子程序,这样就可以在系统中的消息流到达目的窗口过程前监控它们。 钩子是Windows消息机制中的监视点,应用程序可以在这里安装一个监视函数,这样就可以捕捉自己进程或者其他进程发生的事件。通过SetWindowsHookEx函数就可以做到这一点。SetWindowsHookEx函数定义了监视函数的位置和监视消息的类型,这样,每当发生我们感兴趣的消息时,Windows就会将消息发送给监视函数,监视函数是一个处理消息的回调函数,也称为“钩子函数”。 Windows安装的钩子有两种类型:局部的和远程的。它们处理消息的范围不同。局部钩子仅钩挂属于自身进程的事件;远程钩子除了可以钩挂自身进程的事件,还可以钩挂其他进程中发生的事件。远程钩子又分两种:基于线程的和系统范围的。基于线程的远程钩子用来捕获其他进程中某一特定线程的事件;而系统范围的远程钩子将捕捉系统中所有进程中发生的事件消息。 安装钩子会影响系统的性能,因为系统在处理所有的相关事件时都会调用钩子函数,特别是监视范围是整个系统范围的全局钩子。如果钩子函数中的处理代码过多的话,系统运行速度将会明显减慢,所以对于全局钩子一定要小心使用,不需要的时候应该立刻卸载。在 DOS操作系统下编写中断服务程序的时候,如果代码有错误的话会影响其他调用它的程序。同样,由于钩子函数可以预先截获其他进程的消息,所以一旦钩子函数存在问题的话,也会影响其他进程的运行。 表1 钩子的类型 钩 子 名 称 监视消息的类型和时机 WH_CALLWNDPROC 每当调用SendMessage函数时,函数将消息发送给目标窗口过程前首先调用钩子函数 WH_CALLWNDPROCRET 每当调用SendMessage函数时,函数将消息发送给目标窗口过程后再调用钩子函数 WH_GETMESSAGE 每当调用GetMessage或PeekMessage函数时,函数从程序的消息队列中获取一个消息后调 用钩子函数 WH_KEYBOARD 每当调用GetMessage或PeekMessage函数时,如果从消息队列中得到的是WM_KEYUP或 WM_KEYDOWN消息, 则调用钩子函数 WH_MOUSE 每当调用GetMessage或PeekMessage函数时,如果从消息队列中得到的是鼠标消息, 则调用钩子函数 WH_HARDWARE 每当调用GetMessage或PeekMessage函数时,如果从消息队列中得到的是非鼠标和键盘消息, 则调用钩子函数 WH_MSGFILTER 当用户对对话框、菜单和滚动条有所操作时,系统在发送对应的消息之前调用钩子函数,这 种钩子只能是局部的 WH_SYSMSGFILTER 同WH_MSGFILTER,不过是系统范围的 WH_SHELL 当Windows shell程序准备接收一些通知事件前调用钩子函数,如shell被激活和重画等 WH_DEBUG 用来给其他钩子函数除错 WH_CBT 当基于计算机的训练(CBT)事件发生时调用钩子函数 WH_JOURNALRECORD 日志记录钩子,用来记录发送给系统消息队列的所有消息 WH_JOURNALPLAYBACK 日志回放钩子,用来回放日志记录钩子记录的系统事件 WH_FOREGROUNDIDLE 系统空闲钩子,当系统空闲的时候调用钩子函数,这样就可以在这里安排一些优先级 很低的任务 在这些钩子中,有些只能当做局部钩子使用,如WH_MSGFILTER钩子;有些只能当做系统范围的远程钩子使用, 如WH_JOURNALRECORD 和 WH_JOURNALPLAYBACK钩子;而大多数的钩子可以在任何范围内使用。 对于不同的钩子,由于它们处理的消息类型不同,所以钩子函数的参数定义也是不同的,在具体的编程中,需要查看Win32 API手册来了解各种钩子函数的参数定义。 另外,远程钩子和局部钩子的程序结构也是不同的。当安装了一个局部钩子时,每当指定的事件发生,Windows就可以调用进程中的钩子函数;但是若安装的是远程钩子,系统不能从其他进程的地址空间中调用钩子函数,因为两个进程的地址空间是隔离的,由于系统中只有DLL程序是可以插入到其他进程的地址空间中去的,所以远程钩子的钩子函数必须位于一个动态链接库中,而且必须是共享数据段的动态链接库(因为写远程钩子要用到动态链接库,所以本书中将两部分内容合在一章中介绍)。 但是也有两个例外:日志记录钩子和日志回放钩子虽然属于远程钩子,但是它们的钩子函数却可以放在安装钩子的程序中,并不需要单独放在一个动态链接库中。Microsoft并没有说明为什么有这样的例外,或许是这两个钩子是用来监控比较底层的硬件事件的,所以钩子函数的调用并不是从其他进程的地址空间中发起的,而是从Windows内部发起的,所以不存在不同进程之间地址空间隔离的问题。 下面的以键盘钩子为例来说明系统范围远程钩子的安装和使用,局部钩子的使用步骤与之类似,只不过不必将钩子函数放在动态链接库中而已,使用起来更加简单,读者可以举一反三自己尝试一下。 远程钩子的安装和使用 1. 钩子程序的结构 钩子程序一般包括3个功能模块: (1)主程序——用来实现界面或者其他功能。 (2)钩子回调函数——用来接收系统发过来的消息。 (3)钩子的安装和卸载程序。 对于局部钩子来说,这些模块可以处在同一个可执行文件中。而对于远程钩子来说,第2部分必须放在一个动态链接库中,第3部分虽然没有要求,但一般也放在动态链接库中,这是因为钩子创建以后得到一个钩子句柄,这个句柄要在钩子回调函数中以及卸载钩子的时候用到,如果把这部分代码放在主程序中的话,还需要创建一个函数将它传回给动态链接库,所以还不如直接放到库中。 下面例子包括两部分文件: HookDll.asm和HookDll.def文件用来生成动态链接库; Main.asm和Main.rc是主程序部分。程序用一个系统范围的远程钩子来实现监视所有键盘输入的功能。由于安装钩子回调函数的动态链接库要求是共享数据段的,所以请读者注意Makefile中dll文件的链接选项,它使用了/section:.bss,S选项。 NAME = Main DLL = Hookdll ML_FLAG = /c /coff LINK_FLAG = /subsystem:windows DLL_LINK_FLAG = /subsystem:windows /section:.bss,S $(DLL).dll $(NAME).exe: $(DLL).dll: $(DLL).obj $(DLL).def Link $(DLL_LINK_FLAG) /Def:$(DLL).def /Dll $(DLL).obj $(NAME).exe: $(NAME).obj $(NAME).res Link $(LINK_FLAG) $(NAME).obj $(NAME).res .asm.obj: ml $(ML_FLAG) $< .rc.res: rc $< clean: del *.obj del *.res del *.exp del *.lib HookDll.asm文件的内容如下: .386 .model flat, stdcall option casemap :none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .data hInstance dd ? .data? hWnd dd ? hHook dd ? dwMessage dd ? szAscii db 4 dup (?) .code DllEntry proc _hInstance,_dwReason,_dwReserved Push _hInstance pop hInstance mov eax,TRUE ret DllEntry Endp HookProc proc _dwCode,_wParam,_lParam local @szKeyState[256]:byte invoke CallNextHookEx,hHook,_dwCode,_wParam,_lParam invoke GetKeyboardState,addr @szKeyState invoke GetKeyState,VK_SHIFT mov @szKeyState + VK_SHIFT,al mov ecx,_lParam shr ecx,16 invoke ToAscii,_wParam,ecx,addr @szKeyState,addr szAscii,0 mov byte ptr szAscii [eax],0 invoke SendMessage,hWnd,dwMessage,dword ptr szAscii,NULL xor eax,eax ret HookProc endp InstallHook proc _hWnd,_dwMessage push _hWnd pop hWnd push _dwMessage pop dwMessage invoke SetWindowsHookEx,WH_KEYBOARD,addr HookProc,/ hInstance,NULL mov hHook,eax ret InstallHook endp UninstallHook proc invoke UnhookWindowsHookEx,hHook ret UninstallHook endp End DllEntry 需要共享的变量被放在 .data?段中,如钩子句柄和钩住的按键内容等,仅dll程序的实例句柄不需要共享,不需要共享的变量放在 .data段中。动态链接库的入口函数例行公事地返回了一个TRUE来表示允许被装入。程序中只写了3个函数,HookProc是钩子回调函数,InstallHook和 UninstallHook函数是供主程序使用的钩子安装函数和卸载函数。这3个函数是需要导出的,所以HookDll.def文件中包括了它们的名称: EXPORTS HookProc InstallHook UninstallHook InstallHook子程序用来安装钩子,程序为它设计了两个参数:窗口句柄和自定义消息ID。动态链接库保存这两个参数,以便在钩子回调函数收到消息的时候将截获的按键通过自定义消息ID转发给父窗口,这样父窗口在初始化完成后只需要等待自定义消息ID就可以了。 在子程序中,通过SetWindowsHookEx函数安装钩子。SetWindowsHookEx函数的用法是: invoke SetWindowsHookEx,idHook,lpHookProc,hInstance,dwThreadID .if eax mov hHook,eax .endif idHook参数指定钩子的类型,它就是表11.1中列出的钩子名称。例子中要安装的是键盘钩子,所以使用WH_KEYBOARD。lpHookProc参数指出钩子回调函数的地址。 hInstance 指定钩子回调函数所在DLL的实例句柄。如果安装的是局部钩子的话,由于局部钩子的回调函数并不需要放在动态链接库中,这时这个参数就使用NULL。 dwThreadID是安装钩子后想监控的线程的ID号。该参数可以决定钩子是局部的还是系统范围的。如果参数指定的是自己进程中的某个线程ID号,那么该钩子是一个局部钩子;如果指定的线程ID是另一个进程中某个线程的ID,那么安装的钩子是一个局部的远程钩子;如果想要安装系统范围的全局钩子的话,可以将这个参数指定为NULL,这样钩子就会被解释成系统范围的,可以用来监控所有的进程及它们的线程。 如果钩子安装成功,函数返回钩子句柄,否则返回NULL。钩子句柄必须被保存下来,因为在回调函数和卸载钩子的时候还要用到这个句柄。动态链接库导出的另一个函数是UninstallHook,用来供主程序卸载钩子。程序在这里使用UnhookWidowHookEx函数卸载钩子,这个函数的输入参数只有一个,就是安装钩子时返回的句柄 现在来看主程序。Main.rc文件的内容如下: #include #define ICO_MAIN 1000 #define DLG_MAIN 1000 #define IDC_TEXT 1001 ICO_MAIN ICON "Main.ico" DLG_MAIN DIALOG 208, 130, 234, 167 STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "键盘钩子" FONT 9, "宋体" { EDITTEXT IDC_TEXT, 5, 5, 224, 158, ES_MULTILINE | ES_AUTOVSCROLL | WS_BORDER | WS_VSCROLL | WS_TABSTOP | ES_READONLY } 资源脚本文件中的定义很简单,仅定义了一个对话框,对话框中有个多行的编辑控件,用来显示“钩住”的按键。Main.asm的内容如下: .386 .model flat, stdcall option casemap :none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib include Hookdll.inc includelib Hookdll.lib ICO_MAIN equ 1000 DLG_MAIN equ 1000 IDC_TEXT equ 1001 WM_HOOK equ WM_USER + 100h .code _ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam local @dwTemp mov eax,wMsg .if eax == WM_CLOSE invoke UninstallHook invoke EndDialog,hWnd,NULL .elseif eax == WM_INITDIALOG invoke InstallHook,hWnd,WM_HOOK .if ! eax invoke EndDialog,hWnd,NULL .endif .elseif eax == WM_HOOK mov eax,wParam .if al == 0dh mov eax,0a0dh .endif mov @dwTemp,eax invoke SendDlgItemMessage,hWnd,IDC_TEXT,/ EM_REPLACESEL,0,addr @dwTemp .else mov eax,FALSE ret .endif mov eax,TRUE ret _ProcDlgMain endp start: invoke GetModuleHandle,NULL invoke DialogBoxParam,eax,DLG_MAIN,NULL,/ offset _ProcDlgMain,NULL invoke ExitProcess,NULL end start 为了使用动态链接库中的导出函数InstallHook和UninstallHook,在程序的开头需要用include语句和includelib语句将动态链接库的函数声明和导入库包含进来。 在对话框初始化的消息WM_INITDIALOG 中,程序调用InstallHook函数安装钩子,输入的参数是主窗口句柄和自定义的消息ID:WM_HOOK(Windows系统中ID值在WM_USER以后的值都可以由用户使用,在这里将WM_HOOK定义为WM_USER+100h),这样每当钩子回调函数得到按键消息的时候,就可以通过这个消息ID通知主窗口。接下来程序对返回值进行检查,如果返回值表示失败则直接退出程序。在关闭对话框的WM_CLOSE消息中,程序调用UninstallHook函数卸载钩子。 在平时,主程序等待自定义消息WM_HOOK,并将传递过来的按键字符串通过发送EM_REPLACESEL消息添加到编辑框中,在添加之前先检测按键是否为回车键,如果是,再人为插入一个换行符(0ah),以便将编辑框中的内容换行显示。 2. 钩子回调函数 现在回过头来看HookDll.asm程序中的钩子回调函数,回调函数的写法一般如下: HookProc proc dwCode,wParam,lParam invoke CallNextHookEx,hHook,_dwCode,_wParam,_lParam ;处理消息的代码 mov eax,返回值 ret HookProc endp 各种类型钩子的回调函数的参数都是这样3个,但是它们的定义各不相同,就像窗口过程在收到各种不同消息的时候,wParam和lParam的定义也各不相同。不同类型的钩子回调函数的返回值定义也是各不相同的。 对于键盘钩子来说,参数的定义如下所示。 ● dwCode——键盘消息的处理方式。如果是HC_ACTION,表示收到一个正常的击键消息;如果是HC_NOREMOVE,表示对应消息并没有从消息队列中移去(当某个进程用指定PM_NOREMOVE 标志的PeekMessage函数获取消息时就是如此)。 ● wParam——按键的虚拟码(即Windows.inc中定义的VK_xxx值)。 ● lParam——按键的重复次数、扫描码和标志等数据,不同数据位的定义如下: ■ 位0~15:按键的重复次数。 ■ 位16~23:按键的扫描码。 ■ 位24:按键是否是扩展键(F1与F2等Fx键,小键盘数字键等),如果此位是1表示按键是扩展键。 ■ 位25~28:未定义。 ■ 位29:如果Alt键在按下状态,此位置1,否则置0。 ■ 位30:按键的原先状态,消息发送前按键原来是按下的,此位被设置为1,否则置0。 ■ 位31:按键的当前动作,如果是按键按下,那么此位被设置为0;按键释放的话被设置为1。 对于每个击键动作,钩子回调函数会在键按下和释放的时候被调用两次,只需根据 lParam的位31中的标志来记录一次,否则得到的是重复信息。 另外,回调函数收到的参数是以按键的扫描码和虚拟码表示的,在送给主窗口前需要将它转换成我们认识的ASCII码,但虚拟码或扫描码和ASCII码之间的对应关系并没有规律,必须进行查表操作才能转换。如果在程序中自己转换的话,需要一个键码对应表和查表程序。 Windows中现成的函数ToAscii可以完成这个功能并自动辨认按键的按下或释放动作。代码如下。 HookProc proc _dwCode,_wParam,_lParam local @szKeyState[256]:byte invoke CallNextHookEx,hHook,_dwCode,_wParam,_lParam invoke GetKeyboardState,addr @szKeyState invoke GetKeyState,VK_SHIFT mov @szKeyState + VK_SHIFT,al mov ecx,_lParam shr ecx,16 invoke ToAscii,_wParam,ecx,addr @szKeyState,addr szAscii,0 mov byte ptr szAscii [eax],0 invoke SendMessage,hWnd,dwMessage,dword ptr szAscii,NULL ... ToAscii函数的用法是: invoke ToAscii,dwVirtKey,uScanCode,lpKeyState,lpBuffer,uFlags dwVirtKey参数指定按键的虚拟码,在使用时直接用钩子回调函数的wParam参数就可以了,uScanCode指定按键的扫描码,并用位15来表示是按键按下还是按键释放,和回调函数的lParam参数对比可以看出,lParam参数的高16位就是需要的东西,所以程序将lParam右移16位后用做uScanCode参数。 lpKeyState指向一个256字节的缓冲区,其中存放键盘中所有按键的当前状态,一个字节表示一个按键,数值为1表示按下,为0表示释放,数据在缓冲区中的排列位置按照VK_xx虚拟码的顺序排列。这是为了让函数得知键盘上各种控制键的状态(如Shift,Alt和Ctrl等),因为这些键是否按下对转换结果是有影响的,比如同样是按键“1”,如果Shift键不按下,对应的就是“1”,按下的话函数必须返回“!”才是正确的结果。当然不可能自己去填写这个缓存区,使用GetKeyboardState函数就可以让系统根据当前的键盘状态填写这个缓冲区。 lpBuffer指向一个缓存区,用来接收转换后的ASCII码,最后的uFlags参数表示当前是否有一个菜单在激活状态,0表示没有,1表示有菜单正在激活。 函数的返回值表示转换后返回在lpBuffer缓冲区中的字符数量,它可能是0(如按键放开时不产生字符)、1或者是2,下面的语句根据返回字符数将缓冲区中的字符尾部加上一个NULL: mov byte ptr szAscii [eax],0 对于Shift等控制键来说,GetKeyboardState函数返回的状态是区分左、右键的(分别对应VK_LSHIFT和VK_RSHIFT),而ToAscii函数检测的是VK_SHIFT,不对Shift键进行处理的话,转换结果可能是错误的,所以程序使用GetKeyState函数单独获取VK_SHIFT的状态并手工修改缓冲区中VK_SHIFT位置的状态。 转换完成后,用PostMessage函数将转换后的按键内容传递给主窗口,就大功告成了!不过要注意的还有两点:首先是在这里不要使用SendMessage函数,因为可能造成死循环;其次就是不要向主窗口传递地址,因为钩子DLL被插入到其他进程的地址空间中运行,所以将地址传回去可能是无效的。 不同类型钩子回调函数返回值的定义是不同的。对于键盘钩子,返回0表示允许Windows将消息转发给目标窗口过程,返回非0值表示让Windows将消息丢弃,这样钩子函数可以检测到按键动作,目标程序却无法收到键盘消息,相当于所有的按键都失效了。 3. 钩子链 Windows系统中可以同时存在多个同类型的钩子,多个程序同时安装同一种钩子的时候就会出现这种情况,这些钩子组成一个钩子链,最近加入的钩子放在链表的头部,Windows负责为每种钩子维护一个钩子链。当一个事件发生的时候,Windows调用最后安装的钩子,然后由当前钩子的回调函数发起调用下一个钩子的动作,Windows收到这个动作后,再从链表中取出下一个钩子的地址并将调用传递下去。 在大多数的情况下,一个钩子回调函数最好把消息事件传递下去以便其他的钩子都有获得处理这一消息的机会。调用下一个钩子函数是CallNextHookEx,该函数的用法是: invoke CallNextHookEx,hHook,dwCode,wParam,lParam hHook参数是当前钩子的句柄,dwCode,wParam和lParam参数就是当前钩子收到的参数,这个函数让Windows调用钩子链中的下一个钩子。如果调用成功,函数的返回值是下一个钩子回调函数返回的数值。 四: 日志记录钩子 日志记录钩子是一种特殊的钩子,说它特殊是因为它是远程钩子,却不用放在动态链接库中,这就为监视系统范围的消息提供了方便。本节中尝试用日志记录钩子的办法来实现键盘监视的功能,包括汇编源文件RecHook.asm和资源脚本文件RecHook.rc,其中RecHook.rc文件的内容和上一个例子的Main.rc文件是一样的。 RecHook.asm文件的内容如下: .386 .model flat, stdcall option casemap :none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib ICO_MAIN equ 1000 DLG_MAIN equ 1000 IDC_TEXT equ 1001 .data? hInstance dd ? hWinMain dd ? hHook dd ? szAscii db 32 dup (?) .code HookProc proc _dwCode,_wParam,_lParam local @szKeyState[256]:byte invoke CallNextHookEx,hHook,_dwCode,_wParam,_lParam pushad .if _dwCode == HC_ACTION mov ebx,_lParam assume ebx:ptr EVENTMSG .if [ebx].message == WM_KEYDOWN invoke GetKeyboardState,addr @szKeyState invoke GetKeyState,VK_SHIFT mov @szKeyState + VK_SHIFT,al mov ecx,[ebx].paramH shr ecx,16 invoke ToAscii,[ebx].paramL,ecx,/ addr @szKeyState,addr szAscii,0 mov byte ptr szAscii [eax],0 .if szAscii == 0dh mov word ptr szAscii+1,0ah .endif invoke SendDlgItemMessage,hWinMain,IDC_TEXT,/ EM_REPLACESEL,0,addr szAscii .endif assume ebx:nothing .endif popad ret HookProc endp _ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam mov eax,wMsg .if eax == WM_CLOSE invoke UnhookWindowsHookEx,hHook invoke EndDialog,hWnd,NULL .elseif eax == WM_INITDIALOG push hWnd pop hWinMain invoke SetWindowsHookEx,WH_JOURNALRECORD,/ addr HookProc,hInstance,NULL .if eax mov hHook,eax .else invoke EndDialog,hWnd,NULL .endif .else mov eax,FALSE ret .endif mov eax,TRUE ret _ProcDlgMain endp start: invoke GetModuleHandle,NULL mov hInstance,eax invoke DialogBoxParam,eax,DLG_MAIN,NULL,/ offset _ProcDlgMain,NULL invoke ExitProcess,NULL end start 由于不再需要动态链接库了,钩子回调函数HookProc被移到了主程序中,也取消了InstallHook和UninstallHook两个子程序,相应的内容直接放在WM_INITDIALOG和WM_CLOSE消息中完成。在WM_INITDIALOG消息中用下面的语句完成对钩子的安装: invoke SetWindowsHookEx,WH_JOURNALRECORD,addr HookProc,hInstance,NULL 参数WH_JOURNALRECORD表示安装的钩子是日志记录钩子。 由于钩子回调函数也写在主程序中,所以没有必要再通过自定义的WM_HOOK消息来通信,在回调函数中使用ToAscii函数将监测到的按键扫描码转换成ASCII码字符串以后,程序直接发送EM_REPLACESEL消息将它添加到编辑框中。 程序比较重要的一个不同点在于日志钩子回调函数的参数定义不同,在这里dwCode的参数定义如下: ● HC_ACTION——系统准备从消息队列中移去一条消息,消息的具体信息由lParam参数中指定的EVENTMSG结构定义。 ● HC_SYSMODALOFF——某个系统模态对话框准备被关闭。 ● HC_SYSMODALON——某个系统模态对话框准备被建立。 我们关心的是HC_ACTION标志,这时lParam参数指向一个EVENTMSG结构,其定义为: EVENTMSG STRUCT message DWORD ? ;消息队列中将要移去的消息ID paramL DWORD ? ;消息的wParam参数 paramH DWORD ? ;消息的lParam参数 time DWORD ? ;消息发生的事件 hwnd DWORD ? ;消息对应的窗口句柄 EVENTMSG ENDS 由于日志记录钩子可以截获的不仅是键盘消息,也有鼠标等其他消息,所以需要有个地方指定消息类型,通过检测EVENTMSG结构中的消息ID字段就可以得知截获的究竟是什么消息。如果关心的是按键消息的话,那么发现消息ID为WM_KEYDOWN时进行处理就可以了,同理,如果关心的是鼠标消息的话,使用日志记录钩子也可以完成鼠标钩子完成的工作。例子程序中的相关代码如下: .if _dwCode == HC_ACTION mov ebx,_lParam assume ebx:ptr EVENTMSG .if [ebx].message == WM_KEYDOWN ;处理按键消息 .endif assume ebx:nothing .endif 日志记录钩子回调函数的返回值没有被定义。所以不管返回什么值对消息的传递都没有影响。