WINDOWS的鉤子函數知識與彙編實例

WINDOWS的鉤子函數知識與彙編實例

2006-08-28 來源: 網友評論0  進入電腦報知道

     WINDOWS的鉤子函數可以認爲是WINDOWS的主要特性之一。利用它們,您可以捕捉您自己進程或其它進程發生的事件。通過“鉤掛”,您可以給WINDOWS一個處理或過濾事件的回調函數,該函數也叫做“鉤子函數”,當每次發生您感興趣的事件時,WINDOWS都將調用該函數。一共有兩種類型的鉤子:局部的和遠程的。 
局部鉤子僅鉤掛您自己進程的事件。 
遠程的鉤子還可以將鉤掛其它進程發生的事件。遠程的鉤子又有兩種: 
基於線程的 它將捕獲其它進程中某一特定線程的事件。簡言之,就是可以用來觀察其它進程中的某一特定線程將發生的事件。 
系統範圍的 將捕捉系統中所有進程將發生的事件消息。 
安裝鉤子函數將會影響系統的性能。監測“系統範圍事件”的系統鉤子特別明顯。因爲系統在處理所有的相關事件時都將調用您的鉤子函數,這樣您的系統將會明顯的減慢。所以應謹慎使用,用完後立即卸載。還有,由於您可以預先截獲其它進程的消息,所以一旦您的鉤子函數出了問題的話必將影響其它的進程。記住:功能強大也意味着使用時要負責任。
在正確使用鉤子函數前,我們先講解鉤子函數的工作原理。當您創建一個鉤子時,WINDOWS會先在內存中創建一個數據結構,該數據結構包含了鉤子的相關信息,然後把該結構體加到已經存在的鉤子鏈表中去。新的鉤子將加到老的前面。當一個事件發生時,如果您安裝的是一個局部鉤子,您進程中的鉤子函數將被調用。如果是一個遠程鉤子,系統就必須把鉤子函數插入到其它進程的地址空間,要做到這一點要求鉤子函數必須在一個動態鏈接庫中,所以如果您想要使用遠程鉤子,就必須把該鉤子函數放到動態鏈接庫中去。當然有兩個例外:工作日誌鉤子和工作日誌回放鉤子。這兩個鉤子的鉤子函數必須在安裝鉤子的線程中。原因是:這兩個鉤子是用來監控比較底層的硬件事件的,既然是記錄和回放,所有的事件就當然都是有先後次序的。所以如果把回調函數放在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 當關於WINDOWS外殼事件發生時,譬如任務條需要重畫它的按鈕. 
WH_CBT 當基於計算機的訓練(CBT)事件發生時 
WH_FOREGROUNDIDLE 由WINDOWS自己使用,一般的應用程序很少使用 
WH_DEBUG 用來給鉤子函數除錯
現在我們知道了一些基本的理論,現在開始講解如何安裝/卸載一個鉤子。 
要安裝一個鉤子,您可以調用SetWindowHookEx函數。該函數的原型如下: 

SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD 
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的話,它們就完全一樣了。 
如果該函數調用成功的話,將在eax中返回鉤子的句柄,否則返回NULL。您必須保存該句柄,因爲後面我們還要它來卸載鉤子。
要卸載一個鉤子時調用UnhookWidowHookEx函數,該函數僅有一個參數,就是欲卸載的鉤子的句柄。如果調用成功的話,在eax中返回非0值,否則返回NULL。
現在您知道了如何安裝和卸載一個鉤子了,接下來我們將看看鉤子函數。. 
只要您安裝的鉤子的消息事件類型發生,WINDOWS就將調用鉤子函數。譬如您安裝的鉤子是WH_MOUSE類型,那麼只要有一個鼠標事件發生時,該鉤子函數就會被調用。不管您安裝的時那一類型鉤子,鉤子函數的原型都時是一樣的: 

HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD 
  
nCode 指定是否需要處理該消息 
wParam 和 lParam 包含該消息的附加消息 
HookProc 可以看作是一個函數名的佔位符。只要函數的原型一致,您可以給該函數取任何名字。至於以上的幾個參數及返回值的具體含義各種類型的鉤子都不相同。譬如: 

