In this report

Malware analysis / reverse engineering

PackClient

Reverse Engineering a Modular RAT Framework

Ivan Immanuel Shaji

Published
Research cutoff
09 September 2026 (UTC)
Analysis scope
Launcher-to-Core execution, delivery, persistence, and C2 behavior

Abstract

This report follows PackClient from its signed-host carrier into a suspended 32-bit surrogate, the injected Donut package, Launcher execution, and a 985,088-byte Core recovered directly from historical PLK1 traffic. Reassembled sessions preserve successful authentication, Core delivery, plaintext commands, and 15 PV10 JPEG frames. Analysis of the recovered Core reveals its plugin contracts, application encryption, update storage, clipboard replacement, and built-in screenshot producer.

Key findings

  • A 985,088-byte PackClientCore.dll was recovered directly from historical PLK1 traffic; its .text matches all 352 retained Core memory mappings.
  • A complete PLH1 -> PLC1 -> PLA1 -> PLK1 session continues into bidirectional Core traffic and 15 PV10 JPEG frames.
  • Normal full-EXE execution persists the Tax Notice host, while direct carrier-DLL execution produces a broken rundll32.exe task without the original DLL argument.

From Launcher to recovered Core

One chain connects the signed host, injected Launcher, PLK1 delivery, recovered Core, persistence, and live C2 traffic.

This research follows one PackClient build from its signed host and sideloaded carrier into a suspended 32-bit surrogate, the injected Donut package, Launcher delivery, and the recovered Core.

Area Result
Launcher Recovered framing, authentication, PLK1/cache, active-session, and 1RCP contracts
Core Reconstructed a 985,088-byte PackClientCore.dll directly from historical PLK1 traffic and mapped its exports, configuration, plugin interfaces, commands, and screenshot producer
Historical traffic Reassembled PLH1 -> PLC1 -> PLA1 -> PLK1, followed by bidirectional Core traffic and 15 PV10 JPEG frames
Persistence Distinguished the normal full-EXE path from the broken task produced when a sandbox invoked the carrier DLL directly through rundll32.exe

The Launcher's raw-BGRX 1RCP worker and the Core's GDI/WIC PV10 producer are separate recovered paths. No component linking them was found, and no plugin binary was present in the available traffic, memory, or cache material. The PackClient Core analysis develops those findings, while Samples and provenance lists the complete file identities.

The signed-host execution chain

The signed host loads a colocated carrier, which starts an embedded Donut package inside a suspended 32-bit surrogate.

The July 2026 Tax Notice lineage places a signed NV Access/NVDA executable beside a malicious nvdaHelperRemote.dll. Because the host imports the helper by bare filename, the colocated carrier satisfies the import. At host RVA 0x1004, the executable calls nvdaHelperRemote.dll!injection_initialize through IAT slot 0xF268. Samples and provenance identifies the studied host and carrier; the legitimate library layout is described in the NVDA helper documentation.

Figure 01 / static reconstruction Enlarge ↗
The signed host calls the carrier initializer.
The signed host calls the carrier initializer. At host RVA 0x1004, the signed Tax Notice executable calls the imported nvdaHelperRemote.dll!injection_initialize entry. Source and context ↗

The installed directory preserves the signed host and its colocated carrier as a pair. The same chain creates a suspended SysWOW64\svchost.exe, writes the embedded package into that process, changes its primary-thread context, and resumes execution.

Figure 02 / runtime observation Enlarge ↗
The installed pair uses a signed host and a separate companion DLL.
The installed pair uses a signed host and a separate companion DLL. Explorer shows Tax_Notice_23665.exe beside nvdaHelperRemote.dll under C:\ProgramData\NVIDIA Corporation\NvSvc. A separate properties view lists an embedded NV Access Limited signature for the host. The signature belongs to the host; this view does not authenticate the companion DLL. Source and context ↗

The injected call-over-data package contains a Donut instance and embedded executable A. Four bytes of terminal glue recover the instance pointer from the opening CALL; the next 11,647 bytes exactly match Donut LOADER_EXE_X86 from revision 47758d787209dd1744f58c140102ac91b649df16, followed by 23 zero bytes. Donut maps A, and A maps PackClientLauncher.exe B. The complete package layout appears in the Launcher architecture.

