Nikhil "Kaido" Hegde

M&M: Malware and Musings

View on GitHub

Source

Analysis

Obfuscation

wire_0e36b0=StrReverse("labolGetucexE")
idx_i_fb81 = 1
key_k_b17e = 245
Do Until idx_i_fb81 > Len(enc_zz_a49c)
  ch_hi_27c0 = InStr(pal_a_c624, Mid(enc_zz_a49c, idx_i_fb81, 1)) - 1
  ch_lo_774c = InStr(pal_b_55ba, Mid(enc_zz_a49c, idx_i_fb81 + 1, 1)) - 1
  v_byte_7498 = ((ch_hi_27c0 * 16) Or ch_lo_774c)
  v_byte_7498 = (v_byte_7498 - (((idx_i_fb81 - 1) \ 2) * 169)) And 255
  v_byte_7498 = v_byte_7498 Xor key_k_b17e
  out_r_4a5c = out_r_4a5c & Chr(v_byte_7498)
  key_k_b17e = (key_k_b17e * 123 + 161) And 255
  idx_i_fb81 = idx_i_fb81 + 2
Loop
"ExecuteGlobal"&" out_r_4a5c"
Execute "ExecuteGlobal"&" out_r_4a5c"
Dim tmp_x_6cd5: tmp_x_6cd5 = 59214
Dim tmp_x_5625: tmp_x_5625 = 82095
Dim tmp_x_fff7: tmp_x_fff7 = 39068

Deobfuscation

Utilities: https://github.com/nikhilh-20/re_tools/tree/main/vbs

> python .\vbs_fold_builtin_calls.py --input C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae.vbs --output C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass1.vbs
{"changed":1,"input_bytes":1159926,"output_bytes":1159914,"output_path":"C:\\Users\\Ashura\\Desktop\\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass1.vbs"}

> python .\vbs_fold_constant_loops.py --input C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass1.vbs --output C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass2.vbs
{"changed":1,"loops_folded":1,"loops_declined":0,"input_bytes":1159914,"output_bytes":1739913,"output_path":"C:\\Users\\Ashura\\Desktop\\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass2.vbs"}

> python .\vbs_propagate_constants.py --input C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass2.vbs --output C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass3.vbs
{"changed":4,"substituted_reads":4,"input_bytes":1739913,"output_bytes":1739909,"output_path":"C:\\Users\\Ashura\\Desktop\\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass3.vbs"}

> python .\vbs_fold_concat.py --input C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass3.vbs --output C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass4.vbs
{"changed":1,"input_bytes":1739909,"output_bytes":1739906,"output_path":"C:\\Users\\Ashura\\Desktop\\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass4.vbs"}

> python .\vbs_unwrap_execute.py --input C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass4.vbs --output C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass5.vbs
{"changed":1,"unwrapped":1,"input_bytes":1739906,"output_bytes":1739932,"output_path":"C:\\Users\\Ashura\\Desktop\\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass5.vbs"}

> python .\vbs_remove_deadcode.py --input C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass5.vbs --output C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass6.vbs --aggressive
{"changed":26,"input_bytes":1739932,"output_bytes":580555,"output_path":"C:\\Users\\Ashura\\Desktop\\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass6.vbs"}

Functionality

Prompt

/malware-analysis Analyze @C:\Users\Ashura\Desktop\323b89209895cf53e34e0ed545fd8372f499d1c1a994c7972b5e7f09d17d60ae_pass6.vbs. Write report in markdown format into @report.md It should contain the below sections:

1. Executive summary
2. Details - avoid variable names granularity. retain behavioral specifics like created folder names, C2 contact, etc.
3. IOCs

Reference the source code when stating functionality. Like:
```<source_code>```
<functionality>

You can leverage language-specific deobfuscation utilities that are in @Tools\Inhouse. Their usages should make it to the report as well.

Use multiple Haiku sub-agents to confirm your findings. Use web search to ground your facts and assumptions.

Flowchart Prompt

Based on @report.md, can a Mermaid flowchart be written into FLOW.mmd? Keep the flowchart in natural language. Avoid variable name granularity. You can retain created folder names and contacted C2

Flowchart

