donut: shellcode generation tool
Donut is a shellcode generation tool that creates position-independent shellcode payloads from .NET Assemblies. This shellcode may be used to inject the Assembly into arbitrary Windows processes. Given an arbitrary .NET Assembly, parameters, and an entry point (such as Program.Main), it produces position-independent shellcode that loads it from memory. The .NET Assembly can either be staged from a URL or stageless by being embedded directly in the shellcode. Either way, the .NET Assembly is encrypted with the Chaskey block cipher and a 128-bit randomly generated key. After the Assembly is loaded through the CLR, the original reference is erased from memory to deter memory scanners. The Assembly is loaded into a new Application Domain to allow for running Assemblies in disposable AppDomains.
It can be used in several ways.
Donut includes a bypass system for AMSI and other security features. Currently, we bypass:
- AMSI in .NET v4.8
- Device Guard policy preventing the dynamically generated code from executing
You may customize our bypasses or add your own. The bypass logic is defined in payload/bypass.c.
Each bypass implements the DisableAMSI function with the signature BOOL DisableAMSI(PDONUT_INSTANCE inst) and comes with a corresponding preprocessor directive. We have several #if defined blocks that check for definitions. Each block implements the same bypass function. For instance, our first bypass is called BYPASS_AMSI_A. If donut is built with that variable defined, then that bypass will be used.
Why does it this way? Because it means that only the bypass you are using is built into payload.exe. As a result, the others are not included in your shellcode. This reduces the size and complexity of your shellcode, adds modularity to the design, and ensures that scanners cannot find suspicious blocks in your shellcode that you are not actually using.
Another benefit of this design is that you may write your own AMSI bypass. To build Donut with your new bypass, use an if defined block for your bypass and modify the makefile to add an option that builds with the name of your bypass defined.
If you wanted to, you could extend our bypass system to add in other pre-execution logic that runs before your .NET Assembly is loaded.
Odzhan wrote a blog post on the details of our AMSI bypass research.
These are left as exercises to the reader. I would personally recommend:
- Add environmental keying
- Make donut polymorphic by obfuscating payload every time shellcode is generated
- Integrate donut as a module into your favorite RAT/C2 Framework
How it works
Donut uses the Unmanaged CLR Hosting API to load the Common Language Runtime. If necessary, the Assembly is downloaded into memory. Either way, it is decrypted using the Chaskey block cipher. Once the CLR is loaded into the host process, a new AppDomain will be created using a random name unless otherwise specified. Once the AppDomain is ready, the .NET Assembly is loaded through AppDomain.Load_3. Finally, the Entry Point specified by the user is invoked with any specified parameters.
The logic above describes how the shellcode generated by donut works. That logic is defined in payload.exe. To get the shellcode, exe2h extracts the compiled machine code from the .text segment in payload.exe and saves it as a C array to a C header file. donut combines the shellcode with a Donut Instance (a configuration for the shellcode) and a Donut Module (a structure containing the .NET assembly, class name, method name, and any parameters).
Copyright (c) 2019, TheWover, Odzhan. All rights reserved.