1 前言
在Micrisoft Windows中, 每個進程都有自己的私有地址空間。當我們用指針來引用內存的時候,指針的值表示的是進程自己的自制空間的一個內存地址。進程不能創建一個指針來引用屬於其他進程的內存。
獨立的地址控件對開發人員和用戶來說都是非常有利的。對開發人員來說,系統更有可能捕獲錯誤的內存讀\寫。對用戶而言, 操作系統變得更加健壯。當然這樣的健壯性也是要付出代價的,因為它使我們很難編寫能夠與其他進程通信的應用程序或對其他進程進行操控的應用程序。
在《Windows 核心編程》第二十二章《DLL注入和API攔截》中講解了多種機制,他們可以將一個DLL注入到另一個進程地址的空間中。一旦DLL代碼進入另一個地址空間,那么我們就可以在那個進程中隨心所欲了。
本文主要介紹了如何實現替換Windows上的API函數, 實現Windows API HOOK。API HOOK的實現方法大概不下五六種。本位主要介紹了其中的一種,即如何使用WIndows掛鈎來注入DLL。
2 使用Windows掛鈎來注入DLL
2.1 簡單windows消息HOOK
2.1.1 SetWindowsHookEx
這個是安裝狗子的函數聲明如下:
HHOOK WINAPI SetWindowsHookEx(
__in int idHook, \\鈎子類型
__in HOOKPROC lpfn, \\回調函數地址
__in HINSTANCE hMod, \\實例句柄
__in DWORD dwThreadId); \\線程ID,0表示所有
讓我們通過一個例子來學些下這個API。進程A(一個游戲改建工具,需要接獲鍵盤消息),安裝了WH_KEYBOARD_LL掛鈎,如下所示:
HOOK hHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, hInstDll, 0); 第一個參數表示掛鈎類型, 是一個低級鍵盤鈎子。第二個參數是一個函數地址(在我們的地址空間內),在窗口即將有鍵盤消息的時候,系統應該調用這個函數。第三個參數hInstDll標識一個DLL, 這個DLL中包含了LowLevelKeyboardProc函數。在windows中, hInstDll的值就是就是DLL被映射到的虛擬內存地址。最后一個參數表示要給哪個線程安裝鈎子,0表示給所有GUI線程安裝。
現在讓我們看一看接下來會發生什么。
1)進程B中的一個線程准備向一個窗口發送一個鍵盤消息
2)系統檢查該線程是否已經安裝了WH_KEYBOARD_LL鈎子。
3)系統檢查LowLevelKeyboardProc所在的dll是否已經被映射到進程B的地址空間中。
4)如果DLL尚未被映射,那么系統會強制將該DLL映射到進程B的地址空間中,並將進程B中該DLL的鎖計數器(lock count)遞增。
5)由於DLL的hInstDll是在進程B中映射的, 因此系統會對它進行檢查,看它與該DLL在進程A中的位置是否相同。如果hInstDll相同,那么在兩個進程地址空間中,LowLevelKeyboardProc函數位於相同的位置,在這種情況下, 系統可以直接在進程A的地址空間中調用LowLevelKeyboardProc。如果hInstDll不同,那么系統必須確定LowLevelKeyboardProc函數在進程B地址空間中的虛擬地址,這個地址通過下面的公式得出
LowLevelKeyboardProc B = hInstDll B + (LowLevelKeyboardProc A - hInstDll A);
通過把LowLevelKeyboardProc A減去hInstDll A, 我們得到LowLevelKeyboardProc函數的偏移量,以字節為單位, 再把這個偏移量於hInstDll B相加就得到LowLevelKeyboardProc在B地址空間中位置。
6)系統在進程B中遞增該DLL的鎖計數器(為何再次遞增? 核心編程中這樣寫的,不是很明白)
7)系統在B的地址空間中調用LowLevelKeyboardProc函數
8)當LowLevelKeyboardProc返回的時候,系統減去DLL在進程B中的鎖計數器。
注意, 當系統把掛鈎過濾函數所在的DLL注入或映射到地址空間中時,會映射整個DLL,而不僅僅是掛鈎過濾函數。這意味着,DLL內所有函數存在於進程B中,能夠為進程B中所有線程所調用。
此處參考兩個簡單消息鈎子的demo, 一個是類似於改建的鍵盤鈎子demo,這個demo展示了如何簡單的做一個游戲改建; 一個是在MFC中使用鈎子勾取菜單的相關消息, MFC的菜單不繼承CWnd, 所以菜單相關的創建、銷毀、繪制等消息我們都捕獲不到,所以重繪的時候要改變菜單的一些屬性就需要用到鈎子,。
2.2 API Hook
2.2.1 原理
api hook並不是什么特別不同的hook,它也需要通過基本的hook提高自己的權限,跨越不同進程間訪問的限制,達到修改api函數地址的目的。對於自身進程空間下使用到的api函數地址的修改,是不需要用到api hook技術就可以實現的。
我們知道,系統函數都是以DLL封裝起來的,應用程序應用到系統函數時,應首先把該DLL加載到當前的進程空間中,調用的系統函數的入口地址,可以通過 GetProcAddress函數進行獲取。當系統函數進行調用的時候,首先把所必要的信息保存下來(包括參數和返回地址,等一些別的信息),然后就跳轉到函數的入口地址,繼續執行。其實函數地址,就是系統函數“可執行代碼”的開始地址。那么怎么才能讓函數首先執行我們的函數呢?實際上就是把開始的那段可執行代碼替換為我們自己定制的一小段可執行代碼,這樣系統函數調用時,不就按我們的意圖乖乖行事了嗎? 簡單的說,就可以修改系統函數入口的地方,讓他調轉到我們的函數的入口點就行了。采用匯編代碼就能簡單的實現Jmp XXXX, 其中XXXX就是要跳轉的相對地址。而Jmp后面要求的是相對偏移,也就是我們的函數入口地址到系統函數入口地址之間的差異,再減去我們這條指令的大小。用公式表達如下:
(1)int nDelta = UserFunAddr – SysFunAddr - (我們定制的這條指令的大小);
(2)Jmp nDleta;
為了保持原程序的健壯性,我們的函數里做完必要的處理后,要回調原來的系統函數,然后返回。 否則會發生自己調用自己的死循環。
那么說下程序執行過程。
1) 我們的dll“注射”入被hook的進程 (這一步只需要安裝一個鈎子就能實現)
2)保存系統函數入口處的代碼
3)替換掉進程中的系統函數入口指向我們的函數
4)當系統函數被調用,立即跳轉到我們的函數
5)我們函數處理
6)恢復系統函數入口的代碼
7)調用原來的系統函數
8)再修改系統函數入口指向我們的函數(為了下次hook)-> 返回
來看我們HooK自定義的Add函數、
首先我們創建一個AddFunc的dll工程, 這個dll只有一個導出函數:
int WINAPI add(int a, int b);
這個add函數就是我們稍后需要攔截的函數。有了dll后我們就能可以直接新建一個MFC工程調用Add函數 主要代碼如下:
// HOOK 我的Add方法
voidCHookDemoDlg::OnBnClickedButton7()
{
//函數原型定義
typedefint(WINAPI*AddProc)(inta,intb);
AddProcadd;
staticHINSTANCEs_instadd=NULL;
s_instadd=LoadLibrary(s_path+_T("\\AddFunc.dll"));//加載dll文件
if(s_instadd==NULL)
{
AfxMessageBox(_T("no AddFunc.dll!"));
return;
}
add= (AddProc)::GetProcAddress(s_instadd,"add");//獲取函數地址
intnRet=add(1,1);
CStringcstr;
cstr.Format(_T("%d + %d = %d"),1,1,nRet);
::MessageBoxW(NULL,cstr,NULL,MB_OK);
}
接下來, 我們來進行HOOK即使Hook我們AddFunc.dll中的add函數。新建一個win32的Dll工程HookDll。首先在頭部聲明如下變量:
//全局共享變量
#pragmadata_seg("MySec")
staticHINSTANCEg_hInstance=NULL;
staticHHOOKg_hook=NULL;
#pragmadata_seg()
#pragmacomment(linker,"/section:MySec,rws")
這兩個變量表示能夠在所有調用該dll的進程中共享。如果不加#pragma data_seg()來聲明,g_hInstance 和g_hook將會在每個進程空間中都有一份獨立的數據。編寫鼠標鈎子的安裝卸載函數,注意兩個函數導出。
//鼠標鈎子過程,什么也不做,目錄是注入dll到程序中
LRESULTCALLBACKMouseProc(intnCode,WPARAMwParam,LPARAMlParam)
{
returnCallNextHookEx(hhk,nCode,wParam,lParam);
}
//鼠標鈎子安裝函數:
BOOLInstallHook()
{
hhk=::SetWindowsHookEx(WH_MOUSE,MouseProc,g_hInstance,0);
returnhhk!=NULL;
}
BOOLUninstallHook()
{
if(hhk!=NULL)
{
::UnhookWindowsHookEx(hhk);
hhk=NULL;
}
//HookMessageBoxW::HookOff();
//HookAddFuc::HookOff();
HookTextOutW::HookOff();
returnTRUE;
}
在DLL的入口處DLL_PROCESS_ATTACH添加初始化變臉和進行注入。
BOOLAPIENTRYDllMain(HMODULEhModule,
DWORD ul_reason_for_call,
LPVOIDlpReserved
)
{
g_hInstance=(HINSTANCE)hModule;
switch(ul_reason_for_call)
{
caseDLL_PROCESS_ATTACH:
{
DWORDdwPid=::GetCurrentProcessId();
HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,0,dwPid);
//HookAddFuc::Inject(hProcess);
break;
}
caseDLL_THREAD_ATTACH:
caseDLL_THREAD_DETACH:
caseDLL_PROCESS_DETACH:
{
//HookAddFuc::HookOff();
break;
}
}
returnTRUE;
}
編寫HookAddFuc::Inject(hProcess)注入函數
voidHookAddFuc::Inject(HANDLEh)
{
hProcess=h;
if(!bInjectedAdd)
{
bInjectedAdd=true;
//獲取add.dll中的add()函數
HMODULEhmod=::LoadLibrary(s_path);
add=(AddProc)::GetProcAddress(hmod,"add");
pfadd=(FARPROC)add;
if(pfadd==NULL)
{
MessageBoxW(NULL,L"cannot locate add()",NULL,MB_OK);;
}
// 將add()中的入口代碼保存入OldCode[]
_asm
{
leaedi,OldCode
movesi,pfadd
cld
movsd
movsb
}
NewCode[0]=0xe9;//實際上0xe9就相當於jmp指令
//獲取Myadd()的相對地址
_asm
{
leaeax,Myadd
movebx,pfadd
subeax,ebx
subeax,5
movdwordptr[NewCode+1],eax
}
//填充完畢,現在NewCode[]里的指令相當於Jmp Myadd
HookOn();//可以開啟鈎子了
}
}
//恢復函數地址
voidHookAddFuc::HookOff()
{
DWORDdwTemp=0;
DWORDdwOldProtect;
VirtualProtectEx(hProcess,pfadd,5,PAGE_READWRITE,&dwOldProtect);
WriteProcessMemory(hProcess,pfadd,OldCode,5,0);
VirtualProtectEx(hProcess,pfadd,5,dwOldProtect,&dwTemp);
}
//修改函數地址
voidHookAddFuc::HookOn()
{
DWORDdwTemp=0;
DWORDdwOldProtect;
//將內存保護模式改為可寫,老模式保存入dwOldProtect
VirtualProtectEx(hProcess,pfadd,5,PAGE_READWRITE,&dwOldProtect);
//將所屬進程中add()的前5個字節改為Jmp Myadd
WriteProcessMemory(hProcess,pfadd,NewCode,5,0);
//將內存保護模式改回為dwOldProtect
VirtualProtectEx(hProcess,pfadd,5,dwOldProtect,&dwTemp);
}
//然后,寫我們自己的Myadd()函數
intWINAPIMyadd(inta,intb)
{
//截獲了對add()的調用,我們給a,b都加1
a=a+1;
b=b+1;
HookAddFuc::HookOff();//關掉Myadd()鈎子防止死循環
intret;
ret=add(a,b);
HookAddFuc::HookOn();//開啟Myadd()鈎子
returnret;
}