flowchart TD
    A["VBScript dropper runs<br/>(self-decoding wrapper holding one large encoded string)"] --> B["Wrapper decodes and runs<br/>an embedded second VBScript stage"]
    B --> C["Second stage reassembles a third,<br/>cipher-encoded VBScript fragment from<br/>scattered pieces, then runs it"]

    C --> D["Third stage copies itself to<br/>%LOCALAPPDATA%\Photo Studio\PhotoStudio.vbs"]
    D --> E["Registers Scheduled Task 'PhotoStudioVBS'<br/>(runs the dropped copy via wscript.exe at every logon)<br/>— persistence established"]

    C --> F["Third stage decrypts an embedded,<br/>encrypted blob into a PowerShell script"]
    F --> G["Drops the PowerShell script to a<br/>randomly named file under %TEMP%<br/>and runs it hidden"]
    G --> H["Deletes the temporary script<br/>files afterward"]

    H --> I["PowerShell stage performs anti-analysis checks<br/>(language-mode check, 10-minute self-exit timer,<br/>hides its own console window)"]
    I --> J["Decrypts two embedded .NET programs<br/>from further encoded blobs"]
    J --> K["Loads the first program:<br/>a generic code-injection loader"]
    K --> L["Loader hollows out a legitimate,<br/>Microsoft-signed process —<br/>C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe"]
    L --> M["Injects the second decrypted program<br/>(the final payload) into the hollowed process"]

    M --> N["Final payload runs:<br/>a full remote-access / stealer implant<br/>('LxClient')"]
    N --> N1["Hidden-VNC modules targeting<br/>Discord and Telegram"]
    N --> N2["Browser password/cookie theft<br/>and crypto-wallet theft"]
    N --> N3["Keylogger and<br/>screen/webcam capture ('grabber')"]
    N --> O["Contacts ip-api.com<br/>(looks up victim's IP/location)"]
    N --> P["Contacts Discord's own API<br/>(discord.com/api/v9/users/@me)<br/>to validate/steal a Discord session token"]

    style A fill:#f8d7da,stroke:#842029
    style N fill:#f8d7da,stroke:#842029
    style E fill:#fff3cd,stroke:#997404
    style L fill:#fff3cd,stroke:#997404
    style O fill:#cfe2ff,stroke:#084298
    style P fill:#cfe2ff,stroke:#084298

Report

Executive Summary

This file is a multi-stage, self-decoding VBScript dropper that installs a full-featured .NET Remote Access Trojan / stealer with Hidden-VNC, browser/crypto-wallet credential theft, and keylogging capabilities, delivered via in-memory process hollowing into a trusted Microsoft binary.

Execution chain, top to bottom:

  1. VBScript stage 1 — holds one giant string variable (out_r_4a5c, ~2,460 lines / ~580 KB) and runs it with ExecuteGlobal.
  2. VBScript stage 2 (the content of out_r_4a5c) — ~1,700 lines of real logic buried under hundreds of Dim a,b:a=N:b=a Xor M numeric-junk statements and several multi-kilobyte encoded blobs. It (a) reassembles a 9-part, custom-cipher-encoded VBScript fragment (seq42Ring) via a Scripting.Dictionary and a decode function frm16Node, then ExecuteGlobals it as stage 3; and (b) separately reassembles, decodes (custom-alphabet -> Base64), and stores a large AES key/ciphertext pair (gfxSpec9625 / tblLink5127) used later.
  3. VBScript stage 3 (seq42Ring, fully recovered — see §2.3) — the actual malicious logic: it copies itself to %LOCALAPPDATA%\Photo Studio\PhotoStudio.vbs and registers a Scheduled Task PhotoStudioVBS that re-launches it at every logon (persistence), then AES-256-ECB-decrypts the stage-2 blob into a PowerShell script, writes it to a randomly-named .ps1 in %TEMP%, runs it via powershell.exe -NoProfile -ExecutionPolicy Bypass -File, and deletes the temp artifacts afterward.
  4. PowerShell stage 4 (fully recovered — see §2.4) — ~388 KB. It checks it isn’t running in Constrained Language Mode, arms a 10-minute watchdog self-exit, hides its own console window, then AES-256-CBC-decrypts (+Deflate-decompresses) two embedded .NET assemblies, and uses the first (a generic reflective loader) to inject the second (the real payload) via process hollowing into C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe — a documented living-off-the-land technique also reported publicly in other MSBuild-hollowing loaders.
  5. Final payload (a 571 KB .NET PE, extracted and hashed — see §2.5) — internally named “LxClient”. Extracted strings show it is a full RAT/stealer kit: Hidden-VNC modules for Discord and Telegram, Discord-token theft, browser password/cookie theft across a long list of Chromium-based browsers, crypto-wallet theft (Wasabi Wallet, Steam), IMAP/POP3/SMTP credential theft, a keylogger, and a screen/webcam “grabber”. This bundle of features (RAT + HVNC + Stealer + Grabber) matches the advertised feature set of commodity RAT-builder kits sold on cybercrime forums (e.g. “Venom RAT Pro — HVNC + Stealer + RAT”).

