Voltar para todas as publicações
Publicado em · por Renaud Deraison

Ele pediu para ser executado

A Sonatype está rastreando 846 pacotes npm maliciosos publicados a partir de centenas de contas descartáveis, com nomes gerados por IA. Nenhum deles traz script de instalação. O README manda você carregar o pacote com require(), e o import executa o loader. As defesas que reconhecem um pacote ruim pelo nome, pelo autor ou pelo hook declarado agora estão correndo atrás de um gerador. O Bromure Agentic Coding bloqueia pela única propriedade que um gerador não consegue forjar: a idade do pacote.

Os pacotes não trazem nenhum hook de instalação, então o npm não tem o que interceptar. O README manda carregá-los com require(), e o import executa o loader. Cada vez mais, o leitor que segue essa instrução é um agente de código.

A Sonatype Research Labs divulgou em 5 de agosto uma campanha que chama de Flooding Dropper, contando 846 componentes npm maliciosos e a conta ainda subindo. Paul McCarty, do OpenSourceMalware, rastreia o payload sob o nome WEL1DROPPER. A cobertura mais ampla saiu em 7 de agosto.

Dois detalhes a separam das histórias de supply chain do npm do último mês.

O primeiro é como os pacotes ganham seus nomes. O atacante os gera, rodando slopsquatting como uma linha de produção. Radicais recorrentes como bigops e bnpl se recombinam com outras palavras em nomes como bigops-api e dolyame-boxy-desktop-bnpl-card-gallery, muitos deles compartilhando um padrão de versão 35.x.y. A publicação se espalha por centenas de contas descartáveis com um punhado de pacotes cada, de modo que não há um autor prolífico para banir.

Sem script de instalação

Os pacotes não declaram nenhum hook de ciclo de vida, então o npm install não executa nada. O texto de McCarty diz isso sem rodeios: “Os pacotes NPM não usam script preinstall nem postinstall. Não é preciso.”

Eles trazem um _helpers.js empacotado que roda quando o módulo é importado, junto com um README que manda o leitor importá-lo. Carregue o pacote com require(), diz a documentação, e a documentação está certa. O primeiro estágio faz o fingerprint do sistema operacional e da arquitetura do processador, puxa um segundo estágio nativo por HTTPS de um entre três hosts Cloudflare Workers, e recorre a remontar o binário a partir de pedaços base64 guardados em registros DNS TXT sob wel1.ru quando a busca direta falha.

O segundo estágio varia por plataforma. O Linux recebe um ELF empacotado com UPX que instala um implante C2 Sliver. O macOS recebe verificações de antidepuração, uma sondagem por VMware, um binário de beacon e persistência via LaunchAgent. O Windows recebe patching de ETW e AMSI, detecção de sandbox, execução reflexiva em memória e persistência por uma chave Run do Registro e uma tarefa agendada. McCarty e Sonatype leem a campanha como uma evolução da “Moika”, a onda de confusão de dependências de abril que empurrou uns 250 pacotes construídos em torno do roubo de ambiente.

O atacante tirou o gatilho do gerenciador de pacotes e o colocou no leitor.

Três jeitos de ser irreconhecívelo nome é geradobigops-api, dolyame-boxy-desktop-bnpl-card-gallery, e mais ~844a deny list precisa do nome anteso gerador tem mais nomeso publicador é descartávelcentenas de contas, poucospacotes em cada umabanir o autor bane quatropacotes em oitocentosnão há script de instalaçãosem preinstall, sem postinstallo README diz: use require()_helpers.js roda no importo gatilho é o leitoro import executa o loaderfingerprint de SO e arch, busca estágio nativoCloudflare Workers, ou DNS TXT sob wel1.ru
A campanha é construída para derrotar o reconhecimento. O atacante gera os nomes em vez de escolhê-los, gira os publicadores por centenas de contas descartáveis e moveu o gatilho de execução do hook de ciclo de vida para uma instrução no README, sem deixar script de instalação para um scanner achar nem nome ou autor estável a que uma deny list possa se agarrar.