Across twelve July and August Triage tasks, the protected allocation is 417,792 bytes and has SHA-256 E49581067CC2AA5ABD09C8DF42D6FBD87CB064A9363FE8B52D8369FD1C51FFE5. It contains a 415,071-byte logical record; the additional 2,721 allocation bytes remain unexplained. These are distinct objects, so their hashes are not interchangeable. One task records 53 write-labelled events into PID 844 at 0x00440000, followed by SetThreadContext on primary thread 4600. This sequence supports thread execution hijacking (T1055.003), not classic process hollowing or remote-thread creation. The carrier's precise write call site and the instruction pointer installed by SetThreadContext remain unresolved. See the Launcher architecture for the carrier and A-to-B mapping details.

Figure 03 / reconstructed flow Enlarge ↗
The signed host loads the carrier, which populates a suspended surrogate with the Donut/A/B chain before Launcher delivery of Core.
The signed host loads the carrier, which populates a suspended surrogate with the Donut/A/B chain before Launcher delivery of Core. Reconstructed from the signed host, carrier, embedded package, Launcher, and historical PLK1 traffic.

Launcher B provides transport, PLK1 delivery and caching, active-session handoff, and an early screenshot-worker mode. Once PLK1 completes, the recovered Core supplies the wider command set and the PV10 screenshot path seen in historical traffic.

Object Recorded size Identity anchor
Tax Notice host 120,984 bytes SHA-256 begins 93DD8B7B393289F8
Sideload carrier 455,527 bytes SHA-256 begins 7295090C2CB63EBC
Reconstructed Launcher B 271,872-byte PE32 executable SHA-256 begins 46B34789196733FA
Recovered Core 985,088-byte PE32 DLL SHA-256 begins 4DE6EF8647FB4B59

The sample and object table lists the complete hashes.

Figure 04 / static reconstruction Enlarge ↗
Launcher B retains its PackClientLauncher build identity.
Launcher B retains its PackClientLauncher build identity. The image summary identifies the imported PE and its PackClientLauncher PDB name; the recorded SHA-256 identifies the reconstructed image. Source and context ↗

Launcher screenshot worker

An early command-line branch turns Launcher B into a local screenshot worker with a compact 20-byte protocol.

The worker is selected by an early argument-dispatch branch, before the later active-session/Core path. The interface is /scr_cap_worker <endpoint-utf8> [monitor-index]. Its body runs in the already launched process and inherits that process’s token, session and environment. The wrapper exits after the worker returns; the later token-based launch path cannot be used to infer the worker’s original security context. The screenshot worker analysis traces this activation path.

The endpoint argument is converted to UTF-16 and passed to CreateFileW with read/write access, no sharing and OPEN_EXISTING. There is one open attempt. The recovered path contains no endpoint creator, named-pipe server, wait-for-pipe loop or peer authentication. A named pipe is strongly supported by context, but the argument’s namespace is not recovered; the API itself also accepts other paths. Its endpoint handling is documented with the rest of the screenshot worker interface.

Figure 05 / static reconstruction Enlarge ↗
The worker opens an existing endpoint.
The worker opens an existing endpoint. 1. CreateFileW receives the supplied endpoint, read/write access and disposition 3 (OPEN_EXISTING). 2. The READY header uses magic 0x50435231 and type 1. The endpoint namespace and external peer are not recovered. Source and context ↗
Figure 06 / reconstructed flow Enlarge ↗
The recovered 1RCP worker opens, captures, announces, then reads commands.
The recovered 1RCP worker opens, captures, announces, then reads commands. Reconstructed from the screenshot worker’s capture loop and command dispatch.

After the first successful capture, the worker emits READY. It then accumulates exactly 20 command bytes, checks the magic and dispatches the request type. Correct-magic types other than 3 and 5 are ignored. Bad magic, a failed read or a zero-byte read ends the command loop without a protocol reply.

The header is five little-endian DWORDs. The bytes 31 52 43 50 spell 1RCP; as a DWORD the magic is 0x50435231.

Figure 07 / reconstructed flow Enlarge ↗
The 20-byte header and the raw BGRX payload have separate meanings.
The 20-byte header and the raw BGRX payload have separate meanings. Reconstructed from the worker’s five-DWORD request and response structures.

A normal exchange follows 1 → 3 → 2 → 5: the worker announces READY, the peer requests a capture, the worker returns the frame, and the peer eventually exits.

Type Direction Meaning Dimensions and payload
1 Worker → peer READY Captured width and height; payload length zero
3 Peer → worker Recapture Request DWORDs 2–4 are ignored
2 Worker → peer Frame response Width, height and following pixel-byte length
5 Peer → worker Exit Request DWORDs 2–4 are ignored; no reply