Details

Obfuscation overview

The file pads every statement with a blank line on both sides:

On Error Resume Next
Dim out_r_4a5c
out_r_4a5c = "Dim idxMenu269,svcMenu6700:idxMenu269=9:svcMenu6700=idxMenu269 Xor 56

Dim frmFlag4625,idxPage7608:frmFlag4625=86:idxPage7608=frmFlag4625 Xor 146

Dim prnStat1456:prnStat1456=2531+72

Dim docNode2295:docNode2295=True

Pure visual/volume padding — no semantic effect once collapsed.

Beyond padding, the dominant technique throughout is numeric-constant junk: thousands of Dim a,b:a=<const>:b=a Xor <const> / a=<const1>+<const2> declarations whose values are either never read again (dead code) or trivially fold to a constant. These exist purely to inflate file size and defeat signature/entropy-based scanners.

Stage 1 -> Stage 2 handoff

Line 3 opens a string literal that contains real embedded line breaks and doubled "" quote-escapes:

out_r_4a5c = "Dim idxMenu269,svcMenu6700:idxMenu269=9:svcMenu6700=idxMenu269 Xor 56
...
Dim frmLock5336:frmLock5336=""JKfJy...
...
[≈2,450 more "lines" of content]
...
"
If Len(CStr(Now))>0 Then
ExecuteGlobal out_r_4a5c  ' <deobfuscator> unwrapped Execute
End If

The original obfuscated source built out_r_4a5c as hundreds of "<fragment>" & vbCrLf & "<fragment>" concatenations (one fragment per logical line of the embedded stage-2 script, each internally quote-escaped as "" since it is source-within-a-source-string).

Note: An automated deobfuscation pass folded that concatenation chain to a single literal and wrote the resulting real vbCrLf bytes straight into the file — which is why the file is no longer directly re-runnable via cscript, but *is* a completely faithful, static dump of stage 2’s source once the outer "" -> " escaping is undone.

Toolkit usage and findings

The VBS deobfuscation toolkit was run as an iterative chain against the extracted stage2 VBS. This reduced stage 2 from 1,719 lines / 579 KB to 914 lines / ~561 KB, with all XOR/arithmetic junk resolved and dead cod e removed.

Tool Purpose here Result
vbs_fold_chr_calls.py Fold Chr(N) -> literal char 3 folds
vbs_fold_arithmetic.py Fold constant arithmetic (2531+72, &H...) 1 fold
vbs_fold_concat.py Collapse &-chains of literals 115 folds total across rounds
vbs_fold_builtin_calls.py Evaluate Replace(), Mid(), etc. on constants 16 folds total
vbs_propagate_constants.py Substitute known-constant variables at read sites 405 substitutions
vbs_fold_constant_loops.py Simulate bounded loops over constant inputs 3 loops folded (e.g. the Chr()-building alphabet loops in tblProp2676)
vbs_unwrap_execute.py Inline Execute "<const stmt>" 1 unwrap (Execute hdl_719158&" "&seq42Ring -> ExecuteGlobal seq42Ring)
vbs_remove_deadcode.py --aggressive Remove unread stores / statically-false blocks / unreferenced function definitions  
1,121 statements removed    
vbs_expand_colons.py / vbs_collapse_blanklines.py Readability only
vbs_extract_variables.py Map variables -> decoded values -> sinks (analysis-only, no file written) used to locate ExecuteGlobal/CreateObject sinks
Stage 2 payload: reassembling and running Stage 3

Stage 2’s only meaningful runtime behavior is building and executing a third VBScript stage. It uses a Scripting.Dictionary pair to store 9 real ciphertext fragments plus several decoy/unused entries:

