XWorm is a commodity .NET RAT that has been in continuous development since mid-2022. Its low price point ($20/month on underground markets), broad feature set (remote desktop, keylogger, clipboard stealer, ransomware module), and regular updates have made it one of the most prevalent RATs in active deployment. Configuration is stored as an AES-CBC encrypted blob embedded in the binary - this report documents the key derivation scheme, config structure across four observed variant generations, and a Python extractor applied to a corpus of 200+ campaign samples.
//Config Encryption: AES-CBC
XWorm stores its C2 configuration (host, port, install directory, mutex, campaign tag, and feature flags) as an AES-128-CBC encrypted blob embedded at a fixed offset in the .NET assembly resources. The encryption key and IV are derived from a hardcoded seed string unique to each build, processed through a simple custom KDF.
Key Derivation (Variants 1-3)
In the first three variant generations, the KDF is predictable: the seed string is MD5-hashed to produce the 16-byte AES key, and the IV is the seed string itself zero-padded or truncated to 16 bytes.
# Variant 1-3 key derivation
import hashlib
def derive_key_v1_3(seed: str) -> tuple[bytes, bytes]:
"""Returns (key, iv) for AES-128-CBC"""
key = hashlib.md5(seed.encode()).digest() # 16 bytes
iv = seed.encode().ljust(16, b'\x00')[:16] # pad/truncate to 16
return key, iv
# Example extraction
seed = extract_seed_from_assembly(dotnet_binary) # find in resources or .ctor
key, iv = derive_key_v1_3(seed)
config_blob = extract_encrypted_blob(dotnet_binary) # fixed offset in Resources
config_plaintext = AES.new(key, AES.MODE_CBC, iv).decrypt(config_blob)
config = parse_config(config_plaintext)Key Derivation (Variant 4 - July 2026 update)
The July 2026 XWorm variant changed the KDF to use PBKDF2-HMAC-SHA1 with 1000 iterations and a salt derived from the assembly version string. This change was specifically to break existing automated extractors, but the new scheme is still deterministic and extractable given access to the binary.
# Variant 4 key derivation (patched extractor)
import hashlib
def derive_key_v4(seed: str, asm_version: str) -> tuple[bytes, bytes]:
"""XWorm variant 4 - PBKDF2 with assembly version as salt"""
salt = asm_version.encode() # e.g. "2.1.0.0"
key = hashlib.pbkdf2_hmac("sha1", seed.encode(), salt, 1000, dklen=16)
# IV is still MD5(seed) for variant 4 (lazy implementation)
iv = hashlib.md5(seed.encode()).digest()
return key, iv
# Extract assembly version from .NET PE header
# Located in the CLR metadata stream: 0x8000 offset from metadata root//Configuration Structure
Once decrypted, the config is a pipe-delimited plaintext string with fields in a fixed order. The field order has been consistent across all four variants, with new fields appended to the end in later versions.
# XWorm config field layout (all variants)
# Field: C2_HOST | C2_PORT | MUTEX | INSTALL_DIR | KEY | BTCADDR | VERSION | [flags...]
# Example (redacted real config from corpus):
config_str = "185.220.xxx.xxx|4449|XWorm-Mutex-{UUID}|%AppData%\svchost32|xworm2024|bc1q...|2.1|1|1|0|0"
fields = config_str.split("|")
config = {
"c2_host": fields[0],
"c2_port": int(fields[1]),
"mutex": fields[2],
"install_dir": fields[3],
"aes_key": fields[4], # used for encrypted C2 comms post-init
"btc_addr": fields[5], # ransomware module payment address
"version": fields[6],
"persistence": bool(int(fields[7])),
"startup": bool(int(fields[8])),
"uac_bypass": bool(int(fields[9])),
"antivirus": bool(int(fields[10])),
}//Extractor Implementation
#!/usr/bin/env python3
"""XWorm config extractor - supports variants 1-4"""
import sys, re, struct, hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import dnfile # pip install dnfile
def extract_xworm_config(path: str) -> dict | None:
with open(path, "rb") as f:
data = f.read()
dn = dnfile.dnPE(path)
# Step 1: find seed string in .NET resources
seed = None
for res in dn.net.mdtables.ManifestResource.rows:
# Seed is stored as a string constant in the .ctor of the main class
pass # simplified: search for string constant matching seed pattern
# Fallback: regex for seed pattern in binary
m = re.search(rb'[A-Za-z0-9!@#$%^&*]{8,32}(?=\x00)', data)
if m:
seed = m.group(0).decode("ascii", errors="ignore")
if not seed:
return None
# Step 2: detect variant and derive key
asm_ver = dn.net.Flags.ClrHeader.MajorRuntimeVersion # simplified
if b"pbkdf2" in data.lower() or b"PBKDF2" in data:
# Variant 4
asm_version_str = extract_assembly_version(dn)
key, iv = derive_key_v4(seed, asm_version_str)
else:
key, iv = derive_key_v1_3(seed)
# Step 3: find and decrypt config blob
# Config blob preceded by 4-byte length marker
for match in re.finditer(rb'\x10\x00\x00\x00', data):
offset = match.start() + 4
blob = data[offset:offset+256]
try:
pt = unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(blob), 16)
if b"|" in pt and pt.count(b"|") >= 6:
return parse_config(pt.decode())
except Exception:
continue
return None
if __name__ == "__main__":
result = extract_xworm_config(sys.argv[1])
if result:
import json; print(json.dumps(result, indent=2))
else:
print("Extraction failed")//Campaign Analysis Results
Applying the extractor to the 247-sample corpus yielded 232 successful extractions (94%). The 15 failures were variant 4 samples where the assembly version string was obfuscated, preventing KDF replication.
Campaign analysis summary (232 configs extracted): Unique C2 IPs: 89 Unique C2 ports: 4449 (67%), 4444 (18%), 3389 (8%), custom (7%) Campaign tags: "xworm2024" (34%), "rat2026" (28%), custom (38%) C2 ASN distribution: AS14061 (DigitalOcean): 31% AS16276 (OVH): 22% AS60781 (LeaseWeb): 18% AS51167 (Contabo): 14% Other: 15% Operator overlap: 23 C2 IPs shared with AsyncRAT campaigns, confirming the known pattern of dual RAT deployment.
config-extractor v1.4.2, updated to support variant 4. Run with --variant auto for automatic detection. Batch processing: pipe a directory of samples through --batch and specify--output csv for structured output suitable for threat intelligence platforms.185.220.xxx.xxx:4449 (redacted)193.142.xxx.xxx:4449 (redacted)45.142.xxx.xxx:3389 (redacted)Common mutex strings:
XWorm-Mutex-V2, XClient_Mutex_2024, rat2026_mtxInstall paths (from config):
%AppData%\svchost32.exe%Temp%\WindowsDefender.exe%AppData%\Microsoft\Windows\Start Menu\Programs\Startup\update.exe