第二十四课: WINDOWS 钩子
Windows钩子是非常有用的。用它们,你能截获其它进程并且还可以改变它们的行为。
Windows 钩子被认为是windows中强有力的特征之一。用它们,你可以在你自己的进程或是其它进程中捕获将要发生的事件。通过“挂钩”你告诉windows关于一个过滤函数的信息,过滤函数也叫钩子过程,每当你感兴趣的事件发生时,这个函数将被调用。
钩子有两种类型:局部钩子和远程钩子。
局部钩子捕获的是在你自己的进程中将要发生的事件。
远程钩子捕获的是在其它进程中将要发生的事件。远程钩子的类型也有两种:
基于线程的 捕获的事件是在其它进程中特殊线程将要发生的事件。简而言之,你想要观察的
事件发生在一个具体的线程中,而这个线程是一特殊进程的线程。
系统范畴的 捕获系统中所有进程的全部线程将要发生的事件。(捕捉系统中所有进程将发生
的事件消息。)
系统范畴的钩子大多数是臭名昭著的。因为所有相关事件都将被你的过滤函数过滤 ,你的系统可能会变的很慢。所以,如果你用系统范畴的钩子,你应该明智的用它并且一旦你不需要它们时就让它们脱钩。同样,你也有较高的机会让其它进程发生异常,因为你能干涉其它进程并且如果你的钩子函数出了问题,它能拉倒其它进程直到被湮没。铭记一点:性能和责任一起供给。能力伴随责任(功能强大也意味着使用时要负责任。)
当你创建了一个钩子,相应的windows在内存中就创建了一数据结构,这个数据结构包含了关于钩子的相关信息,然后把这个钩子增加到已有的钩子链表中去。新的钩子添加在老钩子的前面。当一个事件发生时,如果你安装的是局部钩子,过滤函数在你的进程中被调用,所以这是相当简单的。但是,如果它是远程钩子,系统必须为这个钩子函数在其它进程的地址空间中注入代码。并且,只有当函数驻留在DLL中时系统才能这样做。因此,如果你想使用一个远程钩子,你的钩子函数必须驻留在一个DLL中。这条规则的两个例外:工作日志钩子和工作日志回放钩子。这两个钩子的钩子过程必须驻留在安装钩子的线程中。为什么必须是这样的原因是:这两个钩子处理的是底层硬件的输入事件监听。输入事件必须是已记录的或是回放的,事件的发生也是有顺序的。如果这两个钩子的代码在DLL中,输入事件可能会散布在几个线程中间,所以就不可能知道它们发生的正确顺序。故解决的办法是:这两个钩子的钩子过程必须被安装在单一的线程中,也就是安装钩子的那个线程中。
钩子有14种类型:
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_JOURNALRECORD :当windows从硬件输入队列中获得消息时。
WH_JOURNALPLAYBACK :当一个事件从系统硬件输入队列中被请求时。
WH_SHELL :当一些有趣味的外壳事件发生时,例如,当任务栏需要重绘它的按钮时。
WH_CBT :基于计算机的训练(CBT)事件发生时
WH_FOREGROUNDIDLE :由WINDOWS内部自己使用,一般的应用程序很少使用
WH_DEBUG :用于钩子函数的除错或调试。
现在我们知道了一些理论,我们来学习如何安装和卸载一个钩子。
要安装一个钩子,调用SetWindowsHookEx 函数,该函数句法如下:
SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD
一个上面列出的值。举例来说,WH_MOUSE WH_KEYBOARD
pHookProc :钩子函数的地址,它将被调用来为指定的钩子处理消息。如果是一远程钩子,它必
须驻留在DLL中。否则,它必须在你的进程中。
hInstance :钩子过程驻留的DLL文件的实例句柄。如果是局部钩子,这个值必须为空。
你想安装钩子监视的线程ID号。无论钩子是局部的还是远程的,这个参数都是确定的值。如果这个
参数为空, windows将把这个钩子解释为系统范畴的远程钩子,它将影响系统中的所有线程。如
果你在自己的进程中指定了一线程的ID号,这个钩子是局部钩子。如果你指定的事其它进程的线程
ID号,这个钩子就是为线程指定的远程钩子。这个规则有两种特殊情况:WH_JOURNALRECORD 和
WH_JOURNALPLAYBACK 总是局部系统范畴的钩子,它们不需要在DLL文件中。
WH_SYSMSGFILTER 总是一个系统范围内的远程钩子。其实它和WH_MSGFILTER钩子类
似,如果把参数ThreadID设成0的话,它们就完全一样了。
如果这个调用是成功的,它返回以钩子句柄在eax中。 如果不成功,NULL被返回。为了稍候能脱钩(卸载钩子),你必须保存这个钩子句柄。
通过调用UnhookWindowsHookEx函数,你可以卸载一个钩子。这个函数仅接收一个参数,就是你想卸载的钩子句柄。如果调用成功,它在eax中返回一个非零值,否则,它返回NULL。
现在,我们来分析钩子过程。
只要一个和你安装的钩子类型相关联的事件发生,钩子函数就被调用。例如,如果你安装的是WM_MOUSE钩子,当一个鼠标事件发生时,你的钩子过程将被调用。不管你安装的钩子是哪一类型,钩子过程总是下面这种原型:
HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD
nCode : 指定钩子代码
wParam and lParam :wParam 和lParam 包含事件的附加信息。
HookProc 实际是一个函数名的占位符。你能用任何你喜欢的字符来命名它,只要它有上面的原型。nCode ,wParam 和 lParam 参数的解释依赖于你安装的钩子类型。条件是,从钩子过程返回的值。例如:
WH_CALLWNDPROC
nCode :只能是HC_ACTION ,意味着这是一个发送给窗口的消息。
wParam : 如果它不为零,代表正被发送的消息。
lParam : 指向CWPSTRUCT结构的指针。
返回值:返回零,不使用。
WH_MOUSE
nCode :是HC_ACTION或HC_NOREMOVE
wParam :包含鼠标消息
lParam :包含一个MOUSEHOOKSTRUCT结构的指针。
返回值:0 这个消息应该被处理,1 这个消息应该被抛弃。
概要是:你必须参考win32API手册来获得关于你想安装的钩子的 参数 和返回值的详细资料
现在,这里还有一个关于钩子过程的小问题 。记得钩子被插入到一链表中,最近来的钩子安装在表头。当一个事件发生时,windows将调用链的第一个钩子。所以你的钩子过程有责任调用下一个在链表中的钩子。你能选择不调用下一个钩子但是你最好知道你正在做什么。大多数时候,调用下一个过程以便让其它钩子能试着去处理这事件是一个不错的习惯。
你能通过调用CallNextHookEx函数来调用下一个钩子,这个函数原型如下:
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD
hHook 是你拥有的钩子句柄。这个函数用这个句柄在链表上搜索它下一次应该调用的钩子过程。
nCode wParam 和lParam 您只要把传入的参数简单传给CallNextHookEx即可
重点注意远程钩子: 远程钩子必须驻留在DLL文件中,这个DLL文件被映射到其它进程空间中。当windows映射这个DLL到其它进程空间中时,它的数据段不会被映射。简而言之,所有的进程共享单一的一份dll代码,但是它们自己都拥有dll的数据段节区的单独拷贝。这是一个很容易被忽视的问题。你可能会想,当你在dll 文件的数据节区中用一个变量储存一个值时,这个值将被已加载dll文件到它进程地址空间中的所有进程共享。它是简单的但不正确。在通常情况下,这种行为是称心如意的,因为每一个映射该DLL的进程都有自己的数据段拷贝,
但是,当涉及到windows钩子时,却并非如此。我们希望dll对所有的进程都是相同的,包含数据段。解决方案是:你必须标记数据节区为共享段。你能在连接的时候用连接开关指定这个节区的属性。对于masm,你可以用如下开关:
/SECTION:
初始化的数据段的名字是.DATA未初始化的数据是.bss。例如,如果你想汇编一个包含钩子过程的DLL文件,并且你想未初始化数据段在所有进程间共享,你必须用下面这行:
link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........
S 属性使这个节区被共享。
例子:
这里有两个模块:一个是GUI部分的主程序,一个是安装和卸载钩子的dll 文件,
这是主程序的源代码。
.386
.model flat,stdcall
option casemap:none
include masm32includewindows.inc
include masm32includeuser32.inc
include masm32includekernel32.inc
include mousehook.inc
includelib mousehook.lib
includelib masm32libuser32.lib
includelib masm32libkernel32.lib
wsprintfA proto C :DWORD,:DWORD,:VARARG
wsprintf TEXTEQU
.const
IDD_MAINDLG equ 101
IDC_CLASSNAME equ 1000
IDC_HANDLE equ 1001
IDC_WNDPROC equ 1002
IDC_HOOK equ 1004
IDC_EXIT equ 1005
WM_MOUSEHOOK equ WM_USER+6
DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD
.data
HookFlag dd FALSE
HookText db "&Hook",0
UnhookText db "&Unhook",0
template db "%lx",0
.data?
hInstance dd ?
hHook dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
invoke ExitProcess,NULL
DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL hLib:DWORD
LOCAL buffer[128]:byte
LOCAL buffer1[128]:byte
LOCAL rect:RECT
.if uMsg==WM_CLOSE
.if HookFlag==TRUE
invoke UninstallHook
.endif
invoke EndDialog,hDlg,NULL
.elseif uMsg==WM_INITDIALOG
invoke GetWindowRect,hDlg,addr rect
invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW
.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif
.elseif uMsg==WM_COMMAND
.if lParam!=0
mov eax,wParam
mov edx,eax
shr edx,16
.if dx==BN_CLICKED
.if ax==IDC_EXIT
invoke SendMessage,hDlg,WM_CLOSE,0,0
.else
.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif
.else
invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
.endif
.endif
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgFunc endp
end start
这是dll文件的源代码
.386
.model flat,stdcall
option casemap:none
include masm32includewindows.inc
include masm32includekernel32.inc
includelib masm32libkernel32.lib
include masm32includeuser32.inc
includelib masm32libuser32.lib
.const
WM_MOUSEHOOK equ WM_USER+6
.data
hInstance dd 0
.data?
hHook dd ?
hWnd dd ?
.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif
mov eax,TRUE
ret
DllEntry Endp
MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp
InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp
UninstallHook proc
invoke UnhookWindowsHookEx,hHook
ret
UninstallHook endp
End DllEntry
;---------------------------------------------- This is the makefile of the DLL ----------------------------------------------
NAME=mousehook
$(NAME).dll: $(NAME).obj
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBPATH:c:masmlib $(NAME).obj
$(NAME).obj: $(NAME).asm
ml /c /coff /Cp $(NAME).asm
分析:
这个例子将显示一个对话框,这个对话框有三个编辑框控件,它们被与当前鼠标光标下的窗口相关联的类名,窗口句柄和窗口过程的地址填充。这里还有两个按钮, HOOK和EXIT.当你按下HOOK按钮时,程序钩住鼠标输入并设置按钮的文本为Unhook 当你在一个窗口上移动鼠标光标时,关于这个窗口的信息将显示在例子中的主窗口上。当你按下Unhook按钮时,程序清除鼠标钩子。
主程序用一个对话框作为它的主窗口。它定义了一个自定义消息,WM_MOUSEHOOK。这个消息将被用于主程序和钩子dll之间。当主窗口接收到这个消息时,wParam参数包含鼠标光标所在的那个窗口的句柄。当然,这个可以任意安排。出于简单,我决定发送句柄给wParam。你也能自己选择主程序和钩子dll通讯的方法。
.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif
程序维护一个标志,HookFlag,它用来监视钩子的状态。如果钩子没有被安装它的值为FALSE,如果钩子被安装,它的值就为TRUE。当用户按下Hook按钮时,程序检查钩子是否被安装。如果它没被安装,它就调用在hook dll中的InStallHook函数来安装它。注意,我们传递主对话框的句柄作为函数的参数,这样钩子dll发送的WM_MOUSEHOOK消息就能够传递给正确的窗口。也就是,我们自己拥有的。
当程序被加载时,钩子DLL也被加载。实际上,当程序装进内存后,dlls文件被立即加载。在主程序的第一条指令执行前dll的入口点函数就被调用了。所以当主程序执行时,dll文件已经被初始化了。我们在钩子dll文件的入口点函数后面放入下面的代码:
.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif
为了在InstallHook函数中使用,该钩子dll只不过是保存它的实例句柄在名字为hInstance的全局变量中。因为在dll中的其它函数被调用之前,dll的入口函数就被调用,所以hInstance总是有效的。我们放置hInstance在数据节区中使得每一个进程都有自己一个该变量的值。因为当鼠标光标停留在一个窗口上时,钩子dll被映射进进程的地址空间中。设想一下,钩子dll加载的地址空间已经被另一个dll占用,钩子dll将被重新映射在其它的地址空间。hInstance的值将被更新成那些新加载的地址。当用户按下Unhook按钮或者是Hook按钮,SetWindowsHookEx将被再一次调用。然而,这一次,它将用新加载的地址作为实例句柄,因为在例子进程中,这个钩子dll的地址空间没有被改变,故新加载的地址是错误的。这个钩子是局部变量的一种,它仅能钩挂发生在你窗口中的鼠标事件。这是很难让人满意的。
InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp
InstallHook函数本身是非常简单的。它把作为它的参数传递过来的窗口句柄保存在全局变量hWnd中,已备将来使用。然后调用SetWindowsHookEx函数来安装一个鼠标钩子。为了让UnhookWindowsHookEx将来使用,SetWindowsHookEx函数的返回值被储存在一个名为hHook的全局变量中,
在SetWindowsHookEx被调用之后,鼠标钩子就工作了。只要在系统中有鼠标事件发生,MouseProc(你的钩子过程)就被调用。
MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp
首先,钩子函数要调用CallNextHookEx来让其它钩子有机会处理鼠标事件。在这之后,然后,调用WindowFromPoint函数来得到给定屏幕坐标位置处的窗口句柄。注意,我们用lParam参数指向的MOUSEHOOKSTRUCT结构体指针中的POINT指针作为鼠标的当前坐标。在我们通过PostMessage发送窗口句柄和WM_MOUSEHOOK给主窗口之后。你应该记住的一件事是:你不能在钩子过程中SendMessage函数,它能引起消息死锁。我们建议你使用PostMessage。MOUSEHOOKSTRUCT结构体定义如下:
MOUSEHOOKSTRUCT STRUCT DWORD
pt POINT <>
hwnd DWORD ?
wHitTestCode DWORD ?
dwExtraInfo DWORD ?
MOUSEHOOKSTRUCT ENDS
pt是鼠标光标当前屏幕坐标。
Hwnd 是将要接受鼠标消息的窗口句柄。它通常是在鼠标光标下的窗口,但不总都是。如果一个窗口调用SetCapture,鼠标输入将被重定向到这个窗口。由于这个原因,我并不使用这个结构的hwnd成员但是我们用WindowFromPoint调用代替。
wHitTestCode 指定hit—test 值。这个值给了很多关于当前鼠标光标位置的信息。它指出鼠标光标在窗口的什么部位。为了完成列表,请你查看win32api 手册中的WM_NCHITTEST消息。
DwExtraInfo 包含和消息有关的额外信息。通常这个值被调用的鼠标事件设置,还可以调用GetMessageExtraInfo 获得。
当主窗口接受到WM_MOUSEHOOK消息时,它用在wParam参数中的窗口句柄来检索关于窗口的信息。
.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif
为了避免闪烁,我们检查已经在编辑控件中的文本和我们将要显示的文本是否相同。如果相同,我们就忽略它们。
我们调用GetClassName函数来获取类名,我们传入GCL_WNDPROC然后调用GetClassLong函数来获得窗口过程的地址,然后格式化字符串并把它们放入到合适的编辑控件中。
invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
当用户按下Unhook按钮时,程序调用在钩子dll中的UninstallHook函数,UninstallHook仅仅是调用UnhookWindowsHookEx。在这之后,他将按钮的文本改为Hook,并置HookFlag标志为FALSE然后清除编辑控件中的内容。
注意:makefile中的连接标志如下:
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS
它指定.bss段作为一个共享段以便让所有的进程共享同一个在Hook 动态链接库中的未初始化数据段。没有了这个开关,你的钩子dll 将不能正确的工作了。