Set bufAFMenu=CreateObject("Scripting.Dictionary")
Set apiE1Path=CreateObject("Scripting.Dictionary")
bufAFMenu.CompareMode=1 : apiE1Path.CompareMode=1
bufAFMenu.Add 1033,elm55Hook
bufAFMenu.Add 1062,"ZrYWWkcsPwUgzgHTpVjaITsCRyrkXlizwUEHNiRrAqRdtsjhdmxkDBYfZvf"   ' decoy, never read
bufAFMenu.Add 1155,seqAFGrid
...
bufAFMenu.Add 1030,frm3ATask
...
apiE1Path.Add "s03",1055 : apiE1Path.Add "s00",1155 : apiE1Path.Add "s02",375522
apiE1Path.Add "s08",1040 : apiE1Path.Add "s07",1030 : apiE1Path.Add "s05",1077
apiE1Path.Add "s01",1033 : apiE1Path.Add "s06",1050 : apiE1Path.Add "s04",1066

Only 9 of ~14 dictionary entries (1155,1033,375522,1055,1066,1077,1050,1030,1040, addressed as s00..s08) are ever read back; the rest (1062,1153,1083,1111,1029) are decoy filler.

Each fragment is decoded by frm16Node, a custom stream cipher: it converts each ciphertext character pair to a nibble pair via two lookup tables (Left/Mid slices of a 32-char alphabet), XORs against a second table-driven byte keyed by a running absolute position (idx58Mark + loop counter, not reset between fragments — the 3-bit rotation applied per byte cycles with period 3 over that same running position), and finally bit-rotates the result by one of three schedules chosen by position mod 3:

Function frm16Node(rpcCDLink,idx58Mark)
...
tok64Menu=(tok64Menu-((cfgC3View*CLng(apiAFNode)+CLng(cfgBEWire)+exOff-1) And 255)+256) Mod 256
sigBBStub=(cfgC3View*CLng(mem9AHeap)+CLng(row9FPool)) Mod CLng(stkA3Host)
...
tok64Menu=(tok64Menu Xor que9DSlot) And 255
vTrio=cfgC3View Mod 3
If vTrio=0 Then tok64Menu=(tok64Menu\16) Or ((tok64Menu Mod 16)*16)
If vTrio=1 Then tok64Menu=(tok64Menu\2) Or ((tok64Menu Mod 2)*128)
If vTrio=2 Then tok64Menu=((tok64Menu*4) And 255) Or (tok64Menu\64)
sigF3Grid(prn7FCtrl)=Chr(tok64Menu)

This was hand-ported to Python (see Appendix A for script) and run against all 9 fragments in s00..s08 order. All 9 decoded cleanly to valid, contiguous VBScript totaling 13,558 characters / 211 lines — this is Stage 3, executed via:

Dim hdl_719158:hdl_719158=Replace("ExecuteG$oba$","$","l")
If TypeName(CInt(73))="Integer" Then
Execute hdl_719158&" seq42Ring"
End If

hdl_719158 resolves to the literal string "ExecuteGlobal" — a one-character-substitution obfuscation of that keyword — so this line is functionally ExecuteGlobal seq42Ring.)

Stage 3 in full does the following (all identifiers below are the malware’s own, recovered variable/function names):

Stage 2’s encoded blob feeding this step (tblLink5127/gfxSpec9625) is built from a second, independent obfuscation layer: docName6206 (a 44-char key blob) and a 255-slot sparse array tagPage4426(254) (holding the ~517 KB ciphertext, scattered across 255 decoy-interspersed assignment statements at lines 1053–1661) are each run through a simple custom-alphabet->Base64 substitution:

Function tblProp2676(sysProp1207)
tagLock983="umdgyp9M/nZ58cNVPC=0fvrt6wHjIheFLDUBiGQo14zWRKOqTbaElsk+JAX32Y7Sx"
' tblTask4501 built as A-Z, a-z, 0-9, "+/="  (standard base64 alphabet)
' substitutes each char of sysProp1207 by its position in tagLock983 -> same position in tblTask4501

docName6206/tagPage4426 are ordinary Base64 wearing a rotated alphabet as a disguise.

Stage 4: the decrypted PowerShell loader

Feeding the AES-256-ECB key/ciphertext recovered above (gfxSpec9625/tblLink5127) through the exact algorithm stage 3 builds and PKCS7-unpadding the result yields 387,901 bytes of PowerShell, opening with:

function parseConfig { ... }   # custom base-52 (A-Za-z) big-integer decoder, 10 chars -> 7 bytes

Its structure:

Final payload: the injected .NET binary

Both embedded assemblies were decrypted to disk for identification (never executed) using the scripts in Appendix B — one recovers both PE files from the PowerShell stage’s embedded blobs, the other parses their PE headers and extracts the strings tabulated below.

String extraction from the final payload shows a fully-featured RAT + stealer, internally branded “LxClient”:

