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