.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:

  1. Trivial development. Visual Studio + a handful of NuGet packages produces a working backdoor in an afternoon.
  2. Cheap obfuscation. ConfuserEx, .NET Reactor, SmartAssembly, and “crypters” sold on forums each provide automated control-flow flattening, string encryption, and packing.
  3. 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 #Strings heap → string encryption.
  • Method bodies of trivial helpers full of switch blocks → control-flow flattening (CFF).
  • Custom <Module>.cctor of 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:

  1. Try the de4dot-cex fork, which targets recent ConfuserEx 2 / “Custom” protections.
  2. Failing that, dump runtime IL: launch the binary under ILSpy debug mode or dnSpyEx with breakpoints on Module.GetMethod/MethodBase.Invoke, dump the in-memory Module to disk after the cctor unpacks itself. The “DnSpyEx → File → Save Module…” flow produces a clean unpacked DLL.
  3. 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.Connect and System.Net.WebRequest.GetResponse reveal 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:

  1. Find the constructor where Aes.Create() is called.
  2. Trace the Key/IV assignments back to their source - usually a Convert.FromBase64String("...") or a derivation from a hardcoded UTF-8 string.
  3. With key + iv you can decrypt PCAPs offline using Python’s cryptography library:
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/FilterToConsumerBinding for 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.exe or a renamed assembly under %AppData%) spawns cmd.exe periodically.
  • 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, and WindowsBase.dll together - the screenshot + WMI + sockets combo is a strong RAT indicator.

Reporting Output

The standard analyst report on a sample like this:

  1. Identification: family, version, packer, SHA-256.
  2. C2 infrastructure: hostnames, IPs, ports, certificate hashes, JA3.
  3. Cryptographic material: extracted AES key + IV.
  4. Capabilities matrix: list of supported commands.
  5. Persistence: registry keys / scheduled task / startup paths.
  6. Detection artefacts: YARA rule(s), Sigma rule(s), Suricata signature(s).
  7. 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.

← Home More Malware analysis →