Windows消息攔截技術(shù)的應(yīng)用一、前 言 眾所周知,Windows程式的運(yùn)行是依靠發(fā)生的事件來驅(qū)動(dòng)。換句話說,程式不斷等待一個(gè)消息的發(fā)生,然后對這個(gè)消息的類型進(jìn)行判斷,再做適當(dāng)?shù)奶幚?。處理完此次消息后又回到等待狀態(tài)。從上面對Windows程式運(yùn)行機(jī)制的分析不難發(fā)現(xiàn),消息在用戶與程式之間進(jìn)行交流時(shí)起了一種中間“語言”的作用。在程式中接收和處理消息的主角是窗口,它通過消息泵接收消息,再通過一個(gè)窗口過程對消息進(jìn)行相應(yīng)的處理。 消 息攔截的實(shí)現(xiàn)是在窗口過程處理消息之前攔截到消息并做相關(guān)處理后再傳送給原窗口過程。通常情況下,程序員可以在窗口過程中處理接收到的消息,這就要求窗口 過程必須在開發(fā)程序時(shí)完成,但是在一些應(yīng)用中常常需要獲取和處理另外應(yīng)用程序或其它單元模塊中的消息,實(shí)現(xiàn)此類功能的技術(shù)也就本文要討論的主題――消息攔 截技術(shù)。
二、理解Windows消息機(jī)制 在深入探討消息攔截技術(shù)實(shí)現(xiàn)原理之前,讓我們先來溫習(xí)一下Windows消息機(jī)制原理知識(shí)。 1、 消息的產(chǎn)生 消息作為程序與外界交流的“語言”,它的產(chǎn)生自然來自外界,但這里所說的外界,不只是簡單的指程序之外或軟件系統(tǒng)之外,而是泛指消息處理模塊之外的模塊、Windows系統(tǒng)、其它應(yīng)用程序以及硬件等。通常根據(jù)消息產(chǎn)生的方式將其分為兩大類,即硬件消息和軟件消息。硬件消息,常指由硬件裝置所產(chǎn)生的事件(如鼠標(biāo)或鍵盤被按下),放在系統(tǒng)消息隊(duì)列(System Queue)中,再由系統(tǒng)消息處理機(jī)構(gòu)將消息發(fā)送給應(yīng)用程序消息隊(duì)列中。軟件消息,常指由Windows系統(tǒng)或其它應(yīng)用程序發(fā)送的信息,它直接放入應(yīng)用程序消息隊(duì)列(Application Queue)中,再由應(yīng)用程序消息處理機(jī)構(gòu)將消息傳遞給相應(yīng)的窗口。 2、 消息的組成 一個(gè)消息由一個(gè)消息名稱(UINT),和兩個(gè)參數(shù)(WPARAM,LPARAM)。當(dāng)用戶進(jìn)行了輸入或是窗口的狀態(tài)發(fā)生改變時(shí)系統(tǒng)都會(huì)發(fā)送消息到某一個(gè)窗口。例如當(dāng)菜單轉(zhuǎn)中之后會(huì)有WM_COMMAND消息發(fā)送,WPARAM的高字中(HIWORD(wParam))是命令的ID號(hào),對菜單來講就是菜單ID。當(dāng)然用戶也可以定義自己的消息名稱,也可以利用自定義消息來發(fā)送通知和傳送數(shù)據(jù)。 3、 消息的接收者 一個(gè)消息必須由一個(gè)窗口接收。在窗口過程(WNDPROC)中可以對消息進(jìn)行分析,對應(yīng)用程序要求處理的消息進(jìn)行相應(yīng)的處理工作,對于那么不需要應(yīng)用程序處理的消息可簡單的調(diào)用缺省處理。例如你希望對菜單選擇進(jìn)行處理那么你可以定義對WM_COMMAND進(jìn)行處理的代碼,如果希望在窗口中進(jìn)行圖形輸出就必須對WM_PAINT進(jìn)行處理。 4、 消息的處理 窗口接收到發(fā)送給自己的消息后,將消息結(jié)構(gòu)作為參數(shù)調(diào)用窗口過程對消息進(jìn)行相應(yīng)的處理??梢詫⒋翱谶^程看作消息處理代碼的集合,窗口過程函數(shù)的原型為: long FAR PASCAL WndProc(HWND hWnd,WORD message,WORD wParam,LONG lParam); 在Windows中,應(yīng)用程序不直接調(diào)用任何窗口函數(shù),而是等待Windows調(diào)用窗口函數(shù),請求完成任務(wù)或返回信息。為保證Windows調(diào)用這個(gè)窗口函數(shù),這個(gè)函數(shù)必須先向Windows登記,然后在Windows實(shí)施相應(yīng)操作時(shí)回調(diào),所以窗口函數(shù)又稱為回調(diào)函數(shù)。WndProc是一個(gè)主回調(diào)函數(shù),Windows至少有一個(gè)回調(diào)函數(shù)。它是在應(yīng)用程序進(jìn)行窗口類注冊時(shí)向Windows登記的。
三、利用鉤子(Hook)攔截消息 1、 何為鉤子(Hook)? 鉤子(Hook)機(jī)制允許應(yīng)用程序截獲處理window消息或特定事件。與DOS中斷截獲處理機(jī)制有類似之處。鉤子是Windows消 息處理機(jī)制的一個(gè)平臺(tái),應(yīng)用程序可以在上面設(shè)置子程以監(jiān)視指定窗口的某種消息,而且所監(jiān)視的窗口可以是其他進(jìn)程所創(chuàng)建的。當(dāng)消息到達(dá)后,鉤子可以在目標(biāo)窗 口處理函數(shù)之前處理它并且可以阻止消息的傳遞。每一個(gè)鉤子都有一個(gè)與之相關(guān)聯(lián)的指針列表,稱之為鉤子鏈表,該鏈表中的指針指向這個(gè)鉤子的各個(gè)處理子程。鉤 子的種類很多,每種鉤子可以攔截并處理相應(yīng)種類的消息。當(dāng)鉤子所監(jiān)視的消息出現(xiàn)時(shí),Windows調(diào)用鏈表中的第一個(gè)鉤子子程,第一個(gè)過程完成后將消息傳遞鏈表中的下一個(gè)鉤子子程,直至鏈表中所有鉤子子程都執(zhí)行完成(注意:如果在其中有一個(gè)鉤子在執(zhí)行完成前不執(zhí)行消息傳遞,其后面的鉤子過程和原窗口過程都不會(huì)再接收到消息。)后將消息返回給窗口過程。 2、 鉤子子程函數(shù) 鉤子子程是一個(gè)應(yīng)用程序定義的回調(diào)函數(shù)。用以監(jiān)視系統(tǒng)或某一特定類型的事件,這些事件可以是與某一特定線程關(guān)聯(lián)的,也可以是系統(tǒng)中所有線程的事件。其函數(shù)原型為:
LRESULT CALLBACK HookProc ( int nCode, WPARAM wParam, LPARAM lParam );
其中,nCode參數(shù)是Hook代碼,Hook子程使用這個(gè)參數(shù)來確定任務(wù)。這個(gè)參數(shù)的值依賴于Hook類型,每一種Hook都有自己的Hook代碼特征字符集。 Windows系統(tǒng)提供了多種類型的鉤子,每一種類型的Hook可以使應(yīng)用程序能夠監(jiān)視不同類型的系統(tǒng)消息處理機(jī)制。 wParam和lParam參數(shù)的值依賴于Hook代碼,但是它們的典型值是包含了關(guān)于發(fā)送或者接收消息的信息。 3、鉤子的安裝與卸載 鉤子的安裝是通過SDK API SetWindowsHookEx()來實(shí)現(xiàn)的,它將鉤子子程安裝到系統(tǒng)鉤子鏈表中。其函數(shù)原型
HHOOK SetWindowsHookEx( int idHook,HOOKPROC lpfn,HINSTANCE hMod, DWORD dwThreadId );
其中,idHook是指鉤子的類型。表一中列出部分鉤子的類型及其說明。 (表一)
lpfn是指鉤子子程的地址指針。如果dwThreadId參數(shù)為0 或是一個(gè)由別的進(jìn)程創(chuàng)建的線程的標(biāo)識(shí),lpfn必 須指向DLL中的鉤子子程。除此以外,lpfn可以指向當(dāng)前進(jìn)程的一段鉤子子程代碼。 hMod是指應(yīng)用程序?qū)嵗木浔?。?biāo)識(shí)包含lpfn所指的子程的DLL。如果dwThreadId 標(biāo)識(shí)當(dāng)前進(jìn)程創(chuàng)建的一個(gè)線程,而且子程代碼位于當(dāng)前進(jìn)程,hMod必須為NULL。 dwThreadId是指與安裝的鉤子子程相關(guān)聯(lián)的線程的標(biāo)識(shí)符,如果為0,鉤子子程與所有的線程關(guān)聯(lián)。 函數(shù)成功則返回鉤子的句柄,失敗返回NULL。
鉤子在使用完之后需要用UnHookWindowsHookEx()卸載,否則會(huì)造成麻煩。卸載鉤子比較簡單,UnHookWindowsHookEx()只有一個(gè)參數(shù)。函數(shù)原型如下: UnHookWindowsHookEx ( HHOOK hhk ) 其中,參數(shù)hhk是SetWindowsHookEx()函數(shù)返回鉤子句柄,所以設(shè)計(jì)程序時(shí)一定要保存好這個(gè)句柄,以便卸載時(shí)使用。函數(shù)成功返回TRUE,否則返回FALSE。
4、系統(tǒng)鉤子與線程鉤子 Windows系統(tǒng)根據(jù)鉤子監(jiān)視事件的范圍將鉤子分為系統(tǒng)鉤子(全局鉤子)和線程鉤子(局部鉤子)兩種。由SetWindowsHookEx()函數(shù)的最后一個(gè)參數(shù)決定了此鉤子是系統(tǒng)鉤子還是線程鉤子。線程勾子用于監(jiān)視指定線程的事件消息。線程勾子一般在當(dāng)前線程或者當(dāng)前線程派生的線程內(nèi)。 系統(tǒng)勾子監(jiān)視系統(tǒng)中的所有線程的事件消息。因?yàn)橄到y(tǒng)勾子會(huì)影響系統(tǒng)中所有的應(yīng)用程序,所以勾子函數(shù)必須放在獨(dú)立的動(dòng)態(tài)鏈接庫(DLL) 中。系統(tǒng)自動(dòng)將包含"鉤子回調(diào)函數(shù)"的DLL映射到受鉤子函數(shù)影響的所有進(jìn)程的地址空間中,即將這個(gè)DLL注入了那些進(jìn)程。
5、 鉤子的實(shí)現(xiàn) 本文的實(shí)例實(shí)現(xiàn)攔截記事本(NotePad.exe)程序的WM_CHAR消息的功能。如讀者想實(shí)現(xiàn)其它功能,可直接在鉤子子程函數(shù)中加入代碼。 (1)、選擇MFC AppWizard(DLL)創(chuàng)建項(xiàng)目NotePadhook并選擇MFC Extension DLL(共享MFC拷貝)類型。 (2)、創(chuàng)建NotePadHook.h文件,在其中建立鉤子類: class AFX_EXT_CLASS CNotePadHook:public CObject { public: CNotePadHook(); //鉤子類的構(gòu)造函數(shù) ~CNotePadHook(); //鉤子類的析構(gòu)函數(shù) BOOL StartHook(HWND hWnd); //安裝鉤子函數(shù) BOOL StopHook(); 卸載鉤子函數(shù) }; (3)、在NotePadHook.cpp中加入#include “NotePadHook.h”。 (4)、在NotePadHook.cpp中加入共享數(shù)據(jù)段: #pragma data_seg("sharedata") //共享數(shù)據(jù)段,段內(nèi)的變量可被鉤子所有實(shí)例共享。 HHOOK glhHook=NULL; //鉤子句柄。 HINSTANCE glhInstance=NULL; //DLL實(shí)例句柄。 #pragma data_seg() (5)、僅定義一個(gè)數(shù)據(jù)段還不能達(dá)到共享數(shù)據(jù)的目的,還要告訴編譯器該段的屬性。要在.DEF文件中設(shè)置段的屬性,打開.DEF文件加入如下代碼: SETCTIONS //好像要改為SECTIONS ,否則編譯有錯(cuò)誤 sharedata READ WRITE SHARED
另外一種方法: 也可以在代碼中直接設(shè)置:
(6)、在主文件NotePadHook.cpp的DllMain函數(shù)中加入保存DLL實(shí)例句柄: DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { //如果使用lpReserved參數(shù)則刪除下面這行 UNREFERENCED_PARAMETER(lpReserved);
if (dwReason == DLL_PROCESS_ATTACH) { TRACE0("NOtePadHOOK.DLL Initializing!\n"); //擴(kuò)展DLL僅初始化一次 if (!AfxInitExtensionModule(NotePadHookDLL, hInstance)) return 0; new CDynLinkLibrary(NotePadHookDLL); //把DLL加入動(dòng)態(tài)MFC類庫中 glhInstance = hInstance; //插入保存DLL實(shí)例句柄 } else if (dwReason == DLL_PROCESS_DETACH) { TRACE0("NotePadHOOK.DLL Terminating!\n"); //終止這個(gè)鏈接庫前調(diào)用它 AfxTermExtensionModule(NotePadHookDLL); } return 1; } (7)、類CNotePadHook的成員函數(shù)的具體實(shí)現(xiàn): CNotePadHook::CNotePadHook(){} CNotePadHook::~CNotePadHook(){ StopHook(); } BOOL CNotePadHook::StartHook(HWND hWnd) //安裝鉤子。 { BOOL bResult=FALSE; glhHook=SetWindowsHookEx(WH_CALLWNDPROC,NotePadProc,glhInstance,0); if(glhHook!=NULL) bResult=TRUE; return bResult; } CNotePadHook::StopHook() { BOOL bResult = FALSE; if(glhHook){ bResult=UnhookWindowsHookEx(glhHook); if(bResult) glhHook=NULL; return bResult; } (8)、鉤子子程的實(shí)現(xiàn): LRESULT WINAPI NotePadProc(int nCode,WPARAM wparam,LPARAM lparam) { PCWPSTRUCT pcs = NULL; pcs = (PCWPSTRUCT)lParam; if( pcs && pcs->hwnd!=NULL ) { TCHAR szClass[256]; GetClassName(pcs->hwnd ,szClass,255);//獲得攔截的窗口類名。 if( strcmp(szClass,"Notepad")==0) { if( pcs->message == WM_CHAR ) { AfxMessageBox("HOOK NOTEPAD WM_CHAR OK!!!"); } } } return CallNextHookEx(glhHook,nCode,wParam,lParam);//繼續(xù)傳遞消息。 }
(9)、編譯項(xiàng)目生成NotePadHook.dll。 雖然已經(jīng)完成了鉤子類,但還不能實(shí)現(xiàn)鉤子功能。我們必須寫一個(gè)程序來啟動(dòng)鉤子,將鉤子DLL注入其它程序的內(nèi)存空間并將鉤子加入到系統(tǒng)鉤子鏈表中。由于限于篇幅,在本文就不具體講述鉤子啟動(dòng)程序的實(shí)例,只將編寫啟動(dòng)程序應(yīng)注意的事項(xiàng)說明如下: (1)、將NotePadHook項(xiàng)目中Debug\NotePadHook.lib加入到項(xiàng)目設(shè)置鏈接標(biāo)簽中。 (2)、將NotePadHook項(xiàng)目中NotePadHook.h文件include到stdafx.h。 (3)、首先需要?jiǎng)?chuàng)建一個(gè)CNotePadHook類實(shí)例,啟動(dòng)鉤子時(shí)調(diào)用類成員StartHook(),卸載鉤子時(shí)調(diào)用類成員StopHook()。
四、利用窗口子類化(SubClass)攔截消息 前面已提及,每個(gè)窗口都有一個(gè)在它的窗口類中定義的窗口過程。該窗口過程處理每個(gè)發(fā)送到窗口的消息。如果想自己編寫窗口過程,修改它的行為是沒有問題的。但是,如果該窗口過程屬于別人,則將沒有源代碼進(jìn)行修改。例如,應(yīng)用程序中的每個(gè)按鈕,都是由系統(tǒng)提供的BUTTON窗口創(chuàng)建的,它有完全屬于自己的窗口過程。如果想改變該窗口的外觀,則不能通過改變它的WM_PAINT處理函數(shù)來實(shí)現(xiàn),因?yàn)樗遣豢傻玫摹D敲?,怎樣能改變這些按鈕的外觀,而無需重新編寫原來的控件呢?只要用自己的窗口過程的地址,替換窗口對象的初始窗口過程的地址即可。這種技術(shù)也是本節(jié)討論的主題 – 窗口子類化技術(shù)。 ?。?、窗口子類化原理 應(yīng)用程序在創(chuàng)建一個(gè)新窗口之前要向Windows系統(tǒng)注冊這個(gè)窗口的類,首先要填寫一個(gè)WNDCLASS結(jié)構(gòu),其中的結(jié)構(gòu)參數(shù)lpfnWndProc就是該類窗口函數(shù)的地址,接著調(diào)用RegisterClass()函數(shù)向Windows系統(tǒng)申請注冊這個(gè)窗口類。這時(shí)Windows會(huì)為其分配一塊內(nèi)存來存放該類的全部信息,這個(gè)內(nèi)存塊稱為窗口類內(nèi)存塊。 窗口子類化技術(shù)實(shí)際上就是改變窗口內(nèi)存塊中的有關(guān)參數(shù)。由于這種修改只涉及到一個(gè)窗口的窗口內(nèi)存塊,因此它不會(huì)影響到屬于同一窗口類的其它窗口的功能和表現(xiàn)。窗口子類化中最常見的是修改窗口內(nèi)存塊中的窗口函數(shù)地址(lpfnWndProc),使其指向一個(gè)新的窗口函數(shù),從而改變原窗口函數(shù)的處理方法,以達(dá)到修改其窗口過程的目的。 2、窗口子類化的實(shí)現(xiàn) 窗口子類化實(shí)現(xiàn)的核心是改變窗口過程的地址,可以通過SDK API提供的幾個(gè)函數(shù)來實(shí)現(xiàn)。具體步驟如下: a.編寫子類化窗口過程函數(shù)。該函數(shù)必須為標(biāo)準(zhǔn)的窗口過程函數(shù)格式即: LRESULT CALLBACK SubClassWndProc ( HWND , UINT , WPARAM , LPARAM ) ; 此函數(shù)的參數(shù)意義與前面講述的窗口過程函數(shù)參數(shù)類似。 b.調(diào)用GetWindowLong ( hWnd , GWL_WNDPROC ) 函數(shù)獲得原窗口函數(shù)的地址并保存起來;其中參數(shù)hWnd為待子類化窗口句柄。 C.調(diào)用SetWIndowLong ( hWnd , GWL_WNDPROC , SubClassWndProc ) 把窗口函數(shù)設(shè)置成子類化窗口函數(shù),完成窗口子類化。
為了減少子類化過程中繁瑣的工作,MFC中提供了對子類化的支持,它簡化了子類化過程,利用CWnd類SubClassWindows()函數(shù)來實(shí)現(xiàn)子類化。為了讓讀者對子類化過程有一個(gè)直觀的認(rèn)識(shí),下面將利用MFC實(shí)現(xiàn)對一個(gè)編輯(Edit)控件的子類化。 (1)、創(chuàng)建一個(gè)從MFC控件類CEdit派生的新控件類CSubEdit。 (2)、添加CSubEdit::PreTranslateMessage(MSG* pMsg) BOOL CSubEdit::PreTranslateMessage(MSG* pMsg) { if( pMsg->message==WM_KEYDOWN&&pMsg->wParam==VK_RETURN) { //當(dāng)在Edit控件上按下回車鍵后… ….. //限于篇幅處理內(nèi)容略。 return TRUE; } CEdit::PreTranslateMessage(pMsg); } (3)、在包含此控件的對話框類頭文件中控件變量類型從CEdit改為CSubEdit。 (4)、在包含此控件的對話框類文件中對Edit控件進(jìn)行子類化,代碼如下: HWND HwND; GetDlgItem(IDC_SUB_EDIT,&hWnd);//其中IDC_SUB_EDIT是控件ID。 m_subEdit.SubclassWindow(hWnd); //m_subEdit為控件變量名。
五、小結(jié) 本文討論了實(shí)現(xiàn)消息攔截的兩種方法,其中鉤子技術(shù)用途廣泛,不僅可以實(shí)現(xiàn)對同進(jìn)程內(nèi)消息的攔截,而且還可以實(shí)現(xiàn)對另外進(jìn)程消息的攔截。而子類化技術(shù)主要用于實(shí)現(xiàn)對同一進(jìn)程單元模塊中的窗口消息的攔截。程序員可以根據(jù)實(shí)際應(yīng)用需求選擇其一來實(shí)現(xiàn)消息的擋截。 |
||||||||||||||
|
|