WINDOWS鉤子函式(轉)

heying1229發表於2007-07-28
WINDOWS鉤子函式:

  本課中我們將要學習WINDOWS鉤子函式的使用方法。WINDOWS鉤子函式的功能非常強大,有了它您可以探測其它程式並且改變其它程式的行為。
理論:
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:

, 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

.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中的鉤子就不能正常工作了。

[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10172717/viewspace-928763/,如需轉載,請註明出處,否則將追究法律責任。

相關文章