Capability Evidence (extracted strings)
Hidden VNC HVNC_Discord, HVNC_Discord_Fresh, HVNC_Telegram, HVNC_Telegram_Fresh
Discord account theft discord.com/api/v9/users/@me (token/session validation endpoint)
Browser credential/cookie theft SELECT origin_url, username_value, password_value FROM logins; registry probes for 20+ Chromium-family browsers (Chrome, Chrome Beta/Canary, 360Chrome, OperaGX, Torch, …)
Crypto-wallet theft WalletWasabi\Client\Wallets, Steam config.vdf account queries
Mail credential theft IMAP Password, POP3 Server, SMTP Server
Keylogging Keylogger, get__keylogPath, KeyloggerResponse
Screen/webcam capture grabber_snapshot, grabber_video
Victim fingerprinting http://ip-api.com/json/ (IP geolocation)
Process injection primitives VirtualAllocEx, NtCreateThreadEx, NtClose, SetDllDirectory
Task/self-management TaskScheduleData, PayloadStubUpload, mutexName

No plaintext C2 address was found in any of the five stages; a true C2 endpoint, if present, is inside this payload’s own internal (unexamined) configuration/resources.


IOCs

Type Value
Dropped copy of the VBS %LOCALAPPDATA%\Photo Studio\PhotoStudio.vbs
Persistence folder name Photo Studio
Persistence install command schtasks.exe /create /tn "PhotoStudioVBS" /xml "<temp path>.xml" /f
Scheduled task name PhotoStudioVBS (Logon trigger, runs wscript.exe "PhotoStudio.vbs", hidden, least-privilege)
Transient decrypted PS loader %TEMP%\<random 8-char hex dir>\script.ps1 (AES key/decryptor script)
Transient PS launcher script %TEMP%\<random 8-char hex>.ps1 (deleted after execution)
PowerShell launch command pattern powershell.exe -NoProfile -ExecutionPolicy Bypass -File "<temp>.ps1"
Injection target (process-hollowed) C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe
Type Value Note
URL http://ip-api.com/json/ Legitimate IP-geolocation service, used for victim fingerprinting
URL https://discord.com/api/v9/users/@me Legitimate Discord API, abused to validate/steal a Discord session token

Appendix A: frm16Node decode script

The Python hand-port referenced in §2.3, used to decode all 9 stage-3 ciphertext fragments once the toolkit’s own loop-fold declined the Scripting.Dictionary-driven loop (a scope gap, not a bug — the resolver has no model of Scripting.Dictionary, and no tool inlines a multi-statement function like frm16Node into a caller before folding). It reconstructs each ciphertext blob directly from stage2_raw.vbs’s own self-append/Join(Array(...)) statements (bypassing the toolkit’s fold output entirely for these values, per the workaround described in §2.2), computes each fragment’s loop-carried offset by hand, and replicates frm16Node’s substitution/XOR/rotate cipher byte-for-byte. Its output (seq42ring_decoded_v2.vbs) is the stage-3 script quoted throughout §2.3, and was independently reproduced by the fixed toolkit alone (no manual bypass needed) when validating the toolkit fixes.

import re, io

RAW = r"C:\Users\Ashura\AppData\Local\Temp\claude\C--Users-Ashura-Desktop\4be520e7-3601-4b09-acfa-7a6a199bf95d\scratchpad\vbswork\stage2_raw.vbs"
FOLDED = r"C:\Users\Ashura\AppData\Local\Temp\claude\C--Users-Ashura-Desktop\4be520e7-3601-4b09-acfa-7a6a199bf95d\scratchpad\vbswork\stage2_final_safe.vbs"

raw = io.open(RAW, encoding="utf-8").read()
folded = io.open(FOLDED, encoding="utf-8").read()

def reconstruct_selfappend(varname, text):
    parts = re.findall(re.escape(varname) + r'\s*=\s*' + re.escape(varname) + r'\s*&\s*"((?:[^"])*)"', text)
    if parts:
        return "".join(parts)
    m = re.search(re.escape(varname) + r'\s*=\s*Join\(Array\((.*?)\)\s*,\s*""\)', text, re.S)
    if m:
        return "".join(re.findall(r'"((?:[^"])*)"', m.group(1)))
    return ""

def numlit_folded(fname):
    m = re.search(r'Function ' + re.escape(fname) + r'\(\)\s*.*?' + re.escape(fname) + r'\s*=\s*("?)([^"\n]+)\1\s*\nEnd Function', folded, re.S)
    val = m.group(2).strip()
    return val