Zero-filled request fields are a synthetic test convention, not a worker requirement. The command loop always reads the next 20-byte header and does not consume extra bytes according to the final DWORD. Worker process exit codes are separate from protocol message types.

The selected monitor supplies positive width and height. The DIB uses 32-bit BI_RGB with a negative height, producing top-down rows. Each pixel is blue, green, red and an unused fourth byte; meaningful alpha is not established. Stride is width × 4, with no additional per-row padding. The expected payload is width × height × 4; the IA-32 reconstruction found no separate arithmetic overflow guard. The actual response length is taken from the framebuffer vector’s end-minus-begin distance. The complete field and pixel layout appears in the screenshot worker analysis.

Figure 08 / static reconstruction Enlarge ↗
The recapture branch constructs a type-2 response.
The recapture branch constructs a type-2 response. The literal type 2 and the send-helper call are visible in the recapture path. The surrounding dataflow establishes the field semantics and BGRX payload layout. Source and context ↗

Within Launcher B, the pixel path ends at WriteFile on the supplied endpoint. No call or data edge carries those pixels into Winsock, PLK1, the encrypted transport, or a JPEG/PV10 conversion path. An external peer could transform or forward the pixels, but that component was not recovered. See the screenshot worker analysis for the complete path.

When this mode is not selected, Launcher B continues into the network framing and authentication path described next.

Launcher transport and authentication

Launcher B frames, authenticates, and can decrypt traffic before the Core is loaded; historical captures preserve the plaintext handshake and delivery path.

The outer frame starts with a little-endian word formed by 0x5A400000 | body_length. The receiver checks (frame_word >> 22) == 0x169, bounds the nonzero body at 0x3FFFFF, reads it exactly, and requires at least four bytes for the type. Thus 24 00 40 5A means a 36-byte body and a 40-byte total frame. The complete byte layout is documented in the Launcher protocol.

Offset Bytes Outer-frame meaning
0x00 4 Prefix combined with body length
0x04 4 Little-endian type DWORD, part of the body
0x08 Variable Payload; body length minus four

B sends PLH1 and PLA1 in plaintext type 0x15. Its first framed receive accepts PLC1 either directly in type 0x15 or through an authenticated outer 0x16 envelope with inner type 0x15, provided the envelope keys are initialized. This receive precedes Core loading. PLH1 is a 32-byte client hello; PLC1 is the 24-byte server challenge; PLA1 is the 40-byte client authentication object. PLH1 includes version 1, an unresolved word set to 0x20, fixed fields, a tick count and the process ID. PLC1 contributes a 16-byte challenge at offsets 0x08..0x17; challenge generation lives in the server.

PLA1 uses HMAC-SHA-256 over exactly 42 bytes, rather than the complete hello and challenge:

PLH1[0x10:0x20]  // 16 bytes
|| PLC1[0x08:0x18]  // 16 bytes
|| PLH1[0x0C:0x10]  //  4 bytes
|| PLH1[0x06:0x08]  //  2 bytes
|| "PLK1"          //  4 bytes

The PSK comes from PACK_LAUNCH_PSK; missing or empty values select the 19-byte fallback pack-launch-dev-psk. After sending PLA1, the recovered function does not wait for a separate authentication acknowledgement. The July captures confirm the plaintext PLH1 → PLC1 → PLA1 sequence. The historical traffic record preserves the successful exchange.

Optional encrypted receive

Type 0x16 is an inbound encrypted envelope. In this B image, the send path constructs only type 0x15. The envelope carries a version byte, 16-byte IV, four-byte big-endian ciphertext length, ciphertext, and a 32-byte HMAC tag. The length field’s byte order differs from the surrounding little-endian framing.

Figure 09 / reconstructed flow Enlarge ↗
A valid HMAC is a precondition for AES-CBC decryption.
A valid HMAC is a precondition for AES-CBC decryption. Reconstructed from the Launcher’s authenticated receive path.
Figure 10 / static reconstruction Enlarge ↗
Authentication gates decryption.
Authentication gates decryption. A failed tag comparison exits the receive path before ciphertext reaches the AES-CBC helper. Source and context ↗

The MAC covers version, IV, encoded length and ciphertext, excluding the outer frame/type and the trailing tag. A tag match gates AES-256-CBC decryption with BCrypt block padding; the resulting plaintext must begin with inner type 0x15. The tag comparison exits on the first mismatch and is not constant-time.

A ready byte at B RVA 0x41C44 gates a 32-byte AES key at 0x41C48 and a separate 32-byte HMAC key at 0x41C68. Analysis found reads of these globals but no initializer, setter or derivation. The handshake PSK was not shown to feed them. Unless another in-process component initializes this state, encrypted receive remains disabled. The Launcher protocol documents the key-state reads and the unresolved writer.

