Donut免杀以及进程空心化分析
字数 4067
更新时间 2026-03-24 14:01:04

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文件。其执行流程与关键技术点如下:

  1. 文件读取

    • 硬编码或动态指定Shellcode文件名(如my_shellcode.bin)。
    • 以二进制模式("rb")打开文件,获取文件大小,并动态申请内存(malloc)将文件内容读入。
  2. 内存操作与权限控制

    • 使用VirtualAlloc申请具有PAGE_READWRITE(RW) 权限的内存(exec_mem)。此阶段内存不可执行,符合正常操作模式,降低行为监控风险。
    • 使用RtlMoveMemory将Shellcode从堆内存复制到新申请的RW内存中。
    • 关键安全操作:立即使用SecureZeroMemory清除堆内存(shellcode)中的原始载荷残留,并使用free释放,减少内存中被检测的风险。
  3. 权限翻转与执行

    • 使用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文件

  1. 打开文件:使用CreateFileA打开恶意可执行文件(如HellsGateLoader.exe),获取文件句柄。
  2. 读取到内存:使用GetFileSize获取文件大小,用VirtualAlloc申请具有PAGE_READWRITE权限的本地内存,再用ReadFile将整个文件读入该内存缓冲区(pMaliciousImage)。
  3. 解析PE头:从内存中的PE文件获取关键信息:
    • PIMAGE_DOS_HEADER:通过e_magic验证DOS签名(MZ),通过e_lfanew定位NT头。
    • PIMAGE_NT_HEADERS:验证SignaturePE\0\0),并从中获取两个关键数据:
      • OptionalHeader.SizeOfImage:恶意映像加载到内存后所需的总大小。
      • OptionalHeader.ImageBase:恶意映像的首选加载基址。
      • OptionalHeader.AddressOfEntryPoint:恶意代码的入口点相对偏移(RVA)。

步骤三:获取目标进程基址与内存“掏空”

  1. 获取目标进程基址
    • 使用GetThreadContext获取挂起线程的上下文(CONTEXT结构)。
    • 在x64环境下,从上下文的Rdx寄存器(指向PEB)或通过ReadProcessMemory读取PEB结构,找到ImageBaseAddress,即目标进程在内存中的基址(pTargetImageBaseAddress)。
  2. “掏空”进程内存
    • 获取ntdll.dll模块句柄(GetModuleHandleA("ntdll.dll"))。
    • 获取ZwUnmapViewOfSection函数地址。
    • 调用ZwUnmapViewOfSection(target_pi->hProcess, pTargetImageBaseAddress)。此NTAPI函数会解除目标进程指定基址处内存区域的映射,相当于“清空”了原合法进程的代码和数据,为填入恶意映像做准备。

步骤四:在目标进程中分配内存并写入恶意PE

  1. 分配内存:在目标进程中,使用VirtualAllocEx被清空的原基址pTargetImageBaseAddress)处,申请一块大小为SizeOfImage的内存,权限设置为PAGE_EXECUTE_READWRITE(RWX)。申请在原基址有助于避免重定位问题。
    pHollowAddress = VirtualAllocEx(target_pi->hProcess, pTargetImageBaseAddress, sizeOfMaliciousImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    
  2. 写入PE头与节区
    • 写入头部:使用WriteProcessMemory将恶意PE文件的头部(SizeOfHeaders)写入目标进程刚分配的内存起始处。
    • 写入各节:遍历恶意PE文件的节表(Section Table),计算每个节在文件中的偏移和大小,以及加载到内存后的虚拟地址(VA)。再次使用WriteProcessMemory,将每个节的数据写入目标进程内存的对应位置。

步骤五:修复入口点并恢复执行

  1. 修改线程上下文:再次获取或使用之前保存的线程上下文(CONTEXT)。
  2. 设置新入口点:将上下文中指令指针寄存器(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);
    
  3. 恢复线程:调用ResumeThread(target_pi->hThread),使挂起的合法进程恢复运行,此时它将执行注入的恶意代码。

步骤六:资源清理

CLEANUP标签下,关闭所有打开的句柄(CloseHandle)、释放本地申请的内存(VirtualFree),如果过程失败,可能需要终止创建的目标进程(TerminateProcess),避免留下悬空的挂起进程。

2.3 进程空心化的检测与预防思路

文档中提到,由于该技术隐蔽性强,检测难度较大,但可从以下角度着手:

  1. 行为分析:监控进程的异常行为,例如:

    • 进程启动后立即被挂起。
    • 进程内存区域被大量重写(ZwUnmapViewOfSection + 大量WriteProcessMemory)。
    • 进程内存权限从初始状态变为RWX。
    • 进程图像与磁盘文件不匹配。
  2. 内存取证:对比进程内存空间中的代码与磁盘上对应文件的代码是否一致。若内存中的可执行代码段与原始文件差异巨大,则可能被“空心化”。

  3. API监控:挂钩(Hook)关键API调用序列,如CreateProcess(带CREATE_SUSPENDED) -> ZwUnmapViewOfSection -> VirtualAllocEx(在原基址) -> 多次WriteProcessMemory -> SetThreadContext -> ResumeThread,此序列具有高度指示性。

2.4 编译与实现注意

  • 编译命令:需链接ntdll.libkernel32.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位兼容性判断,是实践中的重要参考。
相似文章
相似文章
 全屏