C++ Hook(鉤子)程式設計,通過內聯彙編,使類成員函式代替全域性函式(靜態函式)
程式語言:C/C++
編譯環境:Visual Studio 2008
核心方法::通過內聯彙編,構造類物件獨享的函式(委託),完成了類成員函式到普通全域性函式的轉化,並在Windows Hook(鉤子)程式設計中得到成功的實踐。
關鍵字:C++,委託,內聯彙編,Hook,成員函式
引文:
前段時間曾編寫了一個自認為很完善的.NET平臺上的Hook(鉤子)動態連結庫(DLL),並進一步完成了GUI介面,但是在部署時卻發現的其侷限性,很多使用者都沒有安裝.NET平臺,其平臺的最小安裝(.NET 2.0)也需要21M,如果使用NOMO(2.0)免安裝釋出的話也需要小10M(而且使用NOMO在自動執行和相容性上也有待商榷)。
因此,我下定決心,準備徹底和託管程式碼決裂,迴歸C/C++,再次實現上述功能,然而真正編寫時才發現,經典的C++果然不是蓋的,昔日被C的各種除錯不通過折磨的記憶還未消退,今日又在開始時陷入苦戰,正如前人所說,C++這種強型別語言,喜歡在繁瑣的符號和語法上做文章。如果處理不好這些,大概做不了大專案,還是去.NET或Java的快樂平臺逍遙去吧~微微感慨,趕快進入正題。
正文:
本文的目的是研究類成員函式與普通函式之區別,以及不同調用方式之間的區別,進而通過內聯彙編語句模仿特定的呼叫,從而完成通過普通函式指標呼叫類成員函式的功能。因此主要部分為研究和嘗試的過程,如希望直接檢視Hook程式設計的結果,可直接檢視尾部程式碼。
使用過Windows Hook程式設計的同志們都知道,在程式中是通過如下語句來載入鉤子,解除安裝鉤子。
C/C++ Code:
- HHOOK SetWindowsHookEx( //載入鉤子
- int idHook,
- HOOKPROC lpfn, //鉤子函式的指標
- HINSTANCE hMod,
- DWORD dwThreadId
- );
- BOOL UnhookWindowsHookEx( //解除安裝鉤子
- HHOOK hhk
- );
其中最重要的就是HOOKPROC lpfn這個函式指標,檢視此函式的形式,為:
C/C++ Code:
- LRESULT CALLBACK HookProcess( //鉤子函式
- int nCode,
- WPARAM wParam,
- LPARAM lParam
- );
//其中CALLBACK 的定義為 #define CALLBACK __stdcall
其中,值得注意的是,這個鉤子函式需要是一個普通__stdcall呼叫的C函式,而在C++中可以理解為某個全域性函式(非類成員函式,全域性訪問)或者是一個類的靜態函式(static,全域性訪問)。
讓我們再觀察一個類成員函式和全域性函式的區別,我建立了一個簡單的演示程式碼,如下程式碼:
C/C++ Code:
- #include <windows.h>
- LRESULT CALLBACK HookProcess1( // HookProcess1是一個全域性函式
- int nCode,
- WPARAM wParam,
- LPARAM lParam){ return NULL;}
- class WinHook
- {
- public:
- int value; //定義一個成員變數,以便於測試訪問
- LRESULT CALLBACK HookProcess2(// HookProcess2是一個類的成員函式
- int nCode,
- WPARAM wParam, LPARAM lParam) {
- this->value++; //檢查成員變數的訪問性
- return NULL;
- }
- };
- int main(){
- //定義一個函式指標HookProcess,它可以成功作為引數傳遞給SetWindowsHookEx
- LRESULT (CALLBACK * HookProcess)
- (int nCode, WPARAM wParam, LPARAM lParam);
- //試圖將HookProcess指向HookProcess1
- HookProcess = HookProcess1; //賦值成功,這個指標能夠指向全域性函式
- HookProcess (0,0,0);//嘗試呼叫之.
- //試圖將HookProcess指向HookProcess2
- WinHook *myHook = new WinHook();
- HookProcess = myHook->HookProcess2; //編譯失敗,提示型別不能轉化
- //此句將在之後修改
- HookProcess (0,0,0);//嘗試呼叫之.
- return 0;
- }
檢視錯誤程式碼,終於發現類的成員函式和普通的全域性函式是不一樣的,
// error C2440:
//“=”: 無法從“LRESULT (__stdcall WinHook::* )(int,WPARAM,LPARAM)”轉換為“LRESULT (__stdcall *)(int,WPARAM,LPARAM)”
關鍵問題就在於“WinHook::”的區別,可以看到,無論如何,一個類的成員函式(非靜態)是不能與普通的C全域性函式轉化的。這點在後面再詳細的說明。
然而,當我們使用C++程式設計的時候,往往不希望使用全域性函式;如果使用類的靜態函式,那麼一個類只能實現一個鉤子,且不能利用C++中類的繼承,多型等特性,從實質上淪落為普通C程式設計。
在網上查找了很多方法,發現可以通過內聯彙編語句強制將指標轉化,如下:
C/C++ Code:
- //修改上述main中程式碼,將類成員函式指標強制轉化為一般函式指標
- WinHook *myHook = new WinHook();
- //HookProcess = myHook->HookProcess2;
- __asm //NEW: 內聯彙編語句
- {
- push eax
- lea eax, WinHook::HookProcess2
- mov HookProcess, eax
- pop eax
- }
- HookProcess (0,0,0);//嘗試呼叫之.
然而在使用這種方法時,卻發現了問題,即在底層呼叫WinHook 的HookProcess2中,其中的this指標為0,因此所有this相關的操作均不可行(即不能訪問本物件的其他成員)。此外,在HookProcess2結束後,底層也發現了堆疊不平衡而導致的錯誤。
繼續閱讀網上的文章,如
(成員函式指標與高效能的C++委託)
(普通函式指標變數可以儲存成員函式地址嗎?)
(使非MFC視窗程式的視窗回撥過程成為C++類的成員函式)
等等
我終於理解的類成員函式與普通函式的區別,那就是this指標,當一個類的成員函式被呼叫時,如:
C/C++ Code:
- WinHook *myHook = new WinHook();
- HookProcess = myHook->HookProcess2(…);
此時,當HookProcess2的引數被傳送之後,myHook的地址也被傳送在CPU暫存器中(或堆疊中,這要看傳呼叫方式的不同,在之後說明),並在之後在HookProcess2函式中賦值給this,使得類成員函式能夠知道呼叫的是哪個類的物件。
為了測試這一點,我們可以在強制轉化了HookProcess2指標之後,在HookProcess2中修復this指標,看函式的this功能是否正常。如下程式碼:
C/C++ Code:
- class WinHook //修改WinHook類的函式HookProcess2
- {
- public:
- int value; //定義一個成員變數,以便於測試訪問
- LRESULT CALLBACK HookProcess2(// HookProcess2是一個類的成員函式
- int nCode,
- WPARAM wParam, LPARAM lParam) {
- //假設主函式中的myHook指標指向0x003d3450,修復this指標
- __asm //內聯彙編語句
- {
- push eax //保護eax
- mov eax, 0x003d3450
- mov this, eax //設定本類的this指標(__asm不檢查訪問性)
- pop eax //還原eax
- }
- //此時檢視this指標,就能夠發現this正常工作
- this->value++;
- return NULL;
- }
- };
注:雖然this指標能夠執行成功,但是函式結束會產生如下錯誤:
這是由於__stdcall的this傳送方式是堆疊,強制訪問HookProcess2卻未將this壓棧會導致堆疊不平衡。因此,將CALLBACK這個修飾符登出掉,即:
C/C++ Code:
- class WinHook //進一步修改WinHook類的函式HookProcess2
- {
- public:
- int value; //定義一個成員變數,以便於測試訪問
- LRESULT /*CALLBACK*/ HookProcess2(// HookProcess2是一個類的成員函式
- int nCode,
- WPARAM wParam, LPARAM lParam) {
- //假設主函式中的myHook指標指向0x003d3450,修復this指標
- __asm //內聯彙編語句
- {
- push eax //保護eax
- mov eax, 0x003d3450
- mov this, eax //設定本類的this指標(__asm不檢查訪問性)
- pop eax //還原eax
- }
- //此時檢視this指標,就能夠發現this正常工作
- this->value++;
- return NULL;
- }
- };
此時,發現整個函式能夠順利執行。(注意,在實際測試時,需要在main中加斷點檢查myHook的地址,並動態的修改mov eax, 0x003d3450的數值。否則不能測試通過)
這是因為,登出掉CALLBACK的修飾符(即__stdcall)之後,函式採用預設的__thiscall呼叫方式,此時,this指標是通過CUP暫存器的ecx傳送,此時就不會產生堆疊不平衡的錯誤了。
進一步的,我們遇到的新的問題,儘管能夠成功模擬一個類成員函式的呼叫,修復了this指標,但是對於SetWindowsHookEx來說,每個類的所有成員是共享一個函式空間的。即如下圖所示。
因此,如果使用HookProcess2的程式入口(記憶體首地址)作為SetWindowsHookEx的引數傳入,在引發鉤子事件的時候就不能夠區分到底引發了哪個類的HookProcess2函式,即不能設定this指標。
為了解決這個問題,我們需要生成一個動態的函式入口,這個入口是每個物件獨享的,因此不同的物件將引發不同的函式,這就能夠區分類的不同物件。根據其他語言對它的描述,我們將這個函式入口暫稱為委託(Delegate)。
在實際上,我們是通過定義一個成員變數(如byte型別的陣列,可便於的賦值)來實現這個委託的,其中實際上儲存了一段機器碼,這段機器碼(根據彙編語法)可以動態的設定this指標,並實現到真正函式首地址的跳轉。這個委託的示意過程如下:
下面說明這個委託的宣告和設定程式碼:
C/C++ Code:
- class WinHook //進一步修改WinHook類的函式HookProcess2
- {
- public:
- byte DelegateThisCall[10]; //定義委託
- WinHook(void){
- //初始化委託
- byte * DelegatePoint = DelegateThisCall;
- DelegateThisCall[0] = 0xb9; //0-4:__asm mov ecx, this
- DelegateThisCall[5] = 0xe9; //5-9:__asm jmp, CoreHookProcess
- __asm
- {
- push eax //保護eax
- push ebx //保護ebx
- mov eax, this //取得this地址
- mov ebx, DelegatePoint //獲取DelegateThisCall地址
- mov dword ptr [ebx+1], eax //this 地址, 雙字(DWORD)
- }
- //計算jmp地址參考:http://zhidao.baidu.com/question/105950930.html
- __asm
- {
- lea eax, HookProcess2 //取得HookProcess2地址
- mov ebx, DelegatePoint //獲取jmp地址= DelegatePoint + 5
- add ebx, 5
- add ebx, 5 //JMP地址=目標地址-(本句地址+本句指令位元組數)
- sub eax, ebx //JMP地址= HookProcess2 – [(DelegatePoint+5) + 5]
- mov dword ptr [ebx-4], eax //HookProcess2地址, 雙字(DWORD)
- pop ebx //還原ebx
- pop eax //還原eax
- }
- }
- int value; //定義一個成員變數,以便於測試訪問
- LRESULT /*CALLBACK*/ HookProcess2(// HookProcess2是一個類的成員函式
- int nCode,
- WPARAM wParam, LPARAM lParam) {
- //檢視this指標,就能夠發現this正常工作
- this->value++;
- return NULL;
- }
- };
進一步的,修改主函式:
C/C++ Code:
- int main(){
- //定義一個函式指標HookProcess,它可以成功作為引數傳遞給SetWindowsHookEx
- LRESULT (CALLBACK * HookProcess)
- (int nCode, WPARAM wParam, LPARAM lParam);
- //將HookProcess指向HookProcess2
- WinHook *myHook = new WinHook();
- //HookProcess = myHook->HookProcess2; //賦值失敗,提示型別不能轉化
- byte * DelegatePoint = myHook->DelegateThisCall; //獲取委託首地址
- __asm //內聯彙編語句
- {
- push eax
- mov eax, DelegatePoint
- mov HookProcess, eax //強制將委託的地址賦值給函式指標
- pop eax
- }
- HookProcess (0,0,0);//嘗試呼叫之. 呼叫成功
- return 0;
- }
以上,就成功的完成了類成員函式的鉤子過程,通過一個委託,完成了此功能。
最後,為了演示上述方法在建立Windows Hook程式設計上的使用,特給出編寫的動態連結庫的實現WinHook功能的BaseHook類的程式碼,此類完成了成員函式的Hook載入解除安裝等管理,使用者通過繼承此類,並重寫HookProcess函式(如下),便可完成所有Hook功能。
C/C++ Code:
- //BaseHook 類的虛擬函式,當Hook事件發生時會呼叫此函式。
- //使用者通過繼承並重寫此函式完成Hook功能
- virtual LRESULT /*CALLBACK*/ HookProcess
- (int nCode, WPARAM wParam, LPARAM lParam);
原始碼部分:
C/C++ Code: BaseHook.h
- /********************************************************* {COPYRIGHT-TOP} *
- * RealZYC Confidential
- * OCO Source Materials
- *
- * (C) Copyright RealZYC Corp. 2011 All Rights Reserved.
- *
- * The source code for this program is not published or otherwise
- * divested of its trade secrets, irrespective of what has been
- * deposited with the China Copyright Office.
- ********************************************************** {COPYRIGHT-END} */
- #pragma once
- #include <windows.h>
- /***************************************
- *The basic defination of windows hook
- ****************************************/
- class BaseHook
- {
- /***************************************
- * Enum
- ****************************************/
- #pragma region Enum
- public:
- /***************************************
- *The available types of windows hook
- ****************************************/
- enum HookTypes: int
- {
- //Message filter hook - WH_MSGFILTER = -1
- MsgFilter = -1,
- //Journal record hook - WH_JOURNALRECORD = 0
- JournalRecord = 0,
- //Journal playback hook - WH_JOURNALPLAYBACK = 1
- JournalPlayback = 1,
- //Keyboard hook - WH_KEYBOARD = 2
- Keyboard = 2,
- //Get message hook - WH_GETMESSAGE = 3
- GetMessage = 3,
- //Call wnd proc hook - WH_CALLWNDPROC = 4
- CallWndProc = 4,
- //CBT hook - WH_CBT = 5
- CBT = 5,
- //System message filter hook - WH_SYSMSGFILTER = 6
- SysMsgFilter = 6,
- //Mouse hook - WH_MOUSE = 7
- Mouse = 7,
- //Hardware hook - WH_HARDWARE = 8
- Hardware = 8,
- //Debug hook - WH_DEBUG = 9
- Debug = 9,
- //Shell hook - WH_SHELL = 10
- Shell = 10,
- //Fore ground idle hook - WH_FOREGROUNDIDLE = 11
- ForeGroundIdle = 11,
- //Call wnd proc ret hook - WH_CALLWNDPROCRET = 12
- CallWndProcRet = 12,
- //Keyboard low level hook - WH_KEYBOARD_LL = 13
- KeyboardLL = 13,
- //Mouse low level hook - WH_MOUSE_LL = 14
- MouseLL = 14
- };
- #pragma endregion
- /***************************************
- * Value
- ****************************************/
- #pragma region Value
- protected:
- //The hook type
- HookTypes int_HookType;
- //The hook object thread id, give 0 for all thread
- DWORD dword_ThreadId;
- //The hook id, give 0 for not set
- HHOOK point_HookID;
- //Dll entrance
- static HINSTANCE hangle_HinstDLL;
- protected:
- //The this call delegate for CoreHookProcess
- byte DelegateThisCall[10];
- #pragma endregion
- /***************************************
- * New
- ****************************************/
- #pragma region New
- public:
- /***************************************
- ''' <summary>
- ''' Initial function
- ''' </summary>
- ''' <param name="HookType">The hook type</param>
- ''' <param name="ThreadId">The hook object thread id, give 0 for all thread</param>
- ****************************************/
- BaseHook(HookTypes HookType, DWORD ThreadId);
- //Dispose function
- ~BaseHook(void);
- #pragma endregion
- /***************************************
- * Property
- ****************************************/
- #pragma region Property
- public:
- //Set / get the hook type
- inline HookTypes GetHookType();
- inline void SetHookType(HookTypes HookType);
- //Set / get the hook object thread id, give 0 for all thread
- inline DWORD GetThreadId();
- inline void SetThreadId(DWORD ThreadId);
- //Set / get whether the hook is running
- bool GetEnabled();
- void SetEnabled(bool Enabled);
- //Set / get dll hinst
- static HINSTANCE GetHinstDll();
- static void SetHinstDll(HINSTANCE HinstDLL);
- #pragma endregion
- /***************************************
- * Sub / Function
- ****************************************/
- #pragma region Sub / Function
- protected:
- /***************************************
- ///<summary>
- /// The defination of core hook process
- ///</summary>
- ///<param name="nCode">Specifies the hook code passed to the current hook procedure. The next hook procedure uses this code to determine how to process the hook information</param>
- ///<param name="wParam">Specifies the wParam value passed to the current hook procedure. The meaning of this parameter depends on the type of hook associated with the current hook chain</param>
- ///<param name="lParam">Specifies the lParam value passed to the current hook procedure. The meaning of this parameter depends on the type of hook associated with the current hook chain</param>
- ///<returns>The nCode, use 0 to pass the information to next hook, others to ignore the current information</returns>
- ///<remarks>Use for SetWindowsHookEx</remarks>
- ****************************************/
- LRESULT /*CALLBACK*/ CoreHookProcess(int nCode, WPARAM wParam, LPARAM lParam);
- public:
- /***************************************
- /// <summary>
- /// Set the hook data
- /// </summary>
- /// <param name="HookType">The hook type</param>
- /// <param name="ThreadId">The hook object thread id, give 0 for all thread</param>
- /// <remarks>Restart the hook after the hook data changed</remarks>
- ****************************************/
- void SetHook(HookTypes HookType, DWORD ThreadId);
- /***************************************
- /// <summary>
- /// Start the hook
- /// </summary>
- /// <remarks>Check the Enabled for operation result</remarks>
- ****************************************/
- void Start();
- /***************************************
- /// <summary>
- /// Stop the hook
- /// </summary>
- /// <remarks>Check the Enabled for operation result</remarks>
- ****************************************/
- void Stop();
- /***************************************
- /// <summary>
- /// The user defined hook process
- /// </summary>
- /// <param name="nCode">Specifies the hook code passed to the current hook procedure. The next hook procedure uses this code to determine how to process the hook information</param>
- /// <param name="wParam">Specifies the wParam value passed to the current hook procedure. The meaning of this parameter depends on the type of hook associated with the current hook chain</param>
- /// <param name="lParam">Specifies the lParam value passed to the current hook procedure. The meaning of this parameter depends on the type of hook associated with the current hook chain</param>
- /// <returns>The nCode, use 0 to pass the information to next hook, others to ignore the current information</returns>
- /// <remarks>Use for CoreHookProcess</remarks>
- ****************************************/
- virtual LRESULT /*CALLBACK*/ HookProcess(int nCode, WPARAM wParam, LPARAM lParam);
- #pragma endregion
- };
C/C++ Code: BaseHook.cpp
- /********************************************************* {COPYRIGHT-TOP} *
- * RealZYC Confidential
- * OCO Source Materials
- *
- * (C) Copyright RealZYC Corp. 2011 All Rights Reserved.
- *
- * The source code for this program is not published or otherwise
- * divested of its trade secrets, irrespective of what has been
- * deposited with the China Copyright Office.
- ********************************************************** {COPYRIGHT-END} */
- #include "BaseHook.h"
- /***************************************
- *class BaseHook
- ****************************************/
- /***************************************
- * Static
- ****************************************/
- #pragma region Static
- HINSTANCE BaseHook::hangle_HinstDLL = NULL;
- #pragma endregion
- /***************************************
- * Property
- ****************************************/
- #pragma region Property
- //Set / get the hook type
- BaseHook::HookTypes BaseHook::GetHookType(){return int_HookType;}
- void BaseHook::SetHookType(HookTypes HookType){int_HookType = HookType;}
- //Set / get the hook object thread id, give 0 for all thread
- DWORD BaseHook::GetThreadId(){return dword_ThreadId;}
- void BaseHook::SetThreadId(DWORD ThreadId){dword_ThreadId = ThreadId;}
- //Set / get whether the hook is running
- bool BaseHook::GetEnabled(){return ( point_HookID != NULL );}
- void BaseHook::SetEnabled(bool Enabled)
- {
- if(Enabled != GetEnabled())
- {
- if(Enabled)Start();
- else Stop();
- }
- }
- //Set / get dll hinst
- HINSTANCE BaseHook::GetHinstDll()
- {
- try
- {
- return (hangle_HinstDLL != NULL)? hangle_HinstDLL:
- GetModuleHandle