Module 4: Donut Module Architecture
The internal data structures that define how Donut packages, encrypts, and configures payloads — DONUT_INSTANCE and DONUT_MODULE.
Module Objective
Understand the two core data structures at the heart of Donut: the DONUT_MODULE (which wraps the payload with metadata) and the DONUT_INSTANCE (which configures the loader with API hashes, keys, and runtime parameters). Learn how compression with aPLib and LZNT1/Xpress reduces shellcode size.
1. Two-Layer Architecture
Donut uses a two-layer data structure design. The outer layer (DONUT_INSTANCE) configures the loader itself — which APIs to resolve, what keys to use, where the module is. The inner layer (DONUT_MODULE) wraps the actual payload with execution metadata.
Data Structure Nesting
Encrypted (Key 1)
API hashes, config, Key 2
Encrypted (Key 2)
Compressed payload + metadata
The loader first decrypts DONUT_INSTANCE using a key derived from its position in the shellcode. This gives it the API hashes needed to resolve functions, plus the key to decrypt DONUT_MODULE. It then decrypts the module, decompresses the payload, and dispatches to the correct handler.
2. The DONUT_MODULE Structure
The DONUT_MODULE contains everything the loader needs to execute the payload. It is defined in donut.h:
Ctypedef struct _DONUT_MODULE {
DWORD type; // Module type: DLL, EXE, .NET, VBS, JS, XSL
DWORD thread; // Run payload in a new thread?
DWORD compress; // Compression engine used (aPLib, LZNT1, Xpress, None)
// .NET-specific fields
CHAR runtime[DONUT_MAX_NAME]; // CLR runtime version (e.g., "v4.0.30319")
CHAR domain[DONUT_MAX_NAME]; // AppDomain name (random or user-supplied)
CHAR cls[DONUT_MAX_NAME]; // Class name for .NET DLL (e.g., "Rubeus.Program")
CHAR method[DONUT_MAX_NAME]; // Method name (e.g., "Main")
// Native DLL-specific fields
CHAR function[DONUT_MAX_NAME]; // Export function to call (for native DLLs)
CHAR param[DONUT_MAX_NAME]; // Parameters / command-line arguments
// Unicode versions of the above (for COM/CLR interfaces)
WCHAR wRuntime[DONUT_MAX_NAME];
WCHAR wDomain[DONUT_MAX_NAME];
WCHAR wClass[DONUT_MAX_NAME];
WCHAR wMethod[DONUT_MAX_NAME];
// Payload data
DWORD mac; // MAC for integrity verification after decryption
DWORD zlen; // Compressed size of the payload
DWORD len; // Original uncompressed size
BYTE data[]; // The payload bytes (flexible array member)
} DONUT_MODULE;
| Field | Purpose | Used By |
|---|---|---|
type | Identifies the payload type, controls dispatch | All loaders |
runtime | CLR version string for .NET hosting | inmem_dotnet.c |
cls / method | Class and method name for .NET DLL invocation | inmem_dotnet.c |
function | Export name for native DLL call | inmem_pe.c |
param | Command-line arguments for the payload | All loaders |
compress | Compression algorithm indicator | Decompressor dispatch |
zlen / len | Compressed and original sizes for decompression | Decompressor |
data[] | The actual payload bytes (compressed + encrypted) | All loaders |
3. Module Type Constants
The type field determines which handler function the loader calls. Donut defines these module types:
C#define DONUT_MODULE_NET_DLL 1 // .NET DLL (requires class + method)
#define DONUT_MODULE_NET_EXE 2 // .NET EXE (has Main entry point)
#define DONUT_MODULE_DLL 3 // Native unmanaged DLL
#define DONUT_MODULE_EXE 4 // Native unmanaged EXE
#define DONUT_MODULE_VBS 5 // VBScript file
#define DONUT_MODULE_JS 6 // JScript file
#define DONUT_MODULE_XSL 7 // XSL file (with embedded script)
The loader uses a simple switch/dispatch based on this value to call the appropriate in-memory execution function: RunPE(), RunDotNET(), or RunScript().
4. The DONUT_INSTANCE Structure
The DONUT_INSTANCE is the loader’s runtime configuration. It contains everything the PIC loader needs to bootstrap itself:
Ctypedef struct _DONUT_INSTANCE {
DWORD len; // Total size of this instance
CHASKEY master; // Chaskey key for decrypting the module
// Chaskey cipher state for module decryption
BYTE mod_key[CIPHER_KEY_LEN]; // Module encryption key
BYTE mod_ctr[CIPHER_BLK_LEN]; // Module CTR nonce
DWORD mod_len; // Size of the encrypted module
ULONGLONG mod_offset; // Offset to DONUT_MODULE from start of instance
// Exit behavior
DWORD exit_opt; // DONUT_OPT_EXIT_THREAD, DONUT_OPT_EXIT_PROCESS, or DONUT_OPT_EXIT_BLOCK
// Entropy / evasion
DWORD entropy; // Entropy level (random names vs. fixed)
DWORD oep; // Original entry point (for PE payloads)
// AMSI/WLDP/ETW bypass configuration
DWORD bypass; // Bypass level (none, abort, continue)
// API hashing: pre-computed hashes for all needed functions
ULONGLONG hash[DONUT_MAX_API]; // Array of API name hashes
// Staging configuration
DWORD type; // Staging type (HTTP, DNS, embedded)
CHAR server[256]; // Staging server URL/hostname
CHAR modname[256]; // Module name on the staging server
// Instance encryption (decrypted by initial key derivation)
BYTE key[CIPHER_KEY_LEN]; // Instance-level Chaskey key
BYTE ctr[CIPHER_BLK_LEN]; // Instance-level CTR nonce
DWORD mac; // MAC for integrity verification
// Resolved API pointers (filled at runtime by the loader)
// These are populated after PEB walking resolves each hash
struct {
// kernel32.dll
LoadLibraryA_t LoadLibraryA;
GetProcAddress_t GetProcAddress;
VirtualAlloc_t VirtualAlloc;
VirtualFree_t VirtualFree;
VirtualProtect_t VirtualProtect;
// oleaut32.dll
SafeArrayCreate_t SafeArrayCreate;
SafeArrayAccessData_t SafeArrayAccessData;
SysAllocString_t SysAllocString;
// ole32.dll
CoInitializeEx_t CoInitializeEx;
CoCreateInstance_t CoCreateInstance;
// ... additional API function pointers
} api;
} DONUT_INSTANCE;
API Hash Table
The hash[] array contains pre-computed hashes of API function names (e.g., hash of "LoadLibraryA", hash of "VirtualAlloc"). The PIC loader walks the PEB, finds loaded DLLs, and compares export names against these hashes. When a match is found, the resolved address is stored in the corresponding api struct field. This avoids having plaintext API strings in the shellcode.
5. Payload Compression
Donut supports three compression engines to reduce shellcode size. Compression is applied to the raw payload bytes before encryption:
| Engine | Constant | Characteristics | Implementation |
|---|---|---|---|
| None | DONUT_COMPRESS_NONE | No compression; payload stored as-is | N/A |
| aPLib | DONUT_COMPRESS_APLIB | Small decompressor (~150 bytes PIC code), good ratio | depack.c (aPLib decompression) |
| LZNT1 | DONUT_COMPRESS_LZNT1 | Uses RtlDecompressBuffer from ntdll.dll | Windows native API |
| Xpress Huffman | DONUT_COMPRESS_XPRESS | Uses RtlDecompressBufferEx, better ratio than LZNT1 | Windows native API |
Compression Trade-offs
aPLib is self-contained — the decompressor is compiled into the PIC loader, so it works without resolving any APIs. This makes it the safest choice. LZNT1 and Xpress produce better compression ratios but require resolving RtlDecompressBuffer from ntdll.dll at runtime, adding API resolution overhead and a dependency on the ntdll export.
C// aPLib decompression in the PIC loader (depack.c)
// Extremely compact decompressor suitable for shellcode
DWORD aP_depack(const void *source, void *destination) {
const BYTE *src = (const BYTE*)source;
BYTE *dst = (BYTE*)destination;
DWORD tag = 0, bitcount = 0;
DWORD offs, len, R0 = ~0;
*dst++ = *src++; // First byte is literal
for (;;) {
// Bit-packed stream: literals, matches, and short matches
// Compact implementation ~150 bytes of x86 code
// ...
}
return (DWORD)(dst - (BYTE*)destination);
}
6. Staging: Embedded vs. Remote
The DONUT_MODULE can be embedded directly in the shellcode (default) or hosted on a remote server and downloaded at runtime:
Embedded (Default)
The encrypted module is appended directly after the DONUT_INSTANCE in the shellcode blob. The loader accesses it via the mod_offset field. This produces a single self-contained shellcode file.
HTTP Staging
The module is hosted on a web server. The DONUT_INSTANCE contains the URL (server field) and module name. At runtime, the loader uses WinINet APIs (InternetOpenA, InternetOpenUrlA, InternetReadFile) to download and decrypt the module. This keeps the initial shellcode small.
DNS Staging
The module is served via DNS TXT records. The loader issues DNS queries to retrieve the module data in chunks, reassembles it, and decrypts. This provides a covert download channel that may bypass network monitoring.
Embedded vs. Staged
Loader + Instance + Module
= single blob
Loader + Instance only
Module downloaded at runtime
Loader + Instance only
Module via DNS TXT
7. The DONUT_CONFIG Structure
At generation time (not runtime), Donut uses DONUT_CONFIG to collect user options before building the shellcode:
Ctypedef struct _DONUT_CONFIG {
DWORD arch; // Target architecture: x86, x64, or x86+x64
DWORD bypass; // AMSI/WLDP/ETW bypass level
DWORD compress; // Compression engine
DWORD entropy; // Entropy level for string randomization
DWORD format; // Output format: raw, base64, C array, Python, etc.
DWORD exit_opt; // Exit behavior: thread, process, or block
DWORD thread; // Create new thread for payload?
DWORD oep; // Overwrite PE entry point after execution
CHAR input[DONUT_MAX_NAME]; // Input file path
CHAR output[DONUT_MAX_NAME]; // Output file path
CHAR cls[DONUT_MAX_NAME]; // .NET class name
CHAR method[DONUT_MAX_NAME]; // .NET method name
CHAR param[DONUT_MAX_NAME]; // Parameters / arguments
CHAR server[256]; // Staging server URL
CHAR modname[256]; // Module name for staging
// Populated during generation
DWORD inst_len; // Instance size
DWORD mod_len; // Module size
LPVOID inst; // Pointer to generated instance
LPVOID mod; // Pointer to generated module
LPVOID pic; // Pointer to final PIC shellcode
DWORD pic_len; // Final shellcode size
} DONUT_CONFIG;
8. Data Flow: From Input to Output
Complete Data Flow
EXE/DLL/.NET/Script
+ metadata fields
aPLib / LZNT1
Chaskey (Key 2)
+ API hashes + Key 2
Chaskey (Key 1)
Final shellcode
Knowledge Check
1. What is the relationship between DONUT_INSTANCE and DONUT_MODULE?
2. Why is aPLib compression preferred over LZNT1 for shellcode payloads?
RtlDecompressBuffer from ntdll.dll, adding an API resolution dependency.3. What does the hash[] array in DONUT_INSTANCE contain?