HOOK API技術

HOOK API

HOOK API 是指截獲特定進程或系統對某個API函數的調用,使得API的指定流程轉向指定的代碼。截獲API使得用戶有機會干預其他應用程序流程。

最常用的一種掛鉤API的方法是改變目標進程中調用API函數的代碼,使得它們對API的調用變爲對用戶自定義函數的調用。

實現原理:

1.在掛鉤API之前,必須將一個可以替代API執行的函數的代碼注入到目標進程中。一般稱這個自定義函數爲代理函數。之所以這樣做,是因爲Windows下應用程序有自己的地址空間,它們只能調用自己地址空間中的函。

2.注入代碼到目標進程中是實現攔截API很重要的一步。我們可以把要注入的代碼寫到DLL中,然後讓目標進程加載這個DLL,這就是所謂的DLL注入技術。一旦程序代碼進入了另一個進程的地址空間,就可以毫無限制的做任何事情。

3.在這個要被注入到目標進程的DLL中寫一個與感興趣的API函數的簽名完全相同的函數(代理函數)。當DLL執行初始化代碼的時候,把目標進程對這個API的調用全部改爲對代理函數的調用,即可實現攔截API函數。

 

使用鉤子注入DLL

任何程序在接收鍵盤輸入時都會先調用DLL中的KeyHookProc函數。

使用Windows鉤子注入特定DLL到其他進程時一般都安裝WH_GETMESSAGE鉤子,而不是安裝WH_KEYBOARD鉤子。因爲許多進程不接收鍵盤輸入,所以Windows就不會將實現鉤子函數的DLL加載到這些進程中,但是Windows下的應用程序大部分都需要調用GetMessage或PeekMessage函數從消息隊列中獲取消息,所以它們都會加載鉤子函數所在的DLL。

安裝WH_GETMESSAGE鉤子的目的是讓其它進程加載鉤子函數所在的DLL,所以一般僅在鉤子函數中調用CallNextHookEx函數,不做什麼有用的工作。

 

HOOK過程

1.導入表的作用:

導入函數是被程序調用,但其實現代碼卻在其它模塊中的函數,API函數全都是導入函數,它們的實現代碼在Kernel32.dll、User32.dll等Win32子系統模塊中。

導入表(Import Table):模塊的導入函數名和這些函數駐留的DLL名等信息都保留在它的導入表中。導入表是一個IMAGE_IMPORT_DESCRIPTOR結構的數組,每個結構對應着一個導入模塊。

作用:有了導入表之後,應用程序啓動時,載入器根據PE文件的導入表記錄的DLL名加載相應的DLL模塊,在根據導入表的hint/name(函數序號/名稱)記錄的函數名取得函數地址,將這些地址保存到導入表的導入地址表(Import Address Table)(FirstThunk指向的數組)中。

應用程序在調用導入函數時,要先到導入表的IAT中找到這個函數的地址,然後再調用

瞭解了上面的知識之後,我們可以發現,只要修改模塊的導入地址表,將導入地址表中的函數地址用一個自定義函數的地址覆蓋掉,就可以實現HOOK API。

2.修改導入地址表: 定位導入表

爲了修改導入地址表(IAT),必須先定位目標模塊PE結構中的導入表的地址,這主要是對PE文件結構的分析。

PE文件結構:PE文件以64字節的DOS文件頭(IMAGE_DOS_HESDER)開始,之後是一小段DOS程序,然後是248字節的NT文件頭(IMAGE_NT_HEADERS)結構。NT文件頭的偏移地址由IMAGE_DOS_HEADER結構的e_lfanew成員給出。NT文件頭的前4個字節是文件簽名("PE00"字符串),緊接着是20字節的IMAGE_FILE_HEADER結構。它的後面是224字節的IMAGE_OPTIONAL_HEADER結構。  IMAGE_OPTINAL_HEADER裏面包含了許多重要的信息,有推薦的模塊基地址、代碼和數據的大小和基地址、線程堆棧和進程堆的配置、程序入口點的地址和我們最感興趣的數據目錄表指針。PE文件保留了16個數據目錄,最常見的有導入表、導出表、資源和重定位表。

