m3rx Ransomware
1. Sample Identification
| Field | Value |
|---|---|
| Family | m3rx (internal tag — LOCAL variant) |
| SHA-256 | b09ece33ffe5efb1903526229595a8c74d983c731505bee09c2a005036c834b8 |
| MD5 | 071e2e0087554d96bba6a4ab73d88cd0 |
| Type | PE32+ x86_64, Windows (console subsystem) |
| Size | 2,580,480 bytes (2,520 KB) |
| Language | Go 1.x (MSVC amd64/windows) |
| Compile timestamp | 0 (stripped) |
| PDB path | stripped (no residual PDB string) |
| Image base | 0x400000 |
| Entry point | _rt0_amd64_windows @ 0x466040 |
| Sections | 6: .text rx (1,136 KB), .rdata r (1,248 KB), .data rw (~480 KB), .idata rw (×2), .reloc r, .symtab (Go stub) |
| Functions | 2,633 total (670 named via pclntab + Lumina) |
Statically-compiled Go binary, standard Go runtime (golang.org/x/sys/windows, golang.org/x/net/dns/dnsmessage, encoding/gob, hash/crc32, path/filepath, crypto/aes, crypto/cipher, crypto/ecdh, crypto/rand, compress/gzip, os/exec, sync, time, os/signal, context). No packer, normal entropy (6.17 on .text, 5.64 on .rdata). The Go symbol table (pclntab) is preserved — main.* and golang.org_x_*.* names are directly readable, which is a huge analysis shortcut. A single static import kernel32.dll (40 functions); everything else (advapi32, ntdll, psapi, winmm, dnsapi, user32, ws2_32, crypt32, secur32, shell32, userenv, version, iphlpapi, netapi32, rstrtmgr, setupapi, wtsapi32, ole32, oleaut32) is resolved dynamically via golang.org/x/sys/windows.LazyDLL / LazyProc (= LoadLibraryA + GetProcAddress). Notable indirect dependency in buildinfo: golang.org/x/sys v0.42.0.
The configuration (attacker X25519 pubkey, extension, note filename and body) is embedded as a gzip + gob blob in .rdata, 4,096 bytes at 0x55ECBF (aT_0). The WMI shellcode for VSS deletion is embedded as a gzip blob, ~794 bytes split across stack literals + .rdata at 0x5A799A.
Static imports (1 DLL)
| DLL | Count | Role |
|---|---|---|
| kernel32.dll | 40 | Only static IAT. Core APIs required by the Go runtime (VirtualAlloc/Free/Query, CreateThread, CreateIoCompletionPort, WaitForSingleObject/Multiple, CreateFileA/WriteFile, GetStdHandle, TlsAlloc, LoadLibraryA/W, GetProcAddress, GetSystemInfo/Directory, GetEnvironmentStringsW, SwitchToThread, SetThreadContext, SuspendThread/ResumeThread, SetEvent/SetWaitableTimer, DuplicateHandle, CreateWaitableTimerExW, AddVectoredExceptionHandler, SetUnhandledExceptionFilter, ExitProcess, …). |
DLLs resolved dynamically (17, via LazyDLL)
| DLL | Primary role |
|---|---|
advapi32.dll |
Token/SID/SCM (check_admin, SID_SubAuthority) |
ntdll.dll |
Go runtime (Rtl*, NtSet*) |
psapi.dll |
Module enumeration (Go runtime) |
winmm.dll |
Go runtime timers |
dnsapi.dll |
Resolution (unused in user code — runtime only) |
user32.dll |
ShowWindow (hide console), wsprintfW (shellcode) |
ws2_32.dll |
Winsock (runtime only) |
crypt32.dll |
CertStore (runtime x/sys) |
secur32.dll |
Security (runtime) |
shell32.dll |
SHEmptyRecycleBinW |
userenv.dll |
CreateEnvironmentBlock (runtime) |
version.dll |
Versioning (runtime) |
iphlpapi.dll |
Network adapters (runtime) |
netapi32.dll |
Net* (runtime) |
rstrtmgr.dll |
Restart Manager — unlock held files (RmStartSession, RmRegisterResources, RmGetList, RmShutdown, RmEndSession) |
setupapi.dll |
SetupDi* (runtime) |
wtsapi32.dll |
WTS (runtime) |
ole32.dll |
COM init for the shadow-copy shellcode (CoInitializeEx, CoInitializeSecurity, CoCreateInstance, CoSetProxyBlanket) |
oleaut32.dll |
BSTR for shellcode (SysAllocString, SysFreeString, VariantClear) |
2. Infrastructure
| Field | Value |
|---|---|
| Onion | pippahtohg6qgioqu3ixrsueefuw7thythmmeanyrgwn3eixcuu6jvqd.onion |
| Chat URL | http://pippahtohg6qgioqu3ixrsueefuw7thythmmeanyrgwn3eixcuu6jvqd.onion/ |
| Victim ID / path token | (26-char alphanumeric, hardcoded in the config) |
| TOX ID | 9A1217BEDA4AB77052A25D17CB6FFB34AFA2BE462E607F2FD8E1DF1DDD4CA16A64E18B1A0BF2 |
| (none) | |
| Note filename | RECOVERY_NOTES.TXT |
| Encryption extension | .8hmlsewu (appended after 16 random chars + .) |
| Mutex name | CRC32("kek" + hostname) in decimal ASCII (e.g. 3141592653) |
| Payment | BTC (negotiated in the Tor chat) |
| Pressure | Double extortion: publication + sharing with competitors |
The attacker public key is present in the config in cleartext (after gzip+gob decode):
X25519 public key (K1) = cdbe4aed37c98d67a005ef469e7e0586e0ff8973b91a8d577d320e67cf46b572
3. Ransom Note
Filename
RECOVERY_NOTES.TXT — dropped recursively into every directory (on the enumerated drives) whose name (case-insensitive, strings.EqualFold) is desktop or documents. Dropped by main.local_drop_note (0x518420) via os.WriteFile(path, content, 0o644). The content is pulled from the embedded config (gob field NT).
Content (decompressed from the gzip+gob blob at aT_0 / 0x55ECBF)
Your files have been stolen from your network and encrypted with a military class algorithm. We work for money and are not associated with politics. All you need to do is contact us and pay decrypt fee.
--- Our interaction process:
1. You contact us.
1. We send you a list of files that were stolen.
2. We decrypt 3 files to confirm that our decryptor works.
3. You pay the amount in BTC, that was established in our negotiations.
4. You get decryptor, approve that all data is secure.
5. We wipe out all your data from our database and give you a detailed security breach report with security improve advices.
--- Client area (use this site to contact us):
Link for Tor Browser: http://pippahtohg6qgioqu3ixrsueefuw7thythmmeanyrgwn3eixcuu6jvqd.onion/[SNIP]
>>> to begin the recovery process.
* In order to access the site, you will need Tor Browser,
you can download it from this link: https://www.torproject.org/
--- Additional contacts:
Support Tox: 9A1217BEDA4AB77052A25D17CB6FFB34AFA2BE462E607F2FD8E1DF1DDD4CA16A64E18B1A0BF2
--- Recommendations:
DO NOT RESET OR SHUTDOWN PC's - files may be damaged.
DO NOT RENAME OR MOVE the encrypted and readme files.
DO NOT DELETE readme files.
--- Important:
If you refuse to pay or do not get in touch with us, we start publishing your files, as well as share them to your competitors.
Classic double-extortion tone: threats of publication AND sharing data with competitors (claims prior exfiltration; this binary does NOT exfiltrate — the wording assumes an upstream stealer, not covered in this analysis).
4. Execution Flow (main.main @ 0x518e60, 4,383 bytes)
1. os/signal.NotifyContext(ctx, SIGINT) — ctx cancellable on Ctrl+C
2. context.WithCancel() ×2 — parent + shutdown
3. makechan(c, 1) — completion channel (main.c_1)
4. makechan(c_0, 0) — log channel (unbuffered)
5. go closure_logger — wraps main.logger (stdout + optional file)
6. go main.main.func2 — signal-handler goroutine (prints "[INTERRUPT] Wait for all threads safe exit..." on ctx.Done())
7. main.mutex_exists — if exists (CreateMutex GetLastError=ALREADY_EXISTS) → log "only one instance" + return
8. main.check_admin — GetTokenInformation(TokenIntegrityLevel) + SID_SubAuthority; require IL ≥ 0x3000 (HIGH)
9. flag.FlagSet.Var ×9 — parse CLI flags (see §11)
10. flag.Parse()
11. if -fast: perc := 100
12. if -hide: main.hide_console (ShowWindow(GetConsoleWindow, SW_HIDE))
13. main.load_config — decompress gzip+gob + X25519 ECDH → shared_secret (32 B)
14. runtime.GC() — scrub transient buffers
15. main.local_init — if -path given use it; else enum drives A-Z via GetDriveTypeW (keep types 2/3/4)
16. print_local_paths → log
17. log "Percentage: %d%%"
18. if -delay N>0: sleep N minutes (loop with 1-second sleep until delay expired, respects ctx)
elif -time HH:MM: time.Parse; loop sleep 1 s until hour+min match, respects ctx
19. log "%s [LOCAL] Started."
20. go closure_progress_logger — wraps main.progress_logger (stats every 5 s)
21. go closure_local_main — wraps main.local_main (spawns walkers + encoders, see §4.1)
22. chanrecv1(c_1, nil) — block until local_main signals done
23. main.delete_shadow — embedded shellcode (VSS deletion via WMI, see §7)
24. main.empty_recycle_bin — SHEmptyRecycleBinW(NULL, NULL, 7)
25. if !-keep: main.self_delete — PowerShell secure-overwrite + Remove-Item loop
26. log "%s [LOCAL] All operations complited. Success - %d file(s), %s. Error - %d file(s), %s"
27. final defer: cancel ctx (propagates to all goroutines)
4.1 Thread architecture (main.local_main @ 0x5185a0)
c_2 = makechan(chan string, 100) — path queue walker → encoders
IF -threads N > 0 :
spawn N encoder workers (closure_local_encoder wrapping main.local_encoder)
FOR each root_path in local_paths :
wg_walkers.Add(1)
go main.local_main.func2(root_path) — wraps main.local_walk
IF -threads == 0 : — default
spawn 4 encoder workers per drive (closure_local_encoder wrapping main.local_encoder)
wg_walkers.Wait()
close(c_2) — signals encoders to drain and exit
wg_encoders.Wait()
chansend1(c_1, "done") — signal main
Thus by default: N_drives × 4 workers + N_drives walkers, pipelined fan-out/fan-in. The walker (main.local_walk) recurses, skips blacklists, drops the note into desktop/documents, and sends eligible file paths to c_2. Encoders consume c_2, rename files (random + extension), unlock via RM if needed, and call encode_file.
4.2 Per-file worker (main.local_encoder @ 0x517c80)
LOOP:
select : path ← c_2 (or ctx.Done())
parent = filepath.Dir(path)
new_name = parent + "/" + randStr(16) + "." + extension(from config)
os.Rename(path, new_name) — 1st attempt
IF rename fails :
time.Sleep(10 ms)
main.unlock_file(path) — rstrtmgr RmShutdown on PIDs holding the file
time.Sleep(10 ms)
main.set_attr(path) — clear FILE_ATTRIBUTE_READONLY
time.Sleep(10 ms)
os.Rename(path, new_name) — 2nd attempt (log error if still fails)
file = os.OpenFile(new_name, RDWR)
IF open fails :
time.Sleep + set_attr + retry
IF still fails : log "OpenFile ERROR", error counters += 1
ELSE :
main.encode_file(ctx, file, basename) — the crypto pipe
update success / bytes counters
close file
5. Encryption System (main.encode_file @ 0x516a60, 3,141 bytes)
5.1 Key exchange (main.load_config @ 0x517740)
| Parameter | Value |
|---|---|
| Algorithm | X25519 (ECDH over Curve25519) |
| Implementation | crypto/ecdh stdlib — x25519Curve.GenerateKey, PrivateKey.PublicKey, x25519Curve.NewPublicKey, PrivateKey.ECDH |
| Attacker public key (K1) | cdbe4aed37c98d67a005ef469e7e0586e0ff8973b91a8d577d320e67cf46b572 (32 B, .rdata → config gob) |
| Ephemeral private key | generated each run via crypto/rand.Reader (not exported, lives in RAM) |
| Ephemeral public key | derived via PublicKey() — written into every file's footer (32 B at +56, cf §5.4) so the attacker can recompute shared_secret = ECDH(attacker_priv, ephemeral_pub) |
| Output shared secret | shared_secret = ECDH(ephemeral_priv, attacker_pub) — 32 B used as the KEK (Key-Encryption-Key) |
load_config step-by-step:
- Builds a
bytes.Bufferinitialized with1F 8B 08 00 00(5 bytes);Write(5×0x00)→ full gzip header (10 bytes, all zeros after the magic). qmemcpycopies the 4,096 BaT_0blob (0x55ECBF) into a local buffer;Write(v77)appends it → thebytes.Bufferholds 4,106 bytes (1F 8B 08 00 00 00 00 00 00 00+ 4,096 B blob).compress/gzip.NewReaderdecompresses into 1,522 bytes of gob plaintext.encoding/gob.NewDecoder(gz).Decode(&EC)whereEC struct { K1 [32]uint8; EX string; NN string; NT []byte }.crypto/ecdh.X25519().GenerateKey(crypto/rand.Reader)→ephemeral_priv.ephemeral_priv.PublicKey()→ephemeral_pub(32 B). Stored in the globalg_ephemeral_x25519_pubkey@0x6C80C0(used at §5.4 to write into each file's footer).X25519().NewPublicKey(K1[:])→attacker_pub_obj(parses bytes → curve point).ephemeral_priv.ECDH(attacker_pub_obj)→shared_secret(32 B). Stored in the globalg_shared_secret@0x6C80E0, used byencode_fileas the KEK.
Expected error strings:
"Config unpack error."(gzip failure) — wrapped into anerrors.errorString"Config decode error."(gob failure) — idem- ECDH errors are propagated
5.2 Symmetric cipher (bulk file encryption)
| Parameter | Value |
|---|---|
| Algorithm | AES-256-CTR |
| Implementation | crypto/aes.NewCipher(key[32]) + crypto/cipher.NewCTR(block, IV[16]) |
| Per-file key | 32 B random (generated on 1st pass via crypto/rand.Reader through does_ReadAtLeast), stored in cleartext in the footer during perc < 100 (partial) passes, overwritten by its GCM-wrapped form at the final pass |
| Per-file IV | 16 B random, stored in cleartext in the footer (offset +8, §5.4) |
5.3 Key wrapping (final pass)
| Parameter | Value |
|---|---|
| Algorithm | AES-256-GCM |
| Implementation | crypto/aes.NewCipher(shared_secret[32]) + crypto/cipher.newGCMWithNonceAndTagSize(block, nonceSize=12, tagSize=16) |
| Nonce | 12 B = first 12 bytes of the footer's IV block |
| Plaintext | the per-file key (32 B) |
| Ciphertext | 48 B (32 + tag 16) written to the footer at offset +88 |
The same shared_secret is also used as an AES-CTR key to encrypt the original filename (v + 34 = 884 B buffer, UTF-8 filepath) — the original name is recoverable once you know the run's ephemeral_pub + the attacker_priv.
5.4 File format (encrypted file)
Layout confirmed via disassembly + Unicorn emulation :
offset size field description
──────────────────────────────────────────────────────────────────────
0 N-1024 encrypted_data AES-256-CTR chunks of the payload
N-1024 4 magic (u32 LE) 0x3828AC45 partial | 0x741FBE88 complete
N-1020 4 perc (u32 LE) value of -perc (1..100)
N-1016 16 iv[16] AES-CTR IV; first 12 bytes also serve
as GCM nonce (final pass)
N-1000 32 file_key[32] per-file AES-256 key (plaintext in pass1,
source for Seal at final pass)
N-968 32 ephemeral_pub[32] X25519 ephemeral public key — material
required for recovery (written only
at final pass, cf §5.4.1)
N-936 48 gcm_sealed[48] AES-256-GCM Seal output (32 B ct + 16 B tag)
— wraps file_key with shared_secret
N-888 884 filename_enc[884] AES-CTR(key=file_key, iv=iv) over the
original UTF-8 name, zero-padded (max 884 B)
N-4 4 chunk_counter (u32) number of chunks processed; cap 51200
──────────────────────────────────────────────────────────────────────
= 1024 B
5.4.1 Where the ephemeral_pub lives — data-flow validated
Traced across main.load_config and main.encode_file:
main.load_config (0x517740) :
PublicKey() result → runtime.memmove(dst=g_ephemeral_x25519_pubkey @ 0x6C80C0, src=..., 32)
ECDH() result → runtime.memmove(dst=g_shared_secret @ 0x6C80E0, src=..., 32)
main.encode_file (0x516a60), final pass @ 0x517584-0x517598 :
rbx = &g_ephemeral_x25519_pubkey
rdx = &footer + 0x38 (= footer+56)
runtime.memmove(rdx, rbx, 32) ← ephemeral_pub is copied at footer+56
rax = &g_shared_secret ← AES-GCM key
crypto_aes_NewCipher(key=rax, 32)
crypto_cipher_newGCMWithNonceAndTagSize(block, 12, 16)
Seal(block, dst=nil, nonce=footer+8[:12], plaintext=footer+24[:32], AAD=nil)
→ new 48-byte slice (ct+tag), memmove'd to footer+88
Recovery procedure (for the attacker holding attacker_priv):
- Read the 1,024 B footer.
ephemeral_pub = footer[+56 : +56+32]shared_secret = ECDH(attacker_priv, ephemeral_pub)(via X25519)file_key = AES-GCM.Open(key=shared_secret, nonce=footer[+8 : +8+12], ct=footer[+88 : +88+48])→ recovers the 32 B per-file keyplaintext = AES-CTR(key=file_key, IV=footer[+8 : +8+16])overfile[0 : filesize-1024]orig_filename = AES-CTR(key=file_key, IV=footer[+8 : +8+16])overfooter[+136 : +136+884], zero-stripped, UTF-8
A single ephemeral_priv per run ⇒ shared_secret is stable across the entire session ⇒ once a given ephemeral_pub has been paired with an attacker_priv computation, every file in that run shares the same KEK. Simple on the recovery side (monolithic).
5.4.2 Caveat for PARTIAL files
If magic = 0x3828AC45 (partial in progress), the footer contains the file_key in cleartext at offset +24 (not yet sealed). An analyst who snapshots the file at this stage can decrypt it without attacker_priv. The Seal only happens on the final pass.
5.5 Intermittent encryption (encode_file loop)
- Chunk size:
chunk_size = (perc << 8 / 100) << 12bytes. Concrete mapping (4 KB-aligned except rounding): perc = 1→ 8,192 B (8 KB)perc = 25→ 256 KBperc = 50→ 512 KBperc = 100→ 1 MiB (-fastforces this)- Stride: chunks are written at offsets
chunk_counter × 1 MiB— fixed 1 MiB stride regardless ofchunk_size. So atperc = 1, each 1 MiB block has only ~7 KB encrypted at the start, rest untouched. Atperc = 100, encryption is contiguous. - Loop:
1.
time.Now()(t0) — throttling start 2.os.File.ReadAt(buf, chunk_size, offset_read); skip if< 1024 Bremain (to not overwrite the footer) 3.os.File.Seek(offset_write, 0)(same position as read, minus 1,024 B footer padding) 4. CTR encryptbuf[:n-1024]5.os.File.Write(buf[:n-1024])6.os.File.Seek(offset_read, 0)(reposition for next iteration) 7.chunk_counter++;os.File.Write(chunk_counter)(4 B at file end) 8. Update global counters (bytes_encrypted, in 1 MiB or tail slices) 9.dt = Now() - t0; ifdt < 10 ms→time.Sleep(1 ms × min(999, ...))(auto I/O throttling, avoids the disk flood that would trigger behavioural detection) - Exit conditions:
byte_6C7CA3[0](-fastflag) → stop after 1 pass (the flag short-circuits the CTR loop past the first chunk)chunk_counter == 51200— guardrail (max 200 GB at 4 MB chunks, but practically caps protection against monster files)ctx.Done()— signal propagated (Ctrl+C or wrapper equivalent)- Finalisation (on loop exit):
1.
Seek(-1024, SEEK_END)→ positions at the footer 2.*magic = 0x741FBE88(complete) 3.memcpy(footer[+56..+88], g_ephemeral_x25519_pubkey[:32])4.AES-256-GCM.Seal(nonce=footer[+8 : +8+12], plaintext=per_file_key, aad=nil)→ 48 B (ct 32 + tag 16) 5. Copy sealed output tofooter[+88..+136]6.AES-256-CTR.XORKeyStream(footer[+136 : +136+884])— encrypts the original filename in place 7.binary.Write(file, footer)— final 1,024 B dump
5.6 Skip rules
- File
< 1024 B→ skipped with error"stat '%s' err: so small, %d" - File already marked
0x741FBE88(complete) → skipped (returns size) - File marked
0x3828AC45(partial in progress) → resumed (reads key/IV/chunk_counter from the footer, continues the CTR loop)
6. File Targeting
6.1 Targeted extensions
All (no allowlist) except what's in the blocklists.
6.2 Drive enumeration (main.local_init @ 0x51b4e0)
- If
-path <p>is given, only that path is added to the root list. - Otherwise: loop
for c = 'A' to 'Z'→Sprintf("%c:\\", c)→UTF16PtrFromString→GetDriveTypeW. Keeps drives returning2 (DRIVE_REMOVABLE),3 (DRIVE_FIXED),4 (DRIVE_REMOTE). Skips1 (DRIVE_NO_ROOT_DIR),5 (DRIVE_CDROM),6 (DRIVE_RAMDISK),0 (DRIVE_UNKNOWN).
6.3 Excluded directories (11, main.is_exclude_dir @ 0x5163e0)
Case-insensitive match (strings.EqualFold) on directory name alone:
| Dir | Reason |
|---|---|
windows |
OS stability |
programdata |
App config (keeps AV/services runnable) |
program files |
Installed binaries |
program files (x86) |
Installed binaries |
$recycle.bin |
Emptied at the end of the flow anyway |
all users |
Shared profiles (chains with programdata) |
winnt |
Legacy OS variant |
appdata |
User config |
application data |
XP variant of appdata |
local settings |
XP variant |
boot |
Windows boot files |
The walker also treats "." (first char == 0x2E) as a self-ref, and ".." (2 chars matching 0x2E2E) → skip.
6.4 Excluded files by exact name (9, main.is_exclude_file @ 0x5165e0)
| File | Reason |
|---|---|
ntldr |
Legacy boot loader |
ntdetect.com |
XP boot |
autoexec.bat |
Legacy boot script |
iconcache.db |
Explorer cache |
bootsect.bak |
Boot backup |
bootfont.bin |
Boot fonts |
bootmgr |
Vista+ boot manager |
thumbs.db |
Explorer cache |
RECOVERY_NOTES.TXT (via config) |
The note itself — prevents re-encryption of an already-dropped note |
6.5 Excluded by glob (8, path/filepath.Match)
ntuser.dat*, *.exe, *.dll, *.sys, *.msi, *.ini, *.inf, *.lnk.
6.6 File size floor
Any file < 1024 B is skipped upstream in the walker (size from DirEntry.Info().Size()); redundant with the check in encode_file which returns "so small" as a fallback.
6.7 Renaming
Final form: <parent>/<randStr(16)>.<EX_from_config> where randStr is 16 chars from [a-z0-9] (crypto/rand.Read + modulo 36). Extension comes from gob field EX → .8hmlsewu here. Victim-ID is NOT in the filename — it's only in the footer (sealed key) and in the Tor note URL.
7. Recovery Inhibition — VSS Deletion via WMI Shellcode
main.delete_shadow @ 0x51c140 (3,627 bytes).
| Step | Implementation |
|---|---|
1. Builds a bytes.Buffer with a 10 B inline gzip header (1F 8B 08 00 00 + 5 zeros) |
Same pattern as in load_config |
| 2. Writes 794 B of embedded blob | 10 B from stack literals (v90.array = 0x3E145154484D545D, v90.len low16 = 0x346F) + 784 B copied from unk_5A799A (.rdata) |
3. gzip.NewReader + io.copyBuffer → output bytes.Buffer |
1,313 B of x86-64 shellcode |
| 4. Resolves APIs via LazyDLL/LazyProc (10 fns stored in an 80 B slice) | kernel32!LocalAlloc, LocalFree, ole32!CoInitializeEx, CoInitializeSecurity, CoCreateInstance, CoSetProxyBlanket, oleaut32!SysAllocString, SysFreeString, VariantClear, user32!wsprintfW |
5. kernel32!VirtualProtect(shellcode, sz, 0x40 /*PAGE_EXECUTE_READWRITE*/, &old) |
Makes the page executable |
6. kernel32!CreateThread(0, 0, shellcode, api_table, 0, NULL) |
Dedicated thread |
7. syscall.WaitForSingleObject(thread) + syscall.CloseHandle(thread) |
Waits for completion |
Exact shellcode behaviour — validated via Capstone disassembly (250 instructions) + Unicorn emulation (full trace below):
API table layout (rbx = table passed by Go to CreateThread) :
+0x00 LocalAlloc +0x08 LocalFree +0x10 CoInitializeEx
+0x18 CoInitializeSecurity +0x20 CoCreateInstance +0x28 CoSetProxyBlanket
+0x30 SysAllocString +0x38 SysFreeString +0x40 VariantClear
+0x48 wsprintfW
Vtable offsets used :
IWbemLocator::ConnectServer +0x18
IWbemServices::DeleteInstance +0x80 ← direct DeleteInstance, NOT ExecMethod
IWbemServices::ExecQuery +0xA0
IEnumWbemClassObject::Next +0x20
IWbemClassObject::Get +0x20
IUnknown::Release +0x10
WMI strings built on the stack (UTF-16-LE, via `mov dword ptr [rsp+X], imm32`) :
"ROOT\CIMV2" → BSTR namespace
"WQL" → BSTR queryLang
"SELECT * FROM Win32_ShadowCopy" → BSTR query
"ID" → property name
"Win32_ShadowCopy.ID='%s'" → wsprintfW format string
CLSID/IID reconstructed (GUID form) :
CLSID_WbemLocator = {4590F811-1D3A-11D0-891F-00AA004B2E24}
IID_IWbemLocator = {DC12A687-737F-11CF-884D-00AA004B2E24}
Execution flow (Unicorn trace, simulated with 2 fake shadow copies):
1. CoInitializeEx(NULL, COINIT_MULTITHREADED=2)
2. CoInitializeSecurity(NULL, -1, NULL, NULL, authnLvl=DEFAULT(0),
impLvl=IMPERSONATE(3), NULL, 0, NULL)
3. CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER(1),
IID_IWbemLocator, &pLoc)
4. SysAllocString("ROOT\\CIMV2") → BSTR ns
5. IWbemLocator::ConnectServer(ns, NULL, NULL, NULL, 0, NULL, NULL, &pSvc)
6. CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT(10), RPC_C_AUTHZ_NONE(0), ...)
7. SysAllocString("WQL") → BSTR queryLang
8. SysAllocString("SELECT * FROM Win32_ShadowCopy") → BSTR query
9. IWbemServices::ExecQuery(queryLang, query,
WBEM_FLAG_FORWARD_ONLY(0x20), NULL, &pEnum)
10. LocalAlloc(LPTR(0x40), 1024) → VARIANT storage buffer
11. LOOP (while Next returns ≥ 1 object) :
Next(WBEM_INFINITE(-1), 1, &pObj, &retCount)
Get("ID", 0, &variant, NULL, NULL)
IF variant.vt == VT_BSTR (8) :
wsprintfW(buffer, L"Win32_ShadowCopy.ID='%s'", variant.bstrVal)
SysAllocString(buffer) → BSTR objPath
IWbemServices::DeleteInstance(objPath, 0, NULL, NULL) ← KILL
SysFreeString(objPath)
VariantClear(&variant)
pObj->Release()
12. LocalFree ; pEnum->Release()
SysFreeString(query) ; SysFreeString(queryLang) ; pSvc->Release()
SysFreeString(ns) ; pLoc->Release()
(no CoUninitialize — thread exits naturally)
Why embedded as shellcode? Go has no native COM support; doing this in pure Go would require extensive syscall.Syscall* or CGo. A compact native shellcode (1,313 B) is simpler. Standard push rbx/rbp/rsi/rdi/r12/r13/r14 + sub rsp, 0xE0 prologue; WMI strings never materialised globally (built char-by-char on the stack via mov dword/word ptr [rsp+X], imm) to avoid static signatures. Complete Unicorn decoder at emulate_m3rx_shellcode.py in this folder.
Mapping: [U0305 / T1070.004] Volume Shadow Copy Service (VSC,VSS) Deletion.
8. Targeted Services
None — no service stop/disable (no OpenService/ControlService/ChangeServiceConfig call). The binary relies on Restart Manager (§6.1 / unlock_file) to resolve locked files at encryption time, not on a pre-kill before enumeration.
9. Targeted Processes
No proactive kill — no CreateToolhelp32Snapshot/TerminateProcess. The only termination vector is Restart Manager in main.unlock_file @ 0x51af60, invoked ONLY on a failed rename/open on a target file:
RmStartSession(&session, 0, key)
RmRegisterResources(session, 1, filepath, 0, NULL, 0, NULL)
RmGetList(session, &procInfoNeeded, &procInfoCount, procInfo, &rebootReasons)
IF procInfoCount > 0 : RmShutdown(session, RmForceShutdown /*1*/, NULL)
RmEndSession(session)
Effect: Windows lifts up every process holding a handle on the target file and force-shutdowns them (RM_SHUTDOWN_TYPE.RmForceShutdown). Very surgical compared to an explicit kill-list: only targets what is actually blocking a file, when it's blocking. Primitives reused multiple times across the run (local mutex m_ guards the API resolution between calls).
10. Persistence & Evasion
No disk/registry persistence. One-shot execution then self-wipe.
10.1 Single-Instance Mutex (main.mutex_exists @ 0x51ae80)
name := fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte("kek" + os.Hostname())))
handle, err := windows.CreateMutex(nil, false, utf16(name))
return err != nil // i.e. ERROR_ALREADY_EXISTS
Global mutex (not Local\ / Global\ prefix — default namespace). Name is the decimal form of CRC32("kek" + Hostname()) — signature varies per hostname.
10.2 Admin / Integrity Level Gate (main.check_admin @ 0x51bb20)
hToken := OpenProcessToken(CurrentProcess, TOKEN_QUERY)
GetTokenInformation(hToken, TokenIntegrityLevel=25, buf, ...)
psid := buf.Label.Sid
il := *GetSidSubAuthority(psid, *GetSidSubAuthorityCount(psid) - 1)
if il < 0x3000 : return errors.New("Current IL = 0x%04X, not elevated.")
Refuses to run unless the token is HIGH integrity (SECURITY_MANDATORY_HIGH_RID = 0x3000) or above (SYSTEM = 0x4000). Not a UAC bypass — just a guardrail: declines to run in Medium. ATT&CK mapping: T1480 (Execution Guardrails).
10.3 Console Hiding (main.hide_console @ 0x51bfe0)
ShowWindow(GetConsoleWindow(), SW_HIDE=0) — triggered by -hide flag (default visible, suggesting an operator-first usage where the console acts as feedback).
10.4 Self-Delete via PowerShell Secure Overwrite (main.self_delete @ 0x51b980)
Command built by runtime.concatstring3:
part1 = "$f='"
part2 = <self executable path> (via runtime.startTheWorld.func1_0 — actually os.Executable misnamed by Lumina)
part3 = "';while(Test-Path -Path $f){$o=new-object byte[] 10485760;(new-object Random).NextBytes($o);[IO.File]::WriteAllBytes($f,$o);Remove-Item -Path $f;Sleep 1;}"
Then os.exec.Command("powershell.exe", "-c", full_cmd).Start() (fire-and-forget). Effect: as long as the file exists, a PS thread overwrites it with 10 MiB of random bytes and then deletes it, with Sleep 1. Once the main .exe exits, the loop succeeds. 10 MB random is overkill for a 2.5 MB binary (ensures overwrite past filesystem slack). Mapping: [U1007] File Melt / T1070.004 / T1059.001 (PowerShell).
10.5 Empty Recycle Bin (main.empty_recycle_bin @ 0x51ba40)
shell32!SHEmptyRecycleBinW(NULL, NULL, SHERB_NOCONFIRMATION|SHERB_NOPROGRESSUI|SHERB_NOSOUND = 7) — silent. Prevents trivial restoration of a file that had been "moved" before encryption.
10.6 File Attribute Fix-Up (main.set_attr @ 0x51b900)
GetFileAttributesW + &= ~FILE_ATTRIBUTE_READONLY (0x1) + SetFileAttributesW. Only clears READONLY; doesn't touch HIDDEN/SYSTEM. Invoked on open failure in the recovery path. Not anti-analysis per se — operational primitive.
10.7 I/O Throttling
Encryption loop with auto-sleep (cf §5.5) — caps effective I/O below ~100 MB/s, makes the burst detectable-less-sharp than a classic encrypt flood.
10.8 Anti-Analysis Summary
| Category | Status | Notes |
|---|---|---|
| Anti-debug | None active in user code | The int 3 / INT 2D detected automatically are in runtime.debugCall* (Go runtime for Delve) — false positive |
| Anti-VM | None | cpuid present in internal_cpu.cpuid (SSE/AVX runtime detection) — no VMware/VBox/hypervisor check |
| Anti-sandbox | None explicit | No "maltest/sandbox/john" username check, no sleep-acceleration detection. time.Sleep is used, but as crypto throttling not as a lure |
| Anti-disasm | None | No junk insns, no overlap, no self-mod (aside from the shellcode loader) |
| String obfusc. | Partial | Config + shellcode are gzip (decrypted at runtime). Go runtime strings and API names are in clear |
| API hashing | No | GetProcAddress via LazyProc.Find (literal name). Automatic [U0217] mapping is a false positive — name is literal |
| Privilege guard | Yes | check_admin ≥ HIGH IL — rejects standard users |
| Kill switch | No | No self-check country/lang (no CIS exclusion) |
Summary: ransomware minimalist on the evasion side (no anti-debug/VM/sandbox tricks), but solid on anti-forensics (secure self-delete, VSS wipe, recycle bin emptied).
11. Command-Line Arguments
Parsed via flag.FlagSet (9 flags, explicit defaults):
| Flag | Type | Default | Description |
|---|---|---|---|
-path <string> |
string | "" |
Root path (else enum drives) |
-delay <int> |
int | 0 |
Pre-exec delay in minutes. Loop sleeps 1 s × 60 × delay, respects ctx |
-time <HH:MM> |
string | "" |
Fixed start time (time.Parse("15:04")). Loop sleeps 1 s until match. Mutually exclusive with -delay |
-perc <int> |
int | 1 |
Percentage of each file to encrypt (chunk size = (perc<<8)/100<<12 bytes). Min 1%, max 100% |
-threads <int> |
int | 0 |
Encryption workers. 0 → 4 workers × N_drives (auto). N > 0 → N global workers |
-keep |
bool | false |
Don't auto-delete the binary after run |
-hide |
bool | false |
Hide the console (SW_HIDE) |
-fast |
bool | false |
Force perc=100 (full encryption) |
-log <string> |
string | "" |
Log file path. Empty → $USERPROFILE\progress.log when -hide or -log is set |
Typical operator usage:
ransomware.exe -hide -perc 10 -threads 8 -log "C:\temp\out.log"
ransomware.exe -fast -time "23:00"
ransomware.exe -path "D:\shared" -keep
12. Static Imports Summary
| Category | Key APIs |
|---|---|
| Crypto | (all via crypto/* stdlib — no Win32 import): aes.NewCipher, cipher.NewCTR, cipher.newGCMWithNonceAndTagSize, ecdh.X25519().GenerateKey/NewPublicKey/ECDH, rand.Reader, hash/crc32.ChecksumIEEE |
| File I/O | Static: kernel32!CreateFileA, WriteFile. Dyn (via Go stdlib os/syscall): CreateFileW, ReadFile, MoveFileExW, GetFileAttributesW, SetFileAttributesW, GetFileInformationByHandleEx |
| Process | Static: CreateThread, ExitProcess. Dyn: CreateProcessW, OpenProcessToken, GetTokenInformation |
| Registry | None |
| Network | ws2_32 present (Go runtime), but no user socket (no online C2, no SMB crawling) |
| Memory | Static: VirtualAlloc, VirtualFree, VirtualQuery. Dyn: VirtualProtect (for the shellcode) |
| COM / WMI | ole32, oleaut32 — via JIT shellcode, no Go-side symbols |
| Restart Manager | rstrtmgr!Rm* — dyn |
| Shell | shell32!SHEmptyRecycleBinW, user32!ShowWindow, kernel32!GetConsoleWindow |
| Exec | os/exec.Command / .Start → CreateProcessW under the hood (PowerShell spawn) |
13. Indicators of Compromise (IOCs)
13.1 Hashes
| Type | Value |
|---|---|
| SHA-256 | b09ece33ffe5efb1903526229595a8c74d983c731505bee09c2a005036c834b8 |
| MD5 | 071e2e0087554d96bba6a4ab73d88cd0 |
13.2 Network
| Type | Value |
|---|---|
| Onion (neg. portal) | pippahtohg6qgioqu3ixrsueefuw7thythmmeanyrgwn3eixcuu6jvqd.onion |
| Tox ID (support) | 9A1217BEDA4AB77052A25D17CB6FFB34AFA2BE462E607F2FD8E1DF1DDD4CA16A64E18B1A0BF2 |
13.3 Files
| Indicator | Value |
|---|---|
| Encrypted extension | .8hmlsewu |
| Ransom note | RECOVERY_NOTES.TXT (in every dir matching desktop/documents, case-insensitive) |
| Renamed files | <randStr16>.<ext> — 16 chars [a-z0-9] + extension |
| Footer magic (complete) | 0x741FBE88 (4 LE bytes: 88 BE 1F 74) at the last 1,024 bytes |
| Footer magic (partial) | 0x3828AC45 (4 LE bytes: 45 AC 28 38) — file in progress |
| Footer size | 1,024 B at end of every encrypted file |
| Log file | %USERPROFILE%\progress.log when -hide or -log is set |
13.4 Registry
None — no key created/modified.
13.5 Mutex
- Global:
str(CRC32("kek" + Hostname()))in decimal. - Example: hostname
DESKTOP-ABC→ CRC32 = some value →Sprintf("%d", crc).
13.6 Behavioural
- Creates a global mutex (name = decimal CRC32 that includes
"kek"+ hostname) - Opens the process token, reads
TokenIntegrityLevel(policy-based guard) - Generates an ephemeral X25519 keypair via
crypto/rand - Enumerates
A:\throughZ:\viaGetDriveTypeW - Recursively walks retained drives, sorts dirs/files, walk/fan-out
- Per eligible file:
MoveFileExW(rename), thenCreateFileW+ chunked AES-CTR encryption - Calls
rstrtmgr!RmStartSession/RmRegisterResources/RmGetList/RmShutdown/RmEndSessionwhen a rename fails (file locked) - Calls
SetFileAttributesWto strip READONLY VirtualProtect+CreateThreadon a.rdata-decompressed blob (WMI shellcode) → COM chain callingWin32_ShadowCopy.Delete()SHEmptyRecycleBinWCreateProcessW("powershell.exe -c ...")with secure self-delete payload- Communication: no outbound connection (victim redirected to Tor Browser out-of-band)
13.7 Distinctive Strings
"Config unpack error."/"Config decode error."— hints at gzip+gob embedded config"check_admin err: %s"+"Current IL = 0x%04X, not elevated.""only one instance"— mutex already exists"%s [LOCAL] Started."/"%s [LOCAL] All operations complited."(NOTE: typo "complited" instead of "completed")"%s [LOCAL] %s per sec. Success - %d file(s), %s. Error - %d file(s), %s\n"— progress ticker"%s [LOCAL] Rename ERROR: %s\n"/"%s [LOCAL] OpenFile ERROR: %s\n"/"%s [LOCAL] encode_file ERROR: %s\n"/"%s [LOCAL] NOTE ERROR: %s\n"(systematic[LOCAL]prefix → suggests aNETWORKvariant)"Percentage: %d%%\n"/"Delayed start after %d min. Waiting.\n"/"Delayed start at %02d:%02d. Current time is %02d:%02d. Waiting.\n"/"Can't decode -time parameter. err: %s""aes.NewCipher '%s' err: %s"/"stat '%s' err: so small, %d"/"Seek '%s' err: %s"/"binary.Read '%s' err: %s"/"binary.Write '%s' err: %s"— error formats"\\progress.log"— default log filename"$USERPROFILE"— log path base"string too long: %d bytes, max %d"(inNameToBuf)"-c"+"powershell.exe"— self-delete spawn"$f='"+"';while(Test-Path -Path $f){$o=new-object byte[] 10485760;(new-object Random).NextBytes($o);[IO.File]::WriteAllBytes($f,$o);Remove-Item -Path $f;Sleep 1;}"— highly distinctive signature"kek"— mutex name prefix"abcdefghijklmnopqrstuvwxyz0123456789"— randStr charset"[LOCAL]"— log prefix- Dynamic DLL name strings:
"rstrtmgr.dll","ole32.dll","oleaut32.dll","shell32.dll","user32.dll","kernel32.dll"(literals) "LocalAlloc","LocalFree","VirtualProtect","CreateThread","GetConsoleWindow","ShowWindow","SHEmptyRecycleBinW"— LazyProc targets"CoInitializeEx","CoInitializeSecurity","CoCreateInstance","CoSetProxyBlanket","SysAllocString","SysFreeString","VariantClear","wsprintfW"— WMI shellcode resolutions"RmStartSession","RmRegisterResources","RmGetList","RmShutdown","RmEndSession"— Restart Manager"GetDriveType"/"TokenIntegrityLevel"/"CreateMutex"— Win32 primitives via x/sys/windows
13.8 Unique Artefacts
- Encrypted file has exactly 1,024 B of footer with the two magics above and a chunk counter at the end; easy YARA signature (look for
88 BE 1F 74 XX XX XX XXat-1024ofEOF+len(file) > 1024). - The
RECOVERY_NOTES.TXTnote is dropped only insideDesktoporDocumentsdirs (case-insensitive) — not at drive root, not in every folder. Hunt path typically:C:\Users\*\Desktop\RECOVERY_NOTES.TXT,C:\Users\*\Documents\RECOVERY_NOTES.TXT. - The
.exeis always auto-deleted unless-keep→ on a post-run host, trace of the binary is gone; remaining: encrypted files, optionalprogress.log, note insideDesktop/Documents.
14. MITRE ATT&CK Mapping
| ID | Technique | Implementation |
|---|---|---|
| T1486 | Data Encrypted for Impact | Per-file AES-256-CTR (§5.2) + AES-256-GCM wrap (§5.3) with X25519 ECDH (§5.1) |
| T1490 | Inhibit System Recovery | main.delete_shadow — WMI shellcode calling Win32_ShadowCopy.Delete() (§7) |
| T1070.004 | File Deletion | main.self_delete (PowerShell secure overwrite + Remove-Item, §10.4); main.empty_recycle_bin (§10.5) |
| T1070 | Indicator Removal on Host | Combination of self-delete + empty recycle bin + no Windows event logs |
| T1140 | Deobfuscate/Decode Files or Information | Config gzip+gob (§5.1); shellcode gzip (§7) |
| T1480 | Execution Guardrails | check_admin (IL ≥ HIGH) refuses to run otherwise (§10.2) |
| T1027 | Obfuscated Files or Information | Config + shellcode gzip-compressed (hide-in-plain-sight of strings) |
| T1027.007 | Dynamic API Resolution | LazyDLL + LazyProc.Find (= LoadLibrary + GetProcAddress) for 17 dynamic DLLs |
| T1027.010 | Command Obfuscation | PowerShell self-delete one-liner built by runtime concatenation |
| T1047 | Windows Management Instrumentation | VSS delete shellcode via WMI COM (§7) |
| T1055 | Process Injection (self) | VirtualProtect(PAGE_EXECUTE_READWRITE) + CreateThread on decompressed buffer (self-injection in the Go process) |
| T1059.001 | Command and Scripting Interpreter: PowerShell | powershell.exe -c for self-delete |
| T1564.003 | Hide Artifacts: Hidden Window | ShowWindow(HWND, SW_HIDE) (-hide flag) |
| T1083 | File and Directory Discovery | main.local_walk (os.ReadDir recursive) |
| T1082 | System Information Discovery | os.Hostname() (for mutex); GetDriveTypeW (for drive filtering); GetTokenInformation(TokenIntegrityLevel) |
| T1106 | Native API | Heavy usage via golang.org/x/sys/windows and dynamic LazyProc |
| T1657 | Financial Theft | Note asks for BTC payment via Tor portal |
| T1561 | Disk Wipe (partial) | -fast or -perc 100 → complete overwrite of all targeted data |
15. Summary
m3rx (variant LOCAL) is a Go-compiled Windows x64 ransomware, minimalist on the evasion side but well-structured on crypto and anti-forensics. Characteristic: the whole flow — CLI parsing, config, crypto, file enumeration, threading pipeline, self-delete — fits in 30 main.* functions (≈ 30 KB of user code) built on Go stdlib (crypto/aes, crypto/cipher, crypto/ecdh, encoding/gob, compress/gzip, os/exec, sync, context). No packer, no code obfuscation, Go symbols preserved via pclntab ⇒ fast analysis after Lumina.
Sound, classic cryptographic architecture, two layers:
- Inter-file: X25519 ECDH with embedded attacker pubkey (gzip+gob 4,096 B blob at
0x55ECBF); one ephemeral pair per instance (= per victim) ⇒shared_secret32 B stable for the whole session, used as KEK. - Intra-file: per-file key 32 B + IV 16 B random, AES-256-CTR in chunks (
(perc<<8)/100<<12B, throttled ≤999 ms), finalised by AES-256-GCM wrap of the file-key (nonce = first 12 B of the IV field, i.e.footer[+8 .. +20]) + AES-CTR of the original filename; magics0x3828AC45(partial) /0x741FBE88(complete) at the start of the 1,024 B footer.
Operational strategy "gentle but thorough": partial encryption by default (1%), -fast to bypass (100%); auto throttling for I/O stealth; rename to <16 random chars>.8hmlsewu + Tor victim-ID to avoid trivial inter-file correlation. Removable/fixed/remote drives enumerated (skip CD/RAM). No service stop, no kill-listed processes; the only reaction to locked files is Restart Manager at the tail (RmShutdown(FORCE)).
Prominent anti-forensics: VSS deletion via embedded WMI shellcode (gzip → JIT via VirtualProtect+CreateThread), silent recycle bin empty, PowerShell self-delete with 10 MiB random overwrite loop before Remove-Item. No online C2 (victim ↔ operator via Tor Browser out-of-band); no exfiltration (the "files have been stolen" wording relies on an external stealer not in this binary).
Simple guardrail T1480: refuses to run in Medium IL — requires prior elevation (UAC already consented); global mutex CRC32("kek"+Hostname()) prevents multi-instance. RECOVERY_NOTES.TXT dropped ONLY in Desktop/Documents (case-insensitive), contains onion + unique chat path (WAJCTZ6FOJF75KB5XVRYBFERX6) + Tox, with double-extortion rhetoric (publication + sharing with competitors).
Sophistication level: medium. Solid crypto and anti-forensics, but: - No anti-debug, no anti-VM, no anti-sandbox. - No propagation (SMB, WMI/PsExec, WMIExec). - No AV/service kill. - Strings in clear except config and shellcode. - Characteristic misspelling "complited" in the final log.
Operational risk level: high as soon as the attacker lands a HIGH IL run — the binary does exactly what's needed to make recovery impossible without the attacker's X25519 private key, and wipes its traces. The incident-response window is short (fire-and-forget self-delete at end of run).
Cryptographic properties observed statically:
- attacker_priv (X25519 private key matching K1) is required to compute shared_secret = ECDH(attacker_priv, ephemeral_pub) and unwrap file_key.
- RNG is crypto/rand; X25519 + AES-CTR + AES-GCM come from the Go stdlib.
- Per-file random 16 B IV, per-file random 32 B file_key; no nonce reuse across files, no key reuse.
- Files with footer magic 0x3828AC45 (partial pass) contain file_key in clear at footer+24.
Appendix A — Extracted artefacts
| Artefact | File | Size |
|---|---|---|
Config gzip+gob blob (raw, from .rdata) |
m3rx_blob_v2.bin |
4,200 B (bytes @ 0x55ECBF) |
| Config gzip (full stream) | m3rx_config_blob.bin |
4,101 B (hdr 5B + blob 4096B) |
| Config gzip (blob only, no header) | m3rx_config_blob_raw.bin |
4,096 B |
| Config plaintext (after gzip.decompress) | m3rx_config_plain.bin |
1,522 B |
| Shellcode gzip blob (raw) | m3rx_shellcode_blob.bin |
794 B |
| WMI shellcode (after gzip.decompress, x86-64) | m3rx_wmi_shellcode.bin |
1,313 B |
Appendix B — Extracted gob struct main.EC
type EC struct {
K1 [32]uint8 // Attacker X25519 public key
EX string // Extension (no leading dot)
NN string // Note filename
NT []byte // Note content (raw bytes)
}
// Decoded values:
// K1 = cdbe4aed37c98d67a005ef469e7e0586e0ff8973b91a8d577d320e67cf46b572
// EX = "8hmlsewu"
// NN = "RECOVERY_NOTES.TXT"
// NT = <1361 bytes, see §3>
Appendix C — Footer layout (1,024 B, little-endian) — VALIDATED
offset size field notes
──────────────────────────────────────────────────────────────────
+0 4 B magic (u32 LE) 0x3828AC45 = partial (file_key in clear)
0x741FBE88 = complete (file_key sealed)
+4 4 B perc (u32 LE) value of -perc flag (1..100)
+8 16 B iv[16] AES-CTR IV (random pass1); first 12 bytes
also serve as GCM nonce
+24 32 B file_key[32] per-file AES-256 key (random)
• pass1 / partial : plaintext (recoverable!)
• final pass : plaintext input to Seal,
sealed form at +88
+56 32 B ephemeral_pub[32] ← X25519 ephemeral public key (written at
final pass via memmove from global
g_ephemeral_x25519_pubkey @ 0x6C80C0)
— required for recovery by the attacker
+88 48 B gcm_sealed[48] AES-256-GCM Seal output = ct(32) || tag(16)
plaintext input = file_key (32 B)
key = g_shared_secret @ 0x6C80E0 (ECDH result)
nonce = iv[0..12]
AAD = none
+136 884 B filename_enc[884] AES-CTR(key=file_key, iv=iv) over the
original UTF-8 filename, zero-padded
+1020 4 B chunk_counter (u32) number of CTR chunks processed; cap 51,200
──────────────────────────────────────────────────────────────────
Total = 1024 B (IDA struct `m3rx_Footer` defined in the IDB)
Appendix D — WMI shellcode fingerprint (first 48 bytes)
E9 00 00 00 00 40 53 56 55 57 41 54 41 55 41 56
48 81 EC E0 00 00 00 45 33 F6 B8 3A 1D 00 00 48
8B D9 66 89 44 24 64 B8 D0 11 00 00 41 8D 56 02
Standard x86-64 prologue (push rbx/rbp/rsi/rdi/r12/r13/r14 + sub rsp, 0xE0); B8 3A 1D 00 00 = mov eax, 0x1D3A — intermediate word of CLSID_WbemLocator (bytes 3A 1D); wide-string construction on the stack via 66 C7 44 24 ?? (D$XX patterns) — standard WMI shellcode with no statically-resolvable strings.
Appendix E — Unicorn emulation trace (WMI shellcode)
Complete trace captured while simulating 2 fake Win32_ShadowCopy instances (see emulate_m3rx_shellcode.py):
1. CoInitializeEx(reserved=0x0, coinit=2) [COINIT_MULTITHREADED]
2. CoInitializeSecurity(pSD=0, cAuth=-1, authSvc=0, reserved1=0,
authnLvl=0, impLvl=3) [DEFAULT, IMPERSONATE]
3. CoCreateInstance(CLSID={4590F811-1D3A-11D0-891F-00AA004B2E24}, [CLSID_WbemLocator]
IID={DC12A687-737F-11CF-884D-00AA004B2E24}, [IID_IWbemLocator]
ctx=1) [CLSCTX_INPROC_SERVER]
4. SysAllocString(L"ROOT\\CIMV2") → BSTR ns
5. IWbemLocator::ConnectServer(ns=L"ROOT\\CIMV2") → *ppSvc=pSvc
6. CoSetProxyBlanket(proxy=pSvc, authnSvc=10, authzSvc=0) [RPC_C_AUTHN_WINNT]
7. SysAllocString(L"WQL") → BSTR queryLang
8. SysAllocString(L"SELECT * FROM Win32_ShadowCopy") → BSTR query
9. IWbemServices::ExecQuery(lang, query, flags=0x20) [WBEM_FLAG_FORWARD_ONLY]
10. LocalAlloc(flags=0x40, size=1024) [LPTR]
11. [LOOP it1] IEnumWbemClassObject::Next() → obj1, retCount=1
12. IWbemClassObject::Get(prop=L"ID") → VT_BSTR, L"{GUID1}"
13. wsprintfW(fmt=L"Win32_ShadowCopy.ID='%s'", L"{GUID1}")→ L"Win32_ShadowCopy.ID='{GUID1}'"
14. SysAllocString(L"Win32_ShadowCopy.ID='{GUID1}'") → BSTR path1
15. *** IWbemServices::DeleteInstance(path1, flags=0x0) *** [KILL]
16. SysFreeString(path1) ; VariantClear() ; Release(obj1)
17. [LOOP it2] IEnumWbemClassObject::Next() → obj2, retCount=1
18. IWbemClassObject::Get(prop=L"ID") → VT_BSTR, L"{GUID2}"
19. wsprintfW + SysAllocString + DeleteInstance(path2) [KILL]
20. SysFreeString + VariantClear + Release(obj2)
21. [LOOP end] IEnumWbemClassObject::Next() → retCount=0 (exit)
22. LocalFree ; Release(pEnum) ; SysFreeString(query) ;
SysFreeString(queryLang) ; Release(pSvc) ; SysFreeString(ns) ;
Release(pLoc)
Key observations:
- No global WMI string — all built on stack via
mov dword/word ptr [rsp+X], imm; invisible tostrings(1)on the extracted shellcode, reconstructible at runtime. - No
CoUninitialize— the thread exits, Windows cleans up. - No rigorous error checking: failures silently fall through via shared epilogue paths.
- Direct
DeleteInstance(vtable +0x80) instead ofExecMethod(..., L"Delete", ...)→ highly discriminant signature.