PLK1 delivery and Core loading

PLK1 reconstruction is only the first gate: the Core must survive a protected cache save and reload before execution.

PLK1 validates the header, original and transferred size bounds, chunk sequence, optional raw-LZ4 decompression, and the final plaintext SHA-256. Both active callers request slot 0, corresponding to PackClientCore.primary.dll; no caller in this Launcher requests the secondary slot supported by shared code. The PackClient Core analysis follows this delivery through to the recovered DLL.

Figure 11 / runtime observation Enlarge ↗
A historical Triage Launcher flow reaches PLK1.
A historical Triage Launcher flow reaches PLK1. Wireshark displays PLH1, PLC1, PLA1 and PLK1 on the filtered historical flow. The selected PLK1 header declares wire version 2, LZ4, 669,717 transferred bytes, 985,088 original bytes and the expected Core plaintext SHA-256. Source and context ↗

A fresh transfer is not loaded directly. Save must first persist the plaintext, then TryLoad must decrypt and re-hash the cached copy. Only the reloaded vector reaches MemoryLoadLibraryEx; successful network delivery alone is insufficient for execution.

Figure 12 / reconstructed flow Enlarge ↗
Freshly received bytes reach the loader only after Save and TryLoad.
Freshly received bytes reach the loader only after Save and TryLoad. Reconstructed from the Launcher’s PLK1 verification and cache-control flow.

Each of the eight complete historical transfers uses eleven ordered type-0x15 chunks: ten carry 65,536 bytes and the final chunk carries 14,357 bytes. The resulting 669,717-byte raw-LZ4 object decompresses to the same 985,088-byte Core and matches the SHA-256 declared by the sender.

The cache resolves CryptProtectData and CryptUnprotectData dynamically and stores the Core by its plaintext SHA-256. This supports current-user DPAPI protection for the relative cache, although the absolute runtime base directory remains unresolved. The Launcher protocol traces the cache-hit and fresh-delivery paths.

Figure 13 / static reconstruction Enlarge ↗
The plaintext must pass digest verification, cache save and cache reload.
The plaintext must pass digest verification, cache save and cache reload. The transfer body checks the plaintext digest, requires Save to succeed, and immediately requires TryLoad to return a nonempty vector. The visible failure branches are SHA256 mismatch, disk_cache_save_failed and disk_cache_reload_failed. Source and context ↗

Inside PackClient Core

Historical PLK1 traffic reconstructs the Core and reveals its exports, transport, plugin system, updates, commands, and screenshot producer.

All eight complete PLK1 transfers—four in 260715-wd77daas7l and four in 260716-dhnz7aft6z—produce the same 985,088-byte x86 PackClientCore.dll, SHA-256 4DE6EF8647FB4B599966A233740CB0514D1E71B8019A1A1792ED7E1E514EDF1C. Across eight Core processes, all 352 retained mappings contain the same 753,664-byte .text section, SHA-256 F06FF7AB6D62B761344CAECBCC6857912F7543F43C0E5FF462D2174BADB0CA3F. The full-file and mapped-section hashes independently identify the recovered build.

Figure 14 / recovery verification Enlarge ↗
Traffic recovery and memory identity agree.
Traffic recovery and memory identity agree. PLK1 supplies the complete PE hash; the mapped .text hash independently links that build to all 352 memory images. Source and context ↗

Ghidra independently identifies the recovered payload as a little-endian 32-bit x86 Portable Executable and records the same full-file SHA-256 produced by PLK1 reconstruction. The complete identity and recovery steps appear in the PackClient Core analysis.

Figure 15 / static reconstruction Enlarge ↗
The recovered Core is a 32-bit x86 PE DLL.
The recovered Core is a 32-bit x86 PE DLL. Ghidra identifies the reconstructed payload as little-endian 32-bit x86 and a Portable Executable, with the same SHA-256 recovered from PLK1 traffic. Source and context ↗

Runtime entry points

The DLL exposes 11 exports in four groups:

  • Main entry modes: Main, MainMinimal, MainWithConfig, RunWithConfig, and RunDualWithConfig
  • Launcher integration: PackClientDll_AbiVersion, PackClientDll_Run, and PackClientDll_TrySystemSessionHandoff
  • Stored-plugin memory: PackClient_AllocStoredPluginImageW and PackClient_FreeStoredPluginImage
  • Probe entry: ProbeEntry

