天天看點

《windows核心程式設計系列》十八談談windows鈎子

  windows應用程式是基于消息驅動的。各種應用程式對各種消息作出響應進而實作各種功能。

      windows鈎子是windows消息處理機制的一個監視點,通過安裝鈎子可以達到監視指定視窗某種類型的消息的功能。所謂的指定視窗并不局限于目前程序的視窗,也可以是其他程序的視窗。當監視的某一消息到達指定的視窗時,在指定的視窗處理消息之前,鈎子函數将截獲此消息,鈎子函數既可以加工處理該消息,也可以不作任何處理繼續傳遞該消息。使用鈎子是實作dll注入的方法之一。其他常用的方法有:系統資料庫注入,遠端線程注入。

       鈎子函數是一個處理消息的程式段。是在安裝鈎子的時候向系統注冊的。

關于windows鈎子要清楚以下三點:

     1:鈎子是用來截獲系統中的消息流的。利用鈎子可以處理任何我們感興趣的消息,當然包括其他程序的消息。

    2:截獲該消息後,用于處理該消息的程式叫做鈎子函數。它是自定義的函數,在安裝鈎子時将此函數的位址告訴windows。

3:系統同一時間可能有多個程序安裝鈎子,多個鈎子構成鈎子鍊。是以截獲消息并處理後,應該将此消息繼續傳遞下去,以便其他鈎子處理這一消息。

      注意:使用鈎子會使系統變慢,因為它增加了系統對每個消息的處理量。是以要僅在必要的時候才安裝鈎子。不需要時要及時解除安裝。

  安裝鈎子:

  1:

[cpp] view plain copy

  1. SetWindowsHookEx(  
  2.          int idHook,                  //要安裝的鈎子的類型。  
  3.          HOOKPROC lpfn,                  //鈎子函數的位址。  
  4.          HINSTANCE hMode,               //鈎子函數所在DLL在程序内的位址。  
  5.          DWORD     dwThread,            //要安裝鈎子的線程。如為0,則為所有線程安裝鈎子。  
  6.          );  

idHook指定要安裝鈎子的類型,他可以是下面的值:

     WH_CALLWNDPROC           //目标線程調用SendMessage發送消息時,鈎子函數被調用。

     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是鈎子函數的位址。鈎子安裝後如果有相應的消息發生,windows将調用此參數指向的函數。一般鈎子函數都是位于一個DLL中。當為其他程序内的線程安裝鈎子時,如果鈎子函數在DLL中,系統會把DLL映射到那個程序内,使他能在該程序内被調用。

     注意:鈎子函數多是被其他程序内的線程調用,而不一定是安裝鈎子的線程。

鈎子函數被調用的過程:

      當程序A一個線程準備向一個視窗發送一個消息,系統檢查該線程是否被安裝了鈎子,如果該線程被安裝了鈎子且該消息與鈎子要截獲的消息類型一緻,此消息将被截獲。系統檢查該鈎子的鈎子函數所在的DLL是否已經被映射程序A的位址空間中。如果尚未映射,系統會強制将該DLL映射到程序A的位址空間。然後獲得鈎子函數在程序A的虛拟位址,并調用鈎子函數。我們可以在鈎子函數内定義我們對該消息處理的過程。

      注意:當系統把鈎子函數所在的DLL映射到某個程序位址空間時,會映射整個DLL,而不僅僅是鈎子函數,這也就說我們可以使用該DLL中的所有導出函數。

     3:hmod參數是鈎子函數所在dll的執行個體句柄,也是該dll在程序内的虛拟位址。如果鈎子函數在目前程序中,此參數應被指定為NULL.

    4:dwThreadid指定要被安裝鈎子的線程的ID号。如果被設為0,就會為系統内的所有GUI線程安裝鈎子。

   5:鈎子函數

   鈎子被安裝後,如果有相應的消息發生,windows将調用鈎子函數。以下為鈎子函數的原型:

  1. LRESULT CALLBACK HookProc(int nCode,WPARAM wParam,LPARAM lParam)  
  2.  {  
  3.    //處理消息的代碼。  
  4.     return CallNextHookEx(hHook,nCode,wParam,lParam);  
  5.  }  

    HookProc為鈎子函數的名稱。

    nCode指定是否必須處理該消息。如果它為HC_ACTION,那麼鈎子函數就必須處理該消息。如果小于0,鈎子函數就必須将該消息傳遞給CallNextHookEx,不對該消息進行處理,并傳回CallNextHookEx的傳回值。

    CallNextHookEx用于把消息傳遞到鈎子鍊中下一個鈎子函數。

    wParam和lParam的值依賴于具體的鈎子類型。請參考MSDN。 

    解除安裝鈎子。

     BOOL UnhookWindowsHookEx(HHOOK hhk);

     hhk為要解除安裝的鈎子句柄。

     下面将要實作一個例子,實作對鍵盤按鍵的監控。一旦有鍵盤被按下,就在主程式視窗顯示一條資訊訓示哪一個鍵被按下。

    程式外觀:

