Skip to content

Zuigetzu/Donut-CustomHost

Repository files navigation

Alt text

Current version: v1.2

Table of contents

  1. Key Differences
  2. Usage
  3. Using Donut as a Library (Fixed & Ready-to-Use Examples)
  4. Disclaimer
  5. Acknowledgments / Credits

1. Key Differences

Advanced OPSEC Features (Fork Exclusives)

To maximize stealth and evade modern EDRs, this specific fork implements several advanced techniques not found in the original branch:

  • Custom CLR Host: By providing custom in-memory implementations of IHostAssemblyManager, IHostAssemblyStore, and IHostMemoryManager, Donut successfully intercepts the assembly loading process (AppDomain.Load_2). Instead of relying on the default Fusion loader (which leaves tracking artifacts and may drop files to disk), our custom Assembly Store provides the payload directly from memory as an IStream matching the exact Fully Qualified Name (FQN).
  • Memory Tracking Evasion: The custom Memory Manager acts as a mirage, returning S_OK to blind EDR heuristics looking for malicious memory tracking events.
  • Architecture-Aware ETW Bypass: We have implemented an advanced Tail Jump technique. Instead of blindly patching NtTraceEvent with a standard RET instruction—which can corrupt the stack on x86/WoW64 environments (where stdcall actually requires RET 10h)—the loader dynamically scans the function for its legitimate return instruction. It then patches the entry point with a relative JMP (0xE9) pointing directly to that native return. This ensures perfect stack alignment and stability across both x64 and x86 architectures, silently dropping all ETW events without triggering process crashes.
  • AMSI/WLDP Patching Removal: Traditional AMSI patching requires modifying memory protections (VirtualProtect) on native DLLs, an action highly monitored by modern EDRs. Since our Custom CLR Host inherently evades the telemetry that triggers these scans, we deliberately removed the default AMSI patches to maintain a pristine, untampered memory footprint.
  • Shellcode Size Optimization (Padding Bug Fix): The original generator had a memory allocation flaw in donut.c. It calculated the payload size by adding sizeof(DONUT_INSTANCE) to c->mod_len. Because DONUT_INSTANCE contains a DONUT_MODULE union, the module structure (~1.3 KB) was allocated twice, leaving dead memory (null bytes) at the end of the final .bin. We fixed this by dynamically calculating the base size with offsetof(DONUT_INSTANCE, module), resulting in a 100% space-efficient payload with zero bloat.

2. Usage

The following table lists switches supported by the command line version of the generator.

Switch Argument Description
-a arch Target architecture for loader : 1=x86, 2=amd64, 3=x86+amd64(default).
-b level Behavior for bypassing AMSI/WLDP : 1=None, 2=Abort on fail, 3=Continue on fail.(default)
-k headers Preserve PE headers. 1=Overwrite (default), 2=Keep all
-j decoy Optional path of decoy module for Module Overloading.
-c class Optional class name. (required for .NET DLL) Can also include namespace: e.g namespace.class
-d name AppDomain name to create for .NET. If entropy is enabled, one will be generated randomly.
-e level Entropy level. 1=None, 2=Generate random names, 3=Generate random names + use symmetric encryption (default)
-f format The output format of loader saved to file. 1=Binary (default), 2=Base64, 3=C, 4=Ruby, 5=Python, 6=PowerShell, 7=C#, 8=Hexadecimal
-m name Optional method or function for DLL. (a method is required for .NET DLL)
-n name Module name for HTTP staging. If entropy is enabled, one is generated randomly.
-o path Specifies where Donut should save the loader. Default is "loader.bin" in the current directory.
-p parameters Optional parameters/command line inside quotations for DLL method/function or EXE.
-r version CLR runtime version. MetaHeader used by default or v4.0.30319 if none available.
-s server URL for the HTTP server that will host a Donut module. Credentials may be provided in the following format:
https://username:password@192.168.0.1/
-t Run the entrypoint of an unmanaged/native EXE as a thread and wait for thread to end.
-w Command line is passed to unmanaged DLL function in UNICODE format. (default is ANSI)
-x option Determines how the loader should exit. 1=exit thread (default), 2=exit process, 3=Do not exit or cleanup and block indefinitely
-y addr Creates a new thread for the loader and continues execution at an address that is an offset relative to the host process's executable. The value provided is the offset. This option supports loaders that wish to resume execution of the host process after donut completes execution.
-z engine Pack/Compress the input file. 1=None, 2=aPLib, 3=LZNT1, 4=Xpress, 5=Xpress Huffman. Currently, the last three are only supported on Windows.

Payload Requirements

There are some specific requirements that your payload must meet in order for Donut to successfully load it.

.NET Assemblies

  • The entry point method must only take strings as arguments, or take no arguments.
  • The entry point method must be marked as public and static.
  • The class containing the entry point method must be marked as public.
  • The Assembly must NOT be a Mixed Assembly (contain both managed and native code).
  • As such, the Assembly must NOT contain any Unmanaged Exports.

Native EXE/DLL

  • Binaries built with Cygwin are unsupported.

Cygwin executables use initialization routines that expect the host process to be running from disk. If executing from memory, the host process will likely crash.

Unmanaged DLLs

  • A user-specified entry point method must only take a string as an argument, or take no arguments. We have provided an example.

3. Using Donut as a Library (Fixed & Ready-to-Use Examples)

In the original repository, using Donut as a library (donut.lib / donut.dll / donut.a) often resulted in compilation and linking errors. This fork explicitly fixes those issues, allowing seamless integration into your custom droppers and C2 tooling.

We have provided ready-to-compile examples inside the lib/ directory of this repository for C, Go, and C#.

1. C/C++ Example (lib/main.c)

Inside the lib folder, you will find main.c and a dedicated Makefile. To build the example tool using the static/dynamic library, simply navigate to lib/ and run nmake (or make):

// Snippet from lib/main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "donut.h"

int main(int argc, char *argv[]) {
    DONUT_CONFIG config;
    int err;

    // ... [Initialization] ...
    
    // Configure parameters
    config.inst_type = DONUT_INSTANCE_EMBED;
    config.arch      = DONUT_ARCH_X84;
    config.bypass    = DONUT_BYPASS_CONTINUE;
    config.headers   = DONUT_HEADERS_OVERWRITE;
    config.format    = DONUT_FORMAT_BINARY;
    config.compress  = DONUT_COMPRESS_NONE;
    config.entropy   = DONUT_ENTROPY_DEFAULT;
    config.exit_opt  = DONUT_OPT_EXIT_THREAD;
    config.unicode   = 0;

    err = DonutCreate(&config);

    if (err == DONUT_ERROR_OK) {
        printf("[+] Shellcode successfully generated at: %s\n", config.output);
    } else {
        printf("[-] Error generating shellcode: %s\n", DonutError(err));
    }

    DonutDelete(&config);
    return 0;
}

2. Golang Example (lib/main.go)

Go can natively interop with Donut through CGO. The main.go file inside lib/ is fully configured with the appropriate CFLAGS and LDFLAGS to link against the fixed library.

// Snippet from lib/main.go
package main

/*
// We tell CGO where to look for the headers (donut.h)
#cgo CFLAGS: -I../include
// We tell CGO which static library to link
#cgo LDFLAGS: -L${SRCDIR} -ldonut

#include <stdlib.h>
#include <string.h>
#include "donut.h"
*/
import "C"
import (
	"fmt"
	"os"
	"unsafe"
)

func main() {
    // ... [Initialization & String conversions] ...

	config.arch = C.DONUT_ARCH_X84
	config.bypass = C.DONUT_BYPASS_CONTINUE
	config.format = C.DONUT_FORMAT_BINARY
	// ... [Other Configs] ...

	err := C.DonutCreate(&config)

	if err == C.DONUT_ERROR_OK {
		outName := C.GoString((*C.char)(unsafe.Pointer(&config.output[0])))
		fmt.Printf("[+] Shellcode successfully generated at: %s\n", outName)
	} else {
		errMsg := C.GoString(C.DonutError(err))
		fmt.Printf("[-] Donut Error. Code: %d - %s\n", err, errMsg)
	}

	C.DonutDelete(&config)
}

3. C# Example via P/Invoke (lib/DonutCsharp/)

When compiling a C# tool that interacts with donut.dll via P/Invoke, a common issue is having to distribute the unmanaged DLL alongside your .exe.

Inside lib/DonutCsharp/ you will find a complete Visual Studio Solution already configured with the Costura.Fody NuGet package. Costura automatically embeds the unmanaged donut.dll (for both x86 and x64) statically inside your managed assembly. You just need to open the .sln and compile. The output will be a single, standalone executable ready for your operations.

// Snippet from lib/DonutCsharp/Program.cs
using System;
using System.Runtime.InteropServices;

namespace DonutCsharp
{
    // ... [Structures and Constants Mapping] ...

    class Program
    {
        [DllImport("donut.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern int DonutCreate(ref DONUT_CONFIG config);

        [DllImport("donut.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern int DonutDelete(ref DONUT_CONFIG config);

        [DllImport("donut.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr DonutError(int err);

        static void Main(string[] args)
        {
            DONUT_CONFIG config = new DONUT_CONFIG();

            config.input = args[0];
            config.output = args.Length >= 2 ? args[1] : "payload.bin";
            config.arch = DonutConstants.DONUT_ARCH_X84;
            // ... [Other Configs] ...

            int err = DonutCreate(ref config);

            if (err == DonutConstants.DONUT_ERROR_SUCCESS)
            {
                Console.WriteLine($"[+] Shellcode successfully generated at: {config.output}");
            }
            else
            {
                IntPtr errorPtr = DonutError(err);
                string errorMessage = Marshal.PtrToStringAnsi(errorPtr);
                Console.WriteLine($"[-] Donut Error: {errorMessage}");
            }

            DonutDelete(ref config);
        }
    }
}

4. Disclaimer

We are not responsible for any misuse of this software or technique. Donut is provided as a demonstration of CLR Injection and in-memory loading through shellcode in order to provide red teamers a way to emulate adversaries and defenders a frame of reference for building analytics and mitigations. This inevitably runs the risk of malware authors and threat actors misusing it. However, we believe that the net benefit outweighs the risk. Hopefully that is correct. In the event EDR or AV products are capable of detecting Donut via signatures or behavioral patterns, we will not update Donut to counter signatures or detection methods. To avoid being offended, please do not ask.

5. Acknowledgments / Credits

This fork's Custom CLR Host implementation was heavily inspired by the excellent research and Proof of Concept provided by the IBM X-Force Red team. Huge thanks to the creator of Being-A-Good-CLR-Host for paving the way on stealthy, Fusion-less .NET execution.

We also thank the original creator of Donut, TheWover, for building the foundational framework that made this OPSEC evolution possible.

About

Advanced OPSEC fork of Donut. Features a Custom in-memory CLR Host, Tail-Jump ETW bypasses, and zero-patch AMSI evasion for stealthy shellcode generation.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors