.NET RAT Unpacking & C2 Protocol Extraction
Static and dynamic analysis of an obfuscated .NET RAT - deobfuscation, behavioral analysis, and YARA signatures. Covers ConfuserEx unpacking, dnSpy/dnSpyEx workflow, anti-analysis defeat, custom protocol reverse-engineering, and detection authoring.
Why .NET RATs Deserve Their Own Workflow
Commodity .NET Remote Access Trojans (AsyncRAT, NanoCore, Quasar, Remcos, NjRAT - and the endless rebrands) make up a disproportionate share of phishing-delivered malware. There are three reasons for that popularity from the operator side:
- Trivial development. Visual Studio + a handful of NuGet packages produces a working backdoor in an afternoon.
- Cheap obfuscation. ConfuserEx, .NET Reactor, SmartAssembly, and “crypters” sold on forums each provide automated control-flow flattening, string encryption, and packing.
- Universal target. Every Windows host post-Win7 ships .NET; payload size is small; the surface for AV to fingerprint is messy because of the obfuscator’s heavy mutation.
For analysts the silver lining is symmetric: .NET assemblies decompile to readable C# (no instruction-by-instruction reverse-engineering required), and the obfuscation layers are generic enough that automated unpackers usually succeed. This post walks the canonical workflow on a representative ConfuserEx-packed RAT.
Triage
The sample was a ConfuserEx-obfuscated .NET assembly disguised as an invoice PDF. Cleaned with de4dot, then analyzed in dnSpy.
Initial Identification
file invoice.exe
# PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows
# DIE / Detect-It-Easy fingerprints the protector
die invoice.exe
# Protector: ConfuserEx (1.0.0)
# Compiler: VB.NET / C# (.NET v4.0.30319)
A few quick static signals:
- High entropy in
#Stringsheap → string encryption. - Method bodies of trivial helpers full of
switchblocks → control-flow flattening (CFF). - Custom
<Module>.cctorof unusual size → ConfuserEx’s runtime-decryption module initializer. - References to
System.Reflection.Emit,Marshal.GetDelegateForFunctionPointer→ runtime IL emission, common in packed .NET.
Defeating the Obfuscator
de4dot is the de-facto first pass:
de4dot invoice.exe -o invoice.cleaned.exe
# CleanedAssembly: invoice.cleaned.exe
# Detected ConfuserEx 1.0.0
# Removing string encryption: 1184 strings decrypted
# Removing control flow obfuscation: 312 methods cleaned
# Removing proxy method calls: 487 calls inlined
# Removing constants encryption: 89 constants decrypted
If de4dot doesn’t recognise the variant (newer or modified ConfuserEx forks ship), workflow:
- Try the de4dot-cex fork, which targets recent ConfuserEx 2 / “Custom” protections.
- Failing that, dump runtime IL: launch the binary under ILSpy debug mode or
dnSpyExwith breakpoints onModule.GetMethod/MethodBase.Invoke, dump the in-memoryModuleto disk after the cctor unpacks itself. The “DnSpyEx → File → Save Module…” flow produces a clean unpacked DLL. - For the most heavily packed multi-stage samples, use .NET ExtractEx or Stage1’s loader hooks to capture every dynamically-emitted assembly.
dnSpy / dnSpyEx Workflow
Open the cleaned binary in dnSpyEx:
- Module → Sort Members brings the class tree to a recognisable order.
- Edit → Find with regex-aware mode for strings like
https://,Pong,keylog,cmd.exe, IP-like addresses. - Analyzer pane → right-click any method → “Analyze” → backtrack callers/callees. Critical for finding the entry-point dispatcher of the C2 command loop.
- Debug → Start under dnSpy’s debugger to single-step through the runtime decryption routines and capture in-memory state.
- Breakpoints on
System.Net.Sockets.TcpClient.ConnectandSystem.Net.WebRequest.GetResponsereveal the C2 endpoint live.
Anti-Analysis
The malware checks for VMs (WMI queries), debugger attachment, and sandbox artifacts before executing.
The full anti-analysis chain on this kind of sample typically chains:
// Pseudocode after de4dot cleanup
private static bool ShouldExecute()
{
// VM checks via WMI
foreach (ManagementObject mo in new ManagementObjectSearcher(
"SELECT Manufacturer, Model FROM Win32_ComputerSystem").Get())
{
var manuf = mo["Manufacturer"]?.ToString() ?? "";
var model = mo["Model"]?.ToString() ?? "";
if (manuf.Contains("VMware") || manuf.Contains("innotek") ||
model.Contains("Virtual"))
return false;
}
// Debugger
if (Debugger.IsAttached || Debugger.IsLogging())
return false;
// Process / driver names
foreach (var p in Process.GetProcesses())
{
var n = p.ProcessName.ToLower();
if (n.Contains("vmtoolsd") || n.Contains("vboxservice") ||
n.Contains("wireshark") || n.Contains("procmon") ||
n.Contains("dnspy") || n.Contains("ida"))
return false;
}
// Username / hostname blacklist
var u = Environment.UserName.ToLower();
var h = Environment.MachineName.ToLower();
string[] black = { "sandbox", "analyst", "malware", "test", "lab" };
foreach (var s in black) if (u.Contains(s) || h.Contains(s)) return false;
// Region / timezone gating
if (CultureInfo.CurrentCulture.Name.StartsWith("ru") ||
CultureInfo.CurrentCulture.Name.StartsWith("uk"))
return false; // Russia/Ukraine exclusion - ironic CIS exclusion
return true;
}
Patching this for analysis is mechanical - set every return false; to return true; in dnSpy’s IL editor, save, and the sample will run inside your VM. Even cleaner: NOP out the call to ShouldExecute and force the post-check flow.
Mutex Singleton Check
Almost every commodity RAT uses a named mutex to prevent multiple instances:
new Mutex(true, "Global\\AsyncRAT_<random>", out bool createdNew);
if (!createdNew) Environment.Exit(0);
The mutex name is a stable IOC and a perfect host-based detection signature.
C2 Communication
HTTPS-based custom protocol beaconing every 30 seconds. Supported commands: shell, upload, download, screenshot, keylogger, and self-destruct.
Reverse-Engineering the Protocol
The full set of supported commands lives in a single dispatch method that switches on a string or integer command code received from the C2. Find it by tracing back from the network read primitive:
private void ProcessCommand(byte[] payload)
{
var msg = Encoding.UTF8.GetString(Decrypt(payload, this.aesKey));
var parts = msg.Split('|');
switch (parts[0])
{
case "ping": Send("pong"); break;
case "shell": RunShell(parts[1]); break;
case "upload": UploadFile(parts[1]); break;
case "download": DownloadFile(parts[1], parts[2]); break;
case "screen": SendScreenshot(); break;
case "keylog": StartKeylogger(); break;
case "kill": SelfDestruct(); break;
}
}
Each branch becomes a YARA string and a behavioural detection point.
Cryptographic Layer
Most commodity RATs encrypt C2 traffic with AES-CBC or AES-GCM, key derived from a hardcoded passphrase via PBKDF2 / Rfc2898DeriveBytes with a fixed salt. To extract the key:
- Find the constructor where
Aes.Create()is called. - Trace the
Key/IVassignments back to their source - usually aConvert.FromBase64String("...")or a derivation from a hardcoded UTF-8 string. - With
key + ivyou can decrypt PCAPs offline using Python’scryptographylibrary:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import base64
key = base64.b64decode("R0hPU1Q...") # extracted from sample
iv = base64.b64decode("AAAAAAAA...")
ct = open("captured.bin","rb").read()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
pt = cipher.decryptor().update(ct) + cipher.decryptor().finalize()
print(pt)
Persistence Mechanism
Common installation paths captured during this analysis:
- Run key:
HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run→%AppData%\Microsoft\<random>.exe. - Scheduled Task:
schtasks /create /sc minute /mo 1 /tn UpdateService /tr <path> /f. - Startup folder:
%AppData%\Microsoft\Windows\Start Menu\Programs\Startup\<random>.lnk. - WMI event subscription:
EventFilter/EventConsumer/FilterToConsumerBindingfor fileless persistence.
Capability Catalogue
After full unpacking, this family’s modules included:
| Module | Purpose | Key indicators |
|---|---|---|
ShellModule |
Reverse-shell via cmd.exe redirection |
spawns cmd.exe child of the RAT process |
KeylogModule |
Global keyboard hook (SetWindowsHookEx WH_KEYBOARD_LL) |
hidden window class, 0x000D hook type |
ScreenModule |
Periodic screenshot capture via Graphics.CopyFromScreen |
frequent BitBlt-style API calls |
PasswordModule |
Browser & WiFi credential theft | reads Login Data SQLite from Chromium browsers |
WebcamModule |
Camera feed via DirectShow | MMDeviceEnumerator, MFCreateSourceReader |
FileModule |
Recursive directory listing & exfil | calls Directory.EnumerateFiles with broad scope |
KillModule |
Self-destruct: terminate, delete file, clear logs | spawns cmd.exe /c choice /T 5 /C Y /D Y >nul & del ... |
YARA Rule
Created a signature matching the C2 API endpoints, command strings, and VM detection functions.
A representative rule that survives ConfuserEx variant changes (because it anchors on post-decompile constants, not packer artefacts):
rule DotNet_Commodity_RAT_Generic
{
meta:
author = "robinx0"
description = "Generic commodity .NET RAT - command dispatcher + AES + persistence"
sample_sha256 = "<add-on-each-finding>"
tlp = "white"
strings:
// .NET module fingerprint
$mscorlib = "mscoree.dll" ascii
$clr_ver = "v4.0.30319" ascii
// Command dispatcher tokens - same family across rebrands
$cmd_ping = "ping" wide ascii
$cmd_shell = "shell" wide ascii
$cmd_screen = "screen" wide ascii
$cmd_keylog = "keylog" wide ascii
$cmd_kill = "kill" wide ascii
// Persistence hint
$reg_run = "Software\\Microsoft\\Windows\\CurrentVersion\\Run" wide ascii
// AES + Rfc2898 (typical key derivation)
$aes = "System.Security.Cryptography.Aes" wide ascii
$pbkdf2 = "Rfc2898DeriveBytes" wide ascii
// Mutex pattern
$mutex = /Global\\[A-Za-z0-9_]{6,32}/ wide ascii
// Command-shell reverse pattern
$cmd_redir = "cmd.exe /c" wide ascii
condition:
uint16(0) == 0x5A4D and
$mscorlib and $clr_ver and
4 of ($cmd_*) and
$aes and $pbkdf2 and $reg_run
}
Add threat-intel-quality rules per family (AsyncRAT, Remcos, etc.) on top of this generic one - the family-specific rules carry the C2 hostnames and certificate fingerprints discovered during unpacking.
Behavioural Detection
Beyond YARA: the host-based behavioural signature is unmistakable:
- A .NET process (parent typically
dotnet.exeor a renamed assembly under%AppData%) spawnscmd.exeperiodically. - The same process opens long-running TCP/TLS sessions to a residential or untrusted hosting IP.
- Module loads include
System.Drawing.dll,System.Net.Sockets.dll,System.Management.dll, andWindowsBase.dlltogether - the screenshot + WMI + sockets combo is a strong RAT indicator.
Reporting Output
The standard analyst report on a sample like this:
- Identification: family, version, packer, SHA-256.
- C2 infrastructure: hostnames, IPs, ports, certificate hashes, JA3.
- Cryptographic material: extracted AES key + IV.
- Capabilities matrix: list of supported commands.
- Persistence: registry keys / scheduled task / startup paths.
- Detection artefacts: YARA rule(s), Sigma rule(s), Suricata signature(s).
- Recommended response: removal steps for each persistence mechanism, IOCs to block at the perimeter.
Wrapping all of that into a Markdown report tied to the sample hash is the deliverable that lets a SOC act on your analysis the same day.
.NET RATs look intimidating until you defeat the obfuscator once. After that, every variant is the same shape: dispatcher + AES + persistence + capability modules. Build a personal toolkit (de4dot wrapper, dnSpy debug profile, AES-decrypt script) and the next sample takes hours instead of days.