HOOK API 函數跳轉詳解
HOOK API 函數跳轉詳解
2011年03月28日
什麼是HOOK API:
Windows下暴露的對開發人員的接口叫做應用程序編程接口,就是我們常說的API。我們在寫應用層應用程序軟件的時候都是通過調用各種API來實現的。有些時候,我們需要監控其他程序調用的API,也就是,當其他應用程序調用我們感興趣的API的時候,我們在他調用前有一個機會做自己的處理,這就是HOOK API的涵義。
思路:
我們知道Windows系統API函數都是被封裝到DLL中,在某個應用程序要調用一個API函數的時候,如果這個函數所在的DLL沒有被加載到本進程中則加載它,然後保存當前環境(各個寄存器和函數調用完後的返回地址等)。接着程序會跳轉到這個API函數的入口地址去執行此處的指令。由此看來,我們想在調用真正的API之前先調用我們的函數,那麼可以修改這個API函數的入口處的代碼,使他先跳轉到我們的函數地址,然後在我們的函數最後再調用原來的API函數。下面以攔截WS2_32.dll中的recv函數爲例說明攔截的主要過程。首先把自己編寫的DLL掛接到系統當前運行的所有進程中(要排除一些Windows系統自身的進程,否則會出現問題,影響系統正常工作),掛接的意思是,要我們的DLL運行在目標進程的地址空間中。可以使用列舉系統進程然後用遠程線程注入的方法,但是這種方法只適用於Win2000以上的操作系統。
當我們的DLL被所有目標進程加載後,我們就可以進行真正的工作了。首先使用Tool Help庫的相關函數列舉目標進程加載的所有模塊,看看是否有ws2_32.dll,如果沒有,說明這個進程沒有使用Winsock提供的函數,那麼我們就不用再給這個進程添亂了。如果找到ws2_32.dll模塊,那麼OK,我們可以開工了。先是用GetProcAddress函數獲得進程中ws2_32.dll模塊的recv函數的入口地址,也就是函數的起始地址。剛纔說過,我們想把recv函數起始位置加入一條跳轉指令,讓它先跳轉到我們的函數中運行。跳轉指令可以用0xE9來表示(0xE9是彙編語言中CALL指令的機器碼),後面還有4個字節的我們函數的相對地址。也就是我們要修改recv函數前5個字節。這5個字節由1個字節的跳轉指令和4個字節的地址組成。這樣當程序運行到這裏的時候,將會跳轉到這4個字節表示的地址處去運行代碼。還要注意的是這4個字節的地址是偏移地址,而偏移地址 = 我們函數的地址 - 原API函數的地址 - 5(我們這條指令的長度)。好了,別忘了我們要先讀取稍後要被覆蓋的recv函數入口處的5個字節的內容,把它保存起來留着以後恢復時使用。因爲在我們的函數中要想調用真正的recv的時候,必須把它前5個字節恢復了,他才能正常工作呢。
通過上面的說明,我們可以整理出這樣的一個流程:
1. 保存recv的前5個字節的內容
2. 把recv的前5個字節的內容改變成CALL xxxx(xxxx是我們的函數的偏移地址)
3. 在我們的函數中恢復recv的前5個字節的內容,並作處理。
4. 我們的函數返回後,再把recv的前5個字節的內容改變成CALL xxxx
慢着,你一定發現問題了吧?當我們爲了調用原來的recv函數而剛剛把recv入口處的5個字節恢復,這時系統中的其他線程調用了recv函數,而這個調用將會成爲漏網之魚而不會進入到我們的函數中來。簡單的解決辦法是使用臨界對象CriticalSection來保證同時只能有一個線程對recv函數入口處5個字節進行讀寫操作。
最後記得在你想要停止攔截的時候恢復所有你修改過的進程和這些進程中被修改的API的前5個字節。其實原理講着容易,在實現的時候會遇到各種各樣的問題,如98下這些系統的DLL被加載到系統內存區供應用程序共享,所以這些內存是受保護的,不能隨意修改,還有nt/2000下權限問題,還要考慮到不要攔截某些系統進程,否則會帶來災難性的後果。這些都是在實踐當中遇到的實際問題。
下面結合代碼給大家講解一下吧,首先我們要實現HOOK模塊,我們給它起個名字叫做MainHookDll.DLL。在此模塊中,主要要實現一個CHookApi的類,這個類完成主要的攔截功能,也是整個項目的技術核心和難點,後面將具體介紹它。而且,MainHookDll模塊就是將來要注入到系統其它進程的模塊,而遠程調用函數是非常困難的事情,所以我們設計此模塊的時候應讓其被加載後自動執行攔截的初始化等工作。這樣,我們只需要讓遠程的進程加載HOOK,然後MainHookDll.dll就能夠自動執行其它操作從而HOOK該進程的相關API。
MainHookDll模塊中的CHookApi類擁有2個向外部提供的主要的方法,HookAllAPI,表示攔截指定進程中的指定API和UnhookAllAPI,表示取消攔截指定進程中的指定API。進行具體設計的時候,會遇到一個問題。大家看到,上文所說的開始將原始API的前5個字節寫成CALL XXXX,而在我們的替換函數中要恢復保存的API原始的5個字節,在調用完成後又要把API前5個字節改爲CALL XXXX。如果我們攔截多個API要在每個替換函數中按照如上的方法進行設置,這樣雖然我們自己明白,但是可能您只是實現HOOKAPI部分,而別人實現調用,這樣會使代碼看起來很難維護,在別人寫的替換函數中加上這些莫名奇妙的語句看來不是一個好主意,當需要攔截多個感興趣的API函數,那樣的話將會在每一個要攔截的函數裏都有這些莫名其妙的代碼將會是件很噁心得事情。而且對於CALL XXXX中的地址,要對於不同的API設置不同的替換函數地址。那麼能不能把這些所有的函數歸納爲一個函數,所有的API函數前5字節都改爲CALL到這個函數的地址,這個函數先恢復API的前5字節,然後調用用戶真正的替換函數,然後再設置API函數的前5字節,這樣可以使真正的替換函數只做自己應該做的事情,而跟HOOK API相關的操作都由我們的通用函數來幹。
這樣的想法是好的,但是有一個突出問題,因爲替換函數的函數聲明與原API一致,所以對於要攔截的不同的API,它們的的參數和返回值是不一樣的。那我們怎樣通過一個函數獲得用戶傳遞給API的參數,然後使用這些參數調用替換函數,最後把替換函數的返回值再返回給調用API的客戶?要想實現這個功能,我們需要了解一個知識,也就是C++究竟是怎樣調用一個函數的。我們以ws2_32.dll中提供的recv函數爲例進行說明,recv函數的聲明如下:
int recv(
SOCKET s,
char* buf,
int len,
int flags
);
可以看出它具有4個參數,返回值類型是int。我們作如下調用:
recv(s,buf,buflen,0);
那麼在調用recv前,這四個參數將按照從右向左的順序壓到棧中,然後用Call指令跳轉到recv函數的地址繼續執行。recv可以從棧中取出參數並執行其他功能,最後返回時返回值將被保存在寄存器EAX中。最後還要說明一點的是,在彙編語言看來這些參數和返回值都是以DWORD類型表示的,所以如果是大於4字節的值,就用這4個字節表示值所在的地址。
有了這些知識我們就可以想到,如果用戶調用recv函數並被攔截跳轉到我們的函數中運行,但是我們並不知道有多少個參數和返回值,那麼我們可以從棧中取出參數,但是參數的個數需要提供,當然我們可以在前面爲每個API函數指定相應的參數個數,然後運行真正的替換函數,最後在返回前把替換函數的返回值放到寄存器EAX中,這樣就解決了不知道參數和返回值個數的問題。那麼我們的函數應該是看起來無參數無返回值的。
基本原理我們大家都清楚了,但是繼續之前我還是想講一講幾個彙編的知識,如果沒有這些知識那麼看下面的代碼就好像天書一樣。
關於參數
我們講過,在調用一個子函數前要把參數按順序壓棧,而子函數會從棧中取出參數。對於棧操作,我們一般使用EBP和ESP寄存器,而ESP是堆棧指針寄存器,所以多數情況下使用EBP寄存器對堆棧進行暫時操作。還是用調用recv函數爲例,假設調用前ESP指向0x00000100處(程序運行時ESP是不可能爲這個值的,此處只是爲了舉例說明問題)。先將參數一次壓棧
push 0 // flags入棧
lea eax, [len]
push eax // len入棧
lea eax, [buf]
push eax // buf入棧
lea eax, [s]
push eax // s入棧
下面使用call調用真正的recv函數,
call dword ptr [recv] // 調用recv
call指令先將返回地址壓入棧中,返回地址就是CALL指令的下一條指令的地址,然後跳轉到recv入口地址處繼續執行。進入recv後,recv使用EBP臨時訪問堆棧之前,要保存EBP的當前內容,以便以後再使用(在關於調用函數時保存各個寄存器的值部分將詳細討論)。所以位於recv函數開始可能是這樣的
push ebp // 保存ebp的當前值
mov ebp,esp // 使把esp負給ebp
堆棧指針[ESP] 堆棧的內容 堆棧內容的含義
0x00000100 flags 參數
0x000000fc len 參數
0x000000f8 buf 參數
0x000000f4 s 參數
0x000000f0 RetAddress 返回的地址
0x00000ec OldEBP 保存EBP的當前值
到此,我們可以知道,如果現在要想通過EBP獲得最後一個入棧的參數,那麼需要用EBP+8來獲得,因爲最後一個入棧的參數被保存在返回地址和EBP原始值的上面(一定記住,棧是由高地址到低地址的)。而返回地址被放在EBP+4處,EBP的原始值放在EBP+0處。
關於調用函數時保存各個寄存器的值
當我們要調用其它函數的時候,程序應該先保存各個寄存器的值,然後轉去調用其它函數,最後會恢復各個寄存器的值使它們恢復成調用其它函數之前的狀態。當然我們使用高級語言寫程序的時候,編譯器爲我們做了這些事情。使用vc調試程序,打開反彙編窗口。運行一個簡單的程序,該程序調用一個我們自己寫的簡單函數,在這個簡單函數中設置斷點,可以看到,編譯器生成的彙編代碼使用堆棧保存各個寄存器的值,上面提到當執行一個函數的時候,首先保存的是EBP的值,然後依次壓入棧中保存的寄存器爲EBX、ESI、EDI,我們在恢復這些寄存器的值的時候將逆向出棧來完成。
關於函數調用的返回
調用子函數前ESP指針會因爲壓棧參數而改變,然後壓入返回地址等,子函數中會使用ret指令從棧中取出返回地址並跳轉到返回地址,而在子函數返回到CALL的下一條指令時棧中還保存着參數,所以我們需要手工的將棧中的參數所佔用的空間釋放,如在調用完成一個4個參數的子函數後,我們應該將ESP指針上移4*4個字節,如
add esp,16
這個操作在調用API的時候是不需要的,因爲,windows API在函數中自己將參數彈出堆棧了。所以這就有一個調用約定的問題,默認情況下調用約定是__cdecl,表示參數從右向左入棧,由調用者清理參數。而windows API使用的是__stdcall調用,表示參數從右向左入棧,由函數自己清理參數。我們的程序API的替換函數使用__stdcall調用約定。所以不用考慮清理棧中的參數的問題,由替換函數自己處理。
有了上面的知識,讓我們回顧一下先前討論的問題,首先,用戶調用API的recv函數,程序運行到recv的入口地址處,此時堆棧中擁有用戶調用recv的參數和用戶代碼中CALL [recv]的下一條指令的地址。堆棧如下圖:
堆棧指針[ESP] 堆棧的內容 堆棧內容的含義
0x00000100 0 參數
0x000000fc len 參數
0x000000f8 buf 參數
0x000000f4 S 參數
0x000000f0 RetUserAddress 用戶調用recv的下一條指令的地址
然後程序指針EIP被修改爲recv入口處的地址,而入口地址處有一條簡單的CALL指令,它使程序將recv的第6個字節的地址壓入棧中(因爲CALL XXXX佔用5個字節,第六個字節被認爲爲返回地址),然後跳轉到我們的無參數無返回值的通用替換函數中去了,好了看看現在堆棧中都有些什麼?如圖:
堆棧指針[ESP] 堆棧的內容 堆棧內容的含義
0x00000100 0 參數
0x000000fc len 參數
0x000000f8 buf 參數
0x000000f4 s 參數
0x000000f0 RetUserAddress 用戶調用recv的下一條指令的地址
0x00000ec RetrecvAddress recv的第六個字節的地址
首先是參數,其次是用戶調用recv後的返回值,然後是recv調用我們的替換函數中的返回值,緊接着就像剛纔提到的那樣,程序將EBP當前內容壓入棧中。如圖
堆棧指針[ESP] 堆棧的內容 堆棧內容的含義
0x00000100 0 參數
0x000000fc len 參數
0x000000f8 buf 參數
0x000000f4 S 參數
0x000000f0 RetUserAddress 用戶調用recv的下一條指令的地址
0x00000ec RetrecvAddress recv的第六個字節的地址
0x000000e8 OldEBP 保存的舊的EBP的內容,然後[EBP]= 0x000000e8
0x000000e4 OldEBX 保存的舊的EBX的內容
0x000000e0 OldESI 保存的舊的ESI的內容
0x000000dc OldEDI 保存的舊的EDI的內容,此[ESP]=0x000000dc
此時我們可以看到,[EBP]爲保存的ebp的值,現在對我們沒有用處,函數返回前用於恢復EBP的值,[EBP+4]是recv函數的CALL XXXX後面指令的地址(也就是第六個字節的地址),我們可以通過將此值減去5來得到recv的入口地址,這樣在我們所有hook的api函數的列表中進行檢索,就可以匹配出用戶調用的是哪一個API函數,從而爲後面恢復和再次改變該API的入口5字節做準備,因爲調用任何我們需要HOOK的API程序都會進入到這個無返回值無參數的函數,所以通過這種方法找到當前HOOK的是哪一個API,從而可以區分不同的API進行特殊的處理。[EBP+8]保存的是用戶調用recv後的返回地址,由於我們執行完替換函數後,應該返回到這個地址,而不應該返回到recv的第6個字節處執行,所以我們還是需要保存下這個值,以便在我們用ret返回前把它壓棧從而使程序返回到用戶調用recv的下一條指令處繼續運行。
我們現在實現它,請注意參考上面堆棧表格。
void CHookApi::CommonFunc(void)
{
DWORD* pdwCall;// 保存被調用前壓在棧中的返回地址,也就是Call XXXX 的地址
DWORD* pdwESP;// 保存ESP內容
DWORD* pdwParam; // 第一個參數的地址
DWORD dwParamSize; // 所有參數所佔用的大小應該=4* dwParamCount
DWORD dwRt; // 保存返回值
DWORD dwRtAddr; // 我們的函數真正要返回的地址
PROCESS_INFORMATION *pPi;// 進程信息
// 得到棧中第一個參數的位置
_asm
{
mov EAX,[EBP+8]
mov [dwRtAddr],EAX
lea EAX,[EBP+4] // call XXXX所在的地址
mov [pdwCall],EAX
lea EAX, [EBP+12] // 第一個參數所在地址
mov [pdwParam],EAX
}
(*pdwCall) -= 5;
vector::iterator it;
APIINFO* pai = NULL;
for(it = m_vpApiInfo.begin(); it != m_vpApiInfo.end(); it++)
{
APIINFO* papiinfo = *it;
if((DWORD)papiinfo->lfOrgApiAddr == *pdwCall)
{
pai = *it;
break;
}
}
if(pai == NULL)
return;
BYTE* pbtapi = (BYTE*)pai->lfOrgApiAddr;
dwParamSize = 4*pai->ParamCount;
EnterCriticalSection(&pai->cs);
// 恢復被修改的5個字節
memcpy(pbtapi,pai->OrgApiBytes,5);
pai->bIsHooked = FALSE;
// 構造參數
_asm
{
sub esp,[dwParamSize]
mov [pdwESP],esp
}
memcpy(pdwESP, pdwParam, dwParamSize);
COMMONFUNC myapifunc = (COMMONFUNC)pai->lfMyApiAddr;
_asm
{
call myapifunc // 調用替換API的函數
mov [dwRt],eax // 保存返回值
}
// 如果是CreateProcess,那麼繼續hook它
pPi = (PROCESS_INFORMATION*)pdwParam[9];
if(strcmpi(pai->szOrgApiName,"CreateProcessA") != 0 || strcmpi(pai->szOrgApiName,"CreateProcessW") != 0)
{
InjectDll(pPi->dwProcessId,m_szDllPathName);
}
// 再修改5字節
pbtapi[0] = CALLCODE;//jmp
DWORD* pdwapi = (DWORD*)&(pbtapi[1]);
pdwapi[0] = (DWORD)CommonFunc - (DWORD)pbtapi - 5;// 我的api的地址偏移
pai->bIsHooked = TRUE;
LeaveCriticalSection(&pai->cs);
// 下面準備返回的操作
_asm
{
mov EDX,[dwRtAddr] // 保存返回地址
mov EAX,dwRt // 設置返回值
mov ECX,[dwParamSize] // 獲得參數的大小
// 下面彈出所有保存的寄存器值(按照入棧的逆順序)
pop EDI // 恢復EDI
pop ESI // 恢復ESI
pop EBX // 恢復EBX
// 我們沒有改動過EBP的值,所以EBP指向堆棧中OldEBP的位置
mov ESP,EBP
pop EBP // 恢復EBP
// 由於堆棧中還剩下參數和兩個返回地址(我們真正要返回的地址和原始API中的第6個字節的地址),所以我們把這些數據也清除出堆棧
add ESP,8 // 清除兩個返回地址
add ESP,ECX // 清除參數
// 由於調用ret返回時,程序先從堆棧中取出返回地址,所以我們把要真正返回的地址壓入堆棧中
push EDX
ret // 返回
}
}
要說明的一點是,如果要執行得API函數是CreateProcess,那麼應該把它新開啓得進程也HOOK掉。以上我們瞭解了通用替換函數的原理,那麼讓我們深入的討論CHookApi類,並且實現它。
前面提到過,我們的CHookApi類主要向外部提供2個方法,HookAllAPI方法和UnhookAllAPI方法。當調用HookAllAPI的時候,將攔截系統所有用戶程序中我們感興趣的API函數。當調用UnhookAllAPI的時候,將撤銷攔截。當攔截啓動之前,我們應該將所有我們感興趣的,即需要攔截的API信息(如API名稱,對應的替換函數名稱、參數個數等)交給CHookApi,CHookApi類內部才能完成所有的攔截工作。以下是CHookApi類的聲明
(HookApi.h)
#include "../include/CommonHeader.h"
#include
#include
#pragma comment(lib,"Psapi.lib")
// 通用替換函數指針聲明
typedef void(*COMMONFUNC)(void);
class CHookApi
{
public:
// 構造函數
CHookApi(void);
// 析構函數
~CHookApi(void);
private:
// 自身路徑
static char m_szDllPathName[MAX_PATH];
// 此容器包含所有要Hook的API信息(在類中使用的有效信息)
static vector m_vpApiInfo;
// 此容器包含所有要Hook的API信息(用戶填寫的信息)
vector* m_vpHookApiInfo;
// 防火牆策略模塊句柄
HMODULE m_hFireWall;
// 加載防火牆策略模塊
BOOL LoadFireWallModule(void);
// 卸載防火牆策略模塊
BOOL UnloadFireWallModule(void);
public:
// 使用遠程線程注入的方法將我們的DLL注入到指定進程
static int WINAPI InjectDll(DWORD dwProcessId, LPTSTR lpDllName);
// 使用遠程線程注入的方法將我們的DLL卸載
static int WINAPI EjectLib(DWORD dwProcessId, LPTSTR lpDllName);
protected:
// 通用替換函數,這是技術核心部分
static void CommonFunc(void);
private:
// 初始化函數
BOOL Init(void);
// 掛鉤一個指定API的函數
BOOL HookOneApi(APIINFO* pai);
// 掛鉤所有指定API的函數
BOOL HookAllApis(void);
// 取消掛鉤一個指定API的函數
BOOL UnhookOneApi(APIINFO* pai);
// 取消掛鉤所有指定API的函數
BOOL UnhookAllApis(void);
// 設置進程內存區域的存取權限
BOOL SetMemmoryAccess(APIINFO* pai,BOOL CanWritten);
// 修改/恢復指定API的前5個字節
BOOL SetCallMemmory(APIINFO* pai,BOOL bHook);
};
其中HOOKAPIINFO結構填寫基本的攔截信息,在CHookApi內部將會把它轉化爲APIINFO結構。HOOKAPIINFO和APIINFO結構定義在CommonHeader.h文件中,CommonHeader.h文件如下:
(CommonHeader.h)
#define FIREWALLDLLNAME "ReplacelDll.Dll" // 實現被攔截的API替換函數的名字
#define MAINHOOKDLLNAME "MainHookDll.Dll" // 攔截API主模塊的名字
#define GETHOOKINFOFUNCNAMEINREPLACEDLL "GetHookApiInfo" // 替換模塊導出函數,它返回要攔截的信息
#define FREEHOOKINFOFUNCNAMEINREPLACEDLL "FreeHookApiInfo" // 替換模塊導出函數,它釋放要攔截的信息
#define CALLCODE 0xE8
#ifndef COMMONHEADER_H
#define COMMONHEADER_H
#include
using namespace std;
typedef void(*CMAPIFUNC)(void);
// 關於API函數信息的結構
typedef struct _APIINFO
{
// 要攔截的API函數名
char szOrgApiName[100];
// 要攔截的API的地址
CMAPIFUNC lfOrgApiAddr;
// 我們要替換原來API的地址
CMAPIFUNC lfMyApiAddr;
// 要保存的原來API入口點的前5個字節
BYTE OrgApiBytes[5];
// 對進程內存的保護狀態,調用VirtualProtect來改變對內存訪問權限時得到的。
DWORD dwOldProtectFlag;
// 參數的個數
int ParamCount;
// 臨界對象,爲了互斥用,避免同時修改原始api的前5個字節
CRITICAL_SECTION cs;
// 是否已經HOOK了
BOOL bIsHooked;
// 用戶需要使用的結構,通過這個結構來了解用戶需要攔截的API的信息
typedef struct _HOOKAPIINFO
{
union
{
struct
{
// 我們要替換的API所在的DLL的名字
char szMyModuleName[100];
// 我們要替換原來API的函數名字
char szMyApiName[50];
} MyApi;
CMAPIFUNC lfMyApi;
};
// 要攔截的API所在DLL的名字
char szOrgModuleName[100];
// 要攔截的API的名字
char szOrgApiName[50];
// 參數的個數
int ParamCount;
// 提供MyAPI的方式
BOOL bMyApiType; // 如果是0代表提供地址,1代表使用模塊名和函數名指定
} HOOKAPIINFO;
// 獲得要攔截的API的信息函數指針
typedef vector*(*GETHOOKAPIINFO)(void);
// 釋放要攔截的API的信息函數指針
typedef void(*FREEHOOKAPIINFO)(void);
// 遠程注入函數指針
typedef int (*INJECTDLL)(DWORD dwProcessId, LPTSTR lpDllName);
#endif
在這個頭文件中,除了定義了HOOKAPIINFO和APIINFO結構還有一些其它定義,FIREWALLDLLNAME宏指定防火牆策略模塊的文件名稱,MAINHOOKDLLNAME宏指定本模塊的文件名稱,GETHOOKINFOFUNCNAMEINREPLACEDLL宏指定我們寫的API替換函數所在的DLL(我們成爲替換模塊)中用來返回用戶的HOOKAPIINFO結構容器指針的函數名稱,程序將會自動加載替換模塊並調用這個函數獲得用戶的HOOKAPIINFO結構容器的指針,根據後面的函數指針的定義不難看出,這個函數必須是一個返回值爲vector*,並且沒有參數的導出函數。FREEHOOKINFOFUNCNAMEINREPLACEDLL 是釋放返回的vector*指針的導出函數名字。CALLCODE宏指定要替換的跳轉指令,這裏只用CALL指令,他的機器碼是E8。
在構造函數中調用Init函數和HookAllApis函數,這樣使該類被構造的時候就能夠自動進行初始化和hook工作。Init函數的主要工作是將用戶提供的HOOKAPIINFO結構轉化成APIINFO結構。HookAllApis函數內部循環調用HookOneApi函數進行真正的Hook操作。詳見源代碼中的註釋。
CHookApi類完成了Hook的核心工作後,我們需要讓系統所有的進程加載我們的MainHookDll.Dll,從而對系統所有進程中的指定API進行攔截。我們的想法是,MainHookDll模塊提供2個導出函數,HookAllProcesses和UnhookAllProcesses函數完成這個功能,當UserUI調用HookAllProcesses函數的時候,系統所有的進程將加載MainHookDll.Dll,在加載的同時讓CHookApi類開始工作,對目標進程中的所有指定API的前5個字節進行替換。當調用UnhookAllProcesses函數的時候,系統所有的進程將卸載MainHookDll.Dll,從而取消對指定API的攔截。在HookAllProcesses和UnhookAllProcesses函數中實際上是列舉當前系統的所有進程,並對所有進程進行相同的操作。所以我們可以再實現兩個函數,用CHookApi類的靜態成員InjectDll和EjectLib函數完成真正的功能,而HookAllProcesses和UnhookAllProcesses中實現列舉系統進程並對所有進程調用InjectDll或EjectLib函數。我們使用遠程線程來實現InjectDll和EjectLib函數。遠程線程是指在當前進程中使目標進程啓動一個線程。可以通過以下方法完成遠程線程的調用。首先使用OpenProcess打開目標進程得到進程句柄,要啓動遠程線程最少需要用PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE, and PROCESS_VM_READ權限打開目標進程(我們使用PROCESS_ALL_ACCESS)。然後調用VirtualAllocEx函數在目標進程中分配內存,使用WriteProcessMemory函數將當前進程中的線程函數的參數寫入在目標進程分配的內存中,最後調用CreateRemoteThread函數使目標進程執行線程函數。(實現過程請參考源代碼中的註釋)。下面給出InjectDll函數的代碼,該函數將指定的Dll文件注入到指定ID號的進程中。
int WINAPI CHookApi::InjectDll(DWORD dwProcessId, LPTSTR lpDllName)
{
PTHREAD_START_ROUTINE pfnRemote =(PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"), "LoadLibraryA");
if(pfnRemote ==NULL)
return -1;
HANDLE hProcess =OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if(hProcess ==NULL)
{
return -1;
}
int iMemSize = (int)strlen(lpDllName)+1;
void *pRemoteMem =VirtualAllocEx(hProcess, NULL, iMemSize, MEM_COMMIT, PAGE_READWRITE);
if(pRemoteMem ==NULL)
{
CloseHandle(hProcess);
return -1;
}
if (!WriteProcessMemory(hProcess, pRemoteMem, lpDllName, iMemSize,NULL))
{
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return -1;
}
HANDLE hThread =CreateRemoteThread(hProcess, NULL, 0, pfnRemote, pRemoteMem, 0, NULL);
if(hThread ==NULL)
{
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return -1;
}
WaitForSingleObject(hThread, INFINITE);
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
函數一開始,指定了要被注入的線程函數的位置,在這裏,我們的線程函數就是LoadLibraryA函數,遠程線程中的線程函數應該是PTHREAD_START_ROUTINE類型的函數指針,該函數指針的聲明如下:
typedef DWORD (*PTHREAD_START_ROUTINE)(LPVOID)
由聲明可知,該函數是一個只有一個參數的,且返回值是DWORD類型的函數,而LoadLibraryA函數符合要求的,所以它可以直接作爲遠程線程的線程函數。當遠程進程調用LoadLibraryA把我們的MainHookDll.Dll加載後,攔截工作就會自動進行。緊接着,用VirtualAllocEx函數在遠程進程中開闢一塊內存,開闢的內存的大小是MainHookDll的全路徑名(注意最後的’\0’)。接着使用WriteProcessMemory函數將參數(即MainHookDll的全路徑名)寫入到剛剛開闢的遠程進程中的地址空間。最後調用CreateRemoteThread函數啓動遠程線程。要注意的是,當線程結束後,要用VirtualFreeEx函數釋放在遠程進程內開闢的內存。下面的代碼列出了EjectLib函數,它是使系統當前進程卸載我們的MainHookDll文件:
int WINAPI CHookApi::EjectLib(DWORD dwProcessId, LPTSTR lpDllName)
{
// open the process
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcessId);
if(hProcess == NULL)
return -1;
// 枚舉進程中的模塊
HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
if(hModuleSnap == INVALID_HANDLE_VALUE)
{
CloseHandle(hProcess);
return -1;
}
MODULEENTRY32 me32;
me32.dwSize = sizeof(MODULEENTRY32);
BOOL bFound = FALSE;
HMODULE hmod = NULL;
if(Module32First(hModuleSnap, &me32))
{
do
{
if(strcmpi(me32.szModule,lpDllName) == 0)
{
hmod = me32.hModule;
bFound = TRUE;
}
}while(!bFound && Module32Next(hModuleSnap, &me32));
}
CloseHandle(hModuleSnap);
if(hmod == NULL)
{
// 沒有指定的模塊
CloseHandle(hProcess);
return 0;
}
// 創建遠程線程
PTHREAD_START_ROUTINE pfnRemote =(PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"),"FreeLibrary");
if(pfnRemote ==NULL)
{
CloseHandle(hProcess);
return -1;
}
HANDLE hThread =CreateRemoteThread(hProcess,NULL,0,pfnRemote,hmod,0,NULL);
if(hThread ==NULL)
{
CloseHandle(hProcess);
return -1;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
我們在函數中使用了Psapi中定義的函數列舉指定進程中加載的模塊,看看是否加載了我們的MainHookDll文件,如果加載了,那麼使用FreeLibrary函數卸載它。
我們寫一個要被測試的程序叫test.exe,在裏面調用要被攔截的API,比如是MessageBox。我們的主程序是MainApp.exe。HOOK模塊是MainHookDll.dll,所有要替換的函數都在Replace.dll裏面導出。當MainApp.exe運行起來後,他先加載MainHookDll.dll,並且調用MainHookDll.dll的InjectDll方法把MainHookDll.dll注入到目標進程。MainHookDll.dll被加載後,先主動加載Replace.dll並調用Replace.dll的GETHOOKINFOFUNCNAMEINREPLACEDLL方法獲得要HOOK的信息。然後就按照剛纔分析的機制對目標進程進行攔截了。
由此可見,我們在寫程序的時候,攔截某個API的處理函數都在Replace.dll中,其他部分的代碼都是固定的。
2011年03月28日
什麼是HOOK API:
Windows下暴露的對開發人員的接口叫做應用程序編程接口,就是我們常說的API。我們在寫應用層應用程序軟件的時候都是通過調用各種API來實現的。有些時候,我們需要監控其他程序調用的API,也就是,當其他應用程序調用我們感興趣的API的時候,我們在他調用前有一個機會做自己的處理,這就是HOOK API的涵義。
思路:
我們知道Windows系統API函數都是被封裝到DLL中,在某個應用程序要調用一個API函數的時候,如果這個函數所在的DLL沒有被加載到本進程中則加載它,然後保存當前環境(各個寄存器和函數調用完後的返回地址等)。接着程序會跳轉到這個API函數的入口地址去執行此處的指令。由此看來,我們想在調用真正的API之前先調用我們的函數,那麼可以修改這個API函數的入口處的代碼,使他先跳轉到我們的函數地址,然後在我們的函數最後再調用原來的API函數。下面以攔截WS2_32.dll中的recv函數爲例說明攔截的主要過程。首先把自己編寫的DLL掛接到系統當前運行的所有進程中(要排除一些Windows系統自身的進程,否則會出現問題,影響系統正常工作),掛接的意思是,要我們的DLL運行在目標進程的地址空間中。可以使用列舉系統進程然後用遠程線程注入的方法,但是這種方法只適用於Win2000以上的操作系統。
當我們的DLL被所有目標進程加載後,我們就可以進行真正的工作了。首先使用Tool Help庫的相關函數列舉目標進程加載的所有模塊,看看是否有ws2_32.dll,如果沒有,說明這個進程沒有使用Winsock提供的函數,那麼我們就不用再給這個進程添亂了。如果找到ws2_32.dll模塊,那麼OK,我們可以開工了。先是用GetProcAddress函數獲得進程中ws2_32.dll模塊的recv函數的入口地址,也就是函數的起始地址。剛纔說過,我們想把recv函數起始位置加入一條跳轉指令,讓它先跳轉到我們的函數中運行。跳轉指令可以用0xE9來表示(0xE9是彙編語言中CALL指令的機器碼),後面還有4個字節的我們函數的相對地址。也就是我們要修改recv函數前5個字節。這5個字節由1個字節的跳轉指令和4個字節的地址組成。這樣當程序運行到這裏的時候,將會跳轉到這4個字節表示的地址處去運行代碼。還要注意的是這4個字節的地址是偏移地址,而偏移地址 = 我們函數的地址 - 原API函數的地址 - 5(我們這條指令的長度)。好了,別忘了我們要先讀取稍後要被覆蓋的recv函數入口處的5個字節的內容,把它保存起來留着以後恢復時使用。因爲在我們的函數中要想調用真正的recv的時候,必須把它前5個字節恢復了,他才能正常工作呢。
通過上面的說明,我們可以整理出這樣的一個流程:
1. 保存recv的前5個字節的內容
2. 把recv的前5個字節的內容改變成CALL xxxx(xxxx是我們的函數的偏移地址)
3. 在我們的函數中恢復recv的前5個字節的內容,並作處理。
4. 我們的函數返回後,再把recv的前5個字節的內容改變成CALL xxxx
慢着,你一定發現問題了吧?當我們爲了調用原來的recv函數而剛剛把recv入口處的5個字節恢復,這時系統中的其他線程調用了recv函數,而這個調用將會成爲漏網之魚而不會進入到我們的函數中來。簡單的解決辦法是使用臨界對象CriticalSection來保證同時只能有一個線程對recv函數入口處5個字節進行讀寫操作。
最後記得在你想要停止攔截的時候恢復所有你修改過的進程和這些進程中被修改的API的前5個字節。其實原理講着容易,在實現的時候會遇到各種各樣的問題,如98下這些系統的DLL被加載到系統內存區供應用程序共享,所以這些內存是受保護的,不能隨意修改,還有nt/2000下權限問題,還要考慮到不要攔截某些系統進程,否則會帶來災難性的後果。這些都是在實踐當中遇到的實際問題。
下面結合代碼給大家講解一下吧,首先我們要實現HOOK模塊,我們給它起個名字叫做MainHookDll.DLL。在此模塊中,主要要實現一個CHookApi的類,這個類完成主要的攔截功能,也是整個項目的技術核心和難點,後面將具體介紹它。而且,MainHookDll模塊就是將來要注入到系統其它進程的模塊,而遠程調用函數是非常困難的事情,所以我們設計此模塊的時候應讓其被加載後自動執行攔截的初始化等工作。這樣,我們只需要讓遠程的進程加載HOOK,然後MainHookDll.dll就能夠自動執行其它操作從而HOOK該進程的相關API。
MainHookDll模塊中的CHookApi類擁有2個向外部提供的主要的方法,HookAllAPI,表示攔截指定進程中的指定API和UnhookAllAPI,表示取消攔截指定進程中的指定API。進行具體設計的時候,會遇到一個問題。大家看到,上文所說的開始將原始API的前5個字節寫成CALL XXXX,而在我們的替換函數中要恢復保存的API原始的5個字節,在調用完成後又要把API前5個字節改爲CALL XXXX。如果我們攔截多個API要在每個替換函數中按照如上的方法進行設置,這樣雖然我們自己明白,但是可能您只是實現HOOKAPI部分,而別人實現調用,這樣會使代碼看起來很難維護,在別人寫的替換函數中加上這些莫名奇妙的語句看來不是一個好主意,當需要攔截多個感興趣的API函數,那樣的話將會在每一個要攔截的函數裏都有這些莫名其妙的代碼將會是件很噁心得事情。而且對於CALL XXXX中的地址,要對於不同的API設置不同的替換函數地址。那麼能不能把這些所有的函數歸納爲一個函數,所有的API函數前5字節都改爲CALL到這個函數的地址,這個函數先恢復API的前5字節,然後調用用戶真正的替換函數,然後再設置API函數的前5字節,這樣可以使真正的替換函數只做自己應該做的事情,而跟HOOK API相關的操作都由我們的通用函數來幹。
這樣的想法是好的,但是有一個突出問題,因爲替換函數的函數聲明與原API一致,所以對於要攔截的不同的API,它們的的參數和返回值是不一樣的。那我們怎樣通過一個函數獲得用戶傳遞給API的參數,然後使用這些參數調用替換函數,最後把替換函數的返回值再返回給調用API的客戶?要想實現這個功能,我們需要了解一個知識,也就是C++究竟是怎樣調用一個函數的。我們以ws2_32.dll中提供的recv函數爲例進行說明,recv函數的聲明如下:
int recv(
SOCKET s,
char* buf,
int len,
int flags
);
可以看出它具有4個參數,返回值類型是int。我們作如下調用:
recv(s,buf,buflen,0);
那麼在調用recv前,這四個參數將按照從右向左的順序壓到棧中,然後用Call指令跳轉到recv函數的地址繼續執行。recv可以從棧中取出參數並執行其他功能,最後返回時返回值將被保存在寄存器EAX中。最後還要說明一點的是,在彙編語言看來這些參數和返回值都是以DWORD類型表示的,所以如果是大於4字節的值,就用這4個字節表示值所在的地址。
有了這些知識我們就可以想到,如果用戶調用recv函數並被攔截跳轉到我們的函數中運行,但是我們並不知道有多少個參數和返回值,那麼我們可以從棧中取出參數,但是參數的個數需要提供,當然我們可以在前面爲每個API函數指定相應的參數個數,然後運行真正的替換函數,最後在返回前把替換函數的返回值放到寄存器EAX中,這樣就解決了不知道參數和返回值個數的問題。那麼我們的函數應該是看起來無參數無返回值的。
基本原理我們大家都清楚了,但是繼續之前我還是想講一講幾個彙編的知識,如果沒有這些知識那麼看下面的代碼就好像天書一樣。
關於參數
我們講過,在調用一個子函數前要把參數按順序壓棧,而子函數會從棧中取出參數。對於棧操作,我們一般使用EBP和ESP寄存器,而ESP是堆棧指針寄存器,所以多數情況下使用EBP寄存器對堆棧進行暫時操作。還是用調用recv函數爲例,假設調用前ESP指向0x00000100處(程序運行時ESP是不可能爲這個值的,此處只是爲了舉例說明問題)。先將參數一次壓棧
push 0 // flags入棧
lea eax, [len]
push eax // len入棧
lea eax, [buf]
push eax // buf入棧
lea eax, [s]
push eax // s入棧
下面使用call調用真正的recv函數,
call dword ptr [recv] // 調用recv
call指令先將返回地址壓入棧中,返回地址就是CALL指令的下一條指令的地址,然後跳轉到recv入口地址處繼續執行。進入recv後,recv使用EBP臨時訪問堆棧之前,要保存EBP的當前內容,以便以後再使用(在關於調用函數時保存各個寄存器的值部分將詳細討論)。所以位於recv函數開始可能是這樣的
push ebp // 保存ebp的當前值
mov ebp,esp // 使把esp負給ebp
堆棧指針[ESP] 堆棧的內容 堆棧內容的含義
0x00000100 flags 參數
0x000000fc len 參數
0x000000f8 buf 參數
0x000000f4 s 參數
0x000000f0 RetAddress 返回的地址
0x00000ec OldEBP 保存EBP的當前值
到此,我們可以知道,如果現在要想通過EBP獲得最後一個入棧的參數,那麼需要用EBP+8來獲得,因爲最後一個入棧的參數被保存在返回地址和EBP原始值的上面(一定記住,棧是由高地址到低地址的)。而返回地址被放在EBP+4處,EBP的原始值放在EBP+0處。
關於調用函數時保存各個寄存器的值
當我們要調用其它函數的時候,程序應該先保存各個寄存器的值,然後轉去調用其它函數,最後會恢復各個寄存器的值使它們恢復成調用其它函數之前的狀態。當然我們使用高級語言寫程序的時候,編譯器爲我們做了這些事情。使用vc調試程序,打開反彙編窗口。運行一個簡單的程序,該程序調用一個我們自己寫的簡單函數,在這個簡單函數中設置斷點,可以看到,編譯器生成的彙編代碼使用堆棧保存各個寄存器的值,上面提到當執行一個函數的時候,首先保存的是EBP的值,然後依次壓入棧中保存的寄存器爲EBX、ESI、EDI,我們在恢復這些寄存器的值的時候將逆向出棧來完成。
關於函數調用的返回
調用子函數前ESP指針會因爲壓棧參數而改變,然後壓入返回地址等,子函數中會使用ret指令從棧中取出返回地址並跳轉到返回地址,而在子函數返回到CALL的下一條指令時棧中還保存着參數,所以我們需要手工的將棧中的參數所佔用的空間釋放,如在調用完成一個4個參數的子函數後,我們應該將ESP指針上移4*4個字節,如
add esp,16
這個操作在調用API的時候是不需要的,因爲,windows API在函數中自己將參數彈出堆棧了。所以這就有一個調用約定的問題,默認情況下調用約定是__cdecl,表示參數從右向左入棧,由調用者清理參數。而windows API使用的是__stdcall調用,表示參數從右向左入棧,由函數自己清理參數。我們的程序API的替換函數使用__stdcall調用約定。所以不用考慮清理棧中的參數的問題,由替換函數自己處理。
有了上面的知識,讓我們回顧一下先前討論的問題,首先,用戶調用API的recv函數,程序運行到recv的入口地址處,此時堆棧中擁有用戶調用recv的參數和用戶代碼中CALL [recv]的下一條指令的地址。堆棧如下圖:
堆棧指針[ESP] 堆棧的內容 堆棧內容的含義
0x00000100 0 參數
0x000000fc len 參數
0x000000f8 buf 參數
0x000000f4 S 參數
0x000000f0 RetUserAddress 用戶調用recv的下一條指令的地址
然後程序指針EIP被修改爲recv入口處的地址,而入口地址處有一條簡單的CALL指令,它使程序將recv的第6個字節的地址壓入棧中(因爲CALL XXXX佔用5個字節,第六個字節被認爲爲返回地址),然後跳轉到我們的無參數無返回值的通用替換函數中去了,好了看看現在堆棧中都有些什麼?如圖:
堆棧指針[ESP] 堆棧的內容 堆棧內容的含義
0x00000100 0 參數
0x000000fc len 參數
0x000000f8 buf 參數
0x000000f4 s 參數
0x000000f0 RetUserAddress 用戶調用recv的下一條指令的地址
0x00000ec RetrecvAddress recv的第六個字節的地址
首先是參數,其次是用戶調用recv後的返回值,然後是recv調用我們的替換函數中的返回值,緊接着就像剛纔提到的那樣,程序將EBP當前內容壓入棧中。如圖
堆棧指針[ESP] 堆棧的內容 堆棧內容的含義
0x00000100 0 參數
0x000000fc len 參數
0x000000f8 buf 參數
0x000000f4 S 參數
0x000000f0 RetUserAddress 用戶調用recv的下一條指令的地址
0x00000ec RetrecvAddress recv的第六個字節的地址
0x000000e8 OldEBP 保存的舊的EBP的內容,然後[EBP]= 0x000000e8
0x000000e4 OldEBX 保存的舊的EBX的內容
0x000000e0 OldESI 保存的舊的ESI的內容
0x000000dc OldEDI 保存的舊的EDI的內容,此[ESP]=0x000000dc
此時我們可以看到,[EBP]爲保存的ebp的值,現在對我們沒有用處,函數返回前用於恢復EBP的值,[EBP+4]是recv函數的CALL XXXX後面指令的地址(也就是第六個字節的地址),我們可以通過將此值減去5來得到recv的入口地址,這樣在我們所有hook的api函數的列表中進行檢索,就可以匹配出用戶調用的是哪一個API函數,從而爲後面恢復和再次改變該API的入口5字節做準備,因爲調用任何我們需要HOOK的API程序都會進入到這個無返回值無參數的函數,所以通過這種方法找到當前HOOK的是哪一個API,從而可以區分不同的API進行特殊的處理。[EBP+8]保存的是用戶調用recv後的返回地址,由於我們執行完替換函數後,應該返回到這個地址,而不應該返回到recv的第6個字節處執行,所以我們還是需要保存下這個值,以便在我們用ret返回前把它壓棧從而使程序返回到用戶調用recv的下一條指令處繼續運行。
我們現在實現它,請注意參考上面堆棧表格。
void CHookApi::CommonFunc(void)
{
DWORD* pdwCall;// 保存被調用前壓在棧中的返回地址,也就是Call XXXX 的地址
DWORD* pdwESP;// 保存ESP內容
DWORD* pdwParam; // 第一個參數的地址
DWORD dwParamSize; // 所有參數所佔用的大小應該=4* dwParamCount
DWORD dwRt; // 保存返回值
DWORD dwRtAddr; // 我們的函數真正要返回的地址
PROCESS_INFORMATION *pPi;// 進程信息
// 得到棧中第一個參數的位置
_asm
{
mov EAX,[EBP+8]
mov [dwRtAddr],EAX
lea EAX,[EBP+4] // call XXXX所在的地址
mov [pdwCall],EAX
lea EAX, [EBP+12] // 第一個參數所在地址
mov [pdwParam],EAX
}
(*pdwCall) -= 5;
vector::iterator it;
APIINFO* pai = NULL;
for(it = m_vpApiInfo.begin(); it != m_vpApiInfo.end(); it++)
{
APIINFO* papiinfo = *it;
if((DWORD)papiinfo->lfOrgApiAddr == *pdwCall)
{
pai = *it;
break;
}
}
if(pai == NULL)
return;
BYTE* pbtapi = (BYTE*)pai->lfOrgApiAddr;
dwParamSize = 4*pai->ParamCount;
EnterCriticalSection(&pai->cs);
// 恢復被修改的5個字節
memcpy(pbtapi,pai->OrgApiBytes,5);
pai->bIsHooked = FALSE;
// 構造參數
_asm
{
sub esp,[dwParamSize]
mov [pdwESP],esp
}
memcpy(pdwESP, pdwParam, dwParamSize);
COMMONFUNC myapifunc = (COMMONFUNC)pai->lfMyApiAddr;
_asm
{
call myapifunc // 調用替換API的函數
mov [dwRt],eax // 保存返回值
}
// 如果是CreateProcess,那麼繼續hook它
pPi = (PROCESS_INFORMATION*)pdwParam[9];
if(strcmpi(pai->szOrgApiName,"CreateProcessA") != 0 || strcmpi(pai->szOrgApiName,"CreateProcessW") != 0)
{
InjectDll(pPi->dwProcessId,m_szDllPathName);
}
// 再修改5字節
pbtapi[0] = CALLCODE;//jmp
DWORD* pdwapi = (DWORD*)&(pbtapi[1]);
pdwapi[0] = (DWORD)CommonFunc - (DWORD)pbtapi - 5;// 我的api的地址偏移
pai->bIsHooked = TRUE;
LeaveCriticalSection(&pai->cs);
// 下面準備返回的操作
_asm
{
mov EDX,[dwRtAddr] // 保存返回地址
mov EAX,dwRt // 設置返回值
mov ECX,[dwParamSize] // 獲得參數的大小
// 下面彈出所有保存的寄存器值(按照入棧的逆順序)
pop EDI // 恢復EDI
pop ESI // 恢復ESI
pop EBX // 恢復EBX
// 我們沒有改動過EBP的值,所以EBP指向堆棧中OldEBP的位置
mov ESP,EBP
pop EBP // 恢復EBP
// 由於堆棧中還剩下參數和兩個返回地址(我們真正要返回的地址和原始API中的第6個字節的地址),所以我們把這些數據也清除出堆棧
add ESP,8 // 清除兩個返回地址
add ESP,ECX // 清除參數
// 由於調用ret返回時,程序先從堆棧中取出返回地址,所以我們把要真正返回的地址壓入堆棧中
push EDX
ret // 返回
}
}
要說明的一點是,如果要執行得API函數是CreateProcess,那麼應該把它新開啓得進程也HOOK掉。以上我們瞭解了通用替換函數的原理,那麼讓我們深入的討論CHookApi類,並且實現它。
前面提到過,我們的CHookApi類主要向外部提供2個方法,HookAllAPI方法和UnhookAllAPI方法。當調用HookAllAPI的時候,將攔截系統所有用戶程序中我們感興趣的API函數。當調用UnhookAllAPI的時候,將撤銷攔截。當攔截啓動之前,我們應該將所有我們感興趣的,即需要攔截的API信息(如API名稱,對應的替換函數名稱、參數個數等)交給CHookApi,CHookApi類內部才能完成所有的攔截工作。以下是CHookApi類的聲明
(HookApi.h)
#include "../include/CommonHeader.h"
#include
#include
#pragma comment(lib,"Psapi.lib")
// 通用替換函數指針聲明
typedef void(*COMMONFUNC)(void);
class CHookApi
{
public:
// 構造函數
CHookApi(void);
// 析構函數
~CHookApi(void);
private:
// 自身路徑
static char m_szDllPathName[MAX_PATH];
// 此容器包含所有要Hook的API信息(在類中使用的有效信息)
static vector m_vpApiInfo;
// 此容器包含所有要Hook的API信息(用戶填寫的信息)
vector* m_vpHookApiInfo;
// 防火牆策略模塊句柄
HMODULE m_hFireWall;
// 加載防火牆策略模塊
BOOL LoadFireWallModule(void);
// 卸載防火牆策略模塊
BOOL UnloadFireWallModule(void);
public:
// 使用遠程線程注入的方法將我們的DLL注入到指定進程
static int WINAPI InjectDll(DWORD dwProcessId, LPTSTR lpDllName);
// 使用遠程線程注入的方法將我們的DLL卸載
static int WINAPI EjectLib(DWORD dwProcessId, LPTSTR lpDllName);
protected:
// 通用替換函數,這是技術核心部分
static void CommonFunc(void);
private:
// 初始化函數
BOOL Init(void);
// 掛鉤一個指定API的函數
BOOL HookOneApi(APIINFO* pai);
// 掛鉤所有指定API的函數
BOOL HookAllApis(void);
// 取消掛鉤一個指定API的函數
BOOL UnhookOneApi(APIINFO* pai);
// 取消掛鉤所有指定API的函數
BOOL UnhookAllApis(void);
// 設置進程內存區域的存取權限
BOOL SetMemmoryAccess(APIINFO* pai,BOOL CanWritten);
// 修改/恢復指定API的前5個字節
BOOL SetCallMemmory(APIINFO* pai,BOOL bHook);
};
其中HOOKAPIINFO結構填寫基本的攔截信息,在CHookApi內部將會把它轉化爲APIINFO結構。HOOKAPIINFO和APIINFO結構定義在CommonHeader.h文件中,CommonHeader.h文件如下:
(CommonHeader.h)
#define FIREWALLDLLNAME "ReplacelDll.Dll" // 實現被攔截的API替換函數的名字
#define MAINHOOKDLLNAME "MainHookDll.Dll" // 攔截API主模塊的名字
#define GETHOOKINFOFUNCNAMEINREPLACEDLL "GetHookApiInfo" // 替換模塊導出函數,它返回要攔截的信息
#define FREEHOOKINFOFUNCNAMEINREPLACEDLL "FreeHookApiInfo" // 替換模塊導出函數,它釋放要攔截的信息
#define CALLCODE 0xE8
#ifndef COMMONHEADER_H
#define COMMONHEADER_H
#include
using namespace std;
typedef void(*CMAPIFUNC)(void);
// 關於API函數信息的結構
typedef struct _APIINFO
{
// 要攔截的API函數名
char szOrgApiName[100];
// 要攔截的API的地址
CMAPIFUNC lfOrgApiAddr;
// 我們要替換原來API的地址
CMAPIFUNC lfMyApiAddr;
// 要保存的原來API入口點的前5個字節
BYTE OrgApiBytes[5];
// 對進程內存的保護狀態,調用VirtualProtect來改變對內存訪問權限時得到的。
DWORD dwOldProtectFlag;
// 參數的個數
int ParamCount;
// 臨界對象,爲了互斥用,避免同時修改原始api的前5個字節
CRITICAL_SECTION cs;
// 是否已經HOOK了
BOOL bIsHooked;
// 用戶需要使用的結構,通過這個結構來了解用戶需要攔截的API的信息
typedef struct _HOOKAPIINFO
{
union
{
struct
{
// 我們要替換的API所在的DLL的名字
char szMyModuleName[100];
// 我們要替換原來API的函數名字
char szMyApiName[50];
} MyApi;
CMAPIFUNC lfMyApi;
};
// 要攔截的API所在DLL的名字
char szOrgModuleName[100];
// 要攔截的API的名字
char szOrgApiName[50];
// 參數的個數
int ParamCount;
// 提供MyAPI的方式
BOOL bMyApiType; // 如果是0代表提供地址,1代表使用模塊名和函數名指定
} HOOKAPIINFO;
// 獲得要攔截的API的信息函數指針
typedef vector*(*GETHOOKAPIINFO)(void);
// 釋放要攔截的API的信息函數指針
typedef void(*FREEHOOKAPIINFO)(void);
// 遠程注入函數指針
typedef int (*INJECTDLL)(DWORD dwProcessId, LPTSTR lpDllName);
#endif
在這個頭文件中,除了定義了HOOKAPIINFO和APIINFO結構還有一些其它定義,FIREWALLDLLNAME宏指定防火牆策略模塊的文件名稱,MAINHOOKDLLNAME宏指定本模塊的文件名稱,GETHOOKINFOFUNCNAMEINREPLACEDLL宏指定我們寫的API替換函數所在的DLL(我們成爲替換模塊)中用來返回用戶的HOOKAPIINFO結構容器指針的函數名稱,程序將會自動加載替換模塊並調用這個函數獲得用戶的HOOKAPIINFO結構容器的指針,根據後面的函數指針的定義不難看出,這個函數必須是一個返回值爲vector*,並且沒有參數的導出函數。FREEHOOKINFOFUNCNAMEINREPLACEDLL 是釋放返回的vector*指針的導出函數名字。CALLCODE宏指定要替換的跳轉指令,這裏只用CALL指令,他的機器碼是E8。
在構造函數中調用Init函數和HookAllApis函數,這樣使該類被構造的時候就能夠自動進行初始化和hook工作。Init函數的主要工作是將用戶提供的HOOKAPIINFO結構轉化成APIINFO結構。HookAllApis函數內部循環調用HookOneApi函數進行真正的Hook操作。詳見源代碼中的註釋。
CHookApi類完成了Hook的核心工作後,我們需要讓系統所有的進程加載我們的MainHookDll.Dll,從而對系統所有進程中的指定API進行攔截。我們的想法是,MainHookDll模塊提供2個導出函數,HookAllProcesses和UnhookAllProcesses函數完成這個功能,當UserUI調用HookAllProcesses函數的時候,系統所有的進程將加載MainHookDll.Dll,在加載的同時讓CHookApi類開始工作,對目標進程中的所有指定API的前5個字節進行替換。當調用UnhookAllProcesses函數的時候,系統所有的進程將卸載MainHookDll.Dll,從而取消對指定API的攔截。在HookAllProcesses和UnhookAllProcesses函數中實際上是列舉當前系統的所有進程,並對所有進程進行相同的操作。所以我們可以再實現兩個函數,用CHookApi類的靜態成員InjectDll和EjectLib函數完成真正的功能,而HookAllProcesses和UnhookAllProcesses中實現列舉系統進程並對所有進程調用InjectDll或EjectLib函數。我們使用遠程線程來實現InjectDll和EjectLib函數。遠程線程是指在當前進程中使目標進程啓動一個線程。可以通過以下方法完成遠程線程的調用。首先使用OpenProcess打開目標進程得到進程句柄,要啓動遠程線程最少需要用PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE, and PROCESS_VM_READ權限打開目標進程(我們使用PROCESS_ALL_ACCESS)。然後調用VirtualAllocEx函數在目標進程中分配內存,使用WriteProcessMemory函數將當前進程中的線程函數的參數寫入在目標進程分配的內存中,最後調用CreateRemoteThread函數使目標進程執行線程函數。(實現過程請參考源代碼中的註釋)。下面給出InjectDll函數的代碼,該函數將指定的Dll文件注入到指定ID號的進程中。
int WINAPI CHookApi::InjectDll(DWORD dwProcessId, LPTSTR lpDllName)
{
PTHREAD_START_ROUTINE pfnRemote =(PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"), "LoadLibraryA");
if(pfnRemote ==NULL)
return -1;
HANDLE hProcess =OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if(hProcess ==NULL)
{
return -1;
}
int iMemSize = (int)strlen(lpDllName)+1;
void *pRemoteMem =VirtualAllocEx(hProcess, NULL, iMemSize, MEM_COMMIT, PAGE_READWRITE);
if(pRemoteMem ==NULL)
{
CloseHandle(hProcess);
return -1;
}
if (!WriteProcessMemory(hProcess, pRemoteMem, lpDllName, iMemSize,NULL))
{
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return -1;
}
HANDLE hThread =CreateRemoteThread(hProcess, NULL, 0, pfnRemote, pRemoteMem, 0, NULL);
if(hThread ==NULL)
{
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return -1;
}
WaitForSingleObject(hThread, INFINITE);
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
函數一開始,指定了要被注入的線程函數的位置,在這裏,我們的線程函數就是LoadLibraryA函數,遠程線程中的線程函數應該是PTHREAD_START_ROUTINE類型的函數指針,該函數指針的聲明如下:
typedef DWORD (*PTHREAD_START_ROUTINE)(LPVOID)
由聲明可知,該函數是一個只有一個參數的,且返回值是DWORD類型的函數,而LoadLibraryA函數符合要求的,所以它可以直接作爲遠程線程的線程函數。當遠程進程調用LoadLibraryA把我們的MainHookDll.Dll加載後,攔截工作就會自動進行。緊接着,用VirtualAllocEx函數在遠程進程中開闢一塊內存,開闢的內存的大小是MainHookDll的全路徑名(注意最後的’\0’)。接着使用WriteProcessMemory函數將參數(即MainHookDll的全路徑名)寫入到剛剛開闢的遠程進程中的地址空間。最後調用CreateRemoteThread函數啓動遠程線程。要注意的是,當線程結束後,要用VirtualFreeEx函數釋放在遠程進程內開闢的內存。下面的代碼列出了EjectLib函數,它是使系統當前進程卸載我們的MainHookDll文件:
int WINAPI CHookApi::EjectLib(DWORD dwProcessId, LPTSTR lpDllName)
{
// open the process
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcessId);
if(hProcess == NULL)
return -1;
// 枚舉進程中的模塊
HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
if(hModuleSnap == INVALID_HANDLE_VALUE)
{
CloseHandle(hProcess);
return -1;
}
MODULEENTRY32 me32;
me32.dwSize = sizeof(MODULEENTRY32);
BOOL bFound = FALSE;
HMODULE hmod = NULL;
if(Module32First(hModuleSnap, &me32))
{
do
{
if(strcmpi(me32.szModule,lpDllName) == 0)
{
hmod = me32.hModule;
bFound = TRUE;
}
}while(!bFound && Module32Next(hModuleSnap, &me32));
}
CloseHandle(hModuleSnap);
if(hmod == NULL)
{
// 沒有指定的模塊
CloseHandle(hProcess);
return 0;
}
// 創建遠程線程
PTHREAD_START_ROUTINE pfnRemote =(PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"),"FreeLibrary");
if(pfnRemote ==NULL)
{
CloseHandle(hProcess);
return -1;
}
HANDLE hThread =CreateRemoteThread(hProcess,NULL,0,pfnRemote,hmod,0,NULL);
if(hThread ==NULL)
{
CloseHandle(hProcess);
return -1;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
我們在函數中使用了Psapi中定義的函數列舉指定進程中加載的模塊,看看是否加載了我們的MainHookDll文件,如果加載了,那麼使用FreeLibrary函數卸載它。
我們寫一個要被測試的程序叫test.exe,在裏面調用要被攔截的API,比如是MessageBox。我們的主程序是MainApp.exe。HOOK模塊是MainHookDll.dll,所有要替換的函數都在Replace.dll裏面導出。當MainApp.exe運行起來後,他先加載MainHookDll.dll,並且調用MainHookDll.dll的InjectDll方法把MainHookDll.dll注入到目標進程。MainHookDll.dll被加載後,先主動加載Replace.dll並調用Replace.dll的GETHOOKINFOFUNCNAMEINREPLACEDLL方法獲得要HOOK的信息。然後就按照剛纔分析的機制對目標進程進行攔截了。
由此可見,我們在寫程序的時候,攔截某個API的處理函數都在Replace.dll中,其他部分的代碼都是固定的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.