題目要求:
設計和實現一個軟件,其功能如下:
1、顯示所有的進程列表;
2、選中一個進程,顯示該進程的所有IAT中的函數;
3、選中一個IAT函數,實現Hooking和 inline Hooking,在Hooking的函數中顯示”組號:姓名”。
顯示所有進程列表
- 函數CreateToolhelp32Snapshot用于獲取指定進程的快照或者所有進程的快照。并且返回快照的句柄。
- 函數Process32First用于獲取進程快照中的第一個進程的信息,并且將這個進程的信息保存在一個PROCESSENTRY32的結構體中。在這個結構體中保存著進程的ID號和名稱。
- 函數Process32Next用于獲取快照中下一個進程的信息。我們循環調用這個函數,那么可以遍歷進程快照中的所有進程。
源程序如下:
#include <windows.h>
#include<iostream>
#include <tlhelp32.h>
#include <stdio.h>
using namespace std;
DWORD GetProcessId(char*myprocess)//枚舉進程函數
{
DWORD Pid = -1;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
//創建系統快照
PROCESSENTRY32 lPrs;
ZeroMemory(&lPrs, sizeof(lPrs));
lPrs.dwSize = sizeof(lPrs);
char *targetFile = myprocess;
bool flag = false;
if (Process32First(hSnap, &lPrs)) {//取得系統快照里第一個進程信息
//printf("name\t\t\tpid\t\tsize\n");
printf("%-50s%u\t\t%u\n", lPrs.szExeFile, lPrs.th32ProcessID, lPrs.dwSize);
if (strstr(targetFile, lPrs.szExeFile))//判斷進程信息是否是explorer.exe
{
Pid = lPrs.th32ProcessID;
flag = true;
}
}
while (1)
{
ZeroMemory(&lPrs, sizeof(lPrs));
lPrs.dwSize = (&lPrs, sizeof(lPrs));
if (!Process32Next(hSnap, &lPrs))//繼續枚舉進程信息
{
break;
}
if (strstr(targetFile, lPrs.szExeFile))//判斷進程信息是否是explorer.exe
{
Pid = lPrs.th32ProcessID;
flag = true;
}
printf("%-50s%u\t\t%u\n", lPrs.szExeFile, lPrs.th32ProcessID, lPrs.dwSize);
}
if(flag)
return Pid;
else return -1;
}
int main() {
printf("start.....\n");
char myprocess[50] = "cloudmusic.exe";
DWORD myPid = GetProcessId(myprocess);
printf("%u\n", myPid);
getchar();
return 0;
}
顯示進程所有的IAT函數
首先獲得模塊的句柄:
// 取得主模塊的模塊句柄(即進程模塊基地址)
HMODULE hModule = ::GetModuleHandleA(NULL);
然后把進程基址賦給pDosHeader,即起始基址就是PE的IMAGE_DOS_HEADER
IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hModule;
然后定位到PE header
基址hMod加上IMAGE_DOS_HEADER結構的e_lfanew成員到達IMAGE_NT_HEADERS
NT文件頭的前4字節是文件簽名("PE00" 字符串),然后是20字節的IMAGE_FILE_HEADER結。即到達IMAGE_OPTIONAL_HEADER結構的地址,獲取了一個指向IMAGE_OPTIONAL_HEADER結構體的指針。
IMAGE_OPTIONAL_HEADER* pOpNtHeader = (IMAGE_OPTIONAL_HEADER*)((BYTE*)hModule + pDosHeader->e_lfanew + 24);
定位導入表:
通過IMAGE_OPTIONAL_HEADER結構中的DataDirectory結構數組中的第二個成員中的VirturalAddress字段定位到IMAGE_IMPORT_DESCRIPTOR結構的起始地址即獲得導入表中第一個IMAGE_IMPORT_DESCRIPTOR結構的指針(導入表首地址)
IMAGE_IMPORT_DESCRIPTOR* pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)hModule + pOpNtHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
通過兩重循環輸出IAT函數,外層函數是對模塊循環,內層循環對一個模塊里面的所有函數進行循環輸出:
while (pImportDesc->FirstThunk)
{
char* pszDllName = (char*)((BYTE*)hModule + pImportDesc->Name);
printf("模塊名稱:%s\n", pszDllName);
DWORD n = 0;
//一個IMAGE_THUNK_DATA就是一個導入函數
IMAGE_THUNK_DATA* pThunk = (IMAGE_THUNK_DATA*)((BYTE*)hModule + pImportDesc->OriginalFirstThunk);
while (pThunk->u1.Function)
{
//取得函數名稱
char* pszFuncName = (char*)((BYTE*)hModule+pThunk->u1.AddressOfData+2); //函數名前面有兩個..
printf("function name:%-25s, ", pszFuncName);
//取得函數地址
PDWORD lpAddr = (DWORD*)((BYTE*)hModule + pImportDesc->FirstThunk) + n; //從第一個函數的地址,以后每次+4字節
printf("addrss:%X\n", lpAddr);
n++; //每次增加一個DWORD
pThunk++;
}
printf("\n");
pImportDesc++;
}
實現Inline hooking
Inline hooking是一種攔截目標函數調用的方法,主要用于抗病毒軟件,沙箱和惡意軟件。 一般的想法是將函數重定向到我們自己的函數,以便在函數執行之前和/或之后執行處理。 這可能包括:檢查參數,勻場,記錄,欺騙返回的數據和過濾呼叫。 Rootkit傾向于使用鉤子來修改從系統調用返回的數據,以隱藏其存在,而安全軟件則使用它們來防止/監視潛在的惡意操作。
hooking通過直接修改目標函數(內聯修改)中的代碼來放置,通常通過用跳轉覆蓋前幾個字節; 這允許在函數進行任何處理之前重定向執行。 大多數引擎引擎使用32位相對跳轉,占用5個字節的空間。
如何實現:
將使用一個基于trampoline的鉤子,它允許我們截取功能,同時仍然可以調用原件。這個鉤子由3部分組成:
- Hook - 一個5字節的相對跳轉,寫入目標函數以掛接它,跳轉將從掛鉤函數跳轉到我們的代碼。
- proxy這是我們指定的函數(或代碼),鉤子放在目標函數上將跳轉到。
- trampoline用于繞過鉤子,所以我們可以正常地調用掛鉤功能。
使用trampoline的原因是:
假設我們要鉤住MessageBoxA,從代理功能中打印出參數,然后顯示消息框:為了顯示消息框,我們需要調用MessageBoxA(它們重定向到我們的代理函數,這反過來又調用MessageBoxA)。 顯然從我們的代理函數中調用MessageBoxA將導致無限遞歸,并且程序由于堆棧溢出而最終崩潰。
我們可以簡單地從代理函數中取消MessageBoxA,調用它,然后重新掛接它; 但是如果多個線程同時調用MessageBoxA,這將導致競爭條件,并可能導致程序崩潰。
相反,我們可以做的是存儲MessageBoxA的前5個字節(這些被我們的鉤子覆蓋),然后當我們需要調用非掛鉤的MessageBoxA時,我們可以執行存儲的前5個字節,然后是5個字節的跳轉 MessageBoxA(直接掛鉤)。
只要前5個字節不是相對指令,它們可以在任何地方執行。
在這個例子中,函數的前5個字節組成了3個指令mov edi,edi; push ebp; mov ebp,esp,但是,例如,如果第一個指令是10個字節長,我們只存儲5個字節trampoline將執行一半的指令,導致程序爆炸。 為了解決這個問題,我們必須使用反匯編來獲取每個指令的長度。 最好的情況是前n個指令總共5個字節,最糟糕的情況是如果第一個指令是4個字節,而第二個指令是16個(x86指令的最大長度),則必須存儲20個字節(4 + 16),這意味著trampoline的大小必須是25個字節(空間最多可達20個字節的指令,5個字節跳回掛鉤的功能)。重要的是,返回跳轉必須跳轉到掛接的函數n個字節,其中n是我們存儲在trampoline中的指令。
代碼編寫
首先,我們需要定義代理函數。我們把hooking函數重定向。對于這個例子,我們只需要在顯示消息框之前打印出參數。
int WINAPI NewMessageBoxA(HWND hWnd, LPCSTR lpText, LPCTSTR lpCaption, UINT uType)
{
printf("MessageBoxA called!ntitle: %sntext: %snn", lpCaption, lpText);
return OldMessageBoxA(hWnd, lpText, lpCaption, uType);
}
int WINAPI NewMessageBoxW(HWND hWnd, LPWSTR lpText, LPCTSTR lpCaption, UINT uType)
{
printf("MessageBoxW called!ntitle: %wsntext: %wsnn", lpCaption, lpText);
return OldMessageBoxW(hWnd, lpText, lpCaption, uType);
}
OldMessageBox只是一個typedef,它將指向25個字節的可執行內存,該掛鉤功能將存儲trampoline。
typedef int (WINAPI *TdefOldMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCTSTR lpCaption, UINT uType);
typedef int (WINAPI *TdefOldMessageBoxW)(HWND hWnd, LPWSTR lpText, LPCTSTR lpCaption, UINT uType);
TdefOldMessageBoxA OldMessageBoxA = (TdefOldMessageBoxA)VirtualAlloc(NULL, 25, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
TdefOldMessageBoxW OldMessageBoxW = (TdefOldMessageBoxW)VirtualAlloc(NULL, 25, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
現在對于hooking函數,我們將具有以下參數:
- name - 掛鉤功能的名稱。
- dll - 目標函數所在的dll。
- proxy - 指向代理函數的指針(NewMessageBox)。
- original - 指向25字節的可執行內存的指針,存儲trampoline。
- length - 指向一個變量的指針,該變量接收存儲在trampoline中的指令值。
在hooking函數內部,我們將獲取目標函數的地址,然后使用Hacker Dissasembler Engine(HDE32)來拆分每個指令并獲取長度,直到我們有5個或更多個字節值得整個指令(hde32_disasm返回長度的第一個參數指向的指令)。
LPVOID FunctionAddress;
DWORD TrampolineLength = 0;
FunctionAddress = GetProcAddress(GetModuleHandleA(dll), name);
if(!FunctionAddress)
return FALSE;
//拆分每個指令的長度,直到我們有5個以上的字節值
while(TrampolineLength < 5)
{
LPVOID InstPointer = (LPVOID)((DWORD)FunctionAddress + TrampolineLength);
TrampolineLength += hde32_disasm(InstPointer, &disam);
}
為了構建實際的trampoline,我們首先將目標函數中的TrampolineLength字節復制到trampoline緩沖區(傳遞給參數“original”中的函數),然后我們將復制的字節用n字節附加到目標函數中,n是Trampoline的長度。
相對跳轉是距離跳轉結束的距離,即:(destination - (source + 5))。 跳躍的來源將是trampoline地址+trampoline長度,目的地將是hooking函數+trampoline長度。
DWORD src = ((DWORD)FunctionAddress + TrampolineLength);
DWORD dst = ((DWORD)original + TrampolineLength + 5);
BYTE jump[5] = {0xE9, 0x00, 0x00, 0x00, 0x00};
//將n個字節從目標函數存儲到trampoline中
memcpy(original, FunctionAddress, TrampolineLength);
//設置跳轉的第二個字節(偏移量),跳轉到我們想要的位置
*(DWORD *)(jump+1) = src - dst;
//將跳轉復制到trampoline的末端
memcpy((LPVOID)((DWORD)original+TrampolineLength), jump, 5);
在我們可以編寫跳轉函數之前,我們需要確保內存是可寫的(通常不可以),我們通過使用VirtualProtect將保護設置為PAGE_EXECUTE_READWRITE來實現。
//確保該功能是可寫的
DWORD OriginalProtection;
if(!VirtualProtect(FunctionAddress, 8, PAGE_EXECUTE_READWRITE, &OriginalProtection))
return FALSE;
要擺放hooking,我們需要做的就是創建一個從目標函數跳轉到代理的跳轉,然后我們可以用它覆蓋目標的前5個字節。 為了避免在寫入跳轉時調用函數的任何風險,我們必須一次性寫完。而atomic functions只能用于基礎2(2,4,8,16等)的大小; 我們的跳轉是5個字節,我們可以復制的最接近的大小是8,所以我們必須制作一個自定義函數SafeMemcpyPadded,它將源緩沖區用來從目的地的字節緩沖到8個字節,這樣最后3個字節保持不變 復制后。
cmpxchg8b將edx:eax中保存的8個字節與目標進行比較,如果它們相等,則復制ecx:ebx中保存的8個字節,我們將edx:eax設置為目標字節,以使副本始終發生。
//構建并寫hooking
*(DWORD *)(jump+1) = (DWORD)proxy - (DWORD)FunctionAddress - 5;
SafeMemcpyPadded(FunctionAddress, Jump, 5);
void SafeMemcpyPadded(LPVOID destination, LPVOID source, DWORD size)
{
BYTE SourceBuffer[8];
if(size > 8)
return;
//使用來自目的地的字節來填充源緩沖區
memcpy(SourceBuffer, destination, 8);
memcpy(SourceBuffer, source, size);
__asm
{
lea esi, SourceBuffer;
mov edi, destination;
mov eax, [edi];
mov edx, [edi+4];
mov ebx, [esi];
mov ecx, [esi+4];
lock cmpxchg8b[edi];
}
}
現在要做的就是恢復頁面保護。 刷新指令緩存,并將length參數設置為Trampoline的長度。
//恢復頁面保護
VirtualProtect(FunctionAddress, 8, OriginalProtection, &OriginalProtection);
//清楚CPU指令緩存
FlushInstructionCache(GetCurrentProcess(), FunctionAddress, TrampolineLength);
*length = TrampolineLength;
return TRUE;
hooking功能可以這樣簡單地調用。
DWORD length;
HookFunction("user32.dll", "MessageBoxA", &NewMessageBoxA, OldMessageBoxA, &length);
通過從OldMessageBox(Trampoline)將length字節復制到hooking函數來完成脫鉤。