# Reverser Dotnet Malware

> .NET malware analysis — dnSpy/ILSpy decompilation, de-obfuscation (ConfuserEx, .NET Reactor, Babel, Crypto Obfuscator), behavioral analysis of managed assemblies, PowerShell cradle extraction, and config decryption for common .NET RAT families.

- Skill: `purpleailab/reverser-dotnet-malware` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/reverser-dotnet-malware`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/reverser-dotnet-malware/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/reverser-dotnet-malware

---


# .NET Malware Analysis

Analyze .NET malware from initial triage through full decompilation, de-obfuscation, config extraction, and behavioral mapping.

## Quick Reference

```bash
# Identify .NET assembly
file <TARGET>
python3 -c "import pefile; pe=pefile.PE('<TARGET>'); print('CLR:', pe.OPTIONAL_HEADER.DATA_DIRECTORY[14].VirtualAddress)"

# Fast metadata check
monodis --assembly <TARGET>
strings -n 10 <TARGET> | grep -iE 'System\.Reflection|Assembly\.Load|FromBase64|Invoke|WebClient|DownloadString'

# De-obfuscate with de4dot
de4dot <TARGET> -o <TARGET>.cleaned.exe

# Decompile to C# project
ilspycmd -p -o ./decompiled/ <TARGET>

# Extract embedded resources
python3 -c "
import dnfile
dn = dnfile.dnPE('<TARGET>')
for r in dn.net.resources:
    print(f'{r.name}  offset={r.offset}  size={r.size}')
"
```

## MITRE ATT&CK Mapping

| Technique | ID | How It Appears |
|---|---|---|
| Software Packing | T1027.002 | ConfuserEx, .NET Reactor, SmartAssembly, Babel packing |
| Deobfuscate/Decode | T1140 | Base64 decode + Assembly.Load, XOR decryption of payloads |
| PowerShell | T1059.001 | Cradle execution: `powershell -enc <base64>` from managed code |
| Obfuscated Files | T1027 | String encryption, control flow flattening, anti-tamper |
| Reflective Code Loading | T1620 | Assembly.Load(byte[]) for in-memory stage loading |

## 1. Identify .NET Assembly

Confirm the sample is managed code before committing to .NET tooling.

```bash
# Check PE headers for CLR data directory (index 14)
python3 << 'EOF'
import pefile, sys
pe = pefile.PE("<TARGET>")
clr_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[14]
if clr_dir.VirtualAddress > 0:
    print(f"[+] .NET assembly confirmed — CLR RVA: {hex(clr_dir.VirtualAddress)}")
    # Check runtime version
    import struct
    clr_offset = pe.get_offset_from_rva(clr_dir.VirtualAddress)
    pe.__data__.seek(clr_offset + 8)
    major, minor = struct.unpack('<HH', pe.__data__.read(4))
    print(f"    Runtime: v{major}.{minor}")
else:
    print("[-] Not a .NET assembly")
    sys.exit(1)
EOF

# Alternative quick checks
file <TARGET>                    # Look for "Mono/.Net assembly"
exiftool <TARGET> | grep -i "assembly\|\.net\|framework"
```

## 2. Detect Obfuscator

Identify what protection was applied before attempting decompilation.

```bash
# de4dot auto-detects most obfuscators
de4dot --detect-only <TARGET>
# Output example: "Detected .NET Reactor 6.x"

# Detect It Easy (DIE) for packer/protector identification
diec <TARGET>

# Manual signature checks
strings <TARGET> | grep -iE 'ConfuserEx|Reactor|SmartAssembly|Babel|Crypto Obfuscator|Dotfuscator|Eazfuscator|AgileDotNet'

# Check for anti-tamper / anti-debug module initializers
monodis --method <TARGET> | grep -i "cctor\|InitializeComponent\|Module"

# Entropy per section — high entropy in .text = packed/encrypted
python3 << 'EOF'
import pefile, math
pe = pefile.PE("<TARGET>")
for s in pe.sections:
    data = s.get_data()
    if len(data) == 0: continue
    counts = [data.count(bytes([b])) for b in range(256)]
    total = len(data)
    ent = -sum((c/total)*math.log2(c/total) for c in counts if c)
    name = s.Name.decode().rstrip('\x00')
    print(f"  {name:10s}  entropy={ent:.2f}  {'PACKED' if ent > 7.2 else 'normal'}")
