Donut v1.0 "Cruller" - ETW Bypasses, Module Overloading, and Much More

TLDR: Version v1.0 “Cruller” of Donut has been released, including Module Overloading for native PEs, ETW bypasses, a Dockerfile, support for binaries without relocation information, and many other minor improvements and bugfixes.

Introduction

_config.yml

Donut is a shellcode generation tool created to generate shellcode payloads from a variety of payload types including native PEs, .NET Assemblies, and scripts (JScript/VBScript).

Today, we are (finally) releasing version 1.0. This blog post details the changes made and discusses their potential use.

Release Notes

ETW Bypasses

The previous release of Donut included a modular system for bypassing AMSI and WLDP. Version 1.0 adds a system for bypassing Event Tracing for Windows (ETW). The default bypass is derived from research by XPN (Adam Chester). It patches the ntdll.dll!EtwEventWrite function to simply return, preventing it from writing any ETW events.

That bypass was published a while ago and others have since been published. In the same way as AMSI, you may add your own modular bypasses for ETW if you prefer to use a different technique. You may refer to the documentation and previous release blog post for details on that system.

Module Overloading

Module Overloading is a technique where a native PE executable is manually mapped into file-backed memory by overwriting a legitimate module on disk. This bypasses certain process injection detections that look for threads executing code in memory that is not mapped by a file on disk. The presumption is that, if code is being executed that does not originate from a file on disk, then it must have been dynamically allocated and that is suspicious. The detection does not work for processes running managed applications such as JIT-compiled languages like C#, Powershell, or Python. However, it is a fair detection for many native processes.

To accomplish this, we map a decoy module via NtCreateSection & NtMapViewOfSection, specifying a file handle and that the section should be mapped as an image. This maps a legitimate executable module into memory, which we then overwrite. We use the space in the memory section to map our payload PE file. At that point, our payload is executing from memory backed by a file on disk.

To use Module Overloading, specify the path of a decoy file using the -j argument. You must specify a decoy file with a size greater than or equal to your payload. As an example, here we run mimikatz from memory backed by dgbeng.dll.

_config.yml

Native PE Headers

In previous versions, Donut wiped all PE headers for native PE payloads. This provided some OpSec advantages, but broke some payloads that expected those PE headers to be there. Of particular note were payloads that contained PE Resources.

Donut now has two modes: By default it will mutate headers. But it may also be directed to preserve PE headers. This is necessary for any payload that references its embedded PE resources to function properly.

When header mutation is allowed and Module Overloading is being used, Donut will overwrite the headers of the payload with the headers of the decoy file on disk. This deters memory scanners (such as pe-sieve) that compares the headers of files in memory with details of the file on disk that backs that memory.

To demonstrate this, let’s use donut with Module Overloading enabled but header mutation turned off. As you can see, pe-sieve shows that one of the modules has been replaced. This is a detection created for Module Overloading.

_config.yml

However, now we run it again with header mutation enabled. Donut will, after mapping the payload PE into the decoy file’s memory, overwrite the payload’s PE headers with those of the decoy file. Now when pe-sieve compares the headers of the file in memory to its headers in the copy on disk, it will find no difference and not state that the module was replaced.

_config.yml

Relocation Information

Donut now appropriates handles relocation information of native PE payloads, or lack thereof. Previously, Donut assumed that payloads possessed relocation information. This will assist in loading some common red team tools such as Cobalt Strike.

Offset Execution

The previous version of Donut included an option -y that allowed the user to specify an absolute address. When the loader finished it would create a new thread at that address.

Based on user feedback, this has been modified to assume that the value provided is an offset of the base address of the main module (executable binary) of the host process. This will support third-party tools such as file infectors that wish to resume execution in the infected file after the donut payload is executed. It also may be used by process hollowers that wish to execute code in the host binary after donut executes.

Docker

Donut now has a Dockerfile (thanks @tijme). Using this docker image will make it easier to generate Donut shellcode when you are running on a platform (MacOS or less common *nix/BSD variants) that may not support all of the libraries or mechanisms that Donut uses. The provided Docker image uses Ubuntu 22.04 LTS and gcc, so it is limited to features available on those platforms.

Indefinite Blocking Option

There is now a third Exit Option. The Exit Options determine what donut does after executing the payload. Previously there were two options: 1) Exit the thread. 2) Exit the process. There is now 3) Do not exit or cleanup and block indefinitely.

This option supports some use cases where the native PE payload that donut is executing immediately creates a new thread. An example would be a C2 implant that is being executed from DllMain and then creates a new thread to initialize the implant. In such cases, the function that donut is calling may immediately return, causing donut to believe that the payload is finished and may be cleaned up in memory. This will cause problems when the payload is actually still running and does not want to be cleaned up; such as when a C2 implant intends to continue executing indefinitely.

This third option will cause donut to not cleanup the payload in memory and then simply sleep its own thread forever. In the case described above, the C2 implant would then be able to continue running indefinitely without being distracted by a donut.

Loader Improvements

LoadLibrary and GetProcAddress have been replaced with custom functions that will only call the Win32 API calls as fallbacks when absolutely necessary. This will significantly reduce the amount that those APIs are called but will not necessarily eliminate them entirely.

Output Generators

There are new output generators for C# and Python, as well as one for UUID Strings as based on this research.

Miscellaneous Changes

  • Native PE Section permissions are more accurate
  • Fixed some issues with the MingW makefile (#96)
  • Fixed and improved all makefiles
  • Added an X86 MSVC makefile (Makefile_x86.msvc) for ease of use
  • The Python module has been updated to the new version and some minor fixes and improvments have been made
  • Added an inject_local.exe that runs shellcode in the current process for testing purposes
  • NTHeaders->OptionalHeader->ImageBase is now updated correctly
  • Sections’ PhysicalAddress is now updated correctly
  • Much more detailed debugging output
  • Better wiping of data in memory to evade scanners
  • Improved error handling
  • Default AppDomain is now used when entropy is disabled
  • Updated reference to go-donut
  • Stack pointer is now correctly aligned
  • Support for HTTP Basic Authentication when using remote modules

Credits

I would like to thank the following people who contributed code or feedback or helped with testing for this release:

  • Kyle Willmon
  • S4ntiagoP
  • Awgh
  • tijme
  • physics-sec
  • Dewara
  • phra
  • checkymander
  • jarilaos
  • m4rvxpn
  • wumb0
  • loadeddypper
  • Checkymander
Written on December 13, 2022