除了可以通過PE文件結構定位模塊的導入表外,還可以使用ImageDirectoryEntryToData函數,這個函數知道模塊基地址後直接返回指定數據目錄表的首地址。爲了調用這個API函數,必須包含ImageHlp.h 和 #pragma comment(lib, "ImageHlp")。

下面這個例子打印了此模塊從其他模塊導入的所有函數的名稱和地址:

#include <Windows.h>
#include <stdio.h>

int main()
{
	//獲得主模塊的模塊句柄
	HMODULE hMod = ::GetModuleHandleA(NULL);	
	//PE問價以DOS文件頭開始
	IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER*)hMod;
	//獲得PE文件中NT文件頭中IMAGE_OPTIONAL_HEADER結構的指針
	IMAGE_OPTIONAL_HEADER * pOptHeader = 
		(IMAGE_OPTIONAL_HEADER*)((BYTE*)hMod + pDosHeader->e_lfanew + 24);
	//取得導入表中第一個IAMGE_IMPORT_DESCRIPTOR結構的指針
	IMAGE_IMPORT_DESCRIPTOR * pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)
		((BYTE*)hMod + pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
	 
	//得到了導入表之後,就可以修改對應導入地址表中的地址了

	//IMAGE_IMPORT_DESCRIPTOR結構中的FirstThunk記錄了導入函數的地址
	while(pImportDesc->FirstThunk){
		//獲得模塊的名稱
		char * pszDllName = (char *)((BYTE*)hMod + pImportDesc->Name);
		printf("\n模塊名稱: %s\n", pszDllName);

		//一個IMAGE_THUNK_DATA結構就是一個雙字,它指定了一個導入函數的地址
		IMAGE_THUNK_DATA * pThunk = (IMAGE_THUNK_DATA*)
			((BYTE*)hMod + pImportDesc->OriginalFirstThunk);

		int n = 0;
		while(pThunk->u1.Function){
			//獲得導入函數的名稱,hint/name表選項前2個字節是函數序號,後面纔是函數名稱字符串
			char * pszFunName = (char *)((BYTE *)hMod +
				(DWORD)pThunk->u1.AddressOfData + 2);
			//獲得函數地址,IAT表就是一個DWORD類型的數組,每個成員記錄一個函數的地址
			PDWORD lpAddr = (DWORD *)((BYTE*)hMod + 
				pImportDesc->FirstThunk) + n; 
			printf("  導出函數:%-25s>  ", pszFunName);
			printf("函數地址: %X\n", lpAddr);

			//使得指向下一個導入函數
			++n;	
			++pThunk;	
		}
		//使得指向下一個模塊的導入表結構
		++pImportDesc;		
	}

}


HOOK API的實現

 

定位導入表之後即可定位導入地址表,爲了截獲API調用,只要用自定義函數的地址覆蓋導入地址表中真是的API函數地址即可。

下面的一個例子是HOOK MessageBoxA函數的例子,在例子中使用自定義函數MyMessageBoxA取代了API函數MessageBoxA,使得主模塊對MessageBoxA的調用都變爲對自定義函數MyMessageBoxA的調用。

1.首先你得定義函數MyMessageBoxA

2.定位MessageBoxA所在模塊在導入表中的位置

3.定位MessageBoxA在導入地址表中的位置,從而找到MessageBoxA地址,然後修該IAT表項,通過使用函數WriteProcessMemory

完整代碼如下: (這個例子中只是掛鉤本模塊中對MessageBoxA的調用,對其他進程中調用MessageBoxA不起作用)

#include <Windows.h>
#include <stdio.h>

//掛鉤指定模塊hMod對MessageBoxA的調用
BOOL SetHook(HMODULE hMod);
//定義MessageBoxA的函數原型
typedef int (WINAPI *PFNMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT uType);
//保存MessageBoxA的真是地址
PROC g_orgProc = (PROC)MessageBoxA;


int main()
{
	::MessageBoxA(NULL, "原函數", "Hook API test", 0);
	SetHook(::GetModuleHandleA(NULL));
	::MessageBoxA(NULL, "原函數", "Hook API test", 0);

}

//代理函數,代替函數 MessageBoxA
int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
	return ((PFNMESSAGEBOX)g_orgProc)(hWnd, "新函數", "Hook API test", uType);
}

