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

A instalação estava limpa

Em julho, o npm deixou de executar por padrão os scripts de instalação das dependências. Em 17 de setembro, a Checkmarx publicou uma campanha que não discute essa decisão: o carregador vive dentro de `BTree.prototype.set`, o método para o qual a biblioteca existe, e dispara na primeira vez que algo guarda uma chave igual a 100. Rodou onze semanas sobre dois milhões de downloads semanais, e o seu canal de comando é um contrato inteligente na Ethereum Sepolia. Um agente de programação é a vítima ideal, porque executar o código é o trabalho. O Bromure Agentic Coding decide o que esse código pode alcançar antes de ele rodar.

Bloquear os scripts de instalação foi a decisão certa, e o ecossistema npm a tomou em julho. Esta campanha mudou a sua execução para a primeira vez em que você usa o pacote: a instalação continua limpa e é a sua suíte de testes que puxa o gatilho.

O seu agente precisa de um mapa ordenado. Escolhe um pacote de árvore B no npm, acrescenta-o ao package.json e instala. Nada roda. O npm 12 já não executa scripts de instalação de dependências, então não há postinstall, não há preinstall, não há hook de build, não há nada que uma política de scripts de ciclo de vida possa recusar. O agente escreve o código que usa a nova dependência, escreve um teste que insere algumas centenas de linhas, e o executa.

A centésima inserção inicia um segundo processo Node.

A Checkmarx publicou esta campanha em 17 de setembro, com pesquisa de Bruno Dias. O BleepingComputer cobriu o caso no dia 20 e o InfoWorld no dia 21. O pacote é o indexed-btree, feito para ser confundido com o verdadeiro sorted-btree, e quando o npm o removeu em 3 de setembro ele puxava perto de dois milhões de downloads por semana. Estivera no ar, na sua forma maliciosa, por cerca de onze semanas.

A defesa que todos tinham acabado de construir

Em junho a CLI do npm inverteu o seu padrão para scripts de instalação, e o npm 12 entregou essa mudança em julho: preinstall, install e postinstall de uma dependência já não rodam a menos que o projeto os autorize pelo nome, e dependências via git ou URL remota também deixaram de resolver por padrão. Os números da JFrog, citados naquela cobertura, colocam esses três vetores em cerca de 53% dos pacotes npm maliciosos vistos no ano anterior. Os arreios de agentes adotaram a mesma postura: --ignore-scripts, ou um proxy que retira os hooks do tarball antes que o gerenciador de pacotes o veja. Contando onze semanas para trás a partir da data de remoção, a primeira versão maliciosa do indexed-btree cai no meio dessa implantação.

Então os operadores pararam de usar scripts de instalação. O carregador vive em BTree.prototype.set, o método para o qual a biblioteca existe. Quando a chave passada a ele é 100, o código lança um carregador de primeiro estágio ofuscado como processo Node desanexado, a partir de extended/sharedLoad.min.js. O resumo da Checkmarx é exato sobre por que isso é difícil de prever: o carregador se esconde dentro do método da própria biblioteca, o que é, nas palavras deles, “uma forma bem construída de passar despercebido pelas ferramentas padrão de análise de contaminação”.

Veja o que o arma: uma chave com o valor 100, numa biblioteca cuja finalidade é guardar chaves. Um placar chega lá, um índice paginado chega lá, e um teste que insere linhas em laço chega lá na centésima iteração. Use o pacote como o README manda e você o arma.

