Module 1: The PE-to-Shellcode Problem
Why you would want to convert a PE file into position-independent shellcode, and the fundamental challenges involved.
Module Objective
Understand what PE-to-shellcode conversion means, why offensive operators need it, how Donut fits into the landscape alongside tools like sRDI and ReflectiveDLLInjection, and the fundamental distinction between a loader and a converter.
1. What Is a PE File?
The Portable Executable (PE) format is the standard binary format on Windows. Every .exe, .dll, .sys, and .NET assembly is a PE file. When you compile a C program with MSVC or MinGW, the output is a PE file that the Windows loader (ntdll!LdrLoadDll) knows how to map into memory.
A PE file is not position-independent by default. It contains:
- Absolute addresses — code references assume a preferred base address (e.g.,
0x00400000) - Import tables — references to external DLLs and functions that must be resolved at load time
- Section alignment — sections have different file vs. memory alignment requirements
- Relocation entries — fixup tables needed when the image loads at a non-preferred base
Shellcode, by contrast, is a raw blob of machine code that can execute from any address in memory with no external dependencies. Converting a PE into shellcode means solving all of these problems in a self-contained way.
2. Why Convert PE to Shellcode?
Shellcode is the universal unit of code injection on Windows. Nearly every injection technique — VirtualAllocEx + WriteProcessMemory + CreateRemoteThread, APC injection, thread hijacking, fiber injection — expects a blob of position-independent code. If your payload is a PE file, you cannot directly inject it without a loader.
| Scenario | Why Shellcode? |
|---|---|
| Process injection | Injection primitives expect a function pointer to PIC code, not a structured PE |
| In-memory execution | Avoid dropping files to disk; execute payloads entirely in memory |
| .NET in unmanaged processes | Run C# assemblies in processes that don’t host the CLR, without spawning new processes |
| Staged payloads | Download shellcode over HTTP/DNS and execute without touching the filesystem |
| Exploit payloads | Exploits deliver shellcode; wrapping your tool as shellcode makes it exploit-deliverable |
| Loader diversity | Same shellcode works with any injection technique — decouple payload from delivery |
3. Loaders vs. Converters
Two fundamentally different approaches exist for running PE files in-memory without the Windows loader:
Reflective Loader (Embedded in the PE)
A reflective loader is compiled into the DLL itself. The DLL contains a special exported function (e.g., ReflectiveLoader) that, when called, maps itself into memory, resolves its own imports, and calls DllMain. The seminal work is Stephen Fewer’s Reflective DLL Injection.
Limitation: only works for DLLs you compile yourself. You must modify the source to include the reflective loader. Does not support EXEs, .NET assemblies, or third-party binaries.
Shellcode Converter (External Tool)
A converter takes an arbitrary PE file as input and produces standalone shellcode as output. The shellcode contains a PIC loader stub plus the original PE payload. At runtime, the stub performs all the steps the Windows loader would: map sections, resolve imports, apply relocations, and transfer control.
This is what Donut does. It supports EXEs, DLLs, .NET assemblies, VBS/JS scripts, and XSL files — without modifying the original binary.
Reflective Loader vs. Shellcode Converter
+ ReflectiveLoader()
Call export
Loader + Payload
4. The Donut Approach
Donut, created by TheWover and Odzhan, is a shellcode generation framework that takes the converter approach to its logical conclusion. It supports the widest range of input formats of any public tool:
| Input Type | How Donut Handles It |
|---|---|
| Native EXE (x86/x64) | PIC loader maps sections, resolves imports, applies relocations, calls entry point |
| Native DLL (x86/x64) | Same as EXE, plus calls DllMain and optionally a named export with arguments |
| .NET EXE | Hosts the CLR, creates an AppDomain, loads the assembly via Assembly::Load, invokes Main() |
| .NET DLL | Same CLR hosting, invokes a specified class method with arguments |
| VBScript / JScript | Creates scripting engine via COM, loads and executes the script in-memory |
| XSL files | Uses IXMLDOMDocument and IXSLProcessor COM interfaces to process the stylesheet |
5. sRDI — The Other Major Converter
Before Donut, the most popular shellcode converter was sRDI (Shellcode Reflective DLL Injection) by monoxgas. sRDI takes a DLL and wraps it with a PIC loader stub. Key differences from Donut:
| Feature | sRDI | Donut |
|---|---|---|
| Input types | DLL only | EXE, DLL, .NET, VBS, JS, XSL |
| Encryption | None (plaintext payload) | Chaskey cipher with random keys |
| Compression | None | aPLib, LZNT1, or Xpress |
| AMSI/ETW bypass | No | Built-in bypass stubs |
| .NET support | No | Full CLR hosting |
| Staging | No | HTTP/DNS staging support |
6. How Donut Shellcode Is Structured
The output shellcode from Donut has a layered structure:
Donut Shellcode Layout
~4-8 KB
Config + Keys
Encrypted + Compressed
Payload
- PIC Loader — position-independent C code compiled to resolve APIs via PEB walking, decrypt the instance, decompress the module, and dispatch to the correct handler based on payload type
- DONUT_INSTANCE — a configuration structure containing API hashes, decryption keys, module size, compression type, bypass flags, and other runtime parameters
- DONUT_MODULE — the encrypted (and optionally compressed) payload with metadata like class name, method name, and arguments
7. The Generation Pipeline
When you run donut -f payload.exe, the following steps occur:
Step-by-Step Generation
- Parse the input — identify file type (PE, .NET, VBS, JS, XSL), architecture, and characteristics
- Build DONUT_MODULE — serialize the payload with metadata (class, method, arguments, runtime version)
- Compress (optional) — compress the module using aPLib, LZNT1, or Xpress Huffman
- Generate random keys — create random Chaskey key, nonce, and counter for encryption
- Encrypt DONUT_MODULE — encrypt the compressed module with Chaskey in CTR mode
- Build DONUT_INSTANCE — populate with API hashes, keys, bypass flags, module metadata
- Encrypt DONUT_INSTANCE — encrypt the instance with a separate Chaskey key
- Concatenate — prepend the PIC loader, append the encrypted instance + module
- Output — write the final shellcode to
loader.bin
8. Course Roadmap
What Comes Next
| Module | Topic | Focus |
|---|---|---|
| 1 (this) | The PE-to-Shellcode Problem | Motivation and landscape |
| 2 | PE Loader Fundamentals | Sections, imports, relocations |
| 3 | .NET CLR Hosting | In-memory .NET execution |
| 4 | Module Architecture | DONUT_MODULE and DONUT_INSTANCE |
| 5 | The Donut Loader | PIC loader internals |
| 6 | Encryption & Anti-Detection | Chaskey, AMSI/WLDP/ETW bypass |
| 7 | Advanced Payload Types | COM-based script execution, exit options |
| 8 | Full Chain & Detection | Integration, YARA, forensics |
Knowledge Check
1. What is the primary advantage of a shellcode converter (like Donut) over a reflective loader?
2. Why can’t a standard PE file be directly injected as shellcode?
3. Which encryption cipher does Donut use to protect the payload?