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指向的資料。如果使用記憶體映射檔案的話沒有這個限制。