Nada roda na instalação. Tudo roda no primeiro uso.o que o desenvolvedor faznpm installnenhum script de ciclo de vidatodas as verificações passamnode_moduleso código fica ali, inertepor minutos ou por semanasnpm testo agente exercita o códigolinhas entram na árvore.set(100, …)o gatilho é dado comumprocesso Node desanexadoo que o carregador faz depoisperfilar a máquinahostname, arquitetura,CPU, memória, uptimepara Slack e Telegramconsultar o contratoum contrato inteligente naEthereum Sepolialido por RPC públicoabrir o estágio doistroca de chaves X25519,AES sobre o segredo ECDHcarga guardada no contratolimpar tudoapagar os arquivos,remover o gatilhodo protótipoOs três destinos de saída são a campanha inteira. Tire-os e o carregador é só um processo que acordou numa máquina sem nada.
As defesas de instalação não veem nada, porque nada acontece na instalação. O carregador fica no próprio método set da biblioteca e dispara quando chega uma chave igual a 100 — dado comum numa biblioteca de estruturas de dados. Ele gera um processo Node desanexado, reporta a máquina a um canal do Slack e a uma conversa do Telegram, depois lê o seu próximo endereço num contrato inteligente na Ethereum Sepolia, decifra um segundo estágio guardado no contrato, e apaga tanto os seus arquivos quanto o gatilho.

Onze semanas, e um repositório que parecia bom

O resto da técnica mira as verificações que você mesmo faria.

O canal de comando é um contrato inteligente na Ethereum Sepolia. O carregador lê nesse contrato o seu próximo endereço, gera um par de chaves X25519, pega a chave pública do operador no contrato, deriva uma chave AES por ECDH, e decifra dois textos cifrados guardados na cadeia para montar o segundo estágio. A Checkmarx é direta sobre a vantagem: “Esta técnica é mais resistente à derrubada de domínio ou de IP do que as abordagens tradicionais de C2, já que usa o contrato inteligente como ponteiro para um novo endereço sempre que o antigo é derrubado.” Nada disso lhe dá um domínio ruim para bloquear. Você recebe um endpoint RPC público e um endereço de contrato, e os operadores podem reapontar o contrato sem tocar numa única cópia instalada do pacote.

O projeto parecia real. Os operadores montaram um repositório no GitHub com um histórico de commits plausível e uma conta de desenvolvedor à altura, e o código desse repositório não contém a parte maliciosa. Ela está apenas no tarball do npm. Ler o fonte no GitHub, que é o conselho que todo mundo dá, teria confirmado que o pacote estava em ordem.

A idade também trabalhou a favor deles. Onze semanas fazem de um pacote uma dependência estabelecida quando você o instala, nada parecido com a versão sequestrada que é retirada em poucas horas. A Checkmarx contou nove pacotes irmãos com o mesmo comportamento, só o btree-core passando de 1,9 milhão de downloads e os outros nas centenas de milhares cada, e diz que os operadores controlam uma carteira com 109 ETH.

Depois, com o segundo estágio no lugar, o carregador apaga os próprios arquivos e retira o gatilho do protótipo. O pacote em disco volta a parecer uma biblioteca de árvore B.

Executar o código é o trabalho

Toda defesa acima pressupõe um momento em que uma pessoa decide executar alguma coisa. Um agente elimina esse momento.

Um agente de programação instala uma dependência, escreve código contra ela, roda a suíte de testes, lê a falha, edita e roda de novo, dezenas de vezes por hora, enquanto você está numa reunião, porque esse laço é toda a proposta de valor. Você não está lendo a centésima inserção. O agente também tem mais motivo do que você para produzir o tipo de dado que aciona esse gatilho: escreve fixtures em laços, popula tabelas com IDs sequenciais, e faz benchmarks com alguns milhares de linhas. Se a condição de armamento é “alguém guardar a chave 100”, um agente chega lá mais cedo e com mais frequência do que uma pessoa, num processo que carrega o que a sessão carrega.

As defesas de instalação cobrem o momento em que um pacote chega. O seu agente passa um minuto por dia ali e todo o resto executando o que chegou.

O que o carregador precisa

Reduza a campanha ao osso e ela precisa de duas coisas: algum lugar para alcançar, e algo que valha a pena levar da máquina em que acordou. Você decide as duas antes de ela rodar, e nenhuma dessas decisões exige reconhecer o pacote.