《windows核心程式設計系列》十八談談windows鈎子

    首先要實作DLL:

     在dll内實作鈎子函數這是毫無疑問的。而安裝鈎子和解除安裝鈎子的函數既可以寫在主程式内,也可以寫在DLL内。寫在主程式内時隻可以在主程式内安裝鈎子。而在dll内實作則可以讓所有載入該dll的程式安裝鈎子。如當某程序将該DLL載入的時候,可以在DllMain中建立一個線程,讓他調用安裝鈎子的函數,實作為此程序内的線程安裝鈎子的目的。為了拓展程式的功能,實作代碼重用,最好是将鈎子函數寫在DLL内。另外這也可以實作子產品化。一旦需求發生更改可以隻修改DLL内的代碼,而不需要改變主程式。

      當鈎子函數被調用的時候,也就是我們被攔截的消息已被觸發,如何讓主程式得到這個通知呢 ?

      我們可以在其他程序内的鈎子函數内給主程式的視窗發送消息。但如何發送呢?

      PostMessage可以實作這個功能。

      看原型:

  1. BOOL WINAPI PostMessage(HWND hWnd,UINT Msg,WPARAM wparam,LPARAM lParam);  

     hWnd即為要接受消息的視窗句柄。

     Msg為要發送的消息。

    wParam和lParam為消息的附加參數。

    雖然可以使用PostMessage實作向主程式的視窗發送消息,但是我們如何獲得主程式的視窗句柄呢?我們知道鈎子函數是在DLL内實作的,而DLL會被加載到各個程序内。在其他程序要想得到主程式的視窗句柄這是一個問題。

    在《windows核心程式設計系列》談談記憶體映射檔案中,我們談到了在可執行檔案内使用共享段,可以實作同一個可執行檔案的多個執行個體共享共享段内的資料的目的。那麼在DLL使用共享段呢?哈哈,或許你已經猜出來了,由于DLL被映射到了各個程序,将資料放在DLL的共享段,可以實作在各個程序内共享DLL内共享段資料的目的。

     我們的解決方法就是:在DLL内建立共享段,将主程式的視窗句柄放在共享段中。在主程式調用安裝鈎子的函數時可以将共享段内的視窗句柄賦為主程式的視窗句柄。進而達到在各個程序内共享資料的目的。到此,我們又學習一種在程序間共享資料的方法,另一種方法是利用記憶體映射檔案。

