程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 《windows核心編程系列》談談windows鉤子

《windows核心編程系列》談談windows鉤子

編輯:關於C語言

 

windows應用程序是基於消息驅動的。各種應用程序對各種消息作出響應從而實現各種功能。

 

      windows鉤子是windows消息處理機制的一個監視點,通過安裝鉤子可以達到監視指定窗口某種類型的消息的功能。所謂的指定窗口並不局限於當前進程的窗口,也可以是其他進程的窗口。當監視的某一消息到達指定的窗口時,在指定的窗口處理消息之前,鉤子函數將截獲此消息,鉤子函數既可以加工處理該消息,也可以不作任何處理繼續傳遞該消息。使用鉤子是實現dll注入的方法之一。其他常用的方法有:注冊表注入,遠程線程注入。

 

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

 

關於windows鉤子要清楚以下三點:

 

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

 

    2:截獲該消息後,用於處理該消息的程序叫做鉤子函數。它是自定義的函數,在安裝鉤子時將此函數的地址告訴windows。

 

3:系統同一時間可能有多個進程安裝鉤子,多個鉤子構成鉤子鏈。所以截獲消息並處理後,應該將此消息繼續傳遞下去,以便其他鉤子處理這一消息。

 

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

 

  安裝鉤子:

 

  1:

 

 

SetWindowsHookEx( 

 

         int idHook,                  //要安裝的鉤子的類型。 

 

         HOOKPROC lpfn,                  //鉤子函數的地址。 

 

         HINSTANCE hMode,               //鉤子函數所在DLL在進程內的地址。 

 

         DWORD     dwThread,            //要安裝鉤子的線程。如為0,則為所有線程安裝鉤子。 

 

         ); 

 

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將調用鉤子函數。以下為鉤子函數的原型:

 

 

 

 

LRESULT CALLBACK HookProc(int nCode,WPARAM wParam,LPARAM lParam) 

 

 { 

 

   //處理消息的代碼。 

 

    return CallNextHookEx(hHook,nCode,wParam,lParam); 

 

 } 

 

 

 

    HookProc為鉤子函數的名稱。

 

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

 

    CallNextHookEx用於把消息傳遞到鉤子鏈中下一個鉤子函數。

 

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

 

    卸載鉤子。

 

     BOOL UnhookWindowsHookEx(HHOOK hhk);

 

     hhk為要卸載的鉤子句柄。

 

 

 

     下面將要實現一個例子,實現對鍵盤按鍵的監控。一旦有鍵盤被按下,就在主程序窗口顯示一條信息指示哪一個鍵被按下。

 

    程序外觀:

 

\

 

 

 

    首先要實現DLL:

 

     在dll內實現鉤子函數這是毫無疑問的。而安裝鉤子和卸載鉤子的函數既可以寫在主程序內,也可以寫在DLL內。寫在主程序內時只可以在主程序內安裝鉤子。而在dll內實現則可以讓所有載入該dll的程序安裝鉤子。如當某進程將該DLL載入的時候,可以在DllMain中創建一個線程,讓他調用安裝鉤子的函數,實現為此進程內的線程安裝鉤子的目的。為了拓展程序的功能,實現代碼重用,最好是將鉤子函數寫在DLL內。另外這也可以實現模塊化。一旦需求發生更改可以只修改DLL內的代碼,而不需要改變主程序。

 

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

 

      我們可以在其他進程內的鉤子函數內給主程序的窗口發送消息。但如何發送呢?

 

      PostMessage可以實現這個功能。

 

      看原型:

 

 

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核心編程》談談內存映射文件。

 

 

 

 

 

<span style="font-size:18px;"> #pragma data_seg("shared") 

  HWND hWnd=NULL; 

  HHOOK hHook=NULL; 

 

 #pragma data_seg() 

 

#pragma comment(linker,"/SECTION:shared,RWS") 

 

</span> 

 

 

 

     怎麼多了個hHook,hHook是創建的鉤子的句柄。由於在鉤子函數中會調用CallNextHookEx將消息傳給鉤子鏈的下一結點。二者都是在其他進程調用的,因此我們也必須把鉤子的句柄設為共享。

 

 

 

     DLL內創建鉤子的代碼:

 

 

<span style="font-size:18px;">    KEYHOOKDLL_API bool SetHook(</span> 

 

<span style="font-size:18px;">                            bool IsInstall,//true表示安裝鉤子,false表示卸載鉤子。</span> 

 

<span style="font-size:18px;">                            HWND hWnd,     //主程序窗口句柄,用於在主程序內傳入設置。</span> 

 

<span style="font-size:18px;">                              int ThreadId)//要安裝鉤子的線程。 

   { 

    ::hWnd=hWnd;//將當前窗口句柄賦給DLL共線段內的窗口句柄。 

    if(IsInstall) 

    { 

        hHook=SetWindowsHookEx( WH_KEYBOARD,KeyHookProc,GetModuleHandle  </span> 

 

<span style="font-size:18px;">                                           ("keyhookdll"),ThreadId); 

        return true; 

 

    } 

    else 

    { 

        UnhookWindowsHookEx(hHook); 

        return true; 

 

    } 

 

}</span> 

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

 

創建鉤子函數功能很簡單,僅僅安裝鉤子和設置共享段內的數據。Thread為要安裝鉤子的線程。主程序在調用時傳入0,表示為所有線程安裝鉤子。

 

 

 

   再看鉤子函數:

 

 

LRESULT CALLBACK KeyHookProc(int nCode ,WPARAM wParam,LPARAM lParam) 

    if(nCode<0||nCode==HC_NOREMOVE) 

    { 

        return CallNextHookEx(hHook,nCode,wParam,lParam); 

    } 

    if(lParam&0x40000000)//只對WM_DOWN進行響應。 

      { 

        PostMessage(hWnd,WM_KEYDOWN,wParam,lParam); 

      }     

 

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

 

     在主程序內我們必須自己實現相應此消息的消息處理函數。

 

     原型為:

 

 

afx_msg LRESULT OnKey(WPARAM wParam,LPARAM lParam); 

   實現:

 

 

     char keyname[100]; 

::GetKeyNameText(lParam,keyname,100);//獲得按鍵的鍵名。 

CString a; 

a.Format("用戶按鍵:%s\r\n",keyname); 

m_output+=a; 

UpdateData(false); 

::MessageBeep(MB_OK); 

CEdit *edit=(CEdit*)GetDlgItem(IDC_EDIT_OUTPUT); 

edit->LineScroll(edit->GetLineCount()); 

return 0; 

    到此為止各主要函數都介紹完畢,剩下都是如何創建dll。此處不再介紹。例子程序2011年12月2日下午實現。

 

 

 

總結:以上程序花了近三個小時實現,此程序看似容易但一旦自己動手實現各種問題接踵而至。所以以後要經常動手實現一些看似容易的程序,不要眼高手低。打這些字的時候鍵盤監控程序仍在工作,顯示著我按下的每一個鍵。有明顯的電腦感覺速度比平常慢了不少,看來使用鉤子,尤其是系統范圍內的鉤子會導致很大的overhead。

 

    windows核心編程中談到注入dll的幾種方式。其中介紹了使用windows鉤子,但是介紹的很簡單。以上內容參考自《windows核心編程》第五版,第四部分和《windows程序設計》第二版,王艷平著。如有錯誤,請指正


 

摘自 ithzhang的專欄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved