back

Rejuvenating Donut: what changes when every build is unique

2026.09

Donut, by TheWover and Odzhan, is the reference position-independent shellcode generator for Windows. It wraps a PE, .NET assembly, VBScript, or JScript into a self-contained blob that maps and runs the payload in memory, without the payload ever hitting disk. It has been open source since 2019, which means seven years of every AV, EDR, and YARA rule vendor writing signatures against its byte patterns.

Fritter is a fork that treats the signature surface itself as the primary bug. The rewrite is built around one axis Donut never claimed and the industry never had to defend against: the generator is polymorphic, the output is polymorphic, and the two polymorphism layers stack. No two fritter builds emit the same shellcode from the same input, and no two shellcodes from the same build emit the same bytes at the same offset.

This is x64-only, BSD-3, and the payload paths (PE mapping, .NET hosting, script execution) are TheWover and Odzhan's work, kept and respected. The changes are in the wrappers around those paths and in the runtime that keeps them out of memory.

Two layers of variation

Per-output randomization runs every time fritter is invoked. The entry stub, the polymorphic decoder, the encryption keys, and many structural elements in the generated shellcode are drawn from fresh entropy per PIC build.

Per-build randomization runs every time fritter itself is compiled. Cipher and hash rotation constants, API resolution table layout, shim-side string scrambling, PEB-walk directions, post-execution wipe patterns, and several structural axes inside the loader and shim are baked at compile time.

The distinction matters because a signature written against a Fritter binary someone else published defeats only Fritter binaries someone else published. Your own make invocation produces a generator whose outputs share none of those constants with anyone else's outputs. Pre-built releases in the repo carry a strongly-worded "build from source" warning for this reason: the pre-builts share per-build constants across every user of the same binary, and they are the least evasive artifact the project produces.

Two build-time tools drive it. tools/gen_poly emits include/poly_seed.h with the cipher constants, hash constants, round counts, salt values, wipe bytes, PEB direction picks, and string-scrambling keys for the build. tools/gen_api_shuffle reads a canonical API list (api_master.h, X-macro) and emits a Fisher-Yates shuffle of it, with slot 0 pinned. Both the typed function-pointer view and the hash array expand the same shuffled X-macro list, so the two views stay in lockstep by construction rather than by discipline. FRITTER_BUILD_SEED in the environment forces a deterministic seed for reproducible builds; leave it unset for a fresh randomization per make.

Function-granular dispatch

The v1.3 change is architectural. The earlier vectored-exception-handler sliding-window model covered the whole loader with one key and lived or died by a single VEH registration. It is gone.

Each protected loader function now has its own PE section, its own XOR key, its own dispatcher, and its own thunk. The dispatcher decrypts on entry, runs the function, and wipes on return. Only one function's bytes are in plaintext at any moment. There is no VEH handler to find and no context struct to scrub, because there is no VEH.

The MSVC build lays each hot routine (hash_cipher, maru, block_cipher, fritter_encrypt, MainProc, ansi2unicode, aP_depack) into its own named PE section via __declspec(code_seg). mingw emits a single .text and therefore falls back to N=1 dispatch (one entry, one key, functionally identical output, single dispatcher of polymorphism instead of many). MSVC is the recommended fast lane on Windows for this reason, and the Linux Makefile.linux cross-compiles the loader via mingw-w64 for developers who do not have Visual Studio.

The dispatcher itself is not a fixed byte pattern. Per build:

  • The two setup movs before the dispatcher call are emitted in either order.
  • r10 and r11 swap roles in the thunk-to-dispatcher handoff.
  • The nonvolatile push/pop order in each dispatcher's prologue and epilogue is Fisher-Yates

permuted.

  • The four dispatcher state roles (PTR, OFFSET, SIZE, KEY) are Fisher-Yates permuted across

{r12, r13, r14, r15}.

  • The per-function decrypt/encrypt loop varies across three axes: helper register, direction,

and loop shape, with junk-NOP gaps at four sites and 50% skip.

  • Alternative address-based loop termination is picked per build as a shape variant.
  • The fixed 4-entry NOP pool is replaced by emit_rnd_nop, which sources 0F 1F /r disp

bytes from CryptGenRandom, and true-random NOPs are inserted at every safe instruction boundary through the dispatcher's middle section (0 or 1 per site).

  • 0-63 bytes of true-random NOP padding precede each dispatcher, and the build-time marker

used to locate fn_table is scrambled after patching.

The -g / --chunked flag is retained for CLI compatibility and does nothing. The dispatch shim is always used.

Per-output polymorphism, module by module

Every module the shellcode is built from was audited and rewritten to eliminate its fixed byte patterns.

Entry stub. The previous EB <len> jump-over-junk pattern is gone; a junk fallthrough prefix draws from a multi-byte safe-instruction pool (NOPs, flag ops, register-form NOPs, xchg rax,rax) at random target length, so there is no fixed opcode at offset 0. The RSP alignment routine is generated per output rather than picked from three templates: save register from a pool, save form (mov vs lea) and restore form chosen independently, junk in prologue and epilogue, CALL displacement computed from the emitted epilogue size. The decoder shim trampoline is generative rather than a fixed 48 8D 15 ?? ?? ?? ?? E9 ?? ?? ?? ?? pattern.

Polymorphic XOR decoder. Key length is picked per output from a small set and propagated to the AND-mask immediate, the JMP-SHORT skip immediate, the trailing key byte count, and the host-side XOR encode mask, which collapses three previously-fixed anchors in one move. Movable instruction groups in the decode loop are emitted in one of three valid orderings. Zero-init opcode swap for the key-index initialization (XOR vs SUB). Leading junk before the push rcx so byte 0 is no longer always 0x51, and junk between pop rcx and the EB skip-jump to break the canonical 3-byte sequence.

Shim. Per-build string scrambling of the stack-built API/DLL strings, with the key forced into a range that avoids zero-byte collisions with printable ASCII. Per-build PEB list pick for DLL resolution. Salt-site placement no longer uses the volatile-temp-then-discard shape; XOR-cancel pairs sit against existing volatile struct fields and appear as immediate operands in real load/store sequences.

Loader. Per-build wipe byte for the post-execution memset of the instance (replacing zeroing). Per-build PEB walk direction, flink or blink. Structural salt sites in MainProc are XOR-cancel pairs against volatile mirrors of inst->len and inst->api_cnt, gated on per-build salt bits, so per-build immediate operands land in the live execution path. GET_PEB is hardcoded to a TEB-indirect variant instead of the canonical gs:[0x60].

Cleanup. Per-build wipe byte for the shim's loader-page wipe loop instead of zeroing. In the v1.2 architecture the g_ctx context struct was scrubbed with a per-build value pattern derived from the wipe byte, replicated to each field's width, with pointer fields scrubbing to non-zero. The context struct itself is gone in v1.3, since there is no VEH.

The NOP-fill problem

v1.2 exposed a signature the loader was carrying without noticing. When the extractor packed the loader into loader.bin, page-aligned __declspec(code_seg) sections were preserved in place with linker padding between them. The result was a blob where roughly 46% of the bytes were 0x90, with the longest contiguous run at 3750 bytes. A fixed 3750-byte NOP sled in every output is a YARA anchor a first-year analyst could write.

The fix is pack-on-extract in exe2h. The extractor walks every IMAGE_SCN_CNT_CODE section, brute-force searches permutations of section ordering for a layout where every single-page-marked hot section sits entirely within one 4096-byte page, then rewrites every cross-section CALL, JMP, Jcc, and LEA REL32 displacement via an in-tree x86-64 length disassembler (loader/exe2h/pack.h, ~330 lines). Adjacent sections concatenate with no gap. loader.bin dropped from ~25 KB to ~13.5 KB and the longest NOP run from 3750 to 1.

Single-section builds (the default mingw path) cannot use the packer, so the fallback fills inter-section gaps with deterministic-LCG random bytes instead of 0x90. The RVA layout is preserved, and the signature footprint disappears either way.

The MSVC vs mingw runtime cliff

MSVC-built shellcode was running an order of magnitude slower than expected in early v1.2 measurements, and the cause turned out to be structural. Hot loops in block_cipher and hash_cipher straddled a page boundary inside the loader's default .text. The VEH sliding window faulted on every loop iteration during instance decrypt and API hash, so the tool was paying a cross-page fault on the inner loop of the two hottest routines in the payload.

The per-function PE section system was v1.2's fix for MSVC: pinning each hot routine into its own single-page section eliminated the boundary, and the 1-page residual RWX exposure budget was preserved. v1.3's dispatch model retired the sliding window entirely, so the cross-page penalty is gone on MSVC.

mingw still runs an order of magnitude slower than MSVC on the bundled calc.exe target (seed-controlled: MSVC ~0.6s, mingw ~5-7s), and the reason is instructive. The per-function section system covers explicitly-tagged hash and cipher routines, but not the untagged PE-loader machinery (RunPE, LoadAssembly, and friends), and mingw's layout of that machinery places their hot loops on page boundaries where MSVC's does not. Both lanes produce OPSEC-equivalent output (no NOP fill on either side); the runtime cliff is a mingw codegen artifact, not a security property.

What is left on execution

Residual footprint after a shellcode run is one small RWX page where the dispatch shim ran. The mapped PE section is intentionally left intact in thread mode so CRT callbacks have continuations to return to; process-exit mode collapses everything. No VEH handler, no context struct, no fixed sled.

Cleanup is not a separate stage anymore, since each function wiped itself on return during execution.

Building it

# Linux, static-musl ELF, no runtime libc
make -f Makefile.linux release

# Windows, MSVC (recommended)
nmake -f Makefile.msvc

Each make runs gen_poly and gen_api_shuffle before any compile step, so the resulting fritter binary is unique per build. Every shellcode from that binary then shares those per-build constants and varies on the per-output axes.

FRITTER_BUILD_SEED=<u32> in the environment forces a specific seed for reproducible builds. __DATE__ and __TIME__ are still present in the orchestrator source, so full byte reproducibility also requires a fixed timezone or a cleanup pass that removes them.

What Fritter does not fix

The generator is polymorphic and the output is polymorphic. That is a defense against static signatures. It is not a defense against a scanner that runs the shellcode in a sandbox and watches what it does, and it is not a defense against a memory scanner that spots PAGE_EXECUTE_READWRITE on a private page during the dispatch window. The per-function dispatch model shrinks the window to one function at a time, but a window exists.

Compression fallback is deferred, so builds where the input is incompressible produce slightly larger shellcode than necessary. The volatile barrier on auto-storage-duration locals is not 100% reliable under -O1 with mingw-w64 GCC, and new salt-cancel sites should target variables that are used downstream to force a stack spill. Deterministic-seed smoke runs are the check for whether a latent fold sneaks through.

Where this sits

Donut made position-independent shellcode generation practical. Fritter's contribution is the observation that the generator itself was a signature surface and that a two-layer polymorphism model (per-build for the generator, per-output for the payload) closes it without touching the payload semantics Donut got right.

Everything here rides on TheWover and Odzhan's PE mapping, .NET hosting, and script execution paths. BSD-3, x64-only, and intended for authorized offensive-security use.