PackClientDll_Run reads five fields from the Launcher's configuration block:

Offset Field
+0x00 Host
+0x04 16-bit port
+0x08 Group
+0x0C Optional tag
+0x10 Optional configuration string

The PackClient Core analysis traces the export preconditions and Launcher-to-Core ABI.

RunWithConfig starts one S1 link, while RunDualWithConfig maintains separate S1 and S2 endpoint states. Core implements reconnect, heartbeat, and periodic DNS re-resolution and reads six local settings for fragmentation, DNS polling, TLS, application authentication, and preview control.

Core transport and encryption

Core's authenticated type-0x16 record uses a little-endian ciphertext length. A nonempty local auth_psk derives independent keys as SHA256("PACKAPP|AES256|v1|" || raw_psk) and SHA256("PACKAPP|HMAC|v1|" || raw_psk); an empty value clears readiness and both keys. Core encrypts the complete original type and payload, then redispatches the decrypted inner record. The Launcher's earlier type-0x16 envelope instead uses a big-endian length, unresolved key state, and accepts only inner type 0x15. The PackClient Core analysis provides the complete application-envelope layout.

Plugin loading and updates

The Core accepts a modern plugin ABI—PackPlugin_GetAbiVersion, PackPlugin_GetFeatureId and PackPlugin_OnLoad—and retains a legacy Main loader. Before dispatch, a candidate plugin must expose at least RunWithConfig or Main; the modern ABI adds further validation. The recovered implementation includes eleven feature-to-DLL mappings, screen-codec sidecars, staged raw-LZ4 delivery, legacy LZ4/zlib delivery, activation policy, and a paired current-user-DPAPI .pblob/JSON cache. The cache writes through a temporary file, flushes, atomically replaces the destination, and records feature, version, architecture, hash, size, and timestamps. It is separate from the Launcher's SHA-256-addressed Core cache.

Core updates use a distinct CLIENTCOREUPD path that stores DPAPI-protected chunks under HKCU\Software\PackMonitorClient\LauncherDllStore\<bits>\Primary. No plugin binary, paired cache, completed plugin delivery, activated mapping, or Core-update transaction appears in the available artifacts. The plugin and update paths are traced in the PackClient Core analysis.

Figure 16 / static reconstruction Enlarge ↗
Core resolves and validates the modern plugin ABI.
Core resolves and validates the modern plugin ABI. The recovered Core resolves PackPlugin_GetAbiVersion and PackPlugin_GetFeatureId before accepting feature-specific host bindings. A separate legacy path resolves Main. Source and context ↗

Commands and capabilities

Beyond the observed startup and preview exchange, analysis identified handlers for:

  • File, process, startup-item, scheduled-task, and registry operations
  • Shell and payload execution
  • Remote input, screen control, keylogging, and clipboard telemetry
  • TCP and UDP proxying
  • Virtual desktops and webcam handling
  • Plugin delivery and Core updates

Captured traffic contains startup, inventory, offline-keylogger status, preview control, and screenshot delivery.

Surface Observed traffic Recovered Core behavior
Session/startup INP HELLO, STARTUP PROBE and STARTUP OK Session/control router and startup-probe branch
Preview Enable/request/ack exchanges and 15 PV10 JPEG frames GDI/WIC capture and PV10 producer
Keylogger KTL OFFLINE telemetry Hook, archive, sync and clipboard-telemetry paths
Plugins No transaction, cache pair or plugin bytes Staged protocol, ABI, loader and DPAPI cache
Core update No transaction CLIENTCOREUPD validation and registry store
Auth envelope No type-0x16 frames Optional Core KDF and AES-CBC/HMAC envelope
ETCHOOK No activation traffic Regex-controlled clipboard replacement

The built-in preview producer uses GDI capture and scaling, GetDIBits, WIC JPEG encoding, and a bytewise PV10 serializer. All 15 historical type-18 frames match that format. This path is distinct from the Launcher's raw-BGRX 1RCP worker, and no recovered component bridges the two.

Figure 17 / static reconstruction Enlarge ↗
Core serializes its built-in preview as PV10.
Core serializes its built-in preview as PV10. The Core serializer emits the PV10 marker in the built-in screenshot-preview path. Packet reconstruction finds 15 matching type-18 JPEG frames; no bridge from the separate Launcher 1RCP endpoint is established. Source and context ↗

Clipboard replacement