def strlit_folded(fname):
    m = re.search(re.escape(fname) + r'\s*=\s*"((?:[^"])*)"', folded)
    return m.group(1)

svc3EPool = strlit_folded("svc3EPool")
mapD5Form = strlit_folded("mapD5Form")
sslA0Span = int(numlit_folded("sslA0Span"))
ctl82Base = int(numlit_folded("ctl82Base"))
buf56Code = int(numlit_folded("buf56Code"))
datEBElem = int(numlit_folded("datEBElem"))
doc52Code = int(numlit_folded("doc52Code"))

print("helper constants: svc3EPool(len)=%d mapD5Form(len)=%d sslA0Span=%d ctl82Base=%d buf56Code=%d datEBElem=%d doc52Code=%d" %
      (len(svc3EPool), len(mapD5Form), sslA0Span, ctl82Base, buf56Code, datEBElem, doc52Code))

# id -> original varname (transcribed by hand from the raw source, verified by grep)
ID_TO_VAR = {
    1155: "seqAFGrid",
    1033: "elm55Hook",
    375522: "log57Menu",
    1055: "cfgB9Span",
    1066: "grp6ENode",
    1077: "cfg50Grid",
    1050: "dns31Span",
    1030: "frm3ATask",
    1040: "rpcF8Page",
}
# apiE1Path "s00".."s08" -> id, taken directly from raw source (Dictionary keys are
# stable literals, not subject to the propagate/fold blob-mixup bug)
API_MAP = {}
for m in re.finditer(r'apiE1Path\.Add\s+""(s\d\d)""\s*,\s*(\d+)', raw):
    API_MAP[m.group(1)] = int(m.group(2))
if not API_MAP:
    for m in re.finditer(r'apiE1Path\.Add\s+"(s\d\d)"\s*,\s*(\d+)', raw):
        API_MAP[m.group(1)] = int(m.group(2))
print("API_MAP from raw:", API_MAP)

ciphertext = {}
for id_, var in ID_TO_VAR.items():
    val = reconstruct_selfappend(var, raw)
    ciphertext[id_] = val
    print(f"  id {id_} ({var}): reconstructed len {len(val)}")

order = sorted(API_MAP.keys())
print("order:", order)

def instr1(hay, ch):
    idx = hay.find(ch)
    return idx + 1 if idx >= 0 else 0

rpcF0Grid = svc3EPool[0:16]
ctl29Menu = svc3EPool[16:32]
hnd95Pool = mapD5Form
apiAFNode = sslA0Span
cfgBEWire = ctl82Base
mem9AHeap = buf56Code
row9FPool = datEBElem
stkA3Host = doc52Code
exOff = 1

