Launcher architecture and lineage
This document describes the recovered July 2026 Tax Notice build. All code locations are relative virtual addresses (RVAs) within Executable B unless explicitly attributed to another component. Internal function names are analyst role labels. Runtime observations are documented in Runtime analysis.
Component identities
| Component | Size, bytes | SHA-256 |
|---|---|---|
| Campaign ZIP | 248,759 | 7108FF29916D064216AA2ECE7FB395F1E3A73D12D19895BFFC0BD46806CBF85A |
Tax_Notice_23665.img |
641,024 | 38EC1F5E23F65B10AE3027BEABFA0BF7F9FB686355A9E33C7E7E44E6A998E04C |
Signed host, Tax_Notice_23665.exe |
120,984 | 93DD8B7B393289F88493596FAA4AE70054D9EB4FE47F2DD334F0C6BB5262F2A8 |
Carrier, nvdahelperremote.dll |
455,527 | 7295090C2CB63EBC43F932451971C41F9D015D2741E97AE3D9855F5AE87CFF94 |
| Transformed package | 415,071 | 0419AE7381CAA97172C40F5AEA601B8A22F1F58D27F3930509AF5808E043F65E |
| Executable A, wrapper/mapper | 397,312 (0x61000) |
28B8EB812E0F0AB724475BD51E3DC1F618BCB08B05998C414D4193009BF8D598 |
Executable B, PackClientLauncher.exe |
271,872 (0x42600) |
46B34789196733FAB62193F0AAEDB198B09F1362F9B10CA1DD70CF81D68B01AD |
Reconstructed PackClientCore.dll |
985,088 | 4DE6EF8647FB4B599966A233740CB0514D1E71B8019A1A1792ED7E1E514EDF1C |
Tax_Notice_23665.img contains the signed host and colocated carrier: carrier extent 0xF000, host extent 0x7E800. The host is an AMD64 NVDA/NV Access executable with only two imports, KERNEL32!ExitProcess and nvdaHelperRemote.dll!injection_initialize; the helper IAT slot is at host RVA 0xF268. The malicious colocated DLL uses a legitimate helper name. This supports a DLL search-order sideload configuration, subsequently corroborated by a runtime Load Image event. See NV Access's helper documentation and MITRE T1574.001.
Carrier and transformed package
The carrier is PE32+ AMD64, with five sections, entry RVA 0x2AE4, and export-library name nvdaHelperRemote_v16_5.dll. Its exports are injection_initialize at carrier RVA 0x1EC0 and injection_terminate at 0x1ED0. At RVA 0x1004, the signed host directly calls the imported nvdaHelperRemote.dll!injection_initialize.
Three carrier call sites establish its process-creation contracts:
| Carrier RVA | Recovered call and arguments | Role |
|---|---|---|
0x1656 |
CreateProcessW, null application name, command built from schtasks /Create /TN NvSvc /TR … /SC ONLOGON /RL HIGHEST /F |
Creates the logon task |
0x18E2 |
ShellExecuteExW, verb runas, current module path, null parameter and directory fields |
Requests self-elevation |
0x1BEB |
CreateProcessW, System32/SysWOW64 svchost.exe path, null command line, flags 0x08000004 (CREATE_NO_WINDOW plus CREATE_SUSPENDED) |
Creates a suspended surrogate |
The surrogate’s process and thread handles flow into native/WOW64 context handling and ResumeThread in the carrier routine at 0x1900. In Triage task 260828-py7ysahr4y, Tax_Notice_23665.exe issued 53 sandbox-labelled WriteProcessMemory operations against a newly created SysWOW64\svchost.exe. A new 0x66000-byte private region appeared at 0x00440000, followed by SetThreadContext on the primary thread. The same sequence recurs throughout the reviewed public July and August Triage telemetry.
Together, the static and runtime evidence establish remote package placement and primary-thread context hijacking. The exact carrier code responsible for the writes and the instruction pointer installed by SetThreadContext remain unresolved. No image replacement or remote-thread creation was observed.
The carrier reads its own or same-directory file through a whole-file read-only mapping. The recovered path uses GetModuleFileNameW at carrier RVA 0x1156, the same-buffer call at 0x1165, and mapping function 0x2100. It calculates the maximum PE section raw end, 0x4E00, and reads the appended record there.
| Carrier file range, half-open | Purpose |
|---|---|
0x00000–0x04E00 |
Conventional mapped PE headers and sections |
0x04E00–0x04E08 |
LE32 key 0x70; LE32 record length 0x6555F |
0x04E08–0x6A367 |
Record transformed bytewise with XOR 0x70 |
0x6A367–0x6F367 |
Separate opaque 0x5000-byte suffix; purpose unresolved |
The header bytes are 70 00 00 00 5F 55 06 00. The deterministic transform produces the package hash above. The earlier long printable 0x70 run becomes NUL padding; constant XOR preserves entropy. The separate 0x5000-byte suffix remains unresolved; its SHA-256 is CA34888C3172DA092E4FFF700C4018C3FC1182BEE6B955C66B430B6FF77176B1.
| Transformed-record range | Interpretation |
|---|---|
0x00000–0x00005 |
Leading E8 rel32 with displacement 0x627C0 |
0x00005–0x627C5 |
Call-over-data span; its first DWORD repeats 0x627C0 |
0x0128D–0x6228D |
Executable A |
0x01CC1–0x442C1 |
Executable B, nested inside A's .rdata |
0x6228D–0x627C5 |
0x538 bytes of NUL padding |
0x627C5–0x6555F |
Four-byte entry stub, 11,647-byte Donut x86 loader, and 23 zero bytes |
At record offset 0x627C5, the four-instruction stub (pop ecx; pop edx; push ecx; push edx) recovers the embedded-instance pointer from the opening CALL while preserving the caller’s return address. The next 11,647 bytes, beginning at 0x627C9, exactly match LOADER_EXE_X86 from Donut source revision 47758d787209dd1744f58c140102ac91b649df16. Their SHA-256 is 0C29CCCFF1B027D57C467564A333E9ADE455144649909A4B797B09B43002AC71. The final 23 bytes are zero.
The Donut instance is embedded in the package rather than staged over HTTP or DNS. It contains 63 API hashes and lists ole32, oleaut32, wininet, mscoree, and shell32 as dependencies. Compression and AMSI/WLDP/ETW bypasses are disabled, and mapped PE headers are overwritten. The loader starts the 397,312-byte unmanaged executable A in a new thread. Exit option 3 blocks indefinitely, while an original-entry-point value of 0 prevents the hijacked primary thread from returning to the original svchost.exe entry point.
A maps B inside the current process
A is a native PE32 x86 executable with three sections, timestamp 0x6A27AE4B, entry RVA 0x1308, preferred base 0x400000, and image size 0x64000.
At A RVA 0x2000, the 52-byte prefix preceding B comprises ten import-thunk DWORDs (nine entries plus terminator), PACKPAY1 at 0x2028, and B's raw size 0x42600 at 0x2030. B begins at A RVA 0x2034.
A validates the descriptor and B’s PE headers, allocates the declared image size, copies the file-backed headers and sections, applies relocations, resolves imports, and sets the final section protections. The mapper supports TLS, although B has no TLS directory. After Donut starts A inside the surrogate, A maps B into the same process and calls B’s entry point from A RVA 0x145F.
B identity and coordinate map
B is a native PE32 x86 GUI executable with six sections, timestamp 0x6A3CB0B5, entry RVA 0x12FAC, preferred base 0x400000, and image size 0x48000. Its export library identifies PackClientLauncher.exe; its CodeView record has GUID EDD31459-13E4-4218-B638-FE7CF8F51C25, age 1, and PDB suffix \Project\Bin\Launcher\Win32\PackClientLauncher.pdb. The PDB path is build metadata, not an available symbol file.
| Section | RVA | Virtual size | B-relative raw range |
|---|---|---|---|
.text |
0x1000 |
0x2E836 |
0x00400–0x2EE00 |
.rdata |
0x30000 |
0x10520 |
0x2EE00–0x3F400 |
.data |
0x41000 |
0x1A9C |
0x3F400–0x40000 |
.fptable |
0x43000 |
0x80 |
0x40000–0x40200 |
.rsrc |
0x44000 |
0x1E0 |
0x40200–0x40400 |
.reloc |
0x45000 |
0x218C |
0x40400–0x42600 |
For a B raw offset, transformed-record offset is raw + 0x1CC1; carrier correspondence is raw + 0x6AC9. This conversion refers to the XOR-transformed correspondence, not identical carrier bytes.
Configuration and Core dispatch
A conditionally reads PACK_MT_LAUNCH_ARGS into a 0x200-byte buffer. Its bridge at A RVA 0x119D parses up to three whitespace-delimited tokens and sets PACK_LAUNCH_PULL_HOST, PACK_LAUNCH_PULL_PORT, and PACK_LAUNCH_GROUP. The source literals 154[.]36[.]18, 6666, and donut seed buffers, but parsing overwrites each field and writes an empty string at end-of-input. Runtime values can overwrite these seed literals through the environment/configuration path described below.
B's loader at 0xCBA2 accepts nonempty environment overrides. Port is decimal and accepted only in 1..65535.
| B configuration offset | Role | Recovered initialization |
|---|---|---|
+0x00 |
Pull host string | 64[.]81[.]30[.]99, with fallback literal 154[.]36[.]18; nonempty host environment override |
+0x18 |
Pull port, uint16 | 6666, then valid nonempty port environment override |
+0x1A |
Core-facing port | Copied from pull port |
+0x1C |
Optional Core host | Empty |
+0x34 |
Stable normalized-host storage | Populated when constructing the Core view |
+0x4C |
Group string | Default, or nonempty group environment override |
+0x64 |
Tag string | Empty |
+0x7C |
Config string | Empty |
Main requests PLK1 slot 0 at RVA 0x484A and retries at 0x4875 after a 1,500 ms failure delay. After successful delivery, RVA 0x4938 passes the returned buffer and its length to wrapper 0x10591, which maps it through MemoryLoadLibraryEx at 0x10136. Before mapping, the wrapper validates MZ, a bounded e_lfanew, PE\0\0, and PE32 magic. Historical PLK1 traffic reconstructs a 985,088-byte PackClientCore.dll with SHA-256 4DE6EF8647FB4B599966A233740CB0514D1E71B8019A1A1792ED7E1E514EDF1C. See Core analysis.
The launcher resolves PackClientDll_AbiVersion, PackClientDll_Run, RunWithConfig, and Main. A present ABI function must return 1. Main requires at least RunWithConfig or Main, even though it also resolves the Run entry. The five-field view contains host, port, group, optional tag, and optional config; ::1 or [::1] is normalized to the fallback host literal. Selector 0x2E2A prefers:
RunWithConfig(host, port, group, tag, config)when available with a view;- otherwise
PackClientDll_Run(config_view, 0); - otherwise
Main().
The lookup wrapper first tries the name, then _<name>@0, then a bounded manual PE export-table walk at 0x32C2. Main unloads the memory module after the selected call returns. The separate GetModuleHandleW(L"PackClientCore.dll") call at 0x8B8F is bootstrap-path discovery for active-session handoff, not acquisition of Core bytes.
Guardian, mutex, and diagnostics
B also implements process-guardian, session-mutex, diagnostic, and crash-dump functionality. The process-guardian state machine was not fully reconstructed.
| Facility | Recovered behavior | Boundary |
|---|---|---|
| Process guardian | HKCU Software\PackMonitorClient\process_guardian; enabled, shutdown_pending, guardian_pid, business_pid, business_gen; guardian-only mode without DLL pull; process watching/respawn with a 20-per-minute limiter |
Guardian --pg-slot is distinct from Core cache slot; no direct edge to secondary promotion |
| Session/pair mutex | Global\PackClientLauncher.Session.%016llx; creation at 0x25B9, owned handle at RVA 0x41BB8; an already-existing name fails the guard |
Runtime named-mutant evidence supports B-specific state; later short-lived children do not prove the exact rejection branch |
| Exported mutex release | _PackLauncher_CloseSessionMutexIfHeld@0, RVA 0x6F2F, closes a non-null held handle and clears the global |
No recovered caller; external consumer identity unresolved |
| Diagnostic storage | debug-3eed5b.log, PackCore-runtime.log, structured diagnostic templates and crash/snapshot writer |
A filename or pre-existing log is not proof of the current run |
The crash writer dynamically resolves MiniDumpWriteDump and supplies the current process/PID, type 0x1001, optional local exception information (ClientPointers=FALSE), and null user-stream/callback arguments. 0x1001 means MiniDumpWithDataSegs | MiniDumpWithThreadInfo; it does not request full/private memory or guarantee manually mapped B/Core/key pages. See Microsoft's minidump flags.
The one-shot crash path tries launcher_crash.dmp beside the current process module, then a PID/tick-count filename there, then %TEMP%\PackClientLauncherDumps\. A separate snapshot path uses launcher_snapshot.dmp. These recovered self-dump contracts are distinct from the broader analyst-collected dumps in Runtime analysis.
Evidence boundaries
The recovered Launcher includes transport, Core delivery and caching, active-session continuity, a screenshot worker, and supporting lifecycle functions. Runtime evidence establishes remote package writes followed by primary-thread context hijacking, but the carrier’s exact write call site and the instruction pointer installed by SetThreadContext remain unresolved. The external 1RCP peer, any bridge between 1RCP and PV10, the Launcher’s envelope-state writer, the mutex-export consumer, and the plugin binaries were not recovered. See limitations, evidence, and Core analysis.