windows應用程序是基于消息驅動的。各種應用程序對各種消息作出響應從而實現各種功能。 windows鉤子是windows消息處理機制的一個監(jiān)視點,通過安裝鉤子可以達到監(jiān)視指定窗口某種類型的消息的功能。所謂的指定窗口并不局限于當前進程的窗口,也可以是其他進程的窗口。當監(jiān)視的某一消息到達指定的窗口時,在指定的窗口處理消息之前,鉤子函數將截獲此消息,鉤子函數既可以加工處理該消息,也可以不作任何處理繼續(xù)傳遞該消息。使用鉤子是實現dll注入的方法之一。其他常用的方法有:注冊表注入,遠程線程注入。 鉤子函數是一個處理消息的程序段。是在安裝鉤子的時候向系統(tǒng)注冊的。 關于windows鉤子要清楚以下三點: 1:鉤子是用來截獲系統(tǒng)中的消息流的。利用鉤子可以處理任何我們感興趣的消息,當然包括其他進程的消息。 2:截獲該消息后,用于處理該消息的程序叫做鉤子函數。它是自定義的函數,在安裝鉤子時將此函數的地址告訴windows。 3:系統(tǒng)同一時間可能有多個進程安裝鉤子,多個鉤子構成鉤子鏈。所以截獲消息并處理后,應該將此消息繼續(xù)傳遞下去,以便其他鉤子處理這一消息。 注意:使用鉤子會使系統(tǒng)變慢,因為它增加了系統(tǒng)對每個消息的處理量。所以要僅在必要的時候才安裝鉤子。不需要時要及時卸載。 安裝鉤子: 1: [cpp] view plaincopy
WH_CALLWNDPROC //目標線程調用SendMessage發(fā)送消息時,鉤子函數被調用。 WH_CALLWNDPROCRET //當SendMessage返回時,鉤子函數被調用。 WH_KEYBOARD //從消息隊列中查詢WM_KEYUP或WM_KEYDOWN時。 WH_GETMESSAGE //目標線程調用GetMessage或PeekMessage時 WH_MOUSE //查詢消息隊列中鼠標事件消息時。 WH_MSGFILTER //以下請參考MSDN。 WH_SYSMSGFILTER WH_JORNALRECORD WHJORNALPLAYBACK WH_SHELL WH_CBT WH_FOREGROUNDIDLE WH_DEBUG 2 : lpfn是鉤子函數的地址。鉤子安裝后如果有相應的消息發(fā)生,windows將調用此參數指向的函數。一般鉤子函數都是位于一個DLL中。當為其他進程內的線程安裝鉤子時,如果鉤子函數在DLL中,系統(tǒng)會把DLL映射到那個進程內,使他能在該進程內被調用。 注意:鉤子函數多是被其他進程內的線程調用,而不一定是安裝鉤子的線程。 鉤子函數被調用的過程: 當進程A一個線程準備向一個窗口發(fā)送一個消息,系統(tǒng)檢查該線程是否被安裝了鉤子,如果該線程被安裝了鉤子且該消息與鉤子要截獲的消息類型一致,此消息將被截獲。系統(tǒng)檢查該鉤子的鉤子函數所在的DLL是否已經被映射進程A的地址空間中。如果尚未映射,系統(tǒng)會強制將該DLL映射到進程A的地址空間。然后獲得鉤子函數在進程A的虛擬地址,并調用鉤子函數。我們可以在鉤子函數內定義我們對該消息處理的過程。 注意:當系統(tǒng)把鉤子函數所在的DLL映射到某個進程地址空間時,會映射整個DLL,而不僅僅是鉤子函數,這也就說我們可以使用該DLL中的所有導出函數。 3:hmod參數是鉤子函數所在dll的實例句柄,也是該dll在進程內的虛擬地址。如果鉤子函數在當前進程中,此參數應被指定為NULL. 4:dwThreadid指定要被安裝鉤子的線程的ID號。如果被設為0,就會為系統(tǒng)內的所有GUI線程安裝鉤子。 5:鉤子函數 鉤子被安裝后,如果有相應的消息發(fā)生,windows將調用鉤子函數。以下為鉤子函數的原型:
[cpp] view plaincopy
HookProc為鉤子函數的名稱。 nCode指定是否必須處理該消息。如果它為HC_ACTION,那么鉤子函數就必須處理該消息。如果小于0,鉤子函數就必須將該消息傳遞給CallNextHookEx,不對該消息進行處理,并返回CallNextHookEx的返回值。 CallNextHookEx用于把消息傳遞到鉤子鏈中下一個鉤子函數。 wParam和lParam的值依賴于具體的鉤子類型。請參考MSDN。 卸載鉤子。 BOOL UnhookWindowsHookEx(HHOOK hhk); hhk為要卸載的鉤子句柄。
下面將要實現一個例子,實現對鍵盤按鍵的監(jiān)控。一旦有鍵盤被按下,就在主程序窗口顯示一條信息指示哪一個鍵被按下。 程序外觀:
首先要實現DLL: 在dll內實現鉤子函數這是毫無疑問的。而安裝鉤子和卸載鉤子的函數既可以寫在主程序內,也可以寫在DLL內。寫在主程序內時只可以在主程序內安裝鉤子。而在dll內實現則可以讓所有載入該dll的程序安裝鉤子。如當某進程將該DLL載入的時候,可以在DllMain中創(chuàng)建一個線程,讓他調用安裝鉤子的函數,實現為此進程內的線程安裝鉤子的目的。為了拓展程序的功能,實現代碼重用,最好是將鉤子函數寫在DLL內。另外這也可以實現模塊化。一旦需求發(fā)生更改可以只修改DLL內的代碼,而不需要改變主程序。 當鉤子函數被調用的時候,也就是我們被攔截的消息已被觸發(fā),如何讓主程序得到這個通知呢 ? 我們可以在其他進程內的鉤子函數內給主程序的窗口發(fā)送消息。但如何發(fā)送呢? PostMessage可以實現這個功能。 看原型: [cpp] view plaincopy
Msg為要發(fā)送的消息。 wParam和lParam為消息的附加參數。 雖然可以使用PostMessage實現向主程序的窗口發(fā)送消息,但是我們如何獲得主程序的窗口句柄呢?我們知道鉤子函數是在DLL內實現的,而DLL會被加載到各個進程內。在其他進程要想得到主程序的窗口句柄這是一個問題。 在《windows核心編程系列》談談內存映射文件中,我們談到了在可執(zhí)行文件內使用共享段,可以實現同一個可執(zhí)行文件的多個實例共享共享段內的數據的目的。那么在DLL使用共享段呢?哈哈,或許你已經猜出來了,由于DLL被映射到了各個進程,將數據放在DLL的共享段,可以實現在各個進程內共享DLL內共享段數據的目的。 我們的解決方法就是:在DLL內建立共享段,將主程序的窗口句柄放在共享段中。在主程序調用安裝鉤子的函數時可以將共享段內的窗口句柄賦為主程序的窗口句柄。從而達到在各個進程內共享數據的目的。到此,我們又學習一種在進程間共享數據的方法,另一種方法是利用內存映射文件。 建立和設置共享段的代碼:可以參考《windows核心編程》談談內存映射文件。
[cpp] view plaincopy
怎么多了個hHook,hHook是創(chuàng)建的鉤子的句柄。由于在鉤子函數中會調用CallNextHookEx將消息傳給鉤子鏈的下一結點。二者都是在其他進程調用的,因此我們也必須把鉤子的句柄設為共享。
DLL內創(chuàng)建鉤子的代碼: [cpp] view plaincopy
[cpp] view plaincopy
[cpp] view plaincopy
[cpp] view plaincopy
[cpp] view plaincopy
創(chuàng)建的鉤子類型為WH_KEYBOARD,他可以攔截WM_KEYDOWN 和WM_KEYUP 消息。具體請參考MSDN. 創(chuàng)建鉤子函數功能很簡單,僅僅安裝鉤子和設置共享段內的數據。Thread為要安裝鉤子的線程。主程序在調用時傳入0,表示為所有線程安裝鉤子。
再看鉤子函數: [cpp] view plaincopy
[cpp] view plaincopy
在鉤子函數中首先判斷nCode的值,當他小于零時應該直調用CallNextHookEx,除此之外它也可以有以下取值: ACTION:說明wParam和lParam包含按鍵消息的信息,可以處理。 HC_NOREMOVE:說明wParam和lParam包含按鍵消息的信息,但該消息沒有被從消息隊列中移除。即程序是調用PeekMessage來查詢消息隊列內的消息的。 ( 與GetMessage的區(qū)別與聯系:他們都從消息隊列內查詢消息,有消息時將此消息發(fā)送出去,GetMessage在消息隊列沒有消息時會一直等待,直到有消息到達時才返回。而PeekMessage無論消息隊列中是否有消息都立即返回。) 因此當檢測到nCode小于0或者為WH_NOREMOVE時不能對消息進行處理而要直接調用CallNextHookEx。lParam的第30位為1時說明此時鍵被按下,為零時說明鍵被彈起。此處進行了判斷,僅在鍵被按下時向窗口發(fā)送消息。防止消息每次擊鍵發(fā)送兩次消息。
當某消息到達時我們給主程序窗口發(fā)送的消息為用戶自定義消息:WM_KEY 在主程序內我們必須自己實現相應此消息的消息處理函數。 原型為: [cpp] view plaincopy
實現: [cpp] view plaincopy
到此為止各主要函數都介紹完畢,剩下都是如何創(chuàng)建dll。此處不再介紹。例子程序2011年12月2日下午實現。
總結:以上程序花了近三個小時實現,此程序看似容易但一旦自己動手實現各種問題接踵而至。所以以后要經常動手實現一些看似容易的程序,不要眼高手低。打這些字的時候鍵盤監(jiān)控程序仍在工作,顯示著我按下的每一個鍵。有明顯的電腦感覺速度比平常慢了不少,看來使用鉤子,尤其是系統(tǒng)范圍內的鉤子會導致很大的overhead。 |
|