PureCrypter: Reverse Engineering a .NET Loader From the PureCoder Ecosystem
We recovered two ConfuserEx-obfuscated .NET executables from a multi-stage intrusion. They are builder-generated loaders -- their only purpose is to decrypt an embedded resource, decompress it, and load the result as a .NET assembly via reflection.
As of 2026-02-26, the Sep loader (Fviwknzr.exe) is 49/76 on VirusTotal, and the two Nov loader builds (Erqcke.exe) are both 41/76. The Oct05 and Nov loader hashes are now all present in Tria.ge with completed reports.
Both loaders are PureCrypter, the commercial .NET crypter sold as part of the PureCoder malware-as-a-service ecosystem. The campaign has been publicly tracked by Securonix as SERPENTINE#CLOUD. This analysis focuses on the outer wrapper. The inner payload delivered by one of these loaders is analyzed in PureLogs: Reverse Engineering a .NET RAT.
Tria.ge and VT refresh (2026-02-26)
| Artifact | SHA256 | VT | VT first seen (UTC) | Tria.ge ID | Score | Family | Key signatures |
|---|---|---|---|---|---|---|---|
| Fviwknzr.exe (Sep/Oct05 loader) | dcd22d338a0bc4cf615d126b84dfcda149a34cf18bc43b85f16142dfb019e608 | 49/76 | 2025-09-18 08:09:30 | 260226-na81hacs3e | 7 | - | Unsigned PE, AddClipboardFormatListener, AdjustPrivilegeToken, SetWindowsHookEx |
| Erqcke.exe (Nov10 loader) | 0ab09a4787ea9cb259cadd3f811a56f7bd0058287634bbaf0388b2cd40464505 | 41/76 | 2026-02-22 01:35:20 | 260226-mt3gdsbx4g | 3 | - | Unsigned PE, AdjustPrivilegeToken |
| Erqcke.exe (Nov19 loader) | b1c6659ee4ee35540f5ed043b611ac88a7fce9dc2f564168e7d47c43683163f6 | 41/76 | 2026-02-22 01:36:27 | 260226-mt3r6abx4h | 3 | - | Unsigned PE, AdjustPrivilegeToken |
| Qdjlj.dll (Oct05 inner payload) | cdf87d68885caa3e94713ded9dd5e51c39b7bc7ef9bf7d63a4ff5ab917a96b36 | 17/76 | 2026-02-26 10:43:37 | 260226-mszzwabx2c | 3 | - | Unsigned PE |
| Mvfsxog.dll (Plog inner payload) | 046d0e83c1e6dcaf526127b81b962042e495f5ae3a748f3a9452be62f905acf8 | 37/76 | 2026-02-22 01:33:52 | 260226-mszn4sbx2b | 3 | - | Unsigned PE |
The two Erqcke loader hashes remain low-score unsigned PE samples in Tria.ge despite 41/76 VirusTotal detection. The Oct05 loader scored higher in Tria.ge because detonation reached behaviour associated with the inner payload stage (browser profile reads and hook-related API use). The inner payload DLLs stayed score 3 with unsigned-PE-only static signatures in this refresh run.
The breach timeline also confirms cross-wave reuse: the Nov19 phvnc slot reused this exact Oct05 loader hash (dcd22d...) byte-for-byte.
Sample overview
| Field | Fviwknzr.exe (Sep build) | Erqcke.exe (Nov10/Nov19 builds) |
|---|---|---|
| SHA256 | dcd22d338a0bc4cf615d126b84dfcda149a34cf18bc43b85f16142dfb019e608 | 0ab09a4787ea9cb259cadd3f811a56f7bd0058287634bbaf0388b2cd40464505 (Nov10), b1c6659ee4ee35540f5ed043b611ac88a7fce9dc2f564168e7d47c43683163f6 (Nov19) |
| Size | 588,288 bytes | 597,288 bytes (Nov10), 590,848 bytes (Nov19) |
| Type | PE32 .NET (x86), CLR v4.0.30319 | PE32 .NET (x86), CLR v4.0.30319 |
| PE timestamp | 2025-09-04 | 2025-11-xx |
| Obfuscator | ConfuserEx v1.x | ConfuserEx v1.x |
| VT first seen | 2025-09-18 08:09:30 UTC | 2026-02-22 01:35:20 UTC (Nov10), 2026-02-22 01:36:27 UTC (Nov19) |
| VT detections | 49/76 (trojan.msil/androm) | 41/76 on both hashes (trojan.msil/jalapeno) |
| 3DES key (base64) | Aab+zuk+b7ZNNoojdAhx7w== | KxqBSIkA5EkfaAuH0P+Png== |
| 3DES IV (base64) | 226dKBKRPbM= | XOW98LRYqZg= |
| Resource name | Jaglt (555,016 bytes) | Ctjady (555,704 bytes) |
| Inner payload | Qdjlj.dll (PureLogs fat client) | Mvfsxog.dll (PureLogs plugin stager) |
| Entry class | bvvHVYU940gLdcXoTjh.jp8DQgU0wWFLN2sjUR8 | Y89SbSJ6S8vOgcIg63.EL2VsZRu4no3pyibCR |
| Entry method | DYDUwZwO1R | zoPKRGIHs |
| Namespace | Fviwknzr.DataProcessing | Erqcke.EventManagement |
| Classes | 14 (+ Module + AssemblyInfo) | 15 (+ Module + AssemblyInfo) |
Both loaders deliver structurally different PureLogs variants to the same campaign infrastructure. The inner payloads are analyzed separately -- this post covers only the loader framework.
Architecture: event-driven pipeline
Both loaders use the same architecture. An orchestrator class wires five event handlers into a chain. Each handler does one thing, fires the next event, and the pipeline cascades from start to finish:
Main()
-> new Orchestrator()
-> constructor wires 5 event handlers:
DecryptRequested -> 3DES-CBC decrypt
Decrypted -> GZip decompress
Decompressed -> Assembly.Load
AssemblyLoaded -> Reflection setup
InvokeRequested -> method.Invoke
-> Orchestrator.Start(resourceBytes, key, IV, className, methodName)
-> fires DecryptRequested
-> fires Decrypted
-> fires Decompressed
-> fires AssemblyLoaded
-> fires InvokeRequested
-> done
Each stage fires a StageChanged event with a string label: "Start", "Decrypt", "Decompress", "AssemblyLoad", "Reflect", "Complete". Errors at any stage fire ErrorOccurred and re-throw.
Here is Main() from the Sep build (Fviwknzr.exe), deobfuscated:
// Fviwknzr.Jfvihql.Main() -- deobfuscated
public static void Main()
{
Orchestrator orch = new Orchestrator();
try
{
orch.Start(
Resources.Jaglt, // encrypted resource bytes
Convert.FromBase64String("Aab+zuk+b7ZNNoojdAhx7w=="), // 3DES key
Convert.FromBase64String("226dKBKRPbM="), // 3DES IV
"bvvHVYU940gLdcXoTjh.jp8DQgU0wWFLN2sjUR8", // target class
"DYDUwZwO1R" // target method
);
}
catch { }
}
The Nov build (Erqcke.exe) is identical in structure with different randomized names and keys:
// Erqcke.Vavbij.Main() -- deobfuscated
public static void Main()
{
Orchestrator orch = new Orchestrator();
try
{
orch.Start(
Resources.Ctjady,
Convert.FromBase64String("KxqBSIkA5EkfaAuH0P+Png=="),
Convert.FromBase64String("XOW98LRYqZg="),
"Y89SbSJ6S8vOgcIg63.EL2VsZRu4no3pyibCR",
"zoPKRGIHs"
);
}
catch { }
}
The silent catch {} is characteristic of PureCrypter -- if anything fails, the process exits silently. No error logging, no crash dialog.
3DES decryption
The first stage handler decrypts the embedded resource using 3DES-CBC with PKCS7 padding. The key and IV are base64-encoded strings stored as static readonly byte[] fields in a dedicated key-store class.
Deobfuscated decrypt method (Sep build):
// Fviwknzr.Extraction.AccessibleExtractor -- deobfuscated
internal static byte[] Decrypt(byte[] data, byte[] key, byte[] iv)
{
// key must be 16 or 24 bytes, IV must be 8 bytes
using var tdes = new TripleDESCryptoServiceProvider();
tdes.Mode = CipherMode.CBC;
tdes.Padding = PaddingMode.PKCS7;
tdes.Key = key;
tdes.IV = iv;
using var ms = new MemoryStream();
using var cs = new CryptoStream(ms, tdes.CreateDecryptor(), CryptoStreamMode.Write);
cs.Write(data, 0, data.Length);
cs.FlushFinalBlock();
return ms.ToArray();
}
The Nov build adds key zeroing after use (Array.Clear(key, 0, key.Length)) and descriptive error messages ("IV must be 8 bytes for 3DES", "Key must be 16 or 24 bytes"). The Sep build throws bare ArgumentException() with no message.
Extracted keys
| Build | Key (base64) | Key (hex) | IV (base64) | IV (hex) |
|---|---|---|---|---|
| Sep (Fviwknzr) | Aab+zuk+b7ZNNoojdAhx7w== | 01a6fecee93e6fb64d368a23740871ef | 226dKBKRPbM= | db6e9d2812913db3 |
| Nov (Erqcke) | KxqBSIkA5EkfaAuH0P+Png== | 2b1a81488900e4491f680b87d0ff8f9e | XOW98LRYqZg= | 5ce5bdf0b458a998 |
Both keys are 16 bytes (128-bit 3DES). The builder generates random keys and IVs for each build.
GZip decompression
After decryption, the result is GZip-compressed with a 4-byte little-endian size header prepended. The decompressor reads the expected size, decompresses, and validates the output length matches.
Deobfuscated (Sep build):
// Fviwknzr.DataProcessing.FilterEncryptor -- deobfuscated
internal static byte[] Decompress(byte[] data, int headerSize, int maxSize)
{
// headerSize = 4, maxSize = 16777216 (16 MB)
int expectedSize = BitConverter.ToInt32(data, 0);
if (maxSize > 0 && expectedSize > maxSize)
throw new InvalidDataException();
using var stream = new MemoryStream(data, headerSize, data.Length - headerSize);
using var gzip = new GZipStream(stream, CompressionMode.Decompress);
using var output = new MemoryStream(expectedSize > 0 ? expectedSize : 0);
gzip.CopyTo(output);
byte[] result = output.ToArray();
if (result.Length != expectedSize)
throw new InvalidDataException();
return result;
}
The Nov build replaces the CopyTo() call with a manual 4KB-buffer read loop (for .NET 3.5 compatibility) and adds a 100 MB hard limit on decompressed size before allocation.
Assembly loading and reflection invoke
The decompressed bytes are a .NET assembly. The loader calls Assembly.Load(byte[]), resolves the target type and method by name, and invokes it.
Sep build (deobfuscated):
// Assembly.Load stage
Assembly asm = Assembly.Load(decompressedBytes);
// Reflection setup stage
MethodInfo method = asm
.GetType("bvvHVYU940gLdcXoTjh.jp8DQgU0wWFLN2sjUR8", throwOnError: true)
.GetMethod("DYDUwZwO1R", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
// Invoke stage
method.Invoke(null, null); // no instance, no parameters
Nov build (deobfuscated):
Assembly asm = Assembly.Load(decompressedBytes);
Type type = asm.GetType("Y89SbSJ6S8vOgcIg63.EL2VsZRu4no3pyibCR", throwOnError: true);
MethodInfo method = type.GetMethod("zoPKRGIHs",
BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
object target = method.IsStatic ? null : Activator.CreateInstance(type);
method.Invoke(target, args); // can pass parameters to inner assembly
Two differences. The Nov build supports both static and instance methods (it creates an instance with Activator.CreateInstance if the method is not static). It also passes an object[] argument array instead of null -- the builder can bake arguments into the loader that get forwarded to the inner assembly. The Sep build only supports static methods with no arguments.
The Nov build also validates the decompressed bytes before loading -- it checks for the MZ header (bytes[0] == 0x4D && bytes[1] == 0x5A) and a minimum length of 64 bytes.
Obfuscation
Both builds are protected with ConfuserEx. The obfuscation is aggressive-looking but functionally trivial once you understand the pattern.
Control flow flattening
Every method in both assemblies uses the same structure:
int num = <initial>;
while (true)
{
switch (num)
{
case 0:
// some operation
num = 3;
break;
case 1:
return result;
case 2:
// another operation
num = 0;
break;
// ...
}
}
Linear code is broken into blocks, each assigned a case number. The while(true) { switch } dispatcher jumps between blocks by setting num to the next case. The original control flow is straightforward -- you just need to trace the case transitions to reconstruct it. Every method in this post has been through that reconstruction.
Opaque predicates
The <Module> class in each assembly contains over 100 int fields. These are initialized in a flattened static constructor via XOR expressions that mostly evaluate to 0:
// From <Module>{47d0803d-c8a4-4c82-9451-bbb9c835a0a8}
m_0529e0fe017441aabcb8f0cb2aa9b130 = 0x6CBF766 ^ 0x6CBF766; // = 0
m_0991f046cea44229a53b00bd4b1e1bef = 0x4F4DEAD1 ^ 0x4F4DEAD1; // = 0
// ... 100+ more, all zero
These fields are referenced throughout the code in dead branches:
int num = 0;
if (<Module>.m_0529e0fe... != 0) // always false
{
num = 0; // dead code
}
switch (num) // always 0
{
// ...
}
The predicates never change the program's behavior. They exist to break static analysis tools that cannot resolve the field values. Once you know every predicate field is 0, the dead branches collapse and the real logic emerges.
Dead code injection
Beyond opaque predicates, ConfuserEx injects if (field == 0) { num = 0; } checks between every functional block. These are scattered throughout all methods in both builds. They are pure noise -- each one assigns the same value the variable already holds.
Builder evolution
The Nov Erqcke branch is a newer version of the PureCrypter builder. The differences below show the builder's development between September and November 2025:
| Feature | Sep build (Fviwknzr) | Nov build (Erqcke) |
|---|---|---|
| Error messages | Bare ArgumentException() | Descriptive: "IV must be 8 bytes for 3DES", "Key must be 16 or 24 bytes", "Encrypted data cannot be null or empty" |
| Key zeroing | None | Array.Clear(key, 0, key.Length) after use |
| Size validation | None | 100 MB max decompression limit, MZ header check |
| Stream copy | GZipStream.CopyTo() (.NET 4.0+) | Manual 4KB buffer loop (.NET 3.5 compatible) |
| Invoke arguments | method.Invoke(null, null) | method.Invoke(target, args) -- supports parameters |
| Method binding | Static only | Static + instance (Activator.CreateInstance fallback) |
| Error context | Exception only | Exception + stage name string |
| Class count | 14 | 15 (extra stream utility class) |
The builder generates all class names, method names, field names, and namespace names. Even the sub-namespace naming convention uses "real-sounding" prefixes (.DataProcessing, .DecisionMaking vs .EventManagement, .Management, .Values) -- these are also builder-generated randomized strings.
Both Nov hashes (0ab09a47..., b1c6659e...) map to this Erqcke branch. They are not byte-identical but they preserve the same class layout, decrypt-decompress-reflect pipeline, and 3DES material.
Class mapping
Every class in both builds has a direct functional equivalent with a different randomized name. Here is the complete mapping:
| Role | Fviwknzr.exe (Sep) | Erqcke.exe (Nov) |
|---|---|---|
| Entry point | Jfvihql.Main() | Vavbij.Main() |
| Orchestrator | Vmtxjyn (1,060 lines) | Ggrbywy (1,115 lines) |
| 3DES key store | SetDecider | ElementEvent |
| 3DES decryptor | AccessibleExtractor | ConvertiblePool |
| GZip decompressor | FilterEncryptor | InitializerVisitor |
| Resource accessor | Brldzgjjnpo -> Jaglt | Wtduo -> Ctjady |
| Stream utility | (none) | LiteralValue |
| StageChanged args | Bygcocdvdg (string) | Edpsyp (string) |
| DecryptRequested args | Mokubjtf (byte[] x3) | Aepjgfg (byte[] x3) |
| Decrypted args | Gcbnen (byte[]) | Nhdzdtuhc (byte[]) |
| Decompressed args | Dkjwlljntsb (byte[]) | Eqsknvcizh (byte[]) |
| AssemblyLoaded args | Tgfhhkn (Assembly) | Rgypqjcdloq (Assembly) |
| InvokeRequested args | Mxrkricez (Asm, str, str) | Jdlwwh (Asm, str, str, obj[]) |
| ErrorOccurred args | Tkiist (Exception) | Yqsmuahmhf (Exception, string) |
The Nov build adds LiteralValue, a stream utility class providing a manual copy loop and a safe-dispose extension method. The Sep build uses GZipStream.CopyTo() directly and has no equivalent class.
Indicators of compromise
Loader hashes
| Sample | SHA256 | MD5 | VT | Tria.ge |
|---|---|---|---|---|
| Fviwknzr.exe (Sep/Oct05 loader) | dcd22d338a0bc4cf615d126b84dfcda149a34cf18bc43b85f16142dfb019e608 | 505a7b3074c5a769d85087b7f2e4bddd | 49/76 | 260226-na81hacs3e (score 7) |
| Erqcke.exe (Nov10 loader) | 0ab09a4787ea9cb259cadd3f811a56f7bd0058287634bbaf0388b2cd40464505 | 3954ddd9e691ab0520e82f83d53579d4 | 41/76 | 260226-mt3gdsbx4g (score 3) |
| Erqcke.exe (Nov19 loader) | b1c6659ee4ee35540f5ed043b611ac88a7fce9dc2f564168e7d47c43683163f6 | e3d3cfdde5349c2728b1b7a0c9ccfdc1 | 41/76 | 260226-mt3r6abx4h (score 3) |
3DES parameters
| Build | Key (base64) | IV (base64) | Resource |
|---|---|---|---|
| Sep (Fviwknzr) | Aab+zuk+b7ZNNoojdAhx7w== | 226dKBKRPbM= | Jaglt |
| Nov (Erqcke) | KxqBSIkA5EkfaAuH0P+Png== | XOW98LRYqZg= | Ctjady |
Reflection targets
| Build | Entry class | Entry method |
|---|---|---|
| Sep | bvvHVYU940gLdcXoTjh.jp8DQgU0wWFLN2sjUR8 | DYDUwZwO1R |
| Nov | Y89SbSJ6S8vOgcIg63.EL2VsZRu4no3pyibCR | zoPKRGIHs |
Inner payloads
| Loader | Inner assembly | SHA256 | VT | Tria.ge | Family |
|---|---|---|---|---|---|
| Fviwknzr.exe | Qdjlj.dll (1,290,752 bytes) | cdf87d68885caa3e94713ded9dd5e51c39b7bc7ef9bf7d63a4ff5ab917a96b36 | 17/76 | 260226-mszzwabx2c (score 3) | PureLogs fat client (86 ProtoInclude types, crypto stealer) |
| Erqcke.exe | Mvfsxog.dll (1,319,424 bytes) | 046d0e83c1e6dcaf526127b81b962042e495f5ae3a748f3a9452be62f905acf8 | 37/76 | 260226-mszn4sbx2b (score 3) | PureLogs plugin stager (24 ProtoInclude types) |
Behavioral indicators
- .NET process loading an embedded resource, decrypting with 3DES-CBC, and decompressing with GZip
- 4-byte little-endian size header prepended to GZip stream
- In-memory .NET assembly loading via
Assembly.Load(byte[]) - Reflection invocation of a specific class/method in the loaded assembly
- ConfuserEx control flow flattening (
while(true) { switch }pattern) - Module-level opaque predicate fields (100+
intfields all evaluating to 0)
Connection to PureLogs
Both loaders deliver PureLogs variants. The inner payload from Erqcke.exe (Mvfsxog.dll) is the subject of PureLogs: Reverse Engineering a .NET RAT -- a plugin stager with no built-in offensive capability that waits for .NET assemblies pushed from C2.
The inner payload from Fviwknzr.exe (Qdjlj.dll) is a fat client -- a monolithic build with crypto wallet theft, browser credential harvesting, Telegram/Foxmail data theft, and PowerShell persistence baked in. Same ecosystem, different operational profiles.
The loader coverage now spans Sep, Nov10, and Nov19 hashes. Fviwknzr.exe remains the Oct05 loader branch, and both Nov hashes are Erqcke branch recompiles using the same Ctjady -> Mvfsxog handoff pattern. The loader stage changes names, hashes, and wrappers across waves, but the 3DES -> GZip -> Assembly.Load -> reflection pattern stays stable.
PureCrypter is the delivery mechanism. PureLogs is the payload. Both are sold as part of PureCoder's malware-as-a-service toolkit alongside PureHVNC and repackaged commodity RATs (AsyncRAT, VenomRAT, DcRat, XWorm). The AutoIt-based persistence layer that kept these loaders alive across reboots is analyzed in Remcos Banking Fraud & AutoIt Persistence. The campaign was first publicly documented by Securonix as SERPENTINE#CLOUD.
Kirk
I like the internet. Want to get in touch? kirk@derp.ca