WH_CALLWNDPROC 
nCode 只能是HC_ACTION,它代表有一個消息發送給了一個窗口 
wParam 如果非0,代表正被髮送的消息 
lParam 指向CWPSTRUCT型結構體變量的指針 
return value: 未使用,返回0 
WH_MOUSE 
nCode 爲HC_ACTION 或 HC_NOREMOVE 
wParam 包含鼠標的事件消息 
lParam 指向MOUSEHOOKSTRUCT型結構體變量的指針 
return value: 如果不處理返回0,否則返回非0值 
所以您必須查詢您的WIN32 API 指南來得到不同類型的鉤子的參數的詳細定義以及它們返回值的意義。這裏還有一個問題需要注意:所有的鉤子都串在一個鏈表上,最近加入的鉤子放在鏈表的頭部。當一個事件發生時,WINDOWS將按照從鏈表頭到鏈表尾調用的順序。所以您的鉤子函數有責任把消息傳到下一個鏈中的鉤子函數。當然您可以不這樣做,但是您最好明白這時這麼做的原因。在大多數的情況下,最好把消息事件傳遞下去以便其它的鉤子都有機會獲得處理這一消息的機會。調用下一個鉤子函數可以調用函數CallNextHookEx。該函數的原型如下: 

CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD 
hHook 時是您自己的鉤子函數的句柄。利用該句柄可以遍歷鉤子鏈。 
nCode, wParam and lParam 您只要把傳入的參數簡單傳給CallNextHookEx即可。 
請注意:對於遠程鉤子,鉤子函數必須放到DLL中,它們將從DLL中映射到其它的進程空間中去。當WINDOWS映射DLL到其它的進程空間中去時,不會把數據段也進行映射。簡言之,所有的進程僅共享DLL的代碼,至於數據段,每一個進程都將有其單獨的拷貝。這是一個很容易被忽視的問題。您可能想當然的以爲,在DLL中保存的值可以在所有映射該DLL的進程之間共享。在通常情況下,由於每一個映射該DLL的進程都有自己的數據段,所以在大多數的情況下您的程序運行得都不錯。但是鉤子函數卻不是如此。對於鉤子函數來說,要求DLL的數據段對所有的進程也必須相同。這樣您就必須把數據段設成共享的,這可以通過在鏈接開關中指定段的屬性來實現。在MASM中您可以這麼做: 

/SECTION:<section name>, S
已初期化的段名是.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 <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 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 

;---------------------------------------------- DLL的Makefile文件 ---------------------------------------------- 

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”和“Eixt”。當您按下Hook時,應用程序將鉤掛鼠標輸入的事件消息,該按鈕的文本將變成“Unhook”。當您把鼠標關標滑過一個窗口時,該窗口的有關消息將顯示在主窗口中。當您按下“Unhook”時,應用程序將卸載鉤子。 主窗口使用一個對話框來作爲它的主窗口。它自定義了一個消息WM_MOUSEHOOK,用來在主窗口和DLL之間傳遞消息。當主窗口接收到該消息時,wParam中包含了光標所在位置的窗口的句柄。當然這是我們做的安排。我這麼做只是爲了方便。您可以使用您自己的方法在主應用程序和DLL之間進行通訊。 

                    .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 
  

pt 是當前鼠標所在的屏幕位置。 
hwnd 是將接收鼠標消息的窗口的句柄。通常它是鼠標所在處的窗口,但是如果窗口調用了SetCapture,鼠標的輸入將到向到這個窗口。因我們不用該成員變量而是用WindowFromPoint函數。 
wHitTestCode 指定hit-test值,該值給出了更多的鼠標位置值。它指定了鼠標在窗口的那個部位。該值的完全列表,請參考WIN32 API 指南中的WM_NCHITTEST消息。 
dwExtraInfo 該值包含了相關的信息。一般該值由mouse_event函數設定,可以調用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,得到窗口過程調用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中的鉤子就不能正常工作了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章