A automação vence a deny list

O próprio resumo da Sonatype é a frase mais afiada de toda a cobertura:

Leia isso como uma afirmação sobre defesas. Toda camada que protege você reconhecendo o pacote ruim, seja pelo nome, pela conta de publicação ou pelo hook declarado, agora compete com um gerador. Essas camadas continuam pegando o que já viram. Elas são avaliadas por reconhecer uma string específica, e o fornecimento de strings específicas custa uma execução de script.

O ângulo do agente afia tudo isso. O slopsquatting compensa em primeiro lugar porque modelos de linguagem inventam nomes de pacote que soam certos e não existem, e um atacante que registra esses nomes recolhe tráfego de instalação de graça. Some o hook de instalação ausente e você tem um pacote cujo gatilho é uma instrução escrita num README. Um agente de código é o leitor que vai alucinar o nome, achar um pacote debaixo dele, ler a documentação e fazer o que a documentação manda. A campanha é moldada para o agente nas duas pontas.

Escolha uma propriedade que o gerador não consiga forjar.

Idade não é um nome

Todo pacote desta campanha compartilha uma propriedade que nenhuma automação muda: ele é novo. Um nome que um modelo inventou na terça passada não pode ter sob ele um pacote de 2019. Centenas de contas descartáveis publicam centenas de versões recém-saídas, porque uma conta descartável não tem histórico de onde publicar. O modelo depende de o pacote estar lá quando alguém for buscá-lo, então o atacante o cria poucos dias antes, no máximo.

O Bromure Agentic Coding roda o agente dentro de uma VM Linux descartável em Apple Silicon e roteia cada byte do seu tráfego de rede por um proxy no host, fora da caixa em que o agente roda. As buscas de pacote de npm, PyPI, Cargo, RubyGems, Maven, NuGet, módulos Go e Packagist atravessam todas esse proxy, e a primeira política que encontram é um relógio.

A trava de idade, ligada por padrão

O Bromure recusa pacotes mais novos que uma idade mínima, definida em dois dias de fábrica. Referências flutuantes como latest ou um intervalo semver resolvem para a versão mais recente anterior ao corte, de modo que instalações comuns continuam funcionando e caem no lançamento de ontem. Uma referência fixada a uma versão fresca demais recebe um 451 com um erro claro do Bromure nomeando o que aconteceu. Oitocentos pacotes custam tanto para parar quanto oito, porque o Bromure não reconhece nenhum deles.

Ela roda antes de a discussão começar

A parte esperta desta campanha, largar o hook de ciclo de vida e mover a execução para o require(), descreve o que acontece depois que o tarball chega ao disco. A trava de idade fica na busca, no proxy do host, acima de tudo isso. Se o payload teria rodado na instalação ou na importação é uma pergunta sobre um pacote que nunca chegou. O .npmrc e o pip.conf dentro da VM podem apertar essa política e não podem afrouxá-la.

Reputação, atrás do relógio

Com uma chave do socket.dev, o Bromure confere cada pacote contra a base do socket.dev antes de liberar a busca e bloqueia os marcados como comprometidos, malware ou typosquats. Consultas OSV bloqueiam pacotes que carregam vulnerabilidades conhecidas a partir de uma severidade que você escolhe. O Delpi redireciona toda requisição npm para um registry de filtragem. Essas camadas reconhecem coisas, o que as torna úteis e as coloca em segundo lugar.

O README é pontuado pelo que ele é

“Carregue isto com require()” é uma instrução escrita por um estranho, chegando em texto que o agente lê. O Bromure pontua a saída de ferramentas que o agente consome, incluindo conteúdo de arquivos, páginas web e documentação buscada, com um modelo local de injeção de prompt. Ele pontua os arquivos de instrução que o agente trata como autoridade, CLAUDE.md e AGENTS.md e suas variantes, com um classificador dedicado mais um scanner de payloads em Unicode invisível. Nada sai do Mac para isso.