Core also implements regex-controlled ANSI and Unicode clipboard replacement through ETCHOOK. Proofpoint previously reported PackClient's clipboard-replacement capability; analysis of this build adds the built-in patterns and the concrete PERCLIENT and SYNC control formats. ETCHOOK does not appear in the captured traffic.

Active-session handoff

Launcher B can relaunch into the active console session and replace itself when that session changes.

The active-session path resolves a bootstrap executable, preserves a constrained set of special arguments, duplicates the current process token as a primary launch token and assigns it to the target console session before calling CreateProcessAsUserW. A separately selected user token can supply the environment; it is not the launch token. This is separate from the early screenshot mode, which exits through its own wrapper. Token selection and process creation are traced in the active-session handoff analysis.

Handoff detail Behavior
Application and command line Null application-name argument; mutable quoted command line
Environment Optional Unicode block; handles are not inherited
Desktop WinSta0\Default
Window state Hidden with CREATE_NO_WINDOW
Creation flags CREATE_BREAKAWAY_FROM_JOB, CREATE_NO_WINDOW, and CREATE_UNICODE_ENVIRONMENT (0x09000400)
Retry behavior Up to 30 attempts; each failure is followed by a one-second sleep
Drift check Every 200 ms; a valid different console ID triggers replacement

On successful drift replacement, the launcher closes child handles and exits the old process. It uses canonical -acsi state for this handoff. The continuity path is explicit in the Launcher, but the available runtime evidence does not identify which elevation API produced the original high-integrity host. The handoff analysis documents the drift state machine.

Figure 18 / static reconstruction Enlarge ↗
The active-session path retries a token-based launch.
The active-session path retries a token-based launch. The call uses CreateProcessAsUserW with flags 0x09000400. The loop sleeps for 1,000 ms after each failure and stops after at most 0x1E attempts. This helper handles session handoff; the original elevation path remains unresolved. Source and context ↗

Launcher B in process memory

Private executable mappings, a thread start, and Launcher-specific state connect the reconstructed image to the live surrogate.

The first runtime capture preserves a bare 32-bit SysWOW64\svchost.exe, PID 5812, an NvSvc task, VMMap state, a user-mode dump and a late Process Monitor capture. The runtime correspondence below comes from passive examination of those artifacts.

VMMap’s 408 KiB private executable allocation at 0x002E0000 was an investigative lead. Its displayed “Thread Environment Block” label did not identify its contents. The dump’s package structure, embedded offsets, PE metadata and section hashes supplied that identification. Samples and provenance records the corresponding offsets and hashes.

Figure 19 / runtime observation Enlarge ↗
Private executable allocation in svchost.exe, PID 5812.
Private executable allocation in svchost.exe, PID 5812. VMMap shows a 408 KiB Execute/Read allocation at 0x002E0000. Corresponding dump analysis identifies its contents as the transformed A/B package. Source and context ↗
Runtime object Location Identification
Protected package allocation 0x002E0000; 417,792 bytes Contains A and B at the expected logical-record offsets 0x128D and 0x1CC1
Mapped A 0x02C70000; image size 0x64000 Entry RVA 0x1308, timestamp 0x6A27AE4B, and PE layout match the reconstruction
Mapped B 0x05410000; image size 0x48000 Entry RVA 0x12FAC, timestamp 0x6A3CB0B5, and immutable sections match the reconstruction
Thread 1884 Start 0x02C71308 Start address resolves exactly to mapped A plus its entry RVA

The complete protected allocation has SHA-256 E49581067CC2AA5ABD09C8DF42D6FBD87CB064A9363FE8B52D8369FD1C51FFE5; the embedded 415,071-byte logical record has SHA-256 0419AE7381CAA97172C40F5AEA601B8A22F1F58D27F3930509AF5808E043F65E. The mapped images are outside the registered module list. B-specific state includes the named mutant PackClientLauncher.Session.9b2126fc5ed31443, effective configuration, and formatted launcher diagnostics. Together, the package layout, mapped images, thread start, mutex, configuration, and diagnostics identify the live mapping as Launcher B.

No identifiable Core image appears in this local dump, although headerless content, missing pages, or a different capture moment cannot be excluded. The dump also does not reveal how the surrogate was populated. The runtime analysis combines it with cross-task Triage records that show the remote writes and SetThreadContext sequence.

Persistence and execution modes

Process records connect the signed host to the surrogate and explain why full-EXE and direct-DLL execution produce different scheduled tasks.

