donut v0.9.3 releases: 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).
- The -e switch can be used to disable entropy and/or encryption. Options are: 1=none, 2=generate random names, 3=generate random names + use symmetric encryption.
- The -z switch tells the builder to compress the input file. 1=none, 2=aPLib. On Windows, a further three algorithms are supported, which are 3=LZNT1, 4=Xpress and 5=Xpress Huffman.
- The -f switch specifies the output format for loader. 1=binary, 2=base64, 3=c, 4=ruby, 5=python, 6=powershell, 7=c# and 8=hex. On Windows, Base64 strings are copied to the clipboard.
- The -t switch tells the loader to run unmanaged entrypoint for EXE as a thread. This also attempts to intercept exit-related API in Import Address Table by replacing their pointers with the address of RtlExitUserThread.
- The -n switch can be used to specify name of module for HTTP staging. If entropy is enabled, this is generated randomly.
- The -s switch specifies the HTTP server to download module from.
- The -y switch tells loader to create a new thread for the loader and continues executing at a specific address or Original Entry Point (OEP). The address should be provided as a string in hexadecimal format.
- The -x switch can be used to specify how loader terminates. 1=exit thread, 2=exit process.
- The -p switch is used to specify parameters to .NET method, DLL function or command line for an unmanaged EXE file. Wrap multiple parameters inside quotations.
- The -w switch tells the loader to convert parameters to UNICODE before passing to unmanaged DLL function.
- C# generator by n1xbyte: https://github.com/n1xbyte/donutCS
- Go generator by awgh https://github.com/Binject/go-donut
- Command line is no longer parsed using semi-colon or comma as a token. The -p switch now accepts a string with all parameters enclosed in quotation marks. For .NET DLL/EXE, these are separated by the loader using CommandLineToArgvW. For unmanaged DLL, the string is passed to the DLL function without any modification.
- The -u switch to specify URL for HTTP stager is replaced with -s switch to prepare for a DNS stager.
- The -f switch to specify input file is now used to specify output format of loader.
- XSL files are no longer supported.
- Code stub for calling DLL function with multiple arguments.
Copyright (c) 2019, TheWover, Odzhan. All rights reserved.