O payload traça o perfil da máquina errada

Suponha que a busca passe mesmo assim, porque o pacote envelheceu além do corte ou uma entrada de allowlist o dispensa. Siga o WEL1DROPPER por um perfil Bromure e veja-o fazer seu trabalho contra uma máquina que você ia jogar fora.

O primeiro movimento do loader é fazer o fingerprint do host. Num perfil Bromure, o host que ele mede é uma VM Ubuntu em NAT, a um hipervisor de distância do macOS, então ele toma o ramo Linux e planta ali um implante Sliver. Ele nunca pede o ramo macOS: a antidepuração, a sondagem por VMware, o download do beacon_mac.bin, o LaunchAgent que sobreviveria a um reboot, o interesse por domínios russos de pagamento. Tudo isso é código para uma máquina que o dropper nunca viu. O Erase home descarta a camada em que ele pousou e devolve o diretório home ao estado logo após o clone.

Então o estágio infostealer vai atrás do que seu ancestral Moika foi atrás: o ambiente. Num perfil Bromure, o ambiente guarda placeholders brm_…. Nenhuma das chaves reais de Anthropic, OpenAI e xAI, nenhum dos tokens do GitHub e do GitLab, nenhum material da AWS nem credencial de registry fica na memória da VM ou nos seus dotfiles. O Bromure os mantém no host e os substitui na requisição no proxy, no fio, para o único host de destino a que cada um pertence. O roubo roda até o fim e despacha strings que só resolvem em um Mac, que não é a máquina que executou o código.

No Mac do desenvolvedorele faz o fingerprint do hostmacOS, arm64: segue o ramo macbeacon_mac.bin, LaunchAgent, sobrevive ao rebootele lê o ambientechaves de API, credenciais git, material de nuvemtudo real, tudo utilizávele ele ficaa máquina que ele domina é a que você usaDentro de um perfil Bromurea busca encontra um relógio antespublicado hoje, corte de dois dias: 451nenhum nome casado, nenhuma assinaturae se ele um dia chegarLinux numa VM, ramo mac sem usoo ambiente só guarda placeholders brm_e ele não ficaErase home zera a camada em que pousou
O dropper escolhe o payload conforme o host que encontra. Dentro de um perfil Bromure esse host é uma VM Linux descartável, então ele nunca pede o ramo macOS com sua persistência por LaunchAgent, sua sondagem por VMware e seu binário de beacon, e o estágio infostealer lê credenciais placeholder cujos valores reais nunca entraram na VM.

A caçada que eles recomendam, já rodando

McCarty fecha o texto sobre o WEL1DROPPER com conselhos defensivos: caçar consultas DNS TXT para wel1.ru, revisar logs de proxy em busca de tráfego para os hosts Cloudflare Workers, e procurar arquivos temporários e LaunchAgents perdidos.

Leia o segundo item de novo. Seu laptop não mantém log de proxy. Um perfil Bromure mantém um por construção: toda requisição que a VM faz atravessa o proxy do host, e o Security Log e o Trace Inspector registram host, status, latência e relatório de substituição de cada uma. Três destinos *.workers.dev desconhecidos aparecendo durante um build são três linhas numa lista que você pode ler, na máquina em que o build rodou, sem implantar nada para obtê-las.

Os registries vão continuar derrubando esses pacotes, e as derrubadas funcionam: 846 deles sumiram ou estão sumindo. Mas a campanha já passou de mil, publicada a partir de contas que não custam nada para criar sob nomes que não custam nada para inventar, e a próxima onda está a uma execução de script de distância. Reconhecimento é a competição errada para entrar.

Um pacote de dois dias não merece uma confiança profunda que falte a um de duas horas. Ele fica fora da janela em que essa economia opera, e você pode mover seu grafo de dependências para trás dessa janela com um ajuste que já está ligado.

Instale o Bromure Agentic Coding, e deixe a busca encontrar um relógio antes de encontrar sua máquina.