建立和設定共享段的代碼:可以參考《windows核心程式設計》談談記憶體映射檔案。

  1. <span style="font-size:18px;"> #pragma data_seg("shared")  
  2.   HWND hWnd=NULL;  
  3.   HHOOK hHook=NULL;  
  4.  #pragma data_seg()  
  5. #pragma comment(linker,"/SECTION:shared,RWS")  
  6. </span>  

     怎麼多了個hHook,hHook是建立的鈎子的句柄。由于在鈎子函數中會調用CallNextHookEx将消息傳給鈎子鍊的下一結點。二者都是在其他程序調用的,是以我們也必須把鈎子的句柄設為共享。

     DLL内建立鈎子的代碼:

  1. <span style="font-size:18px;">    KEYHOOKDLL_API bool SetHook(</span>  
  1. <span style="font-size:18px;">                            bool IsInstall,//true表示安裝鈎子,false表示解除安裝鈎子。</span>  
  1. <span style="font-size:18px;">                            HWND hWnd,     //主程式視窗句柄,用于在主程式内傳入設定。</span>  
  1. <span style="font-size:18px;">                              int ThreadId)//要安裝鈎子的線程。  
  2.    {  
  3.     ::hWnd=hWnd;//将目前視窗句柄賦給DLL共線段内的視窗句柄。  
  4.     if(IsInstall)  
  5.     {  
  6.         hHook=SetWindowsHookEx( WH_KEYBOARD,KeyHookProc,GetModuleHandle  </span>  
  1. <span style="font-size:18px;">                                           ("keyhookdll"),ThreadId);  
  2.         return true;  
  3.     }  
  4.     else  
  5.         UnhookWindowsHookEx(hHook);  
  6. }</span>  

      建立的鈎子類型為WH_KEYBOARD,他可以攔截WM_KEYDOWN 和WM_KEYUP 消息。具體請參考MSDN.

建立鈎子函數功能很簡單,僅僅安裝鈎子和設定共享段内的資料。Thread為要安裝鈎子的線程。主程式在調用時傳入0,表示為所有線程安裝鈎子。

   再看鈎子函數:

  1. LRESULT CALLBACK KeyHookProc(int nCode ,WPARAM wParam,LPARAM lParam)  
  2. {  
  3.     if(nCode<0||nCode==HC_NOREMOVE)  
  4.         return CallNextHookEx(hHook,nCode,wParam,lParam);  
  5.     if(lParam&0x40000000)//隻對WM_DOWN進行響應。  
  6.       {  
  7.         PostMessage(hWnd,WM_KEYDOWN,wParam,lParam);  
  8.       }      
  1. return CallNextHookEx(hHook,nCode,wParam,lParam);  

   在鈎子函數中首先判斷nCode的值,當他小于零時應該直調用CallNextHookEx,除此之外它也可以有以下取值:

    ACTION:說明wParam和lParam包含按鍵消息的資訊,可以處理。

    HC_NOREMOVE:說明wParam和lParam包含按鍵消息的資訊,但該消息沒有被從消息隊列中移除。即程式是調用PeekMessage來查詢消息隊列内的消息的。

     ( 與GetMessage的差別與聯系:他們都從消息隊列内查詢消息,有消息時将此消息發送出去,GetMessage在消息隊列沒有消息時會一直等待,直到有消息到達時才傳回。而PeekMessage無論消息隊列中是否有消息都立即傳回。)

     是以當檢測到nCode小于0或者為WH_NOREMOVE時不能對消息進行處理而要直接調用CallNextHookEx。lParam的第30位為1時說明此時鍵被按下,為零時說明鍵被彈起。此處進行了判斷,僅在鍵被按下時向視窗發送消息。防止消息每次擊鍵發送兩次消息。

     當某消息到達時我們給主程式視窗發送的消息為使用者自定義消息:WM_KEY

     他被定義為#define WM_KEY  WM_USER+1

     在主程式内我們必須自己實作相應此消息的消息處理函數。

     原型為:

  1. afx_msg LRESULT OnKey(WPARAM wParam,LPARAM lParam);  

   實作:

  1.      char keyname[100];  
  2. ::GetKeyNameText(lParam,keyname,100);//獲得按鍵的鍵名。  
  3. CString a;  
  4. a.Format("使用者按鍵:%s\r\n",keyname);  
  5. m_output+=a;  
  6. UpdateData(false);  
  7. ::MessageBeep(MB_OK);  
  8. CEdit *edit=(CEdit*)GetDlgItem(IDC_EDIT_OUTPUT);  
  9. edit->LineScroll(edit->GetLineCount());  
  10. return 0;