一、什么是dll注入
在Windows操作系統中,運行的每一個進程都生活在自己的程序空間中(保護模式),每一個進程都認為自己擁有整個機器的控制權,每個進程都認為自己擁有計算機的整個內存空間,這些假象都是操作系統創造的(操作系統控制CPU使得CPU啟用保護模式)。理論上而言,運行在操作系統上的每一個進程之間都是互不干擾的,即每個進程都會擁有獨立的地址空間。比如說進程B修改了地址為0x4000000的數據,那么進程C的地址為0x4000000處的數據並未隨着B的修改而發生改變,並且進程C可能並不擁有地址為0x4000000的內存(操作系統可能沒有為進程C映射這塊內存)。因此,如果某進程有一個缺陷覆蓋了隨機地址處的內存(這可能導致程序運行出現問題),那么這個缺陷並不會影響到其他進程所使用的內存。
也正是由於進程的地址空間是獨立的(保護模式),因此我們很難編寫能夠與其它進程通信或控制其它進程的應用程序。
所謂的dll注入即是讓程序A強行加載程序B給定的a.dll,並執行程序B給定的a.dll里面的代碼。注意,程序B所給定的a.dll原先並不會被程序A主動加載,但是當程序B通過某種手段讓程序A“加載”a.dll后,程序A將會執行a.dll里的代碼,此時,a.dll就進入了程序A的地址空間,而a.dll模塊的程序邏輯由程序B的開發者設計,因此程序B的開發者可以對程序A為所欲為。
二、什么時候需要dll注入
應用程序一般會在以下情況使用dll注入技術來完成某些功能:
1.為目標進程添加新的“實用”功能;
2.需要一些手段來輔助調試被注入dll的進程;
3.為目標進程安裝鈎子程序(API Hook);
三、dll注入的方法
一般情況下有如下dll注入方法:
1.修改注冊表來注入dll;
2.使用CreateRemoteThread函數對運行中的進程注入dll;
3.使用SetWindowsHookEx函數對應用程序掛鈎(HOOK)迫使程序加載dll;
4.替換應用程序一定會使用的dll;
5.把dll作為調試器來注入;
6.用CreateProcess對子進程注入dll
7.修改被注入進程的exe的導入地址表。
接下來將詳細介紹如何使用這幾種方式完成dll注入。
四、注入方法詳解
(一)、修改注冊表
如果使用過Windows,那么對注冊表應該不會陌生。整個系統的配置都保存在注冊表中,我們可以通過修改其中的設置來改變系統的行為。
首先打開注冊表並定位到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows項,如下圖所示,他顯示了該注冊表項中的條目。
AppInit_DLLs鍵的值可以是一個dll的文件名或一組dll的文件名(通過逗號或空格來分隔),由於空格是用來分隔文件名的,因此dll文件名不能含有空格。第一個dll的文件名可以包含路徑,但其他的dll包含的路徑將被忽略。
LoadAppInit_DLLs鍵的值表示AppInit_DLLs鍵是否有效,為了讓AppInit_DLLs鍵的值有效,需要將LoadAppInit_DLLs的值設置為1。
這兩個鍵值設定后,當應用程序啟動並加載User32.dll時,會獲得上述注冊表鍵的值,並調用LoadLibrary來調用這些字符串中指定的每一個dll。這時每個被載入的dll可以完成相應的初始化工作。但是需要注意的是,由於被注入的dll是在進程生命期的早期被載入的,因此這些dll在調用函數時應慎重。調用Kernel32.dll中的函數應該沒有問題,因為Kernel32.dll是在User32.dll載入前已被加載。但是調用其他的dll中的函數時應當注意,因為進程可能還未載入相應的dll,嚴重時可能會導致藍屏。
這種方法很簡單,只需要在注冊表中修改兩個鍵的值即可,但是有如下缺點:
1.只有調用了User32.dll的進程才會發生這種dll注入。也就是說某些CUI程序(控制台應用程序)可能無法完成dll注入,比如將dll注入到編譯器或鏈接器中是不可行的。
2.該方法會使得所有的調用了User32.dll的程序都被注入指定的dll,如果你僅僅想對某些程序注入dll,這樣很多進程將成為無辜的被注入着,並且其他程序你可能並不了解,盲目的注入會使得其他程序發生崩潰的可能性增大。
3.這種注入會使得在應用程序的整個生命周期內被注入的dll都不會被卸載。注入dll的原則是值在需要的時間才注入我們的dll,並在不需要時及時卸載。
(二)、使用CreateRemoteThread函數對運行中的進程注入dll
這種方法具有最高的靈活性,同時它要求掌握的知識也很多。從根本上說,dll注入技術要求目標進程中的一個線程調用LoadLibrary函數來載入我們想要注入的dll,由於我們不能輕易的控制別人進程中的線程,因此這種方法要求我們在目標進程中創建一個線程並在線程中執行LoadLibrary函數加載我們要注入的dll。幸運的是Windows為我們提供了CreateRemoteThread函數,它使得在另一個進程中創建一個線程變得非常容易。CreateRemoteThread函數的原型如下:
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 );
該函數與CreateThread僅僅只多出第一個參數hProcess,hProcess表示創建的新線程屬於哪一個進程。
參數lpStartAddress表示線程函數的起始地址,注意這個地址在目標進程的地址空間中。
現在問題來了,我們如何調用讓創建的線程執行LoadLibrary函數來加載我們要注入的dll呢?答案很簡單:只需要創建的線程的線程函數地址是LoadLibrary函數的起始地址即可。我們都知道,每一個線程創建時應該指定一個參數只有4個字節,返回值也只是4個字節的函數即可(從匯編的角度看確實如此,只要保證調用前后棧平衡即可),而LoadLibrary函數就滿足這些條件。LoadLibrary函數的原型如下:
HMODULE WINAPI LoadLibrary( _In_ LPCTSTR lpFileName );
可以發現LoadLibrary函數完全滿足上述條件,LoadLibrary的參數是dll路徑的起始地址,這個參數也就是CreateRemoteThread函數的lpParameter參數。但是參數指向的地址應該是目標進程的地址,並且該地址處應保存被加載dll的路徑字符串。但是一開始我們並不知道目標進程是否存在這樣一個地址並且這個地址恰好保存了我們的dll的完整路徑。解決這一問題的最保險的辦法是使用VirtualAllocEx函數在目標進程中開辟一塊內存存放我們的dll的路徑。VirtualAllocEx函數的原型如下:
LPVOID WINAPI VirtualAllocEx( _In_ HANDLE hProcess, _In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect );
VirtualAllocEx函數允許我們在目標進程中開辟一塊指定大小(以字節為單位)的內存,並返回這塊內存的起始地址。之后就可以用WriteProcessMemory函數將dll文件路徑的數據復制到目標進程中。WriteProcessMemory函數的原型如下:
BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, _In_ LPVOID lpBaseAddress, _In_ LPCVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesWritten );
在開始注入前,還需要確認一件事,就是目標進程使用的字符編碼方式。因為我們所調用的LoadLibrary函數在底層實際調用有兩種可能:
如果目標程序使用的是ANSI編碼方式,LoadLibrary實際調用的是LoadLibraryA,其參數字符串應當是ANSI編碼;
如果目標程序使用的是Unicode編碼方式,LoadLibrary實際調用的是LoadLibraryW,其參數字符串應當是Unicode編碼。
這使得注入過程變得很麻煩,為了減少復雜性,不妨直接使用LoadLibraryA或LoadLibraryW而不是用LoadLibrary函數來避免這一麻煩。另外,即使使用的是LoadLibraryA,LoadLibraryA也會將傳入的ANSI編碼的字符串參數轉換成Unicode編碼后再調用LoadLibraryW。綜上,不妨一致使用LoadLibraryW函數,並且字符串用Unicode編碼即可。
最后,我們可能會為獲得目標進程中LoadLibraryW函數的起始地址而頭疼,但其實這個問題也很簡單,因為目標進程中函數LoadLibraryW的起始地址和我們的進程中的LoadLibraryW函數的起始地址是一樣的。因此我們只需要用GetProcAddress即可獲得LoadLibraryW函數的起始地址。
經過以上漫長的分析,我們對CreateRemoteThread注入方法的原理有了較為清晰的理解,接下來我們就需要總結一下我們必須采取的步驟:
(1).用VirtualAllocEx函數在目標進程的地址空間中分配一塊足夠大的內存用於保存被注入的dll的路徑。
(2).用WriteProcessMemory函數把本進程中保存dll路徑的內存中的數據拷貝到第(1)步得到的目標進程的內存中。
(3).用GetProcAddress函數獲得LoadLibraryW函數的起始地址。LoadLibraryW函數位於Kernel32.dll中。
(4).用CreateRemoteThread函數讓目標進程執行LoadLibraryW來加載被注入的dll。函數結束將返回載入dll后的模塊句柄。
(5).用VirtualFreeEx釋放第(1)步開辟的內存。
在需要卸載dll時我們可以在上述第(5)步的基礎上繼續執行以下步驟:
(6).用GetProcAddress函數獲得FreeLibrary函數的起始地址。FreeLibrary函數位於Kernel32.dll中。
(7).用CreateRemoteThread函數讓目標進程執行FreeLibrary來卸載被注入的dll。(其參數是第(4)步返回的模塊句柄)。
如果不在上述步驟基礎上執行操作,卸載dll時你需要這么做:
(1).獲得被注入的dll在目標進程的模塊句柄。
(2).重復上述步驟的第(6)、(7)兩步。
接下來給出編寫的參考代碼,該程序以控制台應用程序方式運行,並在Windows 10上測試通過。
#include "windows.h" #include "stdio.h" #include "tlhelp32.h" #include "io.h" #include "tchar.h" //判斷某模塊(dll)是否在相應的進程中 //dwPID 進程的PID //szDllPath 查詢的dll的完整路徑 BOOL CheckDllInProcess(DWORD dwPID, LPCTSTR szDllPath) { BOOL bMore = FALSE; HANDLE hSnapshot = INVALID_HANDLE_VALUE; MODULEENTRY32 me = { sizeof(me), }; if (INVALID_HANDLE_VALUE == (hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID)))//獲得進程的快照 { _tprintf(L"CheckDllInProcess() : CreateToolhelp32Snapshot(%d) failed!!! [%d]\n", dwPID, GetLastError()); return FALSE; } bMore = Module32First(hSnapshot, &me);//遍歷進程內得的所有模塊 for (; bMore; bMore = Module32Next(hSnapshot, &me)) { if (!_tcsicmp(me.szModule, szDllPath) || !_tcsicmp(me.szExePath, szDllPath))//模塊名或含路徑的名相符 { CloseHandle(hSnapshot); return TRUE; } } CloseHandle(hSnapshot); return FALSE; } //向指定的進程注入相應的模塊 //dwPID 目標進程的PID //szDllPath 被注入的dll的完整路徑 BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath) { HANDLE hProcess = NULL;//保存目標進程的句柄 LPVOID pRemoteBuf = NULL;//目標進程開辟的內存的起始地址 DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);//開辟的內存的大小 LPTHREAD_START_ROUTINE pThreadProc = NULL;//loadLibreayW函數的起始地址 HMODULE hMod = NULL;//kernel32.dll模塊的句柄 BOOL bRet = FALSE; if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))//打開目標進程,獲得句柄 { _tprintf(L"InjectDll() : OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError()); goto INJECTDLL_EXIT; } pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);//在目標進程空間開辟一塊內存 if (pRemoteBuf == NULL) { _tprintf(L"InjectDll() : VirtualAllocEx() failed!!! [%d]\n", GetLastError()); goto INJECTDLL_EXIT; } if (!WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL))//向開辟的內存復制dll的路徑 { _tprintf(L"InjectDll() : WriteProcessMemory() failed!!! [%d]\n", GetLastError()); goto INJECTDLL_EXIT; } hMod = GetModuleHandle(L"kernel32.dll");//獲得本進程kernel32.dll的模塊句柄 if (hMod == NULL) { _tprintf(L"InjectDll() : GetModuleHandle(\"kernel32.dll\") failed!!! [%d]\n", GetLastError()); goto INJECTDLL_EXIT; } pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");//獲得LoadLibraryW函數的起始地址 if (pThreadProc == NULL) { _tprintf(L"InjectDll() : GetProcAddress(\"LoadLibraryW\") failed!!! [%d]\n", GetLastError()); goto INJECTDLL_EXIT; } if (!CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL))//執行遠程線程 { _tprintf(L"InjectDll() : MyCreateRemoteThread() failed!!!\n"); goto INJECTDLL_EXIT; } INJECTDLL_EXIT: bRet = CheckDllInProcess(dwPID, szDllPath);//確認結果 if (pRemoteBuf) VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE); if (hProcess) CloseHandle(hProcess); return bRet; } //讓指定的進程卸載相應的模塊 //dwPID 目標進程的PID //szDllPath 被注入的dll的完整路徑,注意:路徑不要用“/”來代替“\\” BOOL EjectDll(DWORD dwPID, LPCTSTR szDllPath) { BOOL bMore = FALSE, bFound = FALSE, bRet = FALSE; HANDLE hSnapshot = INVALID_HANDLE_VALUE; HANDLE hProcess = NULL; MODULEENTRY32 me = { sizeof(me), }; LPTHREAD_START_ROUTINE pThreadProc = NULL; HMODULE hMod = NULL; TCHAR szProcName[MAX_PATH] = { 0, }; if (INVALID_HANDLE_VALUE == (hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID))) { _tprintf(L"EjectDll() : CreateToolhelp32Snapshot(%d) failed!!! [%d]\n", dwPID, GetLastError()); goto EJECTDLL_EXIT; } bMore = Module32First(hSnapshot, &me); for (; bMore; bMore = Module32Next(hSnapshot, &me))//查找模塊句柄 { if (!_tcsicmp(me.szModule, szDllPath) || !_tcsicmp(me.szExePath, szDllPath)) { bFound = TRUE; break; } } if (!bFound) { _tprintf(L"EjectDll() : There is not %s module in process(%d) memory!!!\n", szDllPath, dwPID); goto EJECTDLL_EXIT; } if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) { _tprintf(L"EjectDll() : OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError()); goto EJECTDLL_EXIT; } hMod = GetModuleHandle(L"kernel32.dll"); if (hMod == NULL) { _tprintf(L"EjectDll() : GetModuleHandle(\"kernel32.dll\") failed!!! [%d]\n", GetLastError()); goto EJECTDLL_EXIT; } pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "FreeLibrary"); if (pThreadProc == NULL) { _tprintf(L"EjectDll() : GetProcAddress(\"FreeLibrary\") failed!!! [%d]\n", GetLastError()); goto EJECTDLL_EXIT; } if (!CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL)) { _tprintf(L"EjectDll() : MyCreateRemoteThread() failed!!!\n"); goto EJECTDLL_EXIT; } bRet = TRUE; EJECTDLL_EXIT: if (hProcess) CloseHandle(hProcess); if (hSnapshot != INVALID_HANDLE_VALUE) CloseHandle(hSnapshot); return bRet; } int main() { //InjectDll(6836, L"C:\\a.dll"); EjectDll(6836, L"C:\\a.dll"); return 0; }
(三)、使用SetWindowsHookEx函數對應用程序掛鈎(HOOK)迫使程序加載dll
消息鈎子:Windows操作系統為用戶提供了GUI(Graphic User Interface,圖形用戶界面),它以事件驅動方式工作。在操作系統中借助鍵盤、鼠標、選擇菜單、按鈕、移動鼠標、改變窗口大小與位置等都是事件。發生這樣的事件時,操作系統會把事先定義好的消息發送給相應的應用程序,應用程序分析收到的信息后會執行相應的動作。也就是說,在敲擊鍵盤時,消息會從操作系統移動到應用程序。所謂的消息鈎子就是在此期間偷看這些信息。以鍵盤輸入事件為例,消息的流向如下:
1.發生鍵盤輸入時,WM_KEYDOWN消息被添加到操作系統的消息隊列中;
2.操作系統判斷這個消息產生於哪個應用程序,並將這個消息從消息隊列中取出,添加到相應的應用程序的消息隊列中;
3.應用程序從自己的消息隊列中取出WM_KEYDOWN消息並調用相應的處理程序。
當我們的鈎子程序啟用后,操作系統在將消息發送給用用程序前會先發送給每一個注冊了相應鈎子類型的鈎子函數。鈎子函數可以對這一消息做出想要的處理(修改、攔截等等)。多個消息鈎子將按照安裝鈎子的先后順序被調用,這些消息鈎子在一起組成了"鈎鏈"。消息在鈎鏈之間傳遞時任一鈎子函數攔截了消息,接下來的鈎子函數(包括應用程序)將都不再收到該消息。
像這樣的消息鈎子功能是Windows提供的最基本的功能,MS Visual Studio中提供的SPY++就是利用了這一功能來實現的,SPY++是一個十分強大的消息鈎取程序,它能夠查看操作系統中來往的所有消息。
消息鈎子是使用SetWindowsHookEx來實現的。函數的原型如下:
HHOOK WINAPI SetWindowsHookEx( _In_ int idHook, _In_ HOOKPROC lpfn, _In_ HINSTANCE hMod, _In_ DWORD dwThreadId );
idHook參數是消息鈎子的類型,可以選擇的類型在MSDN中可以查看到相應的宏定義。比如我們想對所有的鍵盤消息做掛鈎,其取值將是WH_KEYBOARD,WH_KEYBOARD這個宏的值是2。
lpfn參數是鈎子函數的起始地址,注意:不同的消息鈎子類型的鈎子函數原型是不一樣的,因為不同類型的消息需要的參數是不同的,具體的鈎子函數原型需要查看MSDN來獲得。注意:鈎子函數可以在結束前任意位置調用CallNextHookEx函數來執行鈎鏈的其他鈎子函數。當然,如果不調用這個函數,鈎鏈上的后續鈎子函數將不會被執行。
hMod參數是鈎子函數所在的模塊的模塊句柄。
dwThreadId參數用來指示要對哪一個進程/線程安裝消息鈎子。如果這個參數為0,安裝的消息鈎子稱為“全局鈎子”,此時將對所有的進程(當前的進程以及以后要運行的所有進程)下這個消息鈎子。注意:有的類型的鈎子只能是全局鈎子。
注意:鈎子函數應當放在一個dll中,並且在你的進程中LoadLibrary這個dll。然后再調用SetWindowsHookEx函數對相應類型的消息安裝鈎子。
當SetWindowsHookEx函數調用成功后,當某個進程生成這一類型的消息時,操作系統會判斷這個進程是否被安裝了鈎子,如果安裝了鈎子,操作系統會將相關的dll文件強行注入到這個進程中並將該dll的鎖計數器遞增1。然后再調用安裝的鈎子函數。整個注入過程非常方便,用戶幾乎不需要做什么。
當用戶不需要再進行消息鈎取時只需調用UnhookWindowsHookEx即可解除安裝的消息鈎子,函數的原型如下:
BOOL WINAPI UnhookWindowsHookEx( _In_ HHOOK hhk );
hhk參數是之前調用SetWindowsHookEx函數返回的HHOOK變量。這個函數調用成功后會使被注入過dll的鎖計數器遞減1,當鎖計數器減到0時系統會卸載被注入的dll。
這種類型的dll注入的優點是注入簡單,缺點是只能對windows消息進行Hook並注入dll,而且注入dll可能不是立即被注入,因為這需要相應類型的事件發生。其次是它不能進行其他API的Hook,如果想對其它的函數進行Hook,你需要再在被注入的dll中添加用於API Hook的代碼。
接下來將給出這一dll注入方案的示例程序的代碼,代碼包含兩部分,一部分是dll的源文件,另一部分是控制台程序的源代碼。該程序的功能是屏蔽所有notepad.exe(Windows附帶的記事本程序)的按鍵消息,該程序在Windows xp下測試通過。
#include <stdio.h> #include <tchar.h> #include <windows.h> #pragma warning(disable : 4996) HHOOK ghHook = NULL; HINSTANCE ghInstance = NULL; LRESULT CALLBACK KeyboardProc( _In_ int code, _In_ WPARAM wParam, _In_ LPARAM lParam ) { TCHAR szPath[MAX_PATH] = {0,}; TCHAR sProcessName[MAX_PATH] = {0,}; if (code == 0 && !(lParam & 0x80000000))//如果是釋放按鍵 { GetModuleFileName(NULL, szPath, MAX_PATH); _wsplitpath(szPath, NULL, NULL, sProcessName, NULL); if (0==_wcsicmp(sProcessName, L"notepad"))//如果進程名是notepad { return 1;//刪除消息,不再往下傳遞 } } return CallNextHookEx(ghHook, code, wParam, lParam);//繼續傳遞消息 } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: ghInstance = hModule;//獲得本實例的模塊句柄 break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } extern "C" { __declspec(dllexport) void HookStart() { ghHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, ghInstance, 0); } __declspec(dllexport) void HookStop() { if (ghHook) { UnhookWindowsHookEx(ghHook); ghHook = NULL; } } }
#include <stdio.h> #include <conio.h> #include <tchar.h> #include <windows.h> typedef void(*PFNHOOKSTART)(); typedef void(*PFNHOOKSTOP)(); int main() { HMODULE Hmod = LoadLibraryA("hookdll.dll"); PFNHOOKSTART pHookStart = (PFNHOOKSTART)GetProcAddress(Hmod, "HookStart"); PFNHOOKSTOP pHookStop = (PFNHOOKSTOP)GetProcAddress(Hmod, "HookStop"); pHookStart(); printf("print 'q' to quite!\n"); while (_getch() != 'q'); pHookStop(); FreeLibrary(Hmod); return 0; }
(四)、替換應用程序一定會使用的dll
這種方法通常被編寫惡意代碼的人員用來編寫木馬,因此又被稱為使用木馬dll來注入dll。通常我們應當首先確認目標進程一定會載入的dll,然后替換掉它。舉個例子:比如我們知道目標進程一定會載入Xyz.dll,那么我們可以創建自己的dll並與它起同樣的名字。當然,我們必須將原先被替換掉的Xyz.dll改成別的名字,比如改成Xyz_1.dll。
注意:在我們編寫的Xyz.dll(將被注入的dll)內部,我們要導出原來的Xyz.dll所導出的所有符號。這一點很容易實現,可以用dll的函數轉發器實現(轉發到Xyz_1.dll的相同函數),這樣一來我們只需要對需要HOOK(掛鈎)的函數編寫掛鈎代碼即可,這一過程我們僅僅是多了一些重復工作。看起來這個方法是完美的,並且很多木馬程序經常這么干,但是它存在一個很嚴重的問題:如果被替換的dll后來由於程序升級導致替換的dll添加了新的導出函數,而被注入的dll並未及時添加這些新增導出函數的轉發器(或者Hook程序),這將導致使用了新的導出函數的程序不能正常運行。另外,請不要隨意的替換系統的dll,因為在dll注入一般應當只注入到目標進程即可,而注入到別的進程之后將帶來很大的安全隱患。
(五)、把dll作為調試器來注入
使用過OD(OllyDbg)的人員可能會為OD的強大功能感到驚嘆。因為OD可以調試一個程序並任意的修改被調試的程序。OD的工作原理是向目標進程使用了調試功能。調試器可以在被調試進程中執行很多特殊操作,操作系統載入一個被調試程序的時候,會在被調試的主線程尚未開始執行任何代碼前,自動通知調試器(用來調試被調試進程的進程),這時調試器可以將一些代碼注入到被調試進程的地址空間中,保存被調試進程的CONTEXT結構,修改EIP指向我們注入的代碼的起始位置執行這些代碼。最后再讓被調試的進程恢復原來的CONTEXT,繼續執行。整個過程對被調試的進程而言好像沒發生任何事情。
這種注入方式需要對調試功能有所研究,並且能夠對進程的CONTEXT進行操作,最后還需要對不同的CPU平台進行量身操作。此外,我們可能還需要手工編寫一些匯編指令來讓被調試的程序執行。這對編寫人員的能力要求較高。最后,這種方法在調試器終止后,Windows會自動終止被調試的程序。不過調試器可以通過調用DebugSetProcessKillOnExit函數並傳入FALSE,來改變Windows的默認行為。然后調試器就可以調用DebugActiveProcessStop函數來終止調試了。
為什么要在主線程尚未開始執行任何代碼前執行代碼注入呢?因為這個時候注入最安全,其實你可以在任何時候對被調試的程序下斷點並進行以上注入操作,但是為了保證被調試程序的穩定運行你可能需要做更多的工作。
(六)、用CreateProcess對子進程注入dll
這個方法與把dll作為調試器來注入方法有許多相似之處,同樣也具有較大的難度。這里要求目標進程是注入者進程的子進程。當使用CreateProcess函數來創建一個子進程時,可以選擇創建后立即掛起該進程。這樣,創建的子進程並不會開始執行且EIP指向ntdll.dll的RtlUserThreadStart函數的開始位置(在win10上EIP=0X76F9BA60),此時的子進程處於掛起狀態。因此,我們可以有目的地修改EIP的值讓其從另一個位置繼續執行,但隨意的修改EIP的值往往使創建的子程序崩潰。為了讓創建的子進程載入dll必須調用LoadLibrary函數。在使用CreatRemoteProcess方法中也介紹了一點:必須在目標進程(這里指子進程)中寫入載入的dll的完整路徑。因此我們在修改EIP指向我們的代碼之前需要將一部分代碼注入到目標進程中。其中被注入的代碼至少應包括如下操作:將dll路徑首地址壓棧;調用LoadLibrary函數;跳轉回原先EIP位置,讓程序繼續執行,好像什么都沒發生過。
但是,為了程序的穩定運行,這樣做還不夠。注入的代碼應該在執行后能恢復執行前的所有狀態。因此為了注入dll需要向目標進程注入較為安全的代碼應該包含如下操作:
1.保存所有寄存器的值;
2.將dll路徑首地址壓棧;
3.調用LoadLibrary函數;
4.恢復所有寄存器的值;
5.跳轉到原先EIP位置,讓程序繼續執行,好像什么都沒發生。
該方法有如下優點:在程序未開始執行前執行了dll注入,一般比較難以被發現。幾乎可以對所有的程序進行注入。
該方法同樣具有缺點:首先需要嚴謹的設計注入的代碼,並根據不同的cpu平台進行設計。其次就是目標進程要是注入着創建的子進程。
接下來將給出一段示例代碼,該程序以控制台方式運行。並在Windows 10和Windows xp上測試通過。(這段代碼參考自看雪論壇的IamHuskar,這里表示感謝!)
#include <windows.h> #include <stdio.h> #pragma warning(disable : 4996) //在子進程創建掛起時注入dll //hProcess 被創建時掛起的進程句柄 //hThread 進程中被掛起的線程句柄 //szDllPath 被注入的dll的完整路徑 BOOL StartHook(HANDLE hProcess, HANDLE hThread, TCHAR *szDllPath) { BYTE ShellCode[30 + MAX_PATH * sizeof(TCHAR)] = { 0x60, //pushad 0x9c, //pushfd 0x68,0xaa,0xbb,0xcc,0xdd, //push xxxxxxxx(xxxxxxxx的偏移為3) 0xff,0x15,0xdd,0xcc,0xbb,0xaa, //call [addr]([addr]的偏移為9) 0x9d, //popfd 0x61, //popad 0xff,0x25,0xaa,0xbb,0xcc,0xdd, //jmp [eip]([eip]的偏移為17) 0xaa,0xaa,0xaa,0xaa, //保存loadlibraryW函數的地址(偏移為21) 0xaa,0xaa,0xaa,0xaa, //保存創建進程時被掛起的線程EIP(偏移為25) 0, //保存dll路徑字符串(偏移為29) }; CONTEXT ctx; ctx.ContextFlags = CONTEXT_ALL; if (!GetThreadContext(hThread, &ctx)) { printf("GetThreadContext() ErrorCode:[0x%08x]\n", GetLastError()); return FALSE; } //在目標進程內存空間調撥一塊可執行的內存 LPVOID LpAddr = VirtualAllocEx(hProcess, NULL, 30 + MAX_PATH * sizeof(TCHAR), MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (LpAddr == NULL) { printf("VirtualAllocEx() ErrorCode:[0x%08x]\n", GetLastError()); return FALSE; } //獲得LoadLibraryW函數的地址 DWORD LoadDllAAddr = (DWORD)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW"); if (LoadDllAAddr == NULL) { printf("GetProcAddress() ErrorCode:[0x%08x]\n", GetLastError()); return FALSE; } printf("原始EIP=0x%08x\n", ctx.Eip); //寫入dllpath memcpy((char*)(ShellCode + 29), szDllPath, MAX_PATH); //寫入push xxxxxxxx *(DWORD*)(ShellCode + 3) = (DWORD)LpAddr + 29; //寫入loadlibraryA地址 *(DWORD*)(ShellCode + 21) = LoadDllAAddr; //寫入call [addr]的[addr] *(DWORD*)(ShellCode + 9) = (DWORD)LpAddr + 21; //寫入原始eip *(DWORD*)(ShellCode + 25) = ctx.Eip; //寫入jmp [eip]的[eip] *(DWORD*)(ShellCode + 17) = (DWORD)LpAddr + 25; //把shellcode寫入目標進程 if (!WriteProcessMemory(hProcess, LpAddr, ShellCode, 30 + MAX_PATH * sizeof(TCHAR), NULL)) { printf("WriteProcessMemory() ErrorCode:[0x%08x]\n", GetLastError()); return FALSE; } //修改目標進程的EIP,執行被注入的代碼 ctx.Eip = (DWORD)LpAddr; if (!SetThreadContext(hThread, &ctx)) { printf("SetThreadContext() ErrorCode:[0x%08x]\n", GetLastError()); return FALSE; } printf("修改后EIP=0x%08x\n", ctx.Eip); return TRUE; }; int main() { STARTUPINFO sti; PROCESS_INFORMATION proci; memset(&sti, 0, sizeof(STARTUPINFO)); memset(&proci, 0, sizeof(PROCESS_INFORMATION)); sti.cb = sizeof(STARTUPINFO); wchar_t ExeName[MAX_PATH] = L"C:\\aimprocess.exe";//子進程的名字及啟動參數 wchar_t DllName[MAX_PATH] = L"C:\\hookdll2.dll";//被注入的dll的完整路徑 if (CreateProcess(NULL, ExeName, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &sti, &proci) ==NULL) { printf("CreateProcess() ErrorCode:[0x%08x]\n", GetLastError()); getchar(); return 0; } if (!StartHook(proci.hProcess, proci.hThread, DllName)) { TerminateProcess(proci.hProcess, 0); printf("Terminated Process\n"); getchar(); return 0; } ResumeThread(proci.hThread); CloseHandle(proci.hProcess); CloseHandle(proci.hThread); return 0; }
現在對以上代碼做分析,程序首先調用CreateProcess函數來創建一個掛起的進程。創建成功后,prosic結構體保存了子進程的進程句柄和主線程的線程句柄。接下來調用StartHook函數進行代碼注入。
現在我們來詳細地分析StartHook函數,首先它創建了一段ShellCode,ShellCode的內容將被會復制到目標進程的空間中。但是當前的ShellCode還不能正常工作。因為它的很多數據要依靠放入目標進程的地址來決定。ShellCode實際上是一段匯編代碼后面附帶了執行這段代碼所需的變量或數據。所有的匯編代碼已在注釋當中進行標注。ShellCode數組的長度由匯編代碼長度和變量的長度的總和。
接下來的工作是修復ShellCode中部分匯編指令引用的地址,這些地址要以目標進程寫入的地址作為基礎偏移量。那么我們首先應該用VirtualAllocEx在目標進程的空間中調撥一塊可執行的物理內存用來保存ShellCode代碼。當然,LoadLibraryW函數的地址還是要從本進程中獲得。當對ShellCode數據修改完畢后,就可以將ShellCode通過WriteProcessMemory函數將ShellCode復制到目標進程中。接下來需要修改目標進程的EIP指針來使主線程從ShellCode的開始處。最后,恢復目標進程,讓其繼續運行即可。
通過以上分析,對上述代碼的執行步驟做如下總結:
1.創建一個掛起的子進程作為目標進程;
2.准備一份預先設計好的ShellCode(應具有上面所述的基本功能);
3.用VirtualAllocEx在目標進程中調撥一塊可執行的內存;
4.以分配的內存為基准修復ShellCode的匯編代碼引用的地址和數據;
5.用WriteProcessMemory函數將修復完畢的ShellCode復制目標進程在第3步分配的內存中;
6.修改目標進程的主線程的EIP指向第3步分配的內存的首地址;
7.恢復目標進程的主線程。
此方法的難點是設計好ShellCode代碼,這需要編寫者具有較高的匯編和分析設計能力。