//掛鉤指定模塊hMod對MessageBoxA的調用
BOOL SetHook(HMODULE hMod)
{
	//定位導入表
	IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER*)hMod;
	IMAGE_OPTIONAL_HEADER *pOptHeader = (IMAGE_OPTIONAL_HEADER*)
		((BYTE*)hMod + pDosHeader->e_lfanew + 24);
	IMAGE_IMPORT_DESCRIPTOR * pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)
		((BYTE*)hMod + pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

	//在導入表中查找user32.dll模塊,因爲MessageBoxA函數從user32.dll模塊導出
	while(pImportDesc->FirstThunk){
		//取得模塊的名稱
		char * pszDllName = (char *)((BYTE*)hMod + pImportDesc->Name);
		if(lstrcmpiA(pszDllName, "user32.dll") ==  0){
			break;
		}
		++pImportDesc;
	}

	//找到對應的模塊之後,就要定位函數MessageBoxA的位置
	if(pImportDesc->FirstThunk){
		//一個IMAGE_THUNK_DATA結構就是一個雙字(DWORD),它指定了一個導入函數
		IMAGE_THUNK_DATA * pThunk = (IMAGE_THUNK_DATA*)
			((BYTE*)hMod + pImportDesc->FirstThunk);
		
		//逐個取出函數地址來比較
		while(pThunk->u1.Function){
			//lpAddr指向的內存保存了函數的地址
			DWORD * lpAddr = (DWORD *)&(pThunk->u1.Function);
			
			if(*lpAddr == (DWORD)g_orgProc){	//看看是否找到MessageBoxA的地址
				//修改導入地址表(IAT),使其指向我們自定義的函數
				//相當於語句 *lpAddr = (DWORD)MyMessageBOxA;
				DWORD * lpNewProc = (DWORD *)MyMessageBoxA;

				//寫入內存
				::WriteProcessMemory(GetCurrentProcess(), lpAddr, &lpNewProc, 
					sizeof(DWORD), NULL);
				printf("找到了\n");
				return true;
			}
			//指向下一個函數地址
			++pThunk;
		}
	}
	return false;
}

 

注意:當利用WriteProcessMemory函數寫內存時,如果在Debug版本下運行沒有問題,但是在Release版本下程序對WriteProcessMemory的調用將會失敗,原因是此時lpAddr指向的內存僅是可讀的。 要想寫這塊內存,必須調用VirtualProtect函數改變內存地址所在頁的頁屬性,將它改爲可寫。

				/*
					修改內存頁的屬性
				*/
				DWORD dwOldProtect;
				MEMORY_BASIC_INFORMATION mbi;
				VirtualQuery(lpAddr, &mbi, sizeof(mbi));
				VirtualProtect(lpAddr, sizeof(DWORD), PAGE_READWRITE, &dwOldProtect);


				//寫內存
				::WriteProcessMemory(GetCurrentProcess(), lpAddr, &lpNewProc, 
					sizeof(DWORD), NULL);

				//恢復頁的保護屬性
				VirtualProtect(lpAddr, sizeof(DWORD), dwOldProtect, 0);


如果是掛鉤其它進程中特定API的調用,就要將類似SetHook函數的代碼寫入DLL,在DLL初始化的時候調用它,然後將這個DLL注入到目標進程,這樣的代碼就會在目標進程中的地址空間執行,從而改變目標進程模塊的導入地址表。

 

封裝CAPIHook類 ---- 一個很好用的類

 

1.HOOK所有模塊:HOOK一個進程對某個API的調用時,不僅要修改主模塊導入表,還必須遍歷此進程的所有模塊,替換掉每個模塊對目標API的調用。CAPIHook類提供了連個函數來完成這項工作, ReplaceIATEntryInOneMod 和 ReplaceIATEntryAllMod

 

2.防止程序在運行期間動態加載模塊:在HOOK完目標進程當前的所有模塊後,它還可以調用LoadLibrary函數加載新的模塊。爲了能夠將今後目標進程動態加載的模塊也HOOK掉,可以默認掛鉤LoadLibrary之類的函數。在代理函數中首先調用原來的Loadlibrary函數,然後對新加載的模塊調用ReplaceIATEntryInOneMod函數。

