← Technical notesView source on GitHub ↗

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:

  1. RunWithConfig(host, port, group, tag, config) when available with a view;
  2. otherwise PackClientDll_Run(config_view, 0);
  3. 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.

Figure

Enlarged figure

Open figure at full resolution ↗