Donut免杀与进程空心化技术教学文档
一、Donut与Shellcode加载器免杀
1.1 技术原理概述
Donut是一款将PE可执行文件(如Cobalt Strike生成的payload)转换为加密Shellcode的工具。将其与自定义的C++加载器结合,能够有效绕过安全软件的静态特征检测。其核心思想是将恶意可执行文件转换为加密的二进制(.bin)格式,再由加载器动态加载执行,从而规避基于文件特征的查杀。
1.2 Donut工具核心参数与使用
Donut的命令行参数是实现有效转换的关键,以下是各参数详解与推荐配置:
| 参数 | 全称/含义 | 功能说明 | 实战推荐值 |
|---|---|---|---|
-i <路径> |
Input | 指定待转换的原始可执行文件(如.exe)。 |
必填,如-i OpenCalc.exe |
-a <架构> |
Architecture | 控制生成Shellcode的位数。 | -a 2 (生成纯amd64载荷,确保与64位C++加载器完全兼容) |
-o <文件名> |
Output | 自定义输出的.bin文件名。 |
可选,不指定则默认生成loader.bin |
-e <等级> |
Entropy | 控制载荷的加密与混淆强度。 | -e 3 (使用随机名称和强对称加密,极大破坏静态特征) |
-x <动作> |
Exit Behavior | 控制载荷运行结束后的退出行为。 | -x 1 (仅退出当前线程,宿主加载器进程保持存活) |
标准生成命令示例:
.\donut.exe -i OpenCalc.exe -a 2 -o my_shellcode.bin
1.3 Shellcode加载器(C++)实现详解
加载器(如示例中的ClickLoader.cpp)负责读取、解密并执行Donut生成的.bin文件。其执行流程与关键技术点如下:
-
文件读取:
- 硬编码或动态指定Shellcode文件名(如
my_shellcode.bin)。 - 以二进制模式(
"rb")打开文件,获取文件大小,并动态申请内存(malloc)将文件内容读入。
- 硬编码或动态指定Shellcode文件名(如
-
内存操作与权限控制:
- 使用
VirtualAlloc申请具有PAGE_READWRITE(RW) 权限的内存(exec_mem)。此阶段内存不可执行,符合正常操作模式,降低行为监控风险。 - 使用
RtlMoveMemory将Shellcode从堆内存复制到新申请的RW内存中。 - 关键安全操作:立即使用
SecureZeroMemory清除堆内存(shellcode)中的原始载荷残留,并使用free释放,减少内存中被检测的风险。
- 使用
-
权限翻转与执行:
- 使用
VirtualProtect将内存区域的权限从RW修改为PAGE_EXECUTE_READ(RX)。这是实现代码执行的关键步骤。 - 通过
CreateThread创建一个新线程,线程的入口点(LPTHREAD_START_ROUTINE)设置为包含Shellcode的内存地址(exec_mem),从而触发恶意代码执行。 - 使用
WaitForSingleObject等待线程执行完毕,最后用VirtualFree释放申请的内存。
- 使用
核心代码片段(简化逻辑):
// 1. 打开并读取bin文件
FILE *fp = fopen("my_shellcode.bin", "rb");
fread(shellcode, 1, file_size, fp);
fclose(fp);
// 2. 申请RW内存
LPVOID exec_mem = VirtualAlloc(NULL, file_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
RtlMoveMemory(exec_mem, shellcode, file_size);
// 3. 清除残留并翻转权限
SecureZeroMemory(shellcode, file_size);
free(shellcode);
VirtualProtect(exec_mem, file_size, PAGE_EXECUTE_READ, &old_protect);
// 4. 创建线程执行
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
1.4 免杀效果与局限性
- 效果:文档中测试显示,此方法制作的可执行文件(
ClickLoader.exe)在火绒安全软件下未被检出。 - 局限性:加载器进程(如
ClickLoader.exe)本身仍在进程列表中可见,存在一定的可追溯性。为解决此问题,需引入更高级的技术,如进程注入或进程空心化。
二、进程空心化 (Process Hollowing) 高级技术
2.1 技术原理
进程空心化是一种高级的进程注入技术。攻击者创建一个合法的、处于挂起(Suspended)状态的进程(如svchost.exe),然后“掏空”其内存空间,并替换为恶意代码。最终恢复该进程线程执行时,实际运行的是恶意载荷,从而实现了在合法进程外壳下的隐蔽执行。
2.2 完整实现流程与关键技术
以下流程基于文档中的优化代码(兼容32/64位)进行阐述。
步骤一:创建挂起的合法进程
使用CreateProcessA创建目标进程(如svchost.exe),并指定CREATE_SUSPENDED标志,使其主线程一创建即被挂起,为后续“掏空”做准备。
if (CreateProcessA(
(LPSTR)"C:\\Windows\\System32\\svchost.exe",
NULL, NULL, NULL, TRUE,
CREATE_SUSPENDED, // 关键标志:创建即挂起
NULL, NULL,
target_si, target_pi) == 0) {
// 错误处理
}
步骤二:获取并解析恶意PE文件
- 打开文件:使用
CreateFileA打开恶意可执行文件(如HellsGateLoader.exe),获取文件句柄。 - 读取到内存:使用
GetFileSize获取文件大小,用VirtualAlloc申请具有PAGE_READWRITE权限的本地内存,再用ReadFile将整个文件读入该内存缓冲区(pMaliciousImage)。 - 解析PE头:从内存中的PE文件获取关键信息:
PIMAGE_DOS_HEADER:通过e_magic验证DOS签名(MZ),通过e_lfanew定位NT头。PIMAGE_NT_HEADERS:验证Signature(PE\0\0),并从中获取两个关键数据:OptionalHeader.SizeOfImage:恶意映像加载到内存后所需的总大小。OptionalHeader.ImageBase:恶意映像的首选加载基址。OptionalHeader.AddressOfEntryPoint:恶意代码的入口点相对偏移(RVA)。
步骤三:获取目标进程基址与内存“掏空”
- 获取目标进程基址:
- 使用
GetThreadContext获取挂起线程的上下文(CONTEXT结构)。 - 在x64环境下,从上下文的
Rdx寄存器(指向PEB)或通过ReadProcessMemory读取PEB结构,找到ImageBaseAddress,即目标进程在内存中的基址(pTargetImageBaseAddress)。
- 使用
- “掏空”进程内存:
- 获取
ntdll.dll模块句柄(GetModuleHandleA("ntdll.dll"))。 - 获取
ZwUnmapViewOfSection函数地址。 - 调用
ZwUnmapViewOfSection(target_pi->hProcess, pTargetImageBaseAddress)。此NTAPI函数会解除目标进程指定基址处内存区域的映射,相当于“清空”了原合法进程的代码和数据,为填入恶意映像做准备。
- 获取
步骤四:在目标进程中分配内存并写入恶意PE
- 分配内存:在目标进程中,使用
VirtualAllocEx在被清空的原基址(pTargetImageBaseAddress)处,申请一块大小为SizeOfImage的内存,权限设置为PAGE_EXECUTE_READWRITE(RWX)。申请在原基址有助于避免重定位问题。pHollowAddress = VirtualAllocEx(target_pi->hProcess, pTargetImageBaseAddress, sizeOfMaliciousImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); - 写入PE头与节区:
- 写入头部:使用
WriteProcessMemory将恶意PE文件的头部(SizeOfHeaders)写入目标进程刚分配的内存起始处。 - 写入各节:遍历恶意PE文件的节表(
Section Table),计算每个节在文件中的偏移和大小,以及加载到内存后的虚拟地址(VA)。再次使用WriteProcessMemory,将每个节的数据写入目标进程内存的对应位置。
- 写入头部:使用
步骤五:修复入口点并恢复执行
- 修改线程上下文:再次获取或使用之前保存的线程上下文(
CONTEXT)。 - 设置新入口点:将上下文中指令指针寄存器(x64为
Rax,x86为Eax)的值修改为:目标进程分配的内存基址(pHollowAddress) + 恶意PE的入口点偏移(AddressOfEntryPoint)。这使得线程恢复执行时,将从恶意代码的入口开始。#ifdef _WIN64 c.Rax = (SIZE_T)((LPBYTE)pHollowAddress + pNTHeaders->OptionalHeader.AddressOfEntryPoint); #else c.Eax = (SIZE_T)((LPBYTE)pHollowAddress + pNTHeaders->OptionalHeader.AddressOfEntryPoint); #endif SetThreadContext(target_pi->hThread, &c); - 恢复线程:调用
ResumeThread(target_pi->hThread),使挂起的合法进程恢复运行,此时它将执行注入的恶意代码。
步骤六:资源清理
在CLEANUP标签下,关闭所有打开的句柄(CloseHandle)、释放本地申请的内存(VirtualFree),如果过程失败,可能需要终止创建的目标进程(TerminateProcess),避免留下悬空的挂起进程。
2.3 进程空心化的检测与预防思路
文档中提到,由于该技术隐蔽性强,检测难度较大,但可从以下角度着手:
-
行为分析:监控进程的异常行为,例如:
- 进程启动后立即被挂起。
- 进程内存区域被大量重写(
ZwUnmapViewOfSection+ 大量WriteProcessMemory)。 - 进程内存权限从初始状态变为RWX。
- 进程图像与磁盘文件不匹配。
-
内存取证:对比进程内存空间中的代码与磁盘上对应文件的代码是否一致。若内存中的可执行代码段与原始文件差异巨大,则可能被“空心化”。
-
API监控:挂钩(Hook)关键API调用序列,如
CreateProcess(带CREATE_SUSPENDED) ->ZwUnmapViewOfSection->VirtualAllocEx(在原基址) -> 多次WriteProcessMemory->SetThreadContext->ResumeThread,此序列具有高度指示性。
2.4 编译与实现注意
- 编译命令:需链接
ntdll.lib和kernel32.lib库。# 编译64位版本 g++ -m64 -o process_hollow.exe process_hollow.cpp -lntdll -lkernel32 -luser32 -static # 编译32位版本 g++ -m32 -o process_hollow.exe process_hollow.cpp -lntdll -lkernel32 -luser32 -static - 代码健壮性:文档提供的代码示例包含了错误处理、资源清理和32/64位兼容性判断,是实践中的重要参考。