第二十四课 WINDOWS钩子函数
SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD要卸载一个钩子时调用UnhookWidowHookEx函数,该函数仅有一个参数,就是欲卸载的钩子的句柄。如果调用成功的话,在eax中返回非0值,否则返回NULL。如果该函数调用成功的话,将在eax中返回钩子的句柄,否则返回NULL。您必须保存该句柄,因为后面我们还要它来卸载钩子。
- HookType 是我们上面列出的值之一,譬如: WH_MOUSE, WH_KEYBOARD
 - pHookProc 是钩子函数的地址。如果使用的是远程的钩子,就必须放在一个DLL中,否则放在本身代码中
 - hInstance 钩子函数所在DLL的实例句柄。如果是一个局部的钩子,该值为NULL
 - ThreadID 是您安装该钩子函数后想监控的线程的ID号。该参数可以决定该钩子是局部的还是系统范围的。如果该值为NULL,那么该钩子将被解释成系统范围内的,那它就可以监控所有的进程及它们的线程。如果您指定了您自己进程中的某个线程ID 号,那该钩子是一个局部的钩子。如果该线程ID是另一个进程中某个线程的ID,那该钩子是一个全局的远程钩子。这里有两个特殊情况:WH_JOURNALRECORD 和 WH_JOURNALPLAYBACK总是代表局部的系统范围的钩子,之所以说是局部,是因为它们没有必要放到一个DLL中。WH_SYSMSGFILTER 总是一个系统范围内的远程钩子。其实它和WH_MSGFILTER钩子类似,如果把参数ThreadID设成0的话,它们就完全一样了。
 
WH_CALLWNDPROC所以您必须查询您的WIN32 API 指南来得到不同类型的钩子的参数的详细定义以及它们返回值的意义。这里还有一个问题需要注意:所有的钩子都串在一个链表上,最近加入的钩子放在链表的头部。当一个事件发生时,WINDOWS将按照从链表头到链表尾调用的顺序。所以您的钩子函数有责任把消息传到下一个链中的钩子函数。当然您可以不这样做,但是您最好明白这时这么做的原因。在大多数的情况下,最好把消息事件传递下去以便其它的钩子都有机会获得处理这一消息的机会。调用下一个钩子函数可以调用函数CallNextHookEx。该函数的原型如下:WH_MOUSE
- nCode 只能是HC_ACTION,它代表有一个消息发送给了一个窗口
 - wParam 如果非0,代表正被发送的消息
 - lParam 指向CWPSTRUCT型结构体变量的指针
 - return value: 未使用,返回0
 
- nCode 为HC_ACTION 或 HC_NOREMOVE
 - wParam 包含鼠标的事件消息
 - lParam 指向MOUSEHOOKSTRUCT型结构体变量的指针
 - return value: 如果不处理返回0,否则返回非0值
 
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD请注意:对于远程钩子,钩子函数必须放到DLL中,它们将从DLL中映射到其它的进程空间中去。当WINDOWS映射DLL到其它的进程空间中去时,不会把数据段也进行映射。简言之,所有的进程仅共享DLL的代码,至于数据段,每一个进程都将有其单独的拷贝。这是一个很容易被忽视的问题。您可能想当然的以为,在DLL中保存的值可以在所有映射该DLL的进程之间共享。在通常情况下,由于每一个映射该DLL的进程都有自己的数据段,所以在大多数的情况下您的程序运行得都不错。但是钩子函数却不是如此。对于钩子函数来说,要求DLL的数据段对所有的进程也必须相同。这样您就必须把数据段设成共享的,这可以通过在链接开关中指定段的属性来实现。在MASM中您可以这么做:
- hHook 时是您自己的钩子函数的句柄。利用该句柄可以遍历钩子链。
 - nCode, wParam and lParam 您只要把传入的参数简单传给CallNextHookEx即可。
 
/SECTION:<section name>, S已初期化的段名是.data,未初始化的段名是.bss。`加入您想要写一个包含钩子函数的DLL,而且想使它的未初始化的数据段在所有进程间共享,您必须这么做:
link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........S 代表该段是共享段。
;--------------------------------------------- 
主程序的源代码部分-------------------------------------- 
.386 
.model 
flat,stdcall 
option casemap:none 
include 
\masm32\include\windows.inc 
include \masm32\include\user32.inc 
include \masm32\include\kernel32.inc 
include mousehook.inc 
includelib mousehook.lib 
includelib \masm32\lib\user32.lib 
includelib \masm32\lib\kernel32.lib 
wsprintfA proto C :DWORD,:DWORD,:VARARG 
wsprintf TEXTEQU 
<wsprintfA> 
.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 
\masm32\include\windows.inc 
include \masm32\include\kernel32.inc 
includelib \masm32\lib\kernel32.lib 
include 
\masm32\include\user32.inc 
includelib \masm32\lib\user32.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
;---------------------------------------------- DLL的Makefile文件 ----------------------------------------------
NAME=mousehook 
$(NAME).dll: $(NAME).obj 
        Link /SECTION:.bss,S  
/DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib $(NAME).obj 
$(NAME).obj: $(NAME).asm 
        ml /c /coff /Cp 
$(NAME).asm 
  
                    
.if HookFlag==FALSE 
                        
invoke InstallHook,hDlg 
                        
.if eax!=NULL 
                            
mov HookFlag,TRUE 
                            
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText 
                        
.endif 
该应用程序有一个全局变量,HookFlag,它用来监视钩子的状态。如果安装来钩子它就是TRUE,否则是FALSE。 当用户按下Hook按钮时,应用程序检查钩子是否已经安装。如果还没有的话,它将调用DLL中引出的函数InstallHook来安装它。注意我们把主对话框的句柄传递给了DLL,这样这个钩子DLL就可以把WM_MOUSEHOOK消息传递给正确的窗口了。当应用程序加载时,钩子DLL也同时加载。时机上当主程序一旦加载到内存中后,DLL就立即加载。DLL的入口点函数载主程序的第一条语句执行前就前执行了。所以当主程序执行时,DLL已经初始化好了。我们载入口点处放入如下代码:
    .if reason==DLL_PROCESS_ATTACH 
        push hInst 
        pop hInstance 
    .endif 
该段代码把DLL自己的实例句柄放到一个全局变量中保存。由于入口点函数是在所有函数调用前被执行的,所以hInstance总是有效的。我们把该变量放到.data中,使得每一个进程都有自己一个该变量的值。因为当鼠标光标停在一个窗口上时,钩子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函数来安装一个鼠标钩子。该函数的返回值放在全局变量hHook中,将来在UnhookWindowsHookEx中还要使用。在调用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函数,它会引起死锁。MOUSEHOOKSTRUCT的定义如下:
MOUSEHOOKSTRUCT STRUCT DWORD 
  
pt            POINT 
<> 
  
hwnd          
DWORD      ? 
  wHitTestCode  
DWORD      ? 
  dwExtraInfo   
DWORD      ? 
MOUSEHOOKSTRUCT ENDS 
  
当主窗口接收到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,得到窗口过程调用GetClassLong并传入GCL_WNDPROC标志,然后把它们格式化成文本串并放到相关的编辑空间中去。
                        
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函数。该函数调用UnhookWindowsHookEx函数。然后,它把按钮的文本换回“Hook”,HookFlag的值设成FALSE再清除掉编辑控件中的文本。
链接器的开关选项如下: 
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS
它指定.bss段作为一个共享段以便所有映射该DLL的进程共享未初始化的数据段。如果不用该开关,您DLL中的钩子就不能正常工作了。