Num laptop de desenvolvedor, as duas vão para o lado do atacante. O reconhecimento que ele envia — hostname, arquitetura, CPU, memória e uptime — descreve a sua máquina na sua rede. O processo que ele gera roda como você, com o seu shell, o seu ~/.git-credentials, o seu GH_TOKEN, o seu ~/.aws, o seu agente SSH. Os seus três destinos são slack.com, api.telegram.org e um provedor público de RPC Ethereum: dois hosts SaaS com os quais metade do seu ferramental já conversa, e um endpoint de blockchain que parece infraestrutura. Nada nessa lista se lê como malicioso num log de netflow.

O Bromure Agentic Coding responde às duas metades no host, fora da VM em que o agente roda.

As credenciais vêm primeiro, porque são o alvo. Cada segredo que você configura existe dentro da VM do workspace como um marcador que preserva a estrutura (um token do GitHub é ghp_ mais 36 caracteres, uma chave da Anthropic começa com sk-ant-api03-brm-), derivado do valor real com HKDF-SHA256 e um sal por instalação, de modo que gh, git e claude o aceitam e ele nunca parece rotacionar entre sessões. Os valores reais ficam cifrados no seu Mac, e o proxy do host os substitui no fio depois que a requisição deixou a VM, somente para o host para o qual aquela credencial foi cunhada. O manual enuncia o invariante: nenhum arquivo, variável de ambiente ou processo dentro da VM jamais contém uma chave de API real, um token OAuth real, um segredo AWS real ou uma chave privada SSH real. Um ladrão que lê o ambiente recolhe marcadores.

Levar um marcador para onde ele não pertence é, em si, o alarme. O proxy examina cada requisição de saída em busca de falsos destinados a um host para o qual não foram cunhados. Quando um aparece, o proxy bloqueia a requisição com um 451, o destino não recebe nada, e o Bromure pausa a VM. Ele reata à força uma sessão desanexada, tinge a moldura de vermelho, e levanta um alerta nomeando a prévia do token, a credencial, o host a que ela pertencia e o host para onde ia. Você pode desligar o workspace, ou escolher Save for Investigation, que exporta o disco do sistema, o diretório pessoal e cada pasta compartilhada antes de parar. Este carregador apaga os próprios arquivos depois do handshake, então quem olhar depois encontra um disco arrumado. Pausar no meio da requisição lhe dá a máquina antes que ela arrume.

Depois, o alcance. Os guardrails trazem um firewall de saída por workspace: um conjunto ordenado de regras ao estilo pf sobre cada conexão que a VM abre, por host, CIDR, protocolo, porta e, para regras web, por método HTTP individual, com um controle Unmatched traffic ajustado em Allow ou Deny. Ponha em Deny, liste os registries e as APIs de que o trabalho precisa, e um carregador que estende a mão para uma API de bot do Telegram ou um provedor de RPC Ethereum encontra uma recusa no fio. Duas camadas o aplicam, de modo que o convidado não pode contornar nenhuma delas: o switch virtual casa as regras por IP de destino e por hostname obtido do DNS, em TCP e UDP, e o proxy as casa de novo por nome de servidor TLS e por método. O switch desvia os fluxos das portas 80 e 443 da VM para dentro do próprio proxy, de forma que não há variável de proxy para uma carga útil desativar. Cada veredito aparece como uma linha Firewall na Security Timeline, e edições de regras alcançam sessões em execução sem reinicialização.