def frm16Node(rpcCDLink, idx58Mark):
    elmAAPool = len(rpcCDLink) // 2
    out = [None]*elmAAPool
    prn7FCtrl = 0
    while prn7FCtrl < elmAAPool:
        cfgC3View = prn7FCtrl + idx58Mark
        c1 = rpcCDLink[prn7FCtrl*2]
        c2 = rpcCDLink[prn7FCtrl*2+1]
        api4FAttr = 16 - instr1(rpcF0Grid, c1)
        gfxEASpan = 16 - instr1(ctl29Menu, c2)
        tok64Menu = api4FAttr*16 + gfxEASpan
        tok64Menu = (tok64Menu - ((cfgC3View*apiAFNode + cfgBEWire + exOff - 1) & 255) + 256) % 256
        sigBBStub = (cfgC3View*mem9AHeap + row9FPool) % stkA3Host
        h1 = hnd95Pool[sigBBStub*2]
        h2 = hnd95Pool[sigBBStub*2+1]
        elm4CBolt = 16 - instr1(rpcF0Grid, h1)
        que9EPage = 16 - instr1(ctl29Menu, h2)
        que9DSlot = elm4CBolt*16 + que9EPage
        tok64Menu = (tok64Menu ^ que9DSlot) & 255
        vTrio = cfgC3View % 3
        if vTrio == 0:
            tok64Menu = (tok64Menu // 16) | ((tok64Menu % 16) * 16)
        if vTrio == 1:
            tok64Menu = (tok64Menu // 2) | ((tok64Menu % 2) * 128)
        if vTrio == 2:
            tok64Menu = ((tok64Menu * 4) & 255) | (tok64Menu // 64)
        out[prn7FCtrl] = chr(tok64Menu & 0xFF)
        prn7FCtrl += 1
    return "".join(out)

seq42Ring = ""
ref45Task = 0
for skey in order:
    id_ = API_MAP[skey]
    cip = ciphertext[id_]
    dec = frm16Node(cip, ref45Task)
    seq42Ring += dec
    preview = dec[:120].replace("\n","\\n").encode("ascii","backslashreplace").decode("ascii")
    print(f"[{skey}] id={id_} offset={ref45Task} ciphertext_len={len(cip)} decoded_len={len(dec)}")
    print("    preview:", preview)
    ref45Task += len(cip)//2

OUT = r"C:\Users\Ashura\AppData\Local\Temp\claude\C--Users-Ashura-Desktop\4be520e7-3601-4b09-acfa-7a6a199bf95d\scratchpad\vbswork\seq42ring_decoded_v2.vbs"
io.open(OUT, "w", encoding="utf-8", newline="\n").write(seq42Ring)
print("total len:", len(seq42Ring), "written to", OUT)

Appendix B: Final-payload extraction and inspection scripts (§2.5)

Two scripts produced §2.5’s findings. The first takes final_payload.bin — the Stage-4 PowerShell script recovered per §2.4 — extracts its two embedded blobs ($c1/$k1/$v1 and $c2/$k2/$v2), replicates PowerShell’s parseConfig (custom base-52 decoder) and initializeComponent (rotate+XOR pre-whitening -> AES-256-CBC decrypt -> raw-Deflate decompress) exactly, and writes out the two resulting PE files plus their SHA-256/MD5 hashes:

import re, io
from Crypto.Cipher import AES

PAYLOAD = r"C:\Users\Ashura\AppData\Local\Temp\claude\C--Users-Ashura-Desktop\4be520e7-3601-4b09-acfa-7a6a199bf95d\scratchpad\vbswork\final_payload.bin"
data = io.open(PAYLOAD, encoding="utf-8-sig").read()
lines = data.split("\n")

def get_quoted(line):
    m = re.search(r"'([^']*)'", line)
    return m.group(1)

def get_bytearray(line):
    m = re.search(r"=\s*(.+)$", line)
    vals = [int(x.strip(), 16) for x in m.group(1).split(",")]
    return bytes(vals)

c1_s = get_quoted(lines[35])
k1 = get_bytearray(lines[36])
v1 = get_bytearray(lines[37])
c2_s = get_quoted(lines[39])
k2 = get_bytearray(lines[40])
v2 = get_bytearray(lines[41])
print("c1 str len", len(c1_s), "k1 len", len(k1), "v1 len", len(v1))
print("c2 str len", len(c2_s), "k2 len", len(k2), "v2 len", len(v2))

def parseConfig(s):
    lut = {}
    ai = 0
    for c in list(range(65,91)) + list(range(97,123)):
        lut[c] = ai
        ai += 1
    n = len(s)
    out = bytearray()
    i = 0
    sc = [ord(ch) for ch in s]
    while i < n:
        r = n - i
        if r >= 10:
            v = lut[sc[i]]
            for q in range(1,10):
                v = v*52 + lut[sc[i+q]]
            chunk = bytearray(7)
            for q in range(6,-1,-1):
                chunk[q] = v & 0xFF
                v >>= 8
            out += chunk
            i += 10
        else:
            if r == 9: nb = 6
            elif r == 8: nb = 5
            elif r == 6: nb = 4
            elif r == 5: nb = 3
            elif r == 3: nb = 2
            elif r == 2: nb = 1
            else: break
            v = lut[sc[i]]
            for q in range(1, r):
                v = v*52 + lut[sc[i+q]]
            chunk = bytearray(nb)
            for q in range(nb-1,-1,-1):
                chunk[q] = v & 0xFF
                v >>= 8
            out += chunk
            i += r
    return bytes(out)

c1 = parseConfig(c1_s)
c2 = parseConfig(c2_s)
print("c1 decoded bytes:", len(c1))
print("c2 decoded bytes:", len(c2))

def initializeComponent(s, k, v):
    s = bytearray(s)
    kl = len(k)
    rots = [1,4,2,5,3]
    for i in range(len(s)):
        b = s[i]
        r = rots[i % 5]
        rotated = ((b >> r) | (b << (8 - r))) & 0xFF
        s[i] = (rotated ^ k[i % kl] ^ 0x5A) & 0xFF
    cipher = AES.new(k, AES.MODE_CBC, iv=v)
    pt = cipher.decrypt(bytes(s))
    pad = pt[-1]
    if 1 <= pad <= 16 and pt[-pad:] == bytes([pad])*pad:
        pt = pt[:-pad]
    import zlib
    # .NET DeflateStream = raw deflate, no zlib/gzip header
    decompressed = zlib.decompressobj(-15).decompress(pt)
    return decompressed

b1 = initializeComponent(c1, k1, v1)
print("b1 (loader .NET assembly) decompressed len:", len(b1), "magic:", b1[:4])

b2 = initializeComponent(c2, k2, v2)
print("b2 (final PE payload) decompressed len:", len(b2), "magic:", b2[:4])

OUT_DIR = r"C:\Users\Ashura\AppData\Local\Temp\claude\C--Users-Ashura-Desktop\4be520e7-3601-4b09-acfa-7a6a199bf95d\scratchpad\vbswork"
io.open(OUT_DIR + r"\b1_loader.bin", "wb").write(b1)
io.open(OUT_DIR + r"\b2_final_payload.bin", "wb").write(b2)
print("written b1_loader.bin, b2_final_payload.bin")

import hashlib
for name, b in [("b1_loader", b1), ("b2_final_payload", b2)]:
    print(name, "sha256:", hashlib.sha256(b).hexdigest(), "md5:", hashlib.md5(b).hexdigest())

The second runs against the two files that the above script produces (b1_loader.bin, b2_final_payload.bin) — it parses each PE header for the compile timestamp and .NET markers (BSJB metadata magic, the v4.0.30319 runtime string), and extracts printable ASCII/UTF-16LE strings to search for the identity/injection-primitive and RAT/stealer-capability keywords that populate this section’s findings and the capability table:

import re, struct, datetime

def pe_header_info(fn):
    data = open(fn, 'rb').read()
    e_lfanew = struct.unpack_from('<I', data, 0x3C)[0]
    sig = data[e_lfanew:e_lfanew+4]
    machine, nsec = struct.unpack_from('<HH', data, e_lfanew+4)
    timestamp = struct.unpack_from('<I', data, e_lfanew+8)[0]
    ts = datetime.datetime.utcfromtimestamp(timestamp) if 0 < timestamp < 2**31 else 'invalid'
    print(fn, 'sig', sig, 'machine', hex(machine), 'sections', nsec, 'timestamp', timestamp, ts)
    print(' is .NET (BSJB metadata magic present)?', b'BSJB' in data)
    print(' is .NET (v4.0.30319 runtime string present)?', b'v4.0.30319' in data)

def extract_strings(fn):
    data = open(fn, 'rb').read()
    utf16 = re.compile(rb'(?:[\x20-\x7e]\x00){5,}')
    strs_u16 = {m.group().decode('utf-16le') for m in utf16.finditer(data)}
    ascii_re = re.compile(rb'[\x20-\x7e]{5,}')
    strs_ascii = {m.group().decode('ascii') for m in ascii_re.finditer(data)}
    return strs_ascii | strs_u16

for fn in ['b1_loader.bin', 'b2_final_payload.bin']:
    pe_header_info(fn)

print()
print('--- b1_loader.bin: loader-assembly identity / injection primitives ---')
b1_strings = extract_strings('b1_loader.bin')
for k in ['NebulaCactus', 'OrbitSalsa', 'Roguelike', 'CreateProcess',
          'WriteProcessMemory', 'NtUnmapViewOfSection', 'ResumeThread',
          'GetThreadContext', 'SetThreadContext', 'ZwUnmapViewOfSection', '.exe']:
    hits = [s for s in b1_strings if k in s]
    print(k, '=>', hits[:5])

print()
print('--- b2_final_payload.bin: RAT/stealer capability keyword scan ---')
b2_strings = extract_strings('b2_final_payload.bin')
kws = ['RAT', 'Client', 'Server', 'Stub', 'Mutex', 'Async', 'Quasar', 'Xworm',
       'DcRat', 'njRat', 'Remcos', 'Discord', 'webhook', 'Telegram', 'bot',
       'http://', 'https://', '.onion', 'password', 'Chrome', 'Grabber',
       'Stealer', 'keylog', 'Keylog', 'Socket', 'Port', 'Host', 'C2',
       'config', 'Config']
for k in kws:
    hits = [s for s in b2_strings if k.lower() in s.lower()]
    if hits:
        print(k, '=>', hits[:8])