During controlled execution in Windows 11, Process Monitor records successful loading of the adjacent helper after two earlier 0xC0000906 failures. A medium-integrity Tax Notice process, PID 5952, relaunches as high-integrity PID 2116. The elevated host creates the bare surrogate and the task-creation branch below. The session includes separate launch attempts; the process relationships are tied to their recorded creation events. See the runtime analysis for the native process evidence.

Figure 20 / reconstructed flow Enlarge ↗
The recorded host creates a long-lived surrogate and a short-lived task branch.
The recorded host creates a long-lived surrogate and a short-lived task branch. Built from recorded process-creation events; process lifetimes separate reused PIDs.
Figure 21 / runtime observation Enlarge ↗
One parent creates two siblings.
One parent creates two siblings. Process Monitor records parent PID 2116 creating svchost PID 3696 and schtasks PID 4600; schtasks then creates conhost PID 8248. SUCCESS is the Process Create result, not the child's exit code. Source and context ↗

The relationship is specific: svchost 3696 and schtasks 4600 are siblings under 2116. Conhost 8248 belongs below schtasks. The parent exits shortly after creating its children; its continued appearance in a process tree preserves ancestry, not liveness. Later reuse of a numeric PID must be joined to a new process start and lifetime.

Native file and registry records establish installation beneath C:\ProgramData\NVIDIA Corporation\NvSvc, hidden and backdated copies, and HKCU Run/RunOnce writes in this chain. Task XML and the creation record establish NvSvc persistence. Task Scheduler shows the at-logon trigger, ProgramData action, and configured user context. The later capture also records the backdating operation rather than merely showing a timestamp mismatch. The underlying file, registry, and task identities are listed in Samples and provenance.

Figure 22 / runtime observation Enlarge ↗
NvSvc combines an at-logon trigger with a ProgramData action.
NvSvc combines an at-logon trigger with a ProgramData action. Task Scheduler shows NvSvc's enabled at-logon trigger, ProgramData action and configured principal. Highest available privileges for this user do not establish SYSTEM execution. Source and context ↗

Controlled Triage tasks close the apparent persistence discrepancy. Full-EXE execution copies and schedules Tax_Notice_23665.exe. Direct invocation as rundll32.exe …nvdahelperremote.dll,#1 bypasses the expected signed host, so the carrier treats the sandbox host as its executable and persists a copied rundll32.exe without the DLL argument. That action cannot replay the original entry point.

Figure 23 / runtime observation Enlarge ↗
Full-EXE execution persists the Tax Notice host.
Full-EXE execution persists the Tax Notice host. The process view records NvSvc task creation with the ProgramData Tax_Notice_23665.exe action, matching the intended signed-host/carrier chain. Source and context ↗
Figure 24 / runtime observation Enlarge ↗
Direct-DLL execution persists a copied rundll32 host.
Direct-DLL execution persists a copied rundll32 host. The direct rundll32 invocation causes NvSvc to target a copied rundll32.exe without the DLL argument. That action cannot replay the original entry point and is treated as a sandbox-context artifact, not a second variant. Source and context ↗

“Highest privileges” here belongs to the selected current user; it does not mean SYSTEM. A later persisted-path launch is observed, but the saved local evidence does not resolve every trigger as logon versus on-demand. Across the Triage runs, remote writes followed by SetThreadContext on the suspended primary thread identify thread execution hijacking (T1055.003). The precise write primitive and installed instruction pointer remain unresolved.

A separate dump contains the 415,071-byte transformed package and a private mapping of Launcher B. After relocations and imported addresses are normalized, three package sites differ by eight bytes. This confirms that the package and Launcher were resident, but does not prove that the eight bytes form a port patch. Three 6666-to-443 substitutions would explain six bytes; resolving the remaining two requires the original before-and-after bytes from each site. See Samples and provenance for the recorded comparison.

Figure 25 / runtime observation Enlarge ↗
The surviving surrogate has a bare command line.
The surviving surrogate has a bare command line. 1. Bare SysWOW64 svchost command line. 2. Parent 2116 has exited. 3. The process is x86. The Downloads\PackClient current directory supplies additional context. These properties support an anomalous surrogate, without identifying an injection subtype. Source and context ↗

From failed connections to a live session

Local captures preserve failed connection attempts; July Triage traffic preserves the complete authenticated session and delivered Core.

PID 5812’s preserved environment and formatted buffers identify 154[.]36[.]188[.]201:443, a slot-0 pull attempt and WSA=10060. The PID 3696 set adds 373 ICMP host-unreachable messages quoting TCP SYNs to the same destination, plus two direct SYN_SENT socket views. These local artifacts establish attempted connections, not a completed session.

By contrast, the July Triage streams contain server PLC1, client PLA1, complete PLK1 delivery, and bidirectional Core traffic on the same TCP/443 endpoint. The September reruns 260908-zwr5naybjb and 260909-abma8sab28 reached only the greeting/retry stage because the server returned no application data. The complete decoded sequence appears in the PackClient Core analysis.

Figure 26 / runtime observation Enlarge ↗
Connection attempts from svchost.exe, PID 3696.
Connection attempts from svchost.exe, PID 3696. Process Explorer shows two SYN_SENT attempts to 154.36.188.201:443, from local ports 54581 and 50275. Source and context ↗

The packet capture has no PID metadata, so it cannot independently assign traffic to PID 3696; Process Explorer supplies that process-to-socket link. The successful July traffic uses TCP/443 but is raw PackClient rather than TLS or HTTPS. The preserved evidence establishes the different connection outcomes, but not why the server stopped responding. Capture identities and network observations are listed in Samples and provenance.

Analysis tools and detections

Offline decoders, a Wireshark dissector, and detection rules turn the recovered formats and behavior into practical research tools.

Two offline Python tools and a phase-aware Wireshark Lua dissector accompany the analysis. They decode Launcher and Core framing, distinguish the two type-0x16 formats, label observed Core messages, validate PV10, and export verified PLK1/Core and JPEG files when requested. Each export records its source flow, frame positions, size, and hash. All three tools operate passively on supplied files or byte streams. Setup and usage are described under Analysis tools.

The test suite covers framing, authentication, both encryption formats, PLK1 reassembly and extraction, Core commands, PV10, capture reassembly, Wireshark integration, screenshot IPC, and detection rules. Running the tools against the historical captures reproduced the recovered Core and all 15 JPEG frames.

Detection and hunting

The detection set covers three layers:

  • Sigma rules target the observed NvSvc task, 32-bit surrogate creation, screenshot-worker mode, and active-session mode.
  • Suricata rules include standalone Launcher markers that overlap existing Emerging Threats coverage, plus flow-aware challenge-to-PLK1 and Core-startup-to-PV10 sequences.
  • Separate YARA rules identify the recovered Launcher and Core.

On the retained case material, the Launcher rule matched all 86 Launcher mappings and none of 383 other objects. The Core rule matched the recovered DLL and all 352 Core mappings, with no matches among 116 Launcher or control allocations. A preliminary scan of more than 30,000 local Windows and installed-application PE files produced no matches for either rule.

The YARA rules remain experimental because they have not been measured against a broad enterprise-software or unrelated-malware corpus. See the Detection guide for rule logic, validation, and false-positive considerations.

What this research adds

The recovered Launcher and Core extend the public PackClient account from campaign reporting into an execution and protocol reconstruction.

Proofpoint’s 27 August 2026 report describes the Tax Notice chain, PackClient’s modular Launcher/Core architecture, representative protocol markers, and existing Emerging Threats coverage. Deception.Pro later documented PackClient hands-on-keyboard activity and ManageEngine use. This research adds the Launcher’s transport and loading contracts, direct Core reconstruction from PLK1 traffic, the signed-host/Donut/mapper chain, and working analysis and detection tools. Proofpoint campaign report, Deception.Pro later reporting

Several pieces remain outside the recovered chain:

Missing component or event What would resolve it What is known now
Plugin binaries A preserved plugin transfer, cache pair, or post-activation memory image Core contains the protocol, cache, ABI, and loaders, but no plugin PE was captured
Carrier write primitive The precise write call site and the instruction pointer installed by SetThreadContext Remote placement and primary-thread hijacking are established at runtime
External 1RCP peer The endpoint creator and type-2 consumer Launcher B supplies the worker; no bridge to Core PV10 was found
Real 1RCP exchange A complete READY/request/frame sequence from the malware worker The code path and synthetic exchange agree, but the real exchange did not complete
Launcher envelope keys The initializer or live values for the gated key globals The receive format is recovered; its key source is not
Server implementation Server-side code or configuration Historical captures reveal behavior and commands, not the implementation
September non-response Server-side telemetry or controlled infrastructure The client sent PLH1 but received no application data
Production detector accuracy Broader benign and unrelated-malware collections The rules pass case-specific and local benign tests

The PackClient Core note contains the full Core reconstruction. Scope and limitations collects the remaining technical gaps.

Continue reading

Browse the technical notes for the complete protocol, runtime, and tooling write-ups, or open samples and source figures for hashes and figure provenance.

Figure

Enlarged figure

Open figure at full resolution ↗