Hook簡介
微軟的MSDN中,對(duì)Hook的解釋為: A hook is a point in the system message-handling mechanism where an application can install a subroutine to monitor the message traffic in the system and process certain types of messages before they reach the target window procedure.
微軟只是簡單的將Hook解釋為一種過濾(或叫掛鉤)消息的技術(shù)。
我們這里講解的Hook,簡單解釋為:掛鉤,掛鉤一切事物。包含微軟的解釋。
掛鉤的事物通常指的是函數(shù)。
Hook 目的
過濾一些關(guān)鍵函數(shù)調(diào)用,在函數(shù)執(zhí)行前,先執(zhí)行自己的掛鉤函數(shù)。達(dá)到監(jiān)控函數(shù)調(diào)用,改變函數(shù)功能的目的。
Hook 分類
系統(tǒng)消息 Hook
類型:
WH_GETMESSAGE 當(dāng)系統(tǒng)調(diào)用GetMessage和PeekMessage時(shí),調(diào)用Hook函數(shù)
WH_KEYBOARD_LL 當(dāng)系統(tǒng)發(fā)送鍵盤消息到線程輸入隊(duì)列前,先調(diào)用Hook函數(shù)
WH_MOUSE_LL 當(dāng)系統(tǒng)發(fā)送鼠標(biāo)消息到線程輸入隊(duì)列前,先調(diào)用Hook函數(shù)
… …
HHOOK WINAPI SetWindowsHookEx(
int idHook, // 類型
HOOKPROC lpfn, // 回調(diào)函數(shù)
HINSTANCE hMod, // 回調(diào)函數(shù)所在的DLL的Handle(可能在調(diào)用進(jìn)程也可能不在)
DWORD dwThreadId // 針對(duì)哪兒個(gè)線程做Hook
);
此Hook可以以線程為單位進(jìn)行篩選。每個(gè)GUI線程有一個(gè)消息隊(duì)列,用來接受消息,既然是消息Hook,那就應(yīng)該是針對(duì)某個(gè)線程的接收到的消息做Hook。
例如:實(shí)現(xiàn)的防鍵盤勾取,可以使用這種技術(shù)。安裝了一個(gè)WH_KEYBOARD_LL鉤子,在鍵盤按下,發(fā)往任意的線程消息隊(duì)列前,會(huì)先調(diào)用我們的鍵盤鉤子,鉤子里面判斷是不是我們的輸入框的窗口消息,如果不是,忽略。如果是,做加密處理并保存,改變發(fā)送的字符(如改為"*"),發(fā)送至窗口過程中。如果第三方調(diào)用GetWindowText,由于文本框中的都是"*",所以無法獲取出真實(shí)字符。
但這種鉤子誰也可以安裝,Windows將所有程序安裝的鉤子組成了一個(gè)鉤子鏈。如果第三方也安裝了一個(gè)鉤子,還是在我們安裝完之后安裝的,那么它將會(huì)截獲到真實(shí)字符。所以,所以可以進(jìn)一步加強(qiáng)處理,增加一個(gè)定時(shí)器,不停的安裝卸載多個(gè)鉤子。這樣,即便第三方也在不停的安裝鉤子,想截取到完整的PIN,可能性變的很小。但如果這個(gè)惡意程序駐留在用戶系統(tǒng)中很多天,通過每次的統(tǒng)計(jì),可能能得到完整的PIN。
API Hook - Inline Hook
工作方式如下:
-
調(diào)用GetProcAddress對(duì)內(nèi)存中要攔截的函數(shù)進(jìn)行定位(如Kernel32.dll的ExitProcess),得到它的內(nèi)存地址。
-
把這個(gè)函數(shù)起始的幾個(gè)字節(jié)保存到我們的內(nèi)存中。
-
用CPU的JUMP匯編指令來覆蓋這個(gè)函數(shù)的起始幾個(gè)字節(jié),這條JUMP指令用來跳轉(zhuǎn)到我們的替換函數(shù)的內(nèi)存地址。我們替換的函數(shù)需要和原函數(shù)具有完全相同的聲明:參數(shù)相同,返回值相同,調(diào)用約定相同。
-
當(dāng)線程調(diào)用被攔截的函數(shù)時(shí),跳轉(zhuǎn)指令實(shí)際上會(huì)跳轉(zhuǎn)到我們的替代函數(shù),這是,我們可以執(zhí)行相應(yīng)的功能代碼。
-
為了撤銷對(duì)函數(shù)的攔截,我們必須把步驟2中保存的字節(jié)恢復(fù)回被攔截的函數(shù)中。
-
我們調(diào)用被攔截函數(shù)(現(xiàn)在已經(jīng)不再攔截),讓函數(shù)執(zhí)行它的正常處理。
-
當(dāng)原來函數(shù)返回時(shí),我們?cè)俅螆?zhí)行第2,第3步,這樣我們的替代函數(shù)將來還會(huì)被調(diào)用到。
注意事項(xiàng):
解決多線程的辦法(分析來自Mhook庫):
下面是摘自Mhook的一段代碼,它會(huì)掛起除了Hook線程以外的其它線程,并且確保其他線程的執(zhí)行點(diǎn)(IP)不在要Hook的區(qū)域內(nèi)。
//========================================================================= // Internal function: // // Suspend all threads in this process while trying to make sure that their // instruction pointer is not in the given range. //========================================================================= static BOOL SuspendOtherThreads(PBYTE pbCode, DWORD cbBytes) { BOOL bRet = FALSE; // make sure we're the most important thread in the process INT nOriginalPriority = GetThreadPriority(GetCurrentThread()); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); // get a view of the threads in the system HANDLE hSnap = fnCreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetCurrentProcessId()); if (GOOD_HANDLE(hSnap)) { THREADENTRY32 te; te.dwSize = sizeof(te); // count threads in this process (except for ourselves) DWORD nThreadsInProcess = 0; if (fnThread32First(hSnap, &te)) { do { if (te.th32OwnerProcessID == GetCurrentProcessId()) { if (te.th32ThreadID != GetCurrentThreadId()) { nThreadsInProcess++; } } te.dwSize = sizeof(te); } while(fnThread32Next(hSnap, &te)); } ODPRINTF((L"mhooks: SuspendOtherThreads: counted %d other threads", nThreadsInProcess)); if (nThreadsInProcess) { // alloc buffer for the handles we really suspended g_hThreadHandles = (HANDLE*)malloc(nThreadsInProcess*sizeof(HANDLE)); if (g_hThreadHandles) { ZeroMemory(g_hThreadHandles, nThreadsInProcess*sizeof(HANDLE)); DWORD nCurrentThread = 0; BOOL bFailed = FALSE; te.dwSize = sizeof(te); // go through every thread if (fnThread32First(hSnap, &te)) { do { if (te.th32OwnerProcessID == GetCurrentProcessId()) { if (te.th32ThreadID != GetCurrentThreadId()) { // attempt to suspend it g_hThreadHandles[nCurrentThread] = SuspendOneThread(te.th32ThreadID, pbCode, cbBytes); if (GOOD_HANDLE(g_hThreadHandles[nCurrentThread])) { ODPRINTF((L"mhooks: SuspendOtherThreads: successfully suspended %d", te.th32ThreadID)); nCurrentThread++; } else { ODPRINTF((L"mhooks: SuspendOtherThreads: error while suspending thread %d: %d", te.th32ThreadID, gle())); // TODO: this might not be the wisest choice // but we can choose to ignore failures on // thread suspension. It's pretty unlikely that // we'll fail - and even if we do, the chances // of a thread's IP being in the wrong place // is pretty small. // bFailed = TRUE; } } } te.dwSize = sizeof(te); } while(fnThread32Next(hSnap, &te) && !bFailed); } g_nThreadHandles = nCurrentThread; bRet = !bFailed; } } CloseHandle(hSnap); //TODO: we might want to have another pass to make sure all threads // in the current process (including those that might have been // created since we took the original snapshot) have been // suspended. } else { ODPRINTF((L"mhooks: SuspendOtherThreads: can't CreateToolhelp32Snapshot: %d", gle())); } SetThreadPriority(GetCurrentThread(), nOriginalPriority); if (!bRet) { ODPRINTF((L"mhooks: SuspendOtherThreads: Had a problem (or not running multithreaded), resuming all threads.")); ResumeOtherThreads(); } return bRet; } //========================================================================= // Internal function: // // Suspend a given thread and try to make sure that its instruction // pointer is not in the given range. //========================================================================= static HANDLE SuspendOneThread(DWORD dwThreadId, PBYTE pbCode, DWORD cbBytes) { // open the thread HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId); if (GOOD_HANDLE(hThread)) { // attempt suspension DWORD dwSuspendCount = SuspendThread(hThread); if (dwSuspendCount != -1) { // see where the IP is CONTEXT ctx; ctx.ContextFlags = CONTEXT_CONTROL; int nTries = 0; while (GetThreadContext(hThread, &ctx)) { #ifdef _M_IX86 PBYTE pIp = (PBYTE)(DWORD_PTR)ctx.Eip; #elif defined _M_X64 PBYTE pIp = (PBYTE)(DWORD_PTR)ctx.Rip; #endif if (pIp >= pbCode && pIp < (pbCode + cbBytes)) { if (nTries < 3) { // oops - we should try to get the instruction pointer out of here. ODPRINTF((L"mhooks: SuspendOneThread: suspended thread %d - IP is at %p - IS COLLIDING WITH CODE", dwThreadId, pIp)); ResumeThread(hThread); Sleep(100); SuspendThread(hThread); nTries++; } else { // we gave it all we could. (this will probably never // happen - unless the thread has already been suspended // to begin with) ODPRINTF((L"mhooks: SuspendOneThread: suspended thread %d - IP is at %p - IS COLLIDING WITH CODE - CAN'T FIX", dwThreadId, pIp)); ResumeThread(hThread); CloseHandle(hThread); hThread = NULL; break; } } else { // success, the IP is not conflicting ODPRINTF((L"mhooks: SuspendOneThread: Successfully suspended thread %d - IP is at %p", dwThreadId, pIp)); break; } } } else { // couldn't suspend CloseHandle(hThread); hThread = NULL; } } return hThread; }
Inline Hook技術(shù)復(fù)雜,對(duì)CPU有依賴,需要編寫匯編代碼,在搶占式,多線程系統(tǒng)中,很容易出現(xiàn)問題。但目前有比較穩(wěn)定的Hook庫可以使用,也是不錯(cuò)的選擇之一。
API Hook – SEH Hook
這里簡單介紹一下,SEH是windows的結(jié)構(gòu)化異常處理,通過安裝一個(gè)結(jié)構(gòu)化異常處理函數(shù),當(dāng)在保護(hù)的執(zhí)行區(qū)域內(nèi)發(fā)生異常時(shí),會(huì)跳轉(zhuǎn)到此異常函數(shù)。SEH Hook屬于比較另類的Hook方式,它在函數(shù)開頭插入異常代碼,如(INT 3),當(dāng)執(zhí)行到此函數(shù)時(shí),由于觸發(fā)異常,則會(huì)跳到我們的處理函數(shù)中。
API Hook - IAT Hook
IAT - Import Address Table (輸入地址表)
簡單的說,此表格由一連串的函數(shù)地址組成,這些函數(shù)地址從其它模塊中導(dǎo)入的函數(shù)地址。當(dāng)Windows加載器加載一個(gè)PE可執(zhí)行文件時(shí),會(huì)將真正需要的函數(shù)的虛擬地址填入此表格。
輸入函數(shù)是如何被調(diào)用的
通常,我們編寫一個(gè)程序(調(diào)用者),調(diào)用一個(gè)輸入函數(shù)時(shí),此函數(shù)并不在當(dāng)前程序中。我們只需要包含相應(yīng)的頭文件,然后鏈接正確的lib庫(靜態(tài)lib 和 動(dòng)態(tài)DLL對(duì)應(yīng)的lib),這些函數(shù)的實(shí)際代碼存在于外部的一個(gè)DLL中。調(diào)用者只保留函數(shù)的相關(guān)信息(函數(shù)名,DLL文件名等)。由于一個(gè)PE文件沒有被加載到內(nèi)存中,編譯完成的PE文件是無法確定輸入函數(shù)的具體地址的。
下面通過一個(gè)例子來說明輸入函數(shù)的地址是如何確定的。
現(xiàn)在,我調(diào)用一個(gè)Windows API函數(shù):Sleep,原型如下:
extern "C" __declspec(dllimport) void __stdcall Sleep(
unsigned long dwMilliseconds
);
代碼中調(diào)用如下:
Sleep(200);
VC編譯器會(huì)將此函數(shù)匯編如下:
push 0C8h
call dword ptr [00407000]
由于在編譯階段,編譯器是無法確定Sleep在真正運(yùn)行時(shí)候的地址的。所以,編譯器在PE文件中分配一塊導(dǎo)入地址表,call 的只是導(dǎo)入地址的地址,而不是真正的地址。此示例中, 00407000是地址,它指向?qū)氲刂繁淼哪硞€(gè)區(qū)域,指向的內(nèi)容在PE被Windows加載器加載的時(shí)候,根據(jù)DLL實(shí)際加載的地址,找到真正的Sleep地址,將其替換為Sleep的真實(shí)地址。此方法類似于C++的多態(tài)。(父類的指針在運(yùn)行時(shí)可以指向不同類型的子類)。這樣,call dword ptr [00407000],從00407000取出來的地址就是Sleep的地址。如下圖所示。
IAT Hook就是去替換取出來的這個(gè)Sleep地址,將00407000地址指向的內(nèi)存替換為我們的MySleep。當(dāng)再次執(zhí)行到call dword ptr [00407000],此00407000指向的內(nèi)存保存的已經(jīng)是MySleep地址了。
IAT Hook的優(yōu)點(diǎn)在于不依賴CPU,容易實(shí)現(xiàn)。很難出現(xiàn)程序崩潰的問題。
如何找到IAT并定位正確的地址?
需要掌握PE文件的結(jié)構(gòu)。(可以參考看雪論壇出版的加密與解密(第三版))
下面的代碼片段,引自《Windows核心編程(第5版)》 示例代碼 22-LastMsgBoxInfo :
void CAPIHook::ReplaceIATEntryInOneMod(PCSTR pszCalleeModName, PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller) { // Get the address of the module's import section ULONG ulSize; // An exception was triggered by Explorer (when browsing the content of // a folder) into imagehlp.dll. It looks like one module was unloaded... // Maybe some threading problem: the list of modules from Toolhelp might // not be accurate if FreeLibrary is called during the enumeration. PIMAGE_IMPORT_DESCRIPTOR pImportDesc = NULL; __try { pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData( hmodCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize); } __except (InvalidReadExceptionFilter(GetExceptionInformation())) { // Nothing to do in here, thread continues to run normally // with NULL for pImportDesc } if (pImportDesc == NULL) return; // This module has no import section or is no longer loaded // Find the import descriptor containing references to callee's functions for (; pImportDesc->Name; pImportDesc++) { PSTR pszModName = (PSTR) ((PBYTE) hmodCaller + pImportDesc->Name); if (lstrcmpiA(pszModName, pszCalleeModName) == 0) { // Get caller's import address table (IAT) for the callee's functions PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA) ((PBYTE) hmodCaller + pImportDesc->FirstThunk); // Replace current function address with new function address for (; pThunk->u1.Function; pThunk++) { // Get the address of the function address PROC* ppfn = (PROC*) &pThunk->u1.Function; // Is this the function we're looking for? BOOL bFound = (*ppfn == pfnCurrent); if (bFound) { if (!WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL) && (ERROR_NOACCESS == GetLastError())) { DWORD dwOldProtect; if (VirtualProtect(ppfn, sizeof(pfnNew), PAGE_WRITECOPY, &dwOldProtect)) { WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL); VirtualProtect(ppfn, sizeof(pfnNew), dwOldProtect, &dwOldProtect); } } return; // We did it, get out } } } // Each import section is parsed until the right entry is found and patched } }
API Hook - SSDT Hook (Ring0)
SSDT (System Service Dispatch Table) 系統(tǒng)服務(wù)調(diào)度表
此表保存了系統(tǒng)中所有的系統(tǒng)服務(wù)函數(shù)地址,通過改變此表中的地址,達(dá)到Hook某個(gè)系統(tǒng)函數(shù)的目的
為何替換SSDT表中的地址,首先需要了解,Windows內(nèi)核是如何為應(yīng)用程序服務(wù)的?
為了保證每個(gè)進(jìn)程的安全, Windows為不同的進(jìn)程分配了獨(dú)立的進(jìn)程空間,即虛擬內(nèi)存空間。一個(gè)進(jìn)程的虛擬地址是無法指到另一個(gè)進(jìn)程中的數(shù)據(jù)的。對(duì)應(yīng)32位x86系統(tǒng),每個(gè)進(jìn)程的空間是4G,0x00000000-0xFFFFFFFF。為了高效的調(diào)用系統(tǒng)服務(wù),Windows把操作系統(tǒng)的代碼和數(shù)據(jù)映射到所有進(jìn)程的空間中,因此4G劃分為低2G的用戶層和高2G的內(nèi)核層空間。如圖所示:
Windows定義了兩種訪問模式(access mode)
應(yīng)用程序運(yùn)行在用戶模式下,操作系統(tǒng)運(yùn)行在內(nèi)核模式下。內(nèi)核模式對(duì)應(yīng)CPU的高權(quán)限級(jí)別,內(nèi)核模式下可以訪問系統(tǒng)的所有資源,擁有執(zhí)行所有指令的權(quán)限,用戶模式對(duì)應(yīng)CPU較低的權(quán)限,只可以訪問系統(tǒng)允許的其訪問的內(nèi)存空間和資源,并且沒有權(quán)限運(yùn)行一些特殊指令。
用戶模式下的代碼不可以直接訪問內(nèi)核模式下的代碼和數(shù)據(jù),不能直接通過call指令調(diào)用內(nèi)核模式下的任何函數(shù)。如果嘗試,系統(tǒng)會(huì)產(chǎn)生保護(hù)性錯(cuò)誤,程序直接終止。
間接訪問:用戶程序通過調(diào)用系統(tǒng)服務(wù)來間接訪問系統(tǒng)空間的數(shù)據(jù)或間接的來調(diào)用系統(tǒng)空間中的代碼。
下面介紹用戶程序調(diào)用API:ReadFile,如何到達(dá)內(nèi)核層:
對(duì)應(yīng)的匯編代碼如下:
eax中保存的就是SSDT的索引。
在KiSystemServcie中,通過eax,索引SSDT中保存的地址,并調(diào)用之。
所以,如果我們知道SSDT中的每個(gè)地址對(duì)應(yīng)的服務(wù)函數(shù),并且知道此服務(wù)函數(shù)對(duì)應(yīng)到用戶層的API。那么我們可以編寫一個(gè)驅(qū)動(dòng),加載到內(nèi)核空間中,將驅(qū)動(dòng)中的我們自己的函數(shù)地址替換到SSDT中,應(yīng)用層調(diào)用API后,最后會(huì)拐到我們自己的函數(shù)中,達(dá)到Hook的目的。
系統(tǒng)有兩張SSDT表,一個(gè)是導(dǎo)出的KeServiceDescriptorTable(ntoskrnl.exe),一個(gè)是未導(dǎo)出的KeServiceDescriptorTableShadow(ntoskrnl.exe,win32k.sys)
由于內(nèi)核已經(jīng)導(dǎo)出KeServiceDescriptorTable全局變量,所以編寫程序的時(shí)候,不需要定位KeServiceDescriptorTable。KeServiceDescriptorTableShadow未導(dǎo)出,所以需要使用一些非公開的方法來定位此地址,通常都是采用硬性編碼的,沒有系統(tǒng)適應(yīng)性。
這兩張表見圖:
在我在做的反截屏驅(qū)動(dòng)中,就是查找到 KeServiceDescriptorTableShadow地址,替換NtGdiBitBlt(對(duì)應(yīng)的Win32 API為 BitBlt)等內(nèi)核函數(shù)地址,進(jìn)行功能過濾。
SSDT的缺點(diǎn)在于在64位的系統(tǒng)下,基本無法工作,除非你能跨過微軟的安全防護(hù)。目前來看,還沒有人破解win8.1.
Hook其他進(jìn)程
下面的例子是否有問題:
-
示例1:A進(jìn)程中有個(gè)MySleep函數(shù),A進(jìn)程通過IAT Hook的方式,將A進(jìn)程中的IAT中對(duì)應(yīng)的Sleep地址進(jìn)行了替換,替換為MySleep函數(shù)地址, Hook是否能夠成功?
-
示例2:A進(jìn)程中有個(gè)MySleep函數(shù),A進(jìn)程通過IAT Hook的方式,將B進(jìn)程中的IAT中對(duì)應(yīng)的Sleep地址進(jìn)行了替換,替換為MySleep函數(shù)地址,Hook是否能夠成功?
我們知道,Windows下各個(gè)進(jìn)程的空間是相互隔離的。A進(jìn)程雖然能夠替換B進(jìn)程中的IAT地址(通過VirtualProtect,WriteProcessMemory可以實(shí)現(xiàn)),但是替換的MySleep的地址存在于A進(jìn)程中,并不在B進(jìn)程中,B進(jìn)程直接調(diào)用,基本的可能就是崩潰了,如果這個(gè)地址正好是B進(jìn)程的一個(gè)有效函數(shù)地址,還調(diào)用成功了,你會(huì)更加摸不著頭腦。
自己Hook自己通常來說,沒有太大意義,那么如何Hook其它進(jìn)程?
Windows提供了一種DLL注入的技術(shù),注入的方式有:
這幾種注入技術(shù)都在《 Windows核心編程 》詳細(xì)講解到。
使用注冊(cè)表來注入
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\ 中:
有一個(gè)鍵:AppInit_DLLs,可以包含一組DLL(逗號(hào)或空格分隔),將我們的DLL路徑寫入此鍵值中。
創(chuàng)建一個(gè)名為LoadAppInit_DLLs,將其值設(shè)置為1.
當(dāng)user32.dll被映射到一個(gè)新的進(jìn)程時(shí),會(huì)收到DLL_PROCESS_ATTACH通知。當(dāng)user32.dll對(duì)它進(jìn)行處理時(shí),會(huì)取得上述注冊(cè)表鍵值,并調(diào)用LoadLibrary載入這個(gè)鍵值存儲(chǔ)的所有DLL。
缺點(diǎn):
基于CUI的應(yīng)用程序沒有用到user32.dll,因此無法加載DLL。
我可能只想注入某些應(yīng)用中,映射的越多,崩潰的可能性越大。
使用window 系統(tǒng)消息掛鉤來注入
為了能讓系統(tǒng)消息掛鉤正常運(yùn)行,Microsoft被迫設(shè)計(jì)出一種機(jī)制,讓我們可以將DLL注入到另一進(jìn)程空間中(想象一下:調(diào)用SetWindowsHookEx安裝一個(gè)WH_GETMESSAGE鉤子,如果讓其他進(jìn)程運(yùn)行SetWindowsHookEx設(shè)置的函數(shù)?)
SetWindowsHookEx(WH_GETMESSAGE,GetMsgProc,hInstDll,0);
GetMsgProc在調(diào)用此函數(shù)的進(jìn)程空間中,假設(shè)為A進(jìn)程。第三個(gè)參數(shù)為hInstDll,為當(dāng)前進(jìn)程空間中的一個(gè)DLL的句柄,此DLL包含了GetMsgProc函數(shù),最后一個(gè)參數(shù)表示要給哪兒個(gè)線程安裝,0表示給系統(tǒng)中所有的GUI線程安裝掛鉤。
接下來會(huì)發(fā)生什么:
-
B進(jìn)程的一個(gè)線程準(zhǔn)備向一個(gè)窗口發(fā)送一條消息。
-
系統(tǒng)檢測到已經(jīng)安裝了一個(gè)WH_GETMESSAGE鉤子。
-
系統(tǒng)檢測GetMsgProc所在的DLL是否已經(jīng)被映射到進(jìn)程B地址空間中。
-
如果尚未映射,系統(tǒng)會(huì)強(qiáng)制調(diào)用LoadLibrary強(qiáng)制將DLL映射到B進(jìn)程地址空間中,DLL引用計(jì)數(shù)加一。
-
DLL映射到B中的地址可能和A相同,也可能不同。如果不同,需要調(diào)整GetMsgProc的地址。
-
GetMsgProc = hInstDll B + (GetMsgProcA – hInstDll A)
-
系統(tǒng)在進(jìn)程B中地址該DLL的引用計(jì)數(shù)。
-
在B空間調(diào)用GetMsgProc,在此函數(shù)里,可以做API Hook處理,還可以創(chuàng)建一些消息機(jī)制,通過A進(jìn)程進(jìn)行功能控制。
使用遠(yuǎn)程線程來注入
在另一個(gè)進(jìn)程中創(chuàng)建遠(yuǎn)程線程:
HANDLE WINAPI CreateRemoteThread( _In_ HANDLE hProcess, _In_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_T dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_ LPDWORD lpThreadId );
只要使用這個(gè)遠(yuǎn)程線程調(diào)用LoadLibrary,來加載我們的DLL,就能達(dá)到DLL注入的目的。例如:
CreateRemoteThread(hProcessRemote,NULL, 0, LoadLibraryW,L"C:\\MyLib.dll", 0, NULL);
當(dāng)遠(yuǎn)程線程在遠(yuǎn)程進(jìn)程執(zhí)行時(shí),會(huì)立即調(diào)用LoadLibraryW,并傳入DLL路徑。
但并沒有這么簡單
CreateRemoteThread(hProcessRemote,NULL, 0, LoadLibraryW,L"C:\\MyLib.dll", 0, NULL);
但存在兩個(gè)問題:
-
第一個(gè)問題是:
上面我們已經(jīng)講過LoadLibraryW是個(gè)導(dǎo)入函數(shù),使用地址引用IAT的方式來調(diào)用,顯然這個(gè)地址在不同的PE文件里是不同的。在其他進(jìn)程中,這個(gè)地址是不一樣的。
對(duì)CreateRemoteThread調(diào)用,假定本進(jìn)程和遠(yuǎn)程進(jìn)程中,Kernel32.dll被映射到地址空間是同一內(nèi)存。(雖然是假定,到目前為止,都是同一地址,重啟會(huì)變,但啟動(dòng)后不變)。
可以改變?yōu)椋?
PTHREAD_START_ROUNTINE pfnThreadRtn = (PTHREAD_START_ROUNTINE)GetProcAddress(GetModuleHandle(_T("Kernel32")), "LoadLibraryW");
CreateRemoteThread(hProcessRemote,NULL, 0, pfnThreadRtn,L"C:\\MyLib.dll", 0, NULL);
-
第二個(gè)問題:
L"C:\\MyLib.dll"字符串在調(diào)用進(jìn)程中,目標(biāo)進(jìn)程并沒有這個(gè)字符串,如果在目標(biāo)程序執(zhí)行,此地址則是執(zhí)行一個(gè)未知地址,可能會(huì)崩潰。
解決這個(gè)問題的辦法,需要把本地字符串放到遠(yuǎn)程進(jìn)程去。調(diào)用VirtualAllocEx可以讓一個(gè)進(jìn)程在另一個(gè)進(jìn)程中分配一塊空間。WriteProcessMemory可以將字符串從本進(jìn)程復(fù)制到遠(yuǎn)程進(jìn)程中。
Hook 開發(fā)庫
EasyHook(inline)
同時(shí)支持內(nèi)核和用戶層Hook,在使用中發(fā)現(xiàn)內(nèi)核層有不少Bug,據(jù)說用戶層很穩(wěn)定??雌浯a對(duì)多線程沒有處理,可能會(huì)有問題。
Mhook(inline)
只支持用戶層Hook,代碼輕量,清晰。
|