鉤子概述
鉤子(Hook)是Windows訊息處理機制的一個要點(Point)。應用程式可以通過鉤子機制截獲處理Window訊息或是其他一些特定事件。同DOS中斷截獲處理機制類似,應用程式可以在鉤子上設定多個鉤子函數,由其組成一個與鉤子相關聯的指向鉤子函數的指標列表(鉤子鏈表)。當鉤子所監視的訊息出現時,Windows首先將其送到調用鏈表中所指向的第一個鉤子函數中,鉤子函數將根據其各自的功能對訊息進行監視、修改和控制,並在處理完成後把訊息傳遞給下一鉤子函數直至到達鉤子鏈表的末尾。在鉤子函數交出控制權後,被攔截的訊息最終仍將交還給視窗處理函數。雖然鉤子函數對訊息的過濾將會略加影響系統的運行效率,但在很多場合下通過鉤子對訊息的過濾處理可以完成一些其他方法所不能完成的特殊功能。
可以看出,鉤子的本質是一段用以處理系統訊息或特定事件的函數,通過系統調用將其掛入到系統。鉤子的種類有很多,每一種鉤子負責截獲並處理相應的訊息。鉤子機制允許應用程式截獲並處理髮往指定視窗的訊息或特定事件,其監視的視窗即可以是本進程內的也可以是由其他進程所建立的。在特定的訊息發出後、達目的視窗前,鉤子程式擁有對其控制權,此時的鉤子函數除了可以對截獲的訊息進行各種處理外,甚至還可以強行終止訊息的繼續傳遞。
對於多個鉤子的安裝,最近安裝的鉤子將被放置於鉤子鏈的開始,最早安裝的鉤子則放在最後,在鉤子監視的訊息出現時,作業系統調用鏈表開始處的第一個鉤子函數進行處理,也就是說最後加入的鉤子優先獲得控制權。這裡提到的鉤子函數必須是一個回呼函數,而且不能定義為類成員函數,只能是普通的C函數,如:
LRESULT CALLBACK HookProc(int nCode ,WPARAM wParam,LPARAM lParam);
線程局部鉤子與系統全域鉤子
鉤子根據其對訊息監視範圍的不同而分為系統全域鉤子和線程局部鉤子兩大類,其中線程局部鉤子只能監視本進程中某個指定的線程,而全域鉤子則可對在當前系統下啟動並執行所有線程進行監視。顯然,線程鉤子可以看作是全域鉤子的一個子集,全域鉤子雖然功能強大但同時實現起來也比較煩瑣:其鉤子函數的實現必須封裝在獨立的動態連結程式庫中才可以被各種相相關 App程式所使用。
雖然對於線程局部鉤子並不要求其象系統全域鉤子一樣必須放置於動態連結程式庫中,但是推薦的做法仍是將其放到動態連結程式庫中去實現。這樣的處理不僅能使鉤子為系統內的多個進程所訪問,同時也可以在系統中被直接調用。對於一個只供單進程訪問的鉤子,還可以將其鉤子處理過程放在安裝鉤子的同一個線程內。
鉤子的安裝與卸載
系統是通過調用位於鉤子鏈表最開始處的鉤子函數而進行訊息攔截處理的,因此在設定鉤子時要把回呼函數放置於鉤子鏈表的鏈首,作業系統會使其首先被調用。由函數SetWindowsHookEx()負責將回呼函數放置於鉤子鏈表的開始位置。SetWindowsHookEx()函數原型聲明為:
HHOOK SetWindowsHookEx(int idHook;HOOKPROC lpfn;HINSTANCE hMod;DWORD dwThreadId);
其中,參數idHook 指定了鉤子的類型,可以使用的類型有以下13種:
WH_CALLWNDPROC 系統將訊息發送到指定視窗之前的“鉤子”
WH_CALLWNDPROCRET 訊息已經在視窗中處理的“鉤子”
WH_CBT 基於電腦培訓的“鉤子”
WH_DEBUG 差錯“鉤子”
WH_FOREGROUNDIDLE 前台空閑視窗“鉤子”
WH_GETMESSAGE 接收訊息投遞的“鉤子”
WH_JOURNALPLAYBACK 回放以前通過WH_JOURNALRECORD“鉤子”記錄的輸入訊息
WH_JOURNALRECORD 輸入訊息記錄“鉤子”
WH_KEYBOARD 鍵盤訊息“鉤子”
WH_MOUSE 滑鼠訊息“鉤子”
WH_MSGFILTER 對話方塊、訊息框、菜單或捲軸輸入訊息“鉤子”
WH_SHELL 外殼“鉤子”
WH_SYSMSGFILTER 系統訊息“鉤子”
參數lpfn為指向鉤子函數的指標,也即回呼函數的首地址;參數hMod標識了鉤子處理函數所處模組的控制代碼;參數dwThreadId 指定被監視的線程,如果明確指定了某個線程的ID就只監視該線程,此時的鉤子即為線程鉤子;如果該參數被設定為0,則表示此鉤子為監視系統所有線程的全域鉤子。此函數在執行完後將返回一個鉤子控制代碼。
在SetWindowsHookEx()函數完成對鉤子的安裝後,如果被監視的事件發生,系統會立即調用位於相應鉤子鏈表開始處的鉤子函數進行處理,每一個鉤子函數在進行處理時都要考慮是否需要把事件傳遞給下一個鉤子處理函數。如果需要傳遞,就要調用函數CallNestHookEx()。儘管在理論上不調用CallNestHookEx()也並不算錯,但在實際使用時還是強烈建議無論是否需要進行事件傳遞都要在過程的最後調用一次CallNextHookEx( ),否則將會引起一些無法預知的系統行為或是系統鎖定。該函數將返回位於鉤子鏈表中的下一個鉤子處理過程的地址,至於具體的傳回值類型則要視所設定的鉤子類型而定。CallNextHookEx( )的函數原型為:
LRESULT CallNextHookEx(HHOOK hhk;int nCode;WPARAM wParam;LPARAM lParam);
其中,參數hhk為由SetWindowsHookEx()函數返回的當前鉤子控制代碼;參數nCode為傳給鉤子過程的事件代碼;參數wParam和lParam 則為傳給鉤子處理函數的參數值,其具體含義同設定的鉤子類型有關。
由於安裝鉤子對系統的效能有一定的影響,所以在鉤子使用完畢後應及時將其卸載以釋放其所佔資源。釋放鉤子的函數為UnhookWindowsHookEx(),該函數比較簡單只有一個參數用於指定此前由SetWindowsHookEx()函數所返回的鉤子控制代碼,原型聲明如下:
BOOL UnhookWindowsHookEx(HHOOK hhk);
使用滑鼠鉤子
由於系統全域鉤子在功能上完全覆蓋了線程局部鉤子,因此其實際使用範圍要遠比線程局部鉤子廣泛的多。本節也由此著重對系統全域鉤子的使用進行介紹。
滑鼠鉤子是鉤子中比較常用也是使用比較簡單的一類鉤子。下面給出的應用樣本將通過安裝滑鼠全域鉤子來捕獲滑鼠當前所處視窗的視窗標題。由於本常式使用了全域鉤子,因此首先構造全域鉤子的載體——動態連結程式庫。考慮到 Win32 DLL與Win16 DLL存在的差別,在Win32環境下要在多個進程間共用資料,就必須採取一些措施將待共用的資料提取到一個獨立的資料區段,並通過def檔案將其屬性設定為讀寫共用:
#pragma data_seg("mydata")
HWND glhPrevTarWnd = NULL; // 上次滑鼠所指的視窗控制代碼
HWND glhDisplayWnd = NULL; // 顯示目標視窗標題編輯框的控制代碼
HWND glhHook = NULL; // 安裝的滑鼠鉤子控制代碼
HINSTANCE glhInstance = NULL; // DLL執行個體控制代碼
#pragma data_seg()
……
SECTIONS // def檔案中將資料區段TestData設定為讀寫共用
TestData READ WRITE SHARED
在完成上述準備工作後,在動態庫輸出函數StartHook()中調用SetWindowsHookEx()函數完成對全域滑鼠鉤子的安裝,設定滑鼠鉤子函數為MouseProc(),安裝函數返回的鉤子控制代碼儲存於變數glhHook中:
BOOL CMouseHook::StartHook(HWND hWnd)
{
BOOL result = FALSE;
// 安裝鉤子
glhHook = (HWND)SetWindowsHookEx(WH_MOUSE, MouseProc, glhInstance, 0);
if (glhHook != NULL)
result = TRUE;
glhDisplayWnd = hWnd; // 設定顯示目標視窗標題編輯框的控制代碼
return result;
}
在滑鼠鉤子安裝完畢後,系統內的任何滑鼠動作所發出的滑鼠訊息均要經過鉤子函數MouseProc()的攔截過濾處理。在此進行的處理是通過擷取當前滑鼠所在位置下的視窗控制代碼,並以此進一步得到視窗標題。在處理完成後,調用CallNextHookEx()函數將本事件傳遞到鉤子鏈表中的下一個鉤子函數:
LRESULT WINAPI MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
LPMOUSEHOOKSTRUCT pMouseHook = (MOUSEHOOKSTRUCT FAR *) lParam;
if (nCode >= 0) {
HWND glhTargetWnd = pMouseHook->hwnd; // 取目標視窗控制代碼
HWND ParentWnd = glhTargetWnd;
while (ParentWnd != NULL){
glhTargetWnd = ParentWnd;
ParentWnd = GetParent(glhTargetWnd); // 取應用程式主視窗控制代碼
}
if (glhTargetWnd != glhPrevTarWnd) {
char szCaption[100];
GetWindowText(glhTargetWnd, szCaption, 100); // 取目標視窗標題
if (IsWindow(glhDisplayWnd))
SendMessage(glhDisplayWnd, WM_SETTEXT, 0, (LPARAM)(LPCTSTR)szCaption);
glhPrevTarWnd = glhTargetWnd; // 儲存目標視窗
}
}
// 繼續傳遞訊息
return CallNextHookEx((HHOOK)glhHook, nCode, wParam, lParam);
}
此動態連結程式庫還提供有輸出函數StopHook(),調用程式通過對此函數的調用完成對先前載入鉤子的卸載。在此輸出函數內則是通過UnhookWindowsHookEx()函數來卸載指定鉤子的:
BOOL CMouseHook::StopHook()
{
BOOL result = FALSE;
if (glhHook){
result = UnhookWindowsHookEx((HHOOK)glhHook); // 卸載鉤子
if (result)
glhDisplayWnd = glhPrevTarWnd = glhHook = NULL;
}
return result;
}
通過編譯、連結可以得到有關滑鼠全域鉤子的動態連結程式庫,在經過調用程式對其的調用後,可以實現對在當前系統所有線程中的滑鼠訊息的攔截處理。在鉤子動態連結程式庫載入到進程後,只需調用輸出函數StartHook()安裝好全域鉤子即可對滑鼠訊息進行攔截過濾,在調用程式退出前調用輸出函數StopHook()卸載鉤子。
使用鍵盤鉤子
鍵盤鉤子同滑鼠鉤子一樣,也是一類較常用的鉤子。而且總的來說鍵盤鉤子的使用與滑鼠鉤子的使用也是比較類似的,下面通過一個程式執行個體來對鍵盤鉤子的使用進行介紹,在此常式中通過設定鍵盤全域鉤子來記錄當前系統輸入的字元,並將攔截到的字元按日期儲存到檔案。
由於本常式也使用的系統全域鉤子,因此由調用程式和動態連結程式庫程式兩部分組成。動態連結程式庫程式提供了兩個輸出函數InstallLaunchEv()和UnInstall(),分別用於鍵盤鉤子的安裝與卸載:
DllExport void WINAPI InstallLaunchEv()
{
glhHook = (HHOOK)SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)LauncherHook, theApp.m_hInstance, 0); // 安裝鍵盤鉤子
}
DllExport void WINAPI UnInstall()
{
if (glhHook) {
BOOL result = UnhookWindowsHookEx((HHOOK)glhHook); // 卸載鉤子
if (result)
glhHook = NULL;
}
}
在鉤子安裝函數中,通過SetWindowsHookEx()設定了鍵盤鉤子函數LauncherHook()。在鍵盤事件發生後此鉤子函數將被調用,通過SaveLog()函數將捕獲到的字元儲存到檔案:
LRESULT CALLBACK LauncherHook(int nCode, WPARAM wParam, LPARAM lParam)
{
// 將鍵盤事件傳遞給下一鉤子函數
LRESULT Result = CallNextHookEx(glhHook, nCode, wParam, lParam);
if (nCode == HC_ACTION) // 處理鍵盤動作
{
if (lParam & 0x80000000) // 儲存鍵盤動作到檔案
{
char c[1];
c[0] = wParam;
SaveLog(c);
if (c[0] == 13) {
c[0] = 10;
SaveLog(c);
}
}
}
return Result;
}
至於調用程式則非常簡單,只是在程式開始運行後和在程式退出前分別調用動態連結程式庫提供的鉤子設定函數和鉤子卸載函數即可。
鉤子的種類有很多,從前面給出的滑鼠鉤子和鍵盤鉤子也可以看出其編寫過程是非常類似的。通常情況下作為鉤子載體的動態連結程式庫要提供有兩個必要的輸出函數——鉤子安裝函數和鉤子卸載函數。在鉤子安裝函數中通過SetWindowsHookEx()為不同類型的鉤子設定相應的鉤子函數,實質性的工作也主要是在鉤子函數中完成的,具體情況應視鉤子的類型和要完成的功能而定。在鉤子卸載函數中則主要是對UnhookWindowsHookEx()的調用,並輔以必要的保護性代碼。