一個CAPIHook對象僅能掛鉤一個API函數,爲了掛鉤多個API函數,用戶可能申請了多個CAPIHook對象。將所有的CAPIHook對象連成一個鏈表,用一個靜態變量記錄下表頭,在每個CAPIHook對象中在記錄下表中下一個CAPIHook對象的地址。

 

3.防止程序在運行期間動態調用API函數:並不是只有經過導入表才能調用API函數,應用程序可以在運行期間調用GetProcAddress函數取得API函數的地址在調用它。所以也要默認掛鉤GetProcAddress函數CAPIHook類的靜態成員函數GetProcAddress將替換這個API。

 

下面介紹一個HOOK API 的實例: 進程保護器

每當系統內有進程調用了TerminateProcess函數,程序就會將他截獲,在輸出窗口顯示調用進程主模塊的鏡像文件名和傳遞給TerminateProcess的兩個參數。

爲了HOOK掉所有進程對TerminateProcess的調用,我們要創建一個DLL。

#include "APIHook.h"
#include <Windows.h>

extern CAPIHook g_TerminateProcess;

//替代TerminateProcess的函數
BOOL WINAPI Hook_TerminateProcess(HANDLE hProcess, UINT uExitCode)
{
	typedef BOOL (WINAPI*PFNTERMINATEPROCESS)(HANDLE, UINT);
	//保存主模塊的文件名稱
	char szPathName[MAX_PATH];
	//取得主模塊的文件名稱
	::GetModuleFileNameA(NULL, szPathName, MAX_PATH);

	//構建發送給主窗口的字符串
	char sz[2048];
	wsprintf(sz, "\r\n 進程: (%d) %s\r\n\r\n 進程句柄: %x\r\n 退出代碼:%d",
		::GetCurrentProcessId(), szPathName, hProcess, uExitCode);
	//發送字符串到主對話框
	COPYDATASTRUCT cds = {::GetCurrentProcessId(), strlen(sz) + 1, sz};
	if(::SendMessageA(::FindWindowA(NULL, "進程保護器"), WM_COPYDATA, 
		0, (LPARAM)&cds) != -1){
		//如果函數返回的不是-1,我們就允許API執行
		return ((PFNTERMINATEPROCESS)(PROC)g_TerminateProcess)(hProcess,uExitCode);
	}
	return true;
}

//掛鉤TerminateProcess函數
CAPIHook g_TerminateProcess("kernel32.dll", "TerminateProcess", (PROC)Hook_TerminateProcess);

//定義一個數據段 YCIShared
#pragma data_seg("YCIShared")
HHOOK g_hHook = NULL;
#pragma data_seg()

//通過內存地址取得模塊句柄  的幫助函數
static HMODULE ModuleFromAddress(PVOID pv)
{
	MEMORY_BASIC_INFORMATION mbi;
	if(::VirtualQuery(pv, &mbi, sizeof(mbi)) != 0){
		return (HMODULE)mbi.AllocationBase;
	}
	else{
		return NULL;
	}
}

static LRESULT WINAPI GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
	return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}

//負責安裝和卸載WH_GETMESSAGE類型的鉤子
_declspec(dllexport) BOOL WINAPI SetSysHook(BOOL bInstall, DWORD dwThreadId)
{
	BOOL bOK;
	if(bInstall){
		g_hHook = ::SetWindowsHookExA(WH_GETMESSAGE, GetMsgProc,
			ModuleFromAddress(GetMsgProc), dwThreadId);
		bOK = (g_hHook != NULL);
	}
	else{
		bOK = ::UnhookWindowsHookEx(g_hHook);
		g_hHook = NULL;
	}
	return bOK;
}

利用上面的代碼,生成了HookTermProcLib.dll,供另外一個測試程序調用。

WM_COPYDATA消息:

Hook_TerminateProcess函數採用了發送WM_COPYDATA消息的方式向主程序傳遞數據。這是系統定義的用於在進程間傳遞數據的消息。需要注意的是,直接在消息的參數中隔着進程傳遞指針是不行的,因爲進程的地址空間是相互隔離的,接收方接收到的僅僅是一個指針的值,不可能接收到指針所指的內容。如果要傳遞的參數必須由指針來決定,就要使用WM_COPYDATA消息。但是接收方必須認爲接收到的數據是隻讀的,不可以改變lpData指向的數據。如果使用內存映射文件的話沒有這個限制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章