num laptop de desenvolvedoro carregador acorda como vocêGH_TOKEN · ~/.git-credentials · ~/.awstokens reais, hostname real, rede realo perfil que ele envia descreve o seu Macnada entre o processo e a internetSlacko perfil chegaTelegramo perfil chegaRPC Sepoliao estágio dois chegaTrês destinos que parecem tráfego SaaS comum.Depois o carregador se apaga.num workspace Bromureo carregador acorda no convidadoghp_ + 36 chars — um marcador, não um tokenos segredos reais ficam cifrados no Maco perfil que ele envia descreve uma VM Ubuntuo proxy é a única saídafirewall de saída — tráfego não casado: Denyapi.telegram.org:443 tcp — bloqueadoCasado no switch por IP e nome DNS, e de novono proxy por nome TLS e método HTTP.
O mesmo carregador, dois ambientes. Num laptop, ele perfila a sua máquina real, roda com os seus tokens reais, e alcança três destinos que parecem tráfego SaaS comum. Num workspace Bromure, ele perfila uma VM, encontra apenas credenciais marcadoras que não valem nada lá fora e acionam o detector de comprometimento se forem levadas para fora do escopo, e encontra na saída um conjunto ordenado de regras cujo padrão para tráfego não casado pode ser Deny.

Onde o calendário não alcança

O filtro de idade do Bromure, ativo por padrão em dois dias, existe para o padrão do sequestro: uma conta de mantenedor comprometida, uma versão envenenada publicada, tudo isso descoberto e retirado em poucas horas. Ele já cobriu várias dessas histórias. Onze semanas passam por qualquer corte que você definisse, de modo que aqui o trabalho cabe a outras camadas.

O painel Supply Chain é uma pilha por essa razão. A camada feita para pacotes maliciosos desde o início é a filtragem de pacotes: o bloqueio de pacotes comprometidos do socket.dev cobre pacotes sinalizados como malware, typosquats, scripts de instalação suspeitos e código ofuscado, e o Depi substitui o registry do npm por um espelho pré-verificado. Os dois rodam no host, com as suas chaves de API guardadas no seu Mac e nunca exportadas para a VM, e os dois ficam à frente de uma instalação, não atrás. Um bloqueio volta como um HTTP 451 cujo corpo em texto puro o npm imprime palavra por palavra, então o agente lê o motivo e contorna em vez de travar.

Nenhum serviço de reputação pega tudo, e um typosquat que rodou limpo por onze semanas é o caso difícil para todos eles, e é aí que o resto da pilha ganha o seu lugar. As credenciais marcadoras, o detector de comprometimento e o conjunto de regras de saída decidem o que um pacote pode alcançar depois de instalado, e nenhum deles precisa reconhecer o pacote. Eles cobrem código que ninguém classificou ainda, inclusive código que ninguém vai classificar pelas próximas onze semanas.

Suponha que dispara no primeiro uso

Bloquear scripts de ciclo de vida vale a pena, e esta campanha contorna isso. Planeje para uma carga útil que acorda quando a sua suíte de testes chama a biblioteca.

O gatilho é dado válido

Uma chave igual a 100 numa biblioteca de chave-valor. Nenhuma lista de entradas permitidas ajuda, e um agente gera esse dado mais rápido que você.

Não deixe nada no processo para roubar

Um reconhecimento que descreve uma VM e tokens que não valem nada lá fora transformam uma execução bem-sucedida numa execução desperdiçada.

Decida os destinos de antemão

Slack, Telegram e um endpoint público de RPC Ethereum não são bloqueáveis por reputação. Uma lista de permissões não precisa reconhecê-los.

Bloquear scripts de instalação tirou do tabuleiro cerca de metade dos pacotes npm maliciosos. É por isso que os operadores tiveram de construir algo tão elaborado — um repositório falso, um gatilho disfarçado de aritmética e um canal de comando numa blockchain — para colocar código que, um ano atrás, precisava de quatro linhas num package.json. A defesa funcionou, e o preço do ataque subiu.

A suposição por baixo nunca se mexeu. Você ainda trata uma dependência como segura depois de instalada, e executá-la como um ato neutro. Para um agente de programação, executá-la é o trabalho, milhares de vezes por dia, sem ninguém na sala. Decida o que esse processo pode carregar e para onde pode se conectar antes de ele rodar, e a centésima inserção do seu agente acaba como uma linha Firewall bloqueada na Security Timeline.