- 原文地址:Userland API Monitoring and Code Injection Detection
- 原文做者:dtm
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Xekin-FE
- 校對者:Starrier,sunhaokk
本文實屬做者對惡意程式(或者病毒)是如何與 Windows 應用程序編程接口(WinAPI)進行交互的研究成果。當中詳細贅述了惡意程式如何可以將 Payload [譯註:Payload 指一種對設備形成傷害的程序]植入到其餘進程中的基本概念,以及如何經過監控與 Windows 操做系統的通訊來檢測此類功能。而且經過函數鉤子鉤住某些函數的方式來介紹觀察 API 調用的過程,而這些函數正被用來實現代碼注入功能。html
閱前聲明:因爲時間方面的緣由,這是一個相對來講比較短促的項目。因此各位在閱讀時如若發現了可能相關的錯誤信息,我先在此表示十分抱歉,還請儘快地通知我以便及時修正。除此以外,文章隨附的代碼部分在項目延展性上有必定的設計缺陷,也可能會由於版本落後而沒法成功在當下執行。前端
在當下,惡意軟件是由網絡罪犯開發並針對在網絡上那些容易泄露信息的計算機,經過在這些計算機系統上執行惡意任務以謀取利益。在大多數惡意軟件入侵事件中,這些惡意程式都生存於人們的視野以外,由於它們的行動必須保持隱蔽才能不讓管理員發現同時阻止系統殺毒軟件檢測。所以,經過代碼注入讓自身「隱形」成爲了經常使用的入侵手段。react
內聯掛鉤是經過熱補丁修復過程來繞過代碼流的一種行爲。熱補丁修復被定義爲一種能夠經過在程式運行時修改二進制代碼來改變應用行爲的方法[1]。其主要的目的就是爲了可以捕捉程序調用函數的時段,從而實現對程序進行監控和調用。下面是模擬內聯掛鉤在程序正常工做時的過程:android
正常調用函數時的程序
+---------+ +----------+
| Program | ----------------------- calls function -----------------------------> | Function | | execution
+---------+ | . | | of
| . | | function
| . | |
| | v
+----------+
複製代碼
與執行了一個鉤子函數後的程序相比:ios
程序中調用鉤子函數
+---------+ +--------------+ + -------> +----------+
| Program | -- calls function --> | Intermediate | | execution | | Function | | execution
+---------+ | Function | | of calls | . | | of
| . | | intermediate normal | . | | function
| . | | function function | . | |
| . | v | | | v
+--------------+ ------------------+ +----------+
複製代碼
此過程能夠分紅三個執行步驟。在這裏咱們能夠以 WinAPI 方法 MessageBox 來演示整個過程。git
若是咱們要想在函數中掛鉤,咱們首先須要一個必須能複製目標函數參數的中間函數。 MessageBox
方法在微軟開發者網絡(MSDN)中是這樣定義的:github
int WINAPI MessageBox(
_In_opt_ HWND hWnd,
_In_opt_ LPCTSTR lpText,
_In_opt_ LPCTSTR lpCaption,
_In_ UINT uType
);
複製代碼
因此咱們的中間函數也能夠像這樣:算法
int WINAPI HookedMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) {
// our code in here
}
複製代碼
一旦觸發中間函數,代碼執行流將被重定向至某個特定位置。要想在 MessageBox
方法中進行掛鉤,咱們能夠補充代碼的前幾個字節(請記住,咱們必須備份本來的字節,以便於在中間函數執行後恢復原始函數)。如下是在MsgBox方法中相應模塊 user32.dll
中的原始編碼指令:shell
; MessageBox
8B FF mov edi, edi
55 push ebp
8B EC mov ebp, esp
複製代碼
與掛鉤後的函數相比:編程
; MessageBox
68 xx xx xx xx push <HookedMessageBox> ; our intermediate function
C3 ret
複製代碼
基於以往的經驗以及對隱蔽性可靠程度的考慮,這裏我會選擇使用 push-ret
指令組合而不是一個絕對的 jmp
語句。xx xx xx xx
表示 HookedMessageBox
中的低字節序順序地址。
當程序調用 MessageBox
方法時,它將會執行 push-ret
相關指令並立刻插入 HookedMessageBox
函數中,如若執行成功,就能夠調用該函數來徹底控制程序參數和調用自己。例如若是要替換即將在消息對話框中顯示的文本內容,能夠在 HookedMessageBox
中聲明如下內容:
int WINAPI HookedMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) {
TCHAR szMyText[] = TEXT("This function has been hooked!");
}
複製代碼
其中 szMyText
能夠用來替換 MessageBox
中的 LPCTSTR lpText
參數。
要想將替換後的參數轉發,須要讓代碼執行流中的 MessageBox
方法回退到原始狀態,才能讓操做系統顯示對話框。因爲繼續調用 MessageBox
方法只會致使無限遞歸,因此咱們必需要恢復原始字節(正如前面所提到的)。
int WINAPI HookedMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) {
TCHAR szMyText[] = TEXT("This function has been hooked!");
// 還原 MessageBox 中的原始字節
// ...
// 使用已替換參數的 MessageBox 方法,並將值返回給程序
return MessageBox(hWnd, szMyText, lpCaption, uType);
}
複製代碼
若是須要拒絕調用 MessageBox
方法,那就跟返回一個值同樣簡單,最好這個值曾在文檔中被定義過。例如要在一個「確認/取消」對話框中返回「取消」選項,在中間函數中就能夠這樣聲明:
int WINAPI HookedMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) {
return IDNO; // IDNO defined as 7
}
複製代碼
基於函數掛鉤的方法機制,咱們徹底能夠控制函數調用的過程,同時也能夠控制程序裏的全部參數,這也就是咱們實現文檔中標題裏也提到過的 API 監控的概念原理。然而,這裏仍有一個小問題,那就是因爲不一樣的深層 API 實用性也不盡相同,致使這些 API 的調用將是獨一無二的,只不過在淺層調用中它們可能都使用同一組 API,這被稱爲函數嵌套,被定義爲在子程序中調用次級子程序。回到 MessageBox
的例子中,在方法裏,咱們聲明瞭兩個函數 MessageBoxA
和 MessageBoxW
,前者用來包含 ASCII 字符的參數,後者用來包含寬字符的參數。在實際應用中,若是咱們在 MessageBox
方法中掛鉤,就須要對 MessageBoxA
和 MessageBoxW
的前幾個字節都進行補充。而其實遇到這樣的問題時,咱們只須要在函數調用等級最低的公共點進行掛鉤就能夠了。
+---------+
| Program |
+---------+
/ \
| |
+------------+ +------------+
| Function A | | Function B |
+------------+ +------------+
| |
+-------------------------------+
| user32.dll, kernel32.dll, ... |
+-------------------------------+
+---------+ +-------- hook -----------------> |
| API | <---- + +-------------------------------------+
| Monitor | <-----+ | ntdll.dll |
+---------+ | +-------------------------------------+
+-------- hook -----------------> | User mode
-----------------------------------------------------
Kernel mode
複製代碼
下面是模擬調用 Message 方法的層級順序:
在 MessageBoxA
中:
user32!MessageBoxA -> user32!MessageBoxExA -> user32!MessageBoxTimeoutA -> user32!MessageBoxTimeoutW
複製代碼
在 MessageBoxW
中:
user32!MessageBoxW -> user32!MessageBoxExW -> user32!MessageBoxTimeoutW
複製代碼
上面方法中的層層調用最後都會合併到 MessageBoxTimeoutW
函數中,這會是個合適的掛鉤點。對於處在過深層次的函數,伴隨着函數參數的複雜化,對在任何底層的點進行掛鉤都只會帶來不必的麻煩。MessageBoxTimeoutW
是一個沒有在 WinAPI 文檔中說明的一個函數,它的定義以下:
int WINAPI MessageBoxTimeoutW(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType,
WORD wLanguageId,
DWORD dwMilliseconds
);
複製代碼
用法:
int WINAPI MessageBoxTimeoutW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType, WORD wLanguageId, DWORD dwMilliseconds) {
std::wofstream logfile; // declare wide stream because of wide parameters
logfile.open(L"log.txt", std::ios::out | std::ios::app);
logfile << L"Caption: " << lpCaption << L"\n";
logfile << L"Text: " << lpText << L"\n";
logfile << L"Type: " << uType << :"\n";
logfile.close();
// 恢復原始字節
// ...
// pass execution to the normal function and save the return value
int ret = MessageBoxTimeoutW(hWnd, lpText, lpCaption, uType, wLanguageId, dwMilliseconds);
// rehook the function for next calls
// ...
return ret; // 返回原始函數的值
}
複製代碼
只要在 MessageBoxTimeoutW
掛鉤成功,MessageBoxA
和 MessageBoxW
的行爲就均可以被咱們捕獲了。
就本文而言,咱們將代碼注入技術定義爲一種嵌入行爲,它能夠將程序內部可執行代碼在外部甚至是遠程進行調用修改。在 WinAPI 自己就擁有一些可讓咱們實現嵌入的功能。當其中某些函數方法被組合封裝在一塊兒時,就可能實現訪問現有進程,篡改寫入數據而後隱藏在代碼流中遠程執行。在本節中,做者將會介紹在研究中涉及到的代碼注入的相關技術。
在計算機中,代碼能夠存在於多種形式的文件下,其中之一就是 Dynamic Link Library (動態連接庫 DLL)。DLL 文件又被稱爲應用程序拓展庫,顧名思義,它就是經過導出應用子程序後用來給其餘程序進行拓展。本文其他部分將都以此 DLL 文件示例:
extern "C" void __declspec(dllexport) Demo() {
::MessageBox(nullptr, TEXT("This is a demo!"), TEXT("Demo"), MB_OK);
}
bool APIENTRY DllMain(HINSTANCE hInstDll, DWORD fdwReason, LPVOID lpvReserved) {
if (fdwReason == DLL_PROCESS_ATTACH)
::CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)Demo, nullptr, 0, nullptr);
return true;
}
複製代碼
當一個 DLL 文件在程序中加載並初始化後,加載程序將會調用 DllMain
這個方法並判斷 fdwReason
參數是否設置爲 DLL_PROCESS_ATTACH
。在這個例子中,當在進程中加載 DLL 文件時,它將經過 Demo
這個方法顯示一個帶有 Demo
標題和 This is a demo!
文本內容的消息框。要想正確地完成對 DLL 文件地初始化,消息框必須返回 true
值,不然文件就會被拒絕執行。
CreateRemoteThread 是實現 DLL 注入的方法之一,它能夠被使用在某個進程的虛擬空間中執行遠程線程。正如以前所提到過的,咱們所作的一切都是爲了經過注入 DLL 文件使其進程強制執行 LoadLibrary
函數。經過如下代碼咱們將實現這點:
void injectDll(const HANDLE hProcess, const std::string dllPath) {
LPVOID lpBaseAddress = ::VirtualAllocEx(hProcess, nullptr, dllPath.length(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
::WriteProcessMemory(hProcess, lpBaseAddress, dllPath.c_str(), dllPath.length(), &dwWritten);
HMODULE hModule = ::GetModuleHandle(TEXT("kernel32.dll"));
LPVOID lpStartAddress = ::GetProcAddress(hModule, "LoadLibraryA"); // LoadLibraryA for ASCII string
::CreateRemoteThread(hProcess, nullptr, 0, (LPTHREAD_START_ROUTINE)lpStartAddress, lpBaseAddress, 0, nullptr);
}
複製代碼
MSDN 對 LoadLibrary 是這樣定義的:
HMODULE WINAPI LoadLibrary(
_In_ LPCTSTR lpFileName
);
複製代碼
使用上面這個函數時,咱們須要傳入一個參數那就是加載庫的路徑。而在 LoadLibrary
例程中聲明的這個參數將會被傳遞給 CreateRemoteThread
方法中相匹配的路徑參數。這種行爲的目的是爲了能在目標進程的虛擬地址空間中傳遞字符串參數,而後將 CreateRemoteThread
方法的自變量參數分配給空間地址以便調用 LoadLibrary
來加載 DLL。
使用 VirtualAllocEx
函數能夠指定進程的虛擬空間保留或提交內存區域,執行完畢後函數將返回分配內存的首地址。
目標進程的虛擬地址空間:
+--------------------+
| |
VirtualAllocEx +--------------------+
Allocated memory ---> | Empty space |
+--------------------+
| |
+--------------------+
| Executable |
| Image |
+--------------------+
| |
| |
+--------------------+
| kernel32.dll |
+--------------------+
| |
+--------------------+
複製代碼
只要內存初始化成功, DLL 的路徑就能夠被注入到 VirtualAllocEx
使用 WriteProcessMemory
返回的分配內存裏。
目標進程的虛擬地址空間
+--------------------+
| |
WriteProcessMemory +--------------------+
Inject DLL path ----> | "..\..\myDll.dll" |
+--------------------+
| |
+--------------------+
| Executable |
| Image |
+--------------------+
| |
| |
+--------------------+
| kernel32.dll |
+--------------------+
| |
+--------------------+
複製代碼
LoadLibrary
地址因爲全部的系統 DLL 文件都會被映射到全部進程的相同地址空間,因此 LoadLibrary
的地址不須要到目標進程中檢索。只需調用 GetModuleHandle(TEXT("kernel32.dll"))
和 GetProcAddress(hModule, "LoadLibraryA")
就能夠了。
若是咱們須要加載 DLL 文件,LoadLibrary
地址以及 DLL 文件路徑是咱們必須知道的兩個主要參數。在使用 CreateRemoteThread
函數時,LoadLibrary
將會以 DLL 文件路徑做爲參數在目標進程的代碼流中被執行。
目標進程的虛擬地址空間
+--------------------+
| |
+--------------------+
+--------- | "..\..\myDll.dll" |
| +--------------------+
| | |
| +--------------------+ <---+
| | myDll.dll | |
| +--------------------+ |
| | | | LoadLibrary
| +--------------------+ | loads
| | Executable | | and
| | Image | | initialises
| +--------------------+ | myDll.dll
| | | |
| | | |
CreateRemoteThread v +--------------------+ |
LoadLibraryA("..\..\myDll.dll") --> | kernel32.dll | ----+
+--------------------+
| |
+--------------------+
複製代碼
SetWindowsHookEx 函數是 Windows 提供給程序開發人員的一個 API,經過對某一事件流程掛鉤實現對消息攔截的功能,雖然這個函數常常被使用來監視鍵盤按鍵輸入和記錄,但其實也能夠被用於 DLL 注入。如下代碼將演示如何將 DLL 注入事件自己。
int main() {
HMODULE hMod = ::LoadLibrary(DLL_PATH);
HOOKPROC lpfn = (HOOKPROC)::GetProcAddress(hMod, "Demo");
HHOOK hHook = ::SetWindowsHookEx(WH_GETMESSAGE, lpfn, hMod, ::GetCurrentThreadId());
::PostThreadMessageW(::GetCurrentThreadId(), WM_RBUTTONDOWN, (WPARAM)0, (LPARAM)0);
// 捕捉事件的消息隊列
MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0) > 0) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
return 0;
}
複製代碼
SetWindowsHookEx
在 MSDN 中是這樣定義的:
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);
複製代碼
在上面的定義中, HOOKPROC
是由用戶聲明的鉤子函數,當特定的掛鉤事件被觸發時它就會被執行。在咱們的示例中,這一事件指的是 WH_GETMESSAGE
鉤子,它主要負責處理進隊消息的工做[譯註:Windows 中消息分爲進隊消息和不進隊消息]。這段代碼是一個回調函數,它會先將 DLL 文件加載到它本身的虛擬進程空間中,再得到以前導出的 Demo
函數地址,最後在 SetWindowsHookEx
函數中聲明並調用。要想強制執行這個鉤子函數,咱們只需調用 PostThreadMessage
函數並將消息賦值爲 WM_RBUTTONDOWN
就能夠觸發 WH_GETMESSAGE
鉤子以後就能顯示以前所說的消息框了。
使用 QueueUserAPC 接口方法的 DLL 注入和 CreateRemoteThread
相似,都是在分配和注入 DLL 地址到目標進程的虛擬地址空間中後在代碼流中強制調用 LoadLibrary
函數。
int injectDll(const std::string dllPath, const DWORD dwProcessId, const DWORD dwThreadId) {
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, false, dwProcessId);
HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, false, dwThreadId);
LPVOID lpLoadLibraryParam = ::VirtualAllocEx(hProcess, nullptr, dllPath.length(), MEM_COMMIT, PAGE_READWRITE);
::WriteProcessMemory(hProcess, lpLoadLibraryParam, dllPath.data(), dllPath.length(), &dwWritten);
::QueueUserAPC((PAPCFUNC)::GetProcAddress(::GetModuleHandle(TEXT("kernel32.dll")), "LoadLibraryA"), hThread, (ULONG_PTR)lpLoadLibraryParam);
return 0;
}
複製代碼
這個方法和 CreateRemoteThread
有一個主要區別,QueueUserAPC
是隻能在警告狀態下執行調用的。也就是說在 QueueUserAPC
隊列中的異步程序只有在當在線程處於警告狀態時才能調用 APC 函數。
Process hollowing(傀儡進程),又稱爲 RunPE,這是一個常見的用於躲避反病毒檢測的方法。它能夠作到把整個可執行文件注入到目標進程中並在其代碼流中執行。一般咱們會在加密的應用程序中看到,存在 Payload 的磁盤上的某個文件會被選舉爲 host 而且被做爲進程建立,而這個文件的主要執行模塊都被挖空而且替換掉了。這樣一個過程能夠分解爲四步來執行。
爲了將 Payload 注入,首先引導程序必須找到適合引導的主文件。若是 Payload 是一個 .NET 應用程序,那麼主文件也必須是 .NET 應用程序。若是 Payload 是一個能夠調用控制檯子系統的本地可執行程序,則主文件也要具備與其相同的屬性。無論是32位仍是64位的程序都必需要知足這一條件。一旦主文件找到了以後,系統函數 CreateProcess(PATH_TO_HOST_EXE, ..., CREATE_SUSPENDED, ...)
即可建立一個掛起狀態的進程。
主進程中的可執行映像
+--- +--------------------+
| | PE |
| | Headers |
| +--------------------+
| | .text |
| +--------------------+
CreateProcess + | .data |
| +--------------------+
| | ... |
| +--------------------+
| | ... |
| +--------------------+
| | ... |
+--- +--------------------+
複製代碼
爲了使注入後的 Paylaod 正常工做,咱們必須將其映射到與 PE 映像頭的 optional header 的 ImageBase
值相同的虛擬地址空間。
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint; // <---- this is required later
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase; // <----
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // <---- size of the PE file as an image
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
複製代碼
這一點很是重要,由於絕對地址頗有可能會涉及徹底依賴其內存位置的代碼。爲了安全地映射該可執行映像,必須從描述的 ImageBase
值開始的虛擬內存空間卸載映射。因爲許多可執行文件共享通用的基地址(一般爲 0x400000
),所以主進程自己的可執行映像未映射的狀況並不罕見。卸載這一操做能夠經過 NtUnmapViewOfSection(IMAGE_BASE, SIZE_OF_IMAGE)
來完成。
主進程中的可執行映像
+--- +--------------------+
| | |
| | |
| | |
| | |
| | |
NtUnmapViewOfSection + | |
| | |
| | |
| | |
| | |
| | |
| | |
+--- +--------------------+
複製代碼
要將 Payload 注入,咱們必須手動去解析 PE 文件將其從磁盤格式轉換爲映像格式。在使用 VirtualAllocEx
分配完虛擬內存後,PE 映像頭將直接被複制到基地址中。
主進程中的可執行映像
+--- +--------------------+
| | PE |
| | Headers |
+--- +--------------------+
| | |
| | |
WriteProcessMemory + | |
| |
| |
| |
| |
| |
| |
+--------------------+
複製代碼
而若是要將 PE 文件轉換成映像,全部的區塊(節)都必須從文件偏移量裏逐個讀取,而後經過使用 WriteProcessMemory
將其放置到正確的虛擬偏移量中。在這篇 MSDN 文檔中每一個章節的 section header. 都有介紹。
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; // <---- 虛擬偏移量
DWORD SizeOfRawData;
DWORD PointerToRawData; // <---- 文件偏移量
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
複製代碼
主進程中的可執行映像
+--------------------+
| PE |
| Headers |
+--- +--------------------+
| | .text |
+--- +--------------------+
WriteProcessMemory + | .data |
+--- +--------------------+
| | ... |
+---- +--------------------+
| | ... |
+---- +--------------------+
| | ... |
+---- +--------------------+
複製代碼
最後一步就是將執行的首地址指向上面有提到過的(建立主進程)Payload 的 AddressOfEntryPoint
。因爲進程的主線程已經被掛起,因此可使用 GetThreadContext
方法來檢索相關信息。其代碼結構能夠如如下聲明:
typedef struct _CONTEXT
{
ULONG ContextFlags;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
FLOATING_SAVE_AREA FloatSave;
ULONG SegGs;
ULONG SegFs;
ULONG SegEs;
ULONG SegDs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Edx;
ULONG Ecx;
ULONG Eax; // <----
ULONG Ebp;
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG Esp;
ULONG SegSs;
UCHAR ExtendedRegisters[512];
} CONTEXT, *PCONTEXT;
複製代碼
若是要修改首地址,咱們必須將上面的 Eax
數據成員更改成 Payload 的 AddressOfEntryPoint
的虛擬地址。簡單表示,context.Eax = ImageBase + AddressOfEntryPoint
。調用 SetThreadContext
方法,並傳入修改的 CONTEXT
結構,咱們就能夠更改應用到進程線程。以後如今咱們只需調用 ResumeThread
,Payload 應該就能夠開始執行了。
Atom Bombing 是一種代碼注入技術,它利用了 Windows 的全局原子表來實現全局數據存儲。全局原子表中的數據能夠跨全部進程進行訪問,這也正是咱們能實現代碼注入的緣由。表中的數據是以空字符結尾的 C-string 類型,用 16-bit 的整數表示,咱們稱之爲原子(Atom),它相似於 map 數據結構。在 MSDN 中提供了 GlobalAddAtom 方法用於向其添加數據,以下聲明:
ATOM WINAPI GlobalAddAtom(
_In_ LPCTSTR lpString
);
複製代碼
其中 lpString
是要存儲的數據,當方法調用成功時將會返回一個 16-bit 的整數原子。咱們能夠經過 GlobalGetAtomName 來檢索存儲在全局原子表裏面的數據,以下聲明:
UINT WINAPI GlobalGetAtomName(
_In_ ATOM nAtom,
_Out_ LPTSTR lpBuffer,
_In_ int nSize
);
複製代碼
經過 GlobalAddAtom
添加方法返回的標識原子將會被放入 lpBuffer
中並返回該字符串的長度(不包含空終止符)。
Atom bombing 是經過強制讓目標進程加載並執行存儲在全局原子表裏的代碼,這依賴於另外一個關鍵函數,NtQueueApcThread
,一個 QueueUserAPC
接口在用戶領域的調用方法。之因此使用 NtQueueApcThread
而不是 QueueUserAPC
其餘方法的緣由,正如前面所看到的,QueueUserAPC
的 APCProc 方法只能接收一個參數,而 GlobalGetAtomName
須要三個參數[3]。
VOID CALLBACK APCProc( UINT WINAPI GlobalGetAtomName(
_In_ ATOM nAtom,
_In_ ULONG_PTR dwParam -> _Out_ LPTSTR lpBuffer,
_In_ int nSize
); );
複製代碼
然而在 NtQueueApcThread
的底層會容許咱們能夠傳入三個潛在的參數:
NTSTATUS NTAPI NtQueueApcThread( UINT WINAPI GlobalGetAtomName(
_In_ HANDLE ThreadHandle, // target process's thread _In_ PIO_APC_ROUTINE ApcRoutine, // APCProc (GlobalGetAtomName) _In_opt_ PVOID ApcRoutineContext, -> _In_ ATOM nAtom, _In_opt_ PIO_STATUS_BLOCK ApcStatusBlock, _Out_ LPTSTR lpBuffer, _In_opt_ ULONG ApcReserved _In_ int nSize ); ); 複製代碼
下面是咱們用圖形模擬代碼注入的過程:
Atom bombing code injection
+--------------------+
| |
+--------------------+
| lpBuffer | <-+
| | |
+--------------------+ |
+---------+ | | | Calls
| Atom | +--------------------+ | GlobalGetAtomName
| Bombing | | Executable | | specifying
| Process | | Image | | arbitrary
+---------+ +--------------------+ | address space
| | | | and loads shellcode
| | | |
| NtQueueApcThread +--------------------+ |
+---------- GlobalGetAtomName ----> | ntdll.dll | --+
+--------------------+
| |
+--------------------+
複製代碼
這是 Atom bombing 的一種很是簡化的概述,但對於本文的其他部分來講已經足夠了。若是餘姚瞭解更多關於 Atom bombing 的技術信息,請參閱 enSilo 的 AtomBombing: Brand New Code Injection for Windows。
UnRunPE 是一個概念驗證(Proof of concept,簡稱 PoC)工具,是爲了將 API 監控的理論概念應用到實際操做而編寫的。該工具的目的是將選定的可執行文件做爲進程建立並掛起,隨後將帶有鉤子函數的 DLL 經過傀儡進程技術(process hollowing)注入到進程中。
瞭解了相關的代碼注入的基礎知識以後,能夠經過下面的 WinAPI 函數調用鏈來實現傀儡進程技術的注入手段:
CreateProcess
NtUnmapViewOfSection
VirtualAllocEx
WriteProcessMemory
GetThreadContext
SetThreadContext
ResumeThread
其實當中有一些並不必定要按這樣的順序執行,例如,GetThreadContext
能夠在 VirtualAllocEx
以前就調用。不過因爲一些方法須要依賴前面調用的 API,例如 SetThreadContext
必須要在 GetThreadContext
或者 CreateProcess
調用以前調用,不然就沒法將 Payload 注入到目標進程。該工具將假定上述的調用順序做爲參考,嘗試檢測是否有潛在的傀儡進程。
遵循 API 監控的理論,咱們最好是在函數調用等級最低的公共點進行掛鉤,但當被惡意軟件入侵時,咱們最理想的應該是將其可訪問的可能性降到最低。假定在最壞的狀況下,入侵者可能會嘗試繞太高層的 WinAPI 函數,而直接調用最低層的函數,這些函數一般在 ntdll.dll
模塊中能夠找到。下列是傀儡進程當中常常調用的達到上述要求的 WinAPI 函數:
NtCreateUserProcess
NtUnmapViewOfSection
NtAllocateVirtualMemory
NtWriteVirtualMemory
NtGetContextThread
NtSetContextThread
NtResumeThread
一旦咱們在須要的函數中掛鉤成功,目標進程就會被執行而且記錄每一個掛鉤函數的參數,這樣咱們就能跟蹤傀儡進程以及主進程的當前進度。最值得注意的是 NtWriteVirtualMemory
和 NtResumeThread
這兩個鉤子函數,由於前者參與應用了代碼注入,然後者執行了它。除了記錄參數之外,UnRunPE 還會嘗試轉儲使用 NtWriteVirtualMemory
寫入的字節而且當執行 NtResumeThread
時,它將嘗試轉儲整個被注入到主進程的 Payload。要作到這點,函數將須要利用經過 NtCreateUserProcess
記錄的進程和線程句柄參數以及經過 NtUnmapViewOfSection
記錄的基地址及其大小。在這裏,若是使用 NtAllocateVirtualMemory
的參數可能會更合適,但實際應用中出於某些不明緣由,對函數進行掛鉤的過程當中會出現錯誤。當經過 NtResumeThread
將 Payload 成功轉儲後,它將終止目標進程及其宿主進程,同時也阻止了注入後的代碼執行。
爲了演示這點,我選擇了使用以前建立的二進制木馬文件來作實驗。文件中包含了 PEview.exe
以及 PuTTY.exe
做爲隱藏的可執行文件。
Dreadnought 是基於 UnRunPE 構建的 PoC 工具,它提供了更多樣的代碼注入檢測,也就是咱們前面代碼注入入門的所有內容。爲了讓應用程序更全面的檢測代碼注入,強化工具功能也在所必然。
實現代碼注入能夠有不少種方法,因此咱們必需要了解不一樣的技術之間的區別。第一種檢測代碼注入的方法就是經過識別調用 API 的「觸發器」,也就是負責 Payload 遠程執行的 API 調用者。經過識別咱們能夠肯定代碼注入的完成過程以及某種程度上肯定了代碼注入的類型。其類型共分爲如下四種:
由 Karsten Hahn 製做的代碼注入圖形化過程[4]。
如上圖所示(圖片若加載失敗請前往 Github 倉庫查看原文),每個 API 觸發器都列在了 Execute 這一欄下,當其中任何一個觸發器被執行,Dreadnought 工具會當即將代碼轉儲,以後將識別代碼並匹配在此前假定的注入類型,這種方式和 UnRunPE 工具中處理傀儡進程的方式相似,但僅有這點是不夠的,由於每個觸發 API 的行爲均可能混淆了各類底層調用方法,最後仍舊能夠實現上圖中箭頭所指向的功能。
啓發式的邏輯算法將可以使咱們的 Dreadnought 工具更加精準地肯定代碼注入方法。所以在實際開發中,咱們使用了一種很是簡單的啓發式邏輯。從咱們的進程注入信息圖表上看,每一次當任何一個 API 被掛鉤時,該算法將會增長一個或者多個相關的代碼注入類型的權重並存儲在一個 map 數據結構裏。在它跟蹤每一個 API 的調用鏈時,它會嘗試偏向某一種注入類型。一旦 API 觸發器被觸發,它將會識別並把每個有關聯的注入類型的權重對比以後採起適應的措施。
本文旨在讓讀者對代碼注入及其與 WinAPI 的交互具備必定程度的技術理解。此外,在用戶領域監控 API 調用的概念也曾被惡意地利用來繞過反病毒檢測。下面是本文中有關 Dreadnought 工具在實際的使用狀況說明。
目前在理論上,Dreadnought 工具的這套檢測設計方式和啓發式算法確實足夠讓咱們向讀者演示並講述相關的原理知識,但在實際開發中卻不可能這麼理想。由於在咱們操做系統的常規操做中,有很是大的可能性存在那些被用來掛鉤的 API 的替代品。而這些能夠替代它們的行爲或者調用,咱們沒法分辨其是否爲惡意的,也就沒法檢測到它們是否參與了代碼注入。
由此看來,Dreadnought 工具以及它爲用戶領域提供的相關操做,在對抗過於複雜的惡意程序時並不理想,特別是能直接侵入到系統內核並與其進行交互的又或者是具備可以避開通常鉤子能力的惡意程序等等。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。