EOF
```

## 3. De-obfuscate

Strip protections layer by layer.

```bash
# de4dot — handles ConfuserEx, .NET Reactor, SmartAssembly, Babel, etc.
de4dot <TARGET> -o <TARGET>.cleaned.exe

# If de4dot detects "Unknown obfuscator", try force mode:
de4dot <TARGET> --strtyp delegate --strtok 0x06000042 -o <TARGET>.cleaned.exe

# For ConfuserEx specifically:
# 1. Remove anti-tamper first (patches module cctor)
de4dot <TARGET> --un-name "!^<Module>$&!^<" -o step1.exe
# 2. Then full clean
de4dot step1.exe -o <TARGET>.cleaned.exe

# For .NET Reactor:
# Use .NET Reactor Slayer or NETReactorSlayer
NETReactorSlayer.CLI <TARGET>

# Verify result decompiles cleanly
ilspycmd <TARGET>.cleaned.exe | head -50
```

## 4. Decompile and Analyze

Full source recovery from cleaned assembly.

```bash
# ILSpy command-line — full project decompilation
ilspycmd -p -o ./decompiled/ <TARGET>.cleaned.exe

# Search decompiled source for key behaviors
grep -rn "WebClient\|HttpClient\|WebRequest" ./decompiled/
grep -rn "Registry\|RegistryKey\|SetValue" ./decompiled/
grep -rn "Process\.Start\|ProcessStartInfo" ./decompiled/
grep -rn "Assembly\.Load\|Activator\.CreateInstance" ./decompiled/
grep -rn "FromBase64String\|Convert\.FromBase64" ./decompiled/
grep -rn "RijndaelManaged\|AesManaged\|DES\|TripleDES\|RC4" ./decompiled/
grep -rn "Socket\|TcpClient\|NetworkStream" ./decompiled/
grep -rn "Clipboard\|Screenshot\|Keylog\|GetAsyncKeyState" ./decompiled/

# Extract hardcoded C2 / config
grep -rn "http://\|https://\|ftp://\|\b[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\b" ./decompiled/

# Dump embedded resources (payloads, configs, second stages)
python3 << 'EOF'
import dnfile, os
dn = dnfile.dnPE("<TARGET>.cleaned.exe")
os.makedirs("resources", exist_ok=True)
for res in dn.net.resources:
    if hasattr(res, 'data') and res.data:
        path = f"resources/{res.name}"
        open(path, 'wb').write(res.data)
        print(f"[+] Extracted: {path} ({len(res.data)} bytes)")
EOF
```

## 5. PowerShell Cradle Extraction

.NET loaders frequently spawn encoded PowerShell.

```bash
# Find encoded PowerShell invocations in decompiled source
grep -rn "\-enc\|\-EncodedCommand\|\-e " ./decompiled/

# Decode base64 PowerShell payloads
python3 << 'EOF'
import base64, sys
encoded = "<BASE64_PAYLOAD>"
decoded = base64.b64decode(encoded).decode('utf-16-le')
print(decoded)
# Write to file for further analysis
with open("decoded_ps.ps1", "w") as f:
    f.write(decoded)
EOF

# Look for staged downloads
grep -iE 'DownloadString|DownloadFile|DownloadData|Invoke-Expression|IEX|Net\.WebClient' decoded_ps.ps1
```

## 6. Config Extraction for Known Families

```bash
# AsyncRAT config extraction
python3 << 'EOF'
import base64, hashlib
from Crypto.Cipher import AES

# AsyncRAT stores config as encrypted strings in Settings class
# Key is derived from a hardcoded passphrase via PBKDF2
passphrase = "<EXTRACTED_KEY>"  # From Settings.Key in decompiled source
salt = b"<EXTRACTED_SALT>"     # From Settings.aession

key = hashlib.pbkdf2_hmac('sha1', passphrase.encode(), salt, 50000, dklen=32)
# Each config field is Base64(IV + AES-CBC(data))
config_fields = {
    "Host": "<ENC_HOST>",
    "Port": "<ENC_PORT>",
    "Mutex": "<ENC_MUTEX>",
}
for name, enc in config_fields.items():
    raw = base64.b64decode(enc)
    iv, ct = raw[:16], raw[16:]
    cipher = AES.new(key, AES.MODE_CBC, iv)
    pt = cipher.decrypt(ct)
    pt = pt[:-pt[-1]]  # PKCS7 unpad
    print(f"  {name}: {pt.decode()}")
EOF

# QuasarRAT — config in Settings class, AES-encrypted
# njRAT — config in plain text fields, base64-encoded
# AgentTesla — SMTP exfil config in Settings resource
```

## 7. Behavioral Analysis with dnSpy

```
# dnSpy interactive workflow (Windows GUI):
# 1. Load cleaned assembly: File → Open → <TARGET>.cleaned.exe
# 2. Set breakpoints on:
#    - EntryPoint (Main method)
#    - Any method calling WebClient/HttpClient
#    - Any method calling Process.Start
#    - Any method calling Assembly.Load
# 3. Debug → Start (F5) — dnSpy hosts the CLR
# 4. Step through, watch locals for decrypted strings/URLs
# 5. Memory window: watch for in-memory PE headers (4D 5A)

# Headless: dump method bodies via Mono.Cecil
python3 << 'EOF'
# pip install dnfile
import dnfile
dn = dnfile.dnPE("<TARGET>.cleaned.exe")
for row in dn.net.mdtables.MethodDef:
    if row.ImplFlags.miIL:
        print(f"  Token 0x{row.row_index:08X}  {row.Name}  RVA={hex(row.RVA)}")
EOF
```

## Tools & Resources

| Tool | Purpose | Install |
|---|---|---|
| dnSpy | .NET debugger + decompiler (GUI) | github.com/dnSpy/dnSpy |
| ILSpy / ilspycmd | .NET decompiler (GUI + CLI) | github.com/icsharpcode/ILSpy |
| de4dot | .NET de-obfuscator | github.com/de4dot/de4dot |
| dotPeek | JetBrains .NET decompiler | jetbrains.com/decompiler |
| dnlib / dnfile | .NET metadata parsing (Python/C#) | pip install dnfile |
| Mono.Cecil | .NET assembly manipulation | nuget Mono.Cecil |
| NETReactorSlayer | .NET Reactor unpacker | github.com/SychicBT/NETReactorSlayer |
| Detect It Easy | Packer/protector identification | github.com/horsicq/Detect-It-Easy |
| AsmResolver | .NET PE/metadata library | github.com/Washi1337/AsmResolver |

## Detection Signatures

| Indicator | Description | Sigma/YARA |
|---|---|---|
| `Assembly.Load(byte[])` | Reflective loading of in-memory .NET payload | YARA: `$s1 = "Assembly" ascii wide` + `$s2 = "Load" ascii wide` |
| `FromBase64String` + `Assembly.Load` | Base64 decode → reflective load chain | Sigma: process_creation with CommandLine containing `-enc` |
| `WebClient.DownloadString` | Stage 2 download via .NET HTTP client | Network: HTTP GET to non-standard port |
| ConfuserEx metadata | `ConfusedBy` attribute in assembly | YARA: `$conf = "ConfusedByAttribute"` |
| High entropy .text section | Packed/encrypted .NET payload | YARA: `math.entropy(0, filesize) > 7.0` |
| `csc.exe` compilation at runtime | Dynamic code compilation via CodeDom | Sysmon: Event ID 1, process `csc.exe` with parent not devenv/msbuild |

## Error Handling & Edge Cases

| Issue | Resolution |
|---|---|
| de4dot crashes on sample | Try older de4dot fork, or manually patch anti-tamper (NOP the module cctor) |
| Mixed mode assembly (native + managed) | Use dnSpy for managed parts, IDA/Ghidra for native stubs |
| AOT/NativeAOT compiled | Not a traditional .NET assembly — treat as native PE, use Ghidra |
| .NET 5+ single-file bundle | Extract with `dotnet-dump` or manually parse bundle header at end of PE |
| Heavily virtualized (KoiVM/EazVM) | Use specialized devirtualizers: OldRod (KoiVM), EazFixer (Eazfuscator) |
| Anti-debug in module initializer | Patch `<Module>.cctor` or use dnSpy's "Edit IL" to NOP checks |
| Resource encryption | Dump at runtime with dnSpy breakpoints on `ResourceManager.GetObject` |

## Decision Gate

```
IF sample is .NET assembly (CLR header present):
  → Run de4dot to identify + strip obfuscation
  → Decompile with ILSpy/dnSpy
  → Extract configs for known RAT families
  → Map behaviors to ATT&CK techniques
  → Extract IOCs (C2, mutex, registry keys)
ELSE IF .NET single-file or AOT:
  → Extract bundled assemblies first, then proceed above
ELSE:
  → Not .NET — use reverser/malware-triage or reverser/ghidra
```

