To deal with some rainy Sunday depression I decided to investigate how Donut operates in memory and to see what traces it leaves (if any). I’ve been using Donut for quite some time, and I find it extremely useful from an operator’s perspective as it gives a lot of flexibility.

For those who don’t know what Donut is, we could define it as a software that allows the conversion from PE/.NET assemblies/scripts into position independent code (PIC, or shellcode). When I first read the initial release paper, it sounded like black magic to me; but after some time and experience I started realising how it works (at an high level, 90% still magic) it made more sense.

What also surprised me was the lack of technical defensive content on it. I can partially understand why; donut is meant to generate shellcode and most of the times the acts of injecting and executing the shellcode are the most “flagged” actions, not the shellcode itself. An example of such is the classic CreateRemoteThread function used to execute a shellcode we previously injected into another process’ memory; EDRs and AVs are able to detect that quite well (if we do not consider API unhooking). The reason behind this - apparently - is because it’s easier to put hooks and introspect API calls rather than scanning the memory of a process and look for anomalies.

However, it can happen that the injected shellcode leaves some traces in memory.


In-Memory PE

Under the hood, what donut does is “unpacking” the PE that was provided as input and executes it in-memory. The packed PE and the unpacking stub are shipped as a shellcode.

In order to better visualise this, we are going to execute donut ourself:

donut.exe mimikatz.exe

  [ Donut shellcode generator v0.9.3
  [ Copyright (c) 2019 TheWover, Odzhan

  [ Instance type : Embedded
  [ Module file   : ".\mimikatz.exe"
  [ Entropy       : Random names + Encryption
  [ File type     : EXE
  [ Target CPU    : x86+amd64
  [ AMSI/WDLP     : continue
  [ Shellcode     : "loader.bin"

I injected the loader.bin shellcode using UrbanBishop and after that the memory of the injected process was the following:

Note the RWX memory within the notepad’s memory. Isn’t it strange? Digging a bit deeper (which for me usually means just checking for strings) what I found was that it was indeed the Mimikatz PE:

Using the Moneta scanner we can see some IoCs for injected memory:

In fact, private memory marked as read/write/exec is suspicious. I validated this reading the source code of the inmem_pe.c file:

As it is possible to see, the memory allocated with VirtualAlloc is marked as PAGE_EXECUTE_READWRITE. This itself can be an IoC for detecting injected code (note that this issue is not donut specific!)

This is good from a memory forensic/hunting perspective but it must be noted that memory type cannot probably used alone for automatic detection. Usually, what might be flagged by an EDR’s memory scanner should include a weird allocation type and something else like the PE headers floating in-memory (bytes MZ).

To avoid that, donut wipes the PE headers after the loading is complete. We can observe similar features in Cobalt Strike post exploitation modules that use the obfuscate malleable PE option.

### AMSI Bypass

Another feature offered by Donut is the ability to bypass AMSI. As we know, .NET 4.8 introduced the ability to scan assemblies that are loaded via Assembly.Load from the reflection APIs and therefore if we are dealing with the injection of a .NET assembly it might be a sensible thing to do.

Donut bypasses AMSI from an unmanaged perspective, patching the AmsiScanBuffer function. This is the default option within Donut.

What I observed is that AMSI gets patched even if we are not trying to inject a .NET assembly. What does it mean from a detection perspective?

If we look at the bypass.c code, we can see the LoadLibraryA function that will load amsi.dll:

After amsi.dll is loaded, the Donut shellcode will patch its code. The detection from the Moneta scanner shows that:

From WinDbg we can confirm that amsi was indeed patched. Look at the xor eax,eax and ret instructions:

The value of eax = 0 indicates the result of the AmsiScanBuffer equals to AMSI_RESULT_CLEAN.

So what is strange with this? If we inject a shellcode generated using Donut with the default bypass options into a process that usually does not have amsi.dll loaded, Donut will load and bypass it.

An example of an anomalous AMSI load event is shown below:

The event is generated from Sysmon with a modified configuration that logs AMSI load events. The base configuration is the classic SwiftOnSecurity’s:

		<!--COMMENT:	Can cause high system load, disabled by default.-->
		<!--COMMENT:	[ ] [ ] [ ] -->

		<!--DATA: UtcTime, ProcessGuid, ProcessId, Image, ImageLoaded, Hashes, Signed, Signature, SignatureStatus-->
	<RuleGroup name="" groupRelation="or">
		<ImageLoad onmatch="include">
			<ImageLoaded condition="contains">amsi</ImageLoaded>
			<!--NOTE: Using "include" with no rules means nothing in this section will be logged-->

This is an interesting detection for Donut’s default options, but for an operator it would be trivial to modify this behaviour and avoid bypassing AMSI.