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

O desafio de código proibia assistentes de IA

A Kaspersky publicou em 1º de setembro a campanha de falsos recrutadores do Mirage Kitten. A isca é um teste de programação para fazer em casa, e o README faz três pedidos: terminar em três horas, não modificar o server.js porque ele já está livre de bugs, e não usar assistente de IA. A primeira linha do server.js importa um pacote npm trojanizado que nunca foi publicado no npm. Ele veio dentro do zip, já instalado em node_modules. Todo controle de cadeia de suprimentos já construído vigia uma requisição ao registro que este ataque não faz. O Bromure Agentic Coding defende a outra metade: a máquina em que o código roda.

O atacante pediu que você não usasse um agente de código. Uma ferramenta que abre todos os arquivos do repositório, digam as instruções o que disserem, era a única coisa entre esta campanha e um backdoor, e o README convenceu você a abrir mão dela.

Um recrutador manda mensagem no LinkedIn. A vaga parece real, o nome da empresa é conhecido, e o próximo passo é um desafio técnico para fazer em casa. Você recebe um zip. Dentro há um app de gestão de projetos chamado TaskFlow, feito com Express, React e Vite: algumas centenas de arquivos e um README na raiz. Corrija os defeitos do frontend. Você tem três horas.

Essa é a coisa mais comum que acontece com um engenheiro em atividade. É também, segundo a análise publicada pela Kaspersky em 1º de setembro, como um grupo de espionagem iraniano que o relatório chama de Mirage Kitten, rastreado em outros lugares como UNC1549, Smoke Sandstorm ou Nimbus Manticore, vem deixando backdoors multiplataforma em desenvolvedores do Egito, da Etiópia e do Afeganistão, com outros envios vindos da Índia, Turquia, Israel, Iraque, Alemanha e Irlanda. Os arquivos chegam com nomes como Front-Technical-Challenge.zip e Task-FullStack.zip, hospedados na AWS para que o download pareça infraestrutura e não uma carga maliciosa. O The Hacker News cobriu a campanha em 9 de setembro.

Três regras, todas sobre leitura

A Kaspersky registra três instruções no README. Veja o que cada uma rende ao atacante.

O README também impunha um limite de três horas e proibia o uso de assistentes de IA.

Afirmava ainda que o server.js estava livre de bugs e não devia ser modificado.

O relógio é a regra óbvia: três horas para corrigir uma base de código desconhecida é um cronograma que não deixa espaço para curiosidade. A segunda regra aponta sua atenção para o frontend e para longe de um arquivo específico. A terceira é a interessante, porque é o atacante dizendo de qual ferramenta ele tem medo.

Um agente de código a quem se entrega este repositório não respeita o escopo que o README sugere. Aponte o Claude Code ou o Codex para o projeto e peça que corrija o frontend, e ele ainda assim lê o server.js, porque entender como o app sobe é como ele entende com o que o frontend conversa. Ele lê a árvore inteira. Não tem relógio de três horas nem motivo para pular um arquivo que alguém declarou saudável. Banir o assistente remove da sala o único leitor que lê tudo, o que é uma exigência estranha para um processo de contratação.

E o que ele teria lido é a linha um.

A dependência nunca esteve no npm

A primeira linha do server.js importa um pacote chamado colorized_terminal, na versão 2.1.0. Uma amostra irmã usa pretty-log, também 2.1.0. Nenhum dos dois é typosquat de coisa alguma, e nenhum é uma publicação sequestrada, porque nenhum está no registro:

Os atacantes empacotaram a dependência diretamente no diretório node_modules do arquivo do desafio, em vez de publicá-la no registro npm.

Quando importado, o pacote lançava silenciosamente um implante a partir de node_modules/.cache/.320697f1/index.js como processo desanexado em segundo plano.

Nenhum npm install roda aqui, e nenhum script postinstall. Não há entrada de lockfile para verificar, nenhum hash de integridade para comparar, nenhuma atestação de procedência para checar, nenhum carimbo de publicação para envelhecer, nenhum provedor de reputação para consultar, e nenhuma requisição ao registro na qual pendurar qualquer uma dessas coisas. A dependência estava na pasta antes de você abrir a pasta. Rodar o app que o README pede para corrigir, que é a tarefa inteira, significa um require simples, e esse require bifurca um backdoor.

Três anos de defesas contra pacotes maliciosos se apoiam todas numa busca ao registro. O Mirage Kitten não faz nenhuma.

A cadeia de suprimentos sem registroFront-Technical-Challenge.zipREADME.mdtrês horas · sem IA“server.js está sem bugs”server.jslinha 1: require('colorized_terminal')o arquivo que mandaram pularnode_modules/[email protected]já instalado · fora do npmvocê roda o appo import dispara um implantenode_modules/.cache/.320697f1/index.jsprocesso em segundo planoNodeRabbit chama de voltaplugplay.azurewebsites.netrgbteller.azurewebsites.net11 comandos, depois 23o que nunca dispara, e por quênenhuma requisição ao registroidade do pacote — sem dataintegridade — sem lockfileprocedência — nada foi publicadoreputação — nenhum pacote a consultarscripts removidos — sem instalaçãotyposquat — nenhum nome a compararresta a máquina que executa o código
O arquivo chega com as dependências já instaladas, de modo que a cadeia de suprimentos nunca toca um registro. A linha um do arquivo que o README mandou não abrir importa um pacote que não existe em nenhum outro lugar, e importá-lo bifurca um implante a partir de um diretório oculto dentro de node_modules. Sem etapa de instalação, sem checagem de lockfile, sem carimbo de publicação: os controles que guardam buscas de pacote não têm busca alguma a guardar.

Isso não fica na pasta do desafio

O NodeRabbit começa pequeno. A primeira variante da Kaspersky, de uma máquina no Afeganistão, carrega onze comandos: ler um arquivo, escrever um arquivo, listar um diretório, despejar a configuração de rede, iniciar um processo, rodar um script, dormir. A variante egípcia acrescenta evasão de sandbox, suporte a proxy e seleção dinâmica de porta. A terceira, da Etiópia, chega a vinte e três, e as novas são onde a máquina de um desenvolvedor deixa de ser uma máquina só.

outlook:emails colhe artefatos de e-mail. persist:projects:scan percorre o disco procurando repositórios git. persist:project:inject escreve nos que encontra, pousando em .git/hooks/post-merge e .git/hooks/post-checkout para que o implante seja relançado no próximo pull. persist:vscode instala uma extensão maliciosa no seu editor, que então a carrega em todo projeto que você abre. A formulação da Kaspersky é exata quanto à intenção:

Além dos mecanismos de persistência descritos acima, a Variante 3 introduz dois mecanismos adicionais que relançam o malware por meio de fluxos de trabalho comuns de desenvolvedores.

Seus outros repositórios são o alvo. A persistência mais banal por baixo é ajustada por plataforma: uma tarefa agendada ou chave Run disparando às 10h todo dia no Windows, uma entrada cron @reboot no Linux, um LaunchAgent em ~/Library com RunAtLoad e KeepAlive ambos verdadeiros no macOS. O irmão PollCat chega por um desafio React chamado RankChallenge-react cujo README encurta o relógio para uma sessão de uma hora, protegida por um código de seis dígitos que ele afirma girar a cada trinta segundos. Mesmo desenho de pressão, menos tempo.

no laptopo implante herda sua conta inteirapersist:projects:scantodo repo no disco ganha um hookpersist:vscodeuma extensão carregada em todo projetooutlook:emails · fs:read~/.aws · ~/.kube · ~/.git-credentialslimpezaachar os hooks, girar as chaves, torcernum workspaceuma VM Ubuntu, seu disco, seu homeo scan só vê o que você montouapenas pastas compartilhadas, oito no máximoa persistência cai num disco descartávelReset disk · Erase home · checkpointstodo arquivo de credencial guarda um falsoghp_… · brm-k8s-… · sk-ant-api03-brm-…limpezaapagar disco e home, girar nada
Um arquivo, dois raios de destruição. Num laptop, o implante herda a conta inteira: todo repositório git que encontra ganha um hook, o editor ganha uma extensão, o e-mail e a configuração de nuvem são legíveis, e as credenciais no ambiente do shell são as de verdade. Num workspace do Bromure Agentic Coding os mesmos comandos rodam contra uma VM Linux que guarda credenciais de mentira, monta apenas as pastas que você compartilhou e alcança a rede por um socket no host.

Rode, em algum lugar que não seja a sua máquina

O Bromure Agentic Coding roda agentes de código dentro de uma VM Linux virtualizada por hardware no seu Mac, com todos os controles de segurança do lado do host dessa fronteira. O zip de um estranho é exatamente o trabalho para o qual ele foi feito, porque os controles descrevem a sala e não o hóspede. Nenhum deles precisa reconhecer colorized_terminal.

Um workspace é a unidade de isolamento. Uma VM Ubuntu, seu próprio disco de sistema, seu próprio home persistente, suas próprias credenciais e suas próprias políticas, e dois workspaces nunca compartilham um byte. Crie um workspace descartável para o desafio. O implante roda, o import dispara, o processo desanexado começa, e tudo isso acontece numa máquina que passou a existir hoje de manhã e guarda um único arquivo zip. Quando você terminar, Reset disk reclona o disco de sistema a partir da base e Erase home… limpa a imagem do home; os checkpoints de rollback por boot atrás de Restore home… fazem isso com granularidade mais fina. A entrada cron @reboot que o ramo Linux escreve vai junto.

Os ramos de persistência do Windows e do macOS não têm chão onde pisar. Sem AppData, sem chave Run, sem tarefa agendada, sem ~/Library, sem LaunchAgent, sem caixa do Outlook para ler. O hóspede é Linux e não é o seu Linux.

persist:projects:scan vê apenas as pastas que você compartilhou. As pastas do host são anexadas como montagens virtiofs em /home/ubuntu/<basename>, limitadas a oito por workspace, e são a única parte do sistema de arquivos do seu Mac que a VM alcança. Compartilhe a pasta do desafio e mais nada, e a injeção de hooks git encontra um repositório: o que já era hostil. Mantenha a lista curta por um segundo motivo. Uma pasta compartilhada é uma janela viva para o seu Mac, e uma limpeza por comprometimento a deixa intacta por princípio.

fs:read volta com marcadores. O Bromure substitui cada credencial que você configura por um falso que preserva a estrutura, derivado do valor real e de um sal por instalação via HKDF-SHA256: sk-ant-api03-brm-…, um token ghp_ do tamanho certo, glpat-…, brm-k8s-…, brm-docker-…. Os falsos são escritos nas variáveis de ambiente e em ~/.git-credentials, ~/.docker/config.json, ~/.kube/config, ~/.aws/config e nas configurações MCP, que é justamente a lista que um infostealer enumera. Os valores reais ficam cifrados no Mac, e o proxy do host os troca no fio, restritos ao host de destino para o qual cada um foi cunhado. A AWS vai além: o convidado assina com uma chave secreta falsa e o host reassina, de modo que uma requisição que pule o proxy morre na AWS com InvalidSignatureException. Os bytes das chaves privadas SSH nunca entram na VM, e só assinaturas atravessam. Nenhuma chave de configuração governa nada disso, porque o proxy é a única rota da VM para a rede.

Os hosts de comando são exatamente para o que serve uma lista de permissões. A maioria dos servidores de comando do NodeRabbit vive sob *.azurewebsites.net, e os do PollCat também. Nenhum feed de reputação vai algum dia colocar esse domínio em lista negra, e nenhuma heurística de idade de domínio vai sinalizá-lo. Uma lista de permissões não se importa: em Guardrails, ponha Unmatched traffic em Deny e escreva as duas ou três linhas de que o trabalho precisa. A aplicação é do lado do host e dobrada: o switch virtual casa cada fluxo por IP de destino e por nome de host espiado no DNS, em todos os protocolos, e o proxy casa de novo por nome de servidor TLS e método HTTP. A interceptação transparente desvia o tráfego das portas 80 e 443 do convidado para o proxy sem que nada dentro da VM possa desfazer isso, o que é a resposta à variante que trouxe suporte a proxy e seleção dinâmica de porta.

E se o implante tentar levar um token embora, o falso vira um fio de tropeço. O proxy varre cada requisição de saída, cabeçalhos e corpo, atrás de um falso indo para um host para o qual não foi cunhado. Havendo correspondência, ele recusa a requisição sem encaminhar um byte, pausa a VM na hora, registra uma linha vermelha de Credential brokering na Security Timeline, e marca o workspace como comprometido para que o próximo lançamento apague disco e home antes. Você não gira nada depois, porque o que o implante segurava era um marcador.

Você consegue ver a conversa inteira. Com o tracing em Activity only o proxy escreve uma linha de metadados por requisição e nenhum corpo. bromure-cli trace hostnames imprime cada host distinto que o workspace contatou, com contagens, de modo que três endereços azurewebsites.net que você nunca listou ficam a um comando de distância em vez de virar uma perícia forense.

O workspace para criar antes de o zip chegar

Um workspace descartável sem nenhuma credencial configurada além da chave do próprio agente, uma pasta compartilhada para o arquivo, e Unmatched traffic em Deny dentro de Guardrails. Acrescente allow web registry.npmjs.org e o que mais o projeto buscar. Salvar empurra as regras para as sessões em execução sem reiniciar, então você pode afrouxar uma linha por vez conforme o build reclamar.

Os dois comandos depois que você terminar

bromure-cli trace hostnames <workspace> diz aonde o código foi, e bromure-cli trace leaks <workspace> imprime No leaks detected. ✓ quando nada tentou levar um token embora. Depois Reset disk e Erase home…, e o workspace volta à imagem de onde partiu.

O leitor nunca foi o controle

Leia o server.js mesmo assim. É um bom conselho e uma defesa pobre, porque o próximo arquivo vai pôr o import na linha 340 de um arquivo que você não tem motivo para abrir, ou num bundle minificado, ou atrás de um require dinâmico montado a partir de duas strings. As três regras do Mirage Kitten são uma aposta contra a sua atenção, e atenção acaba às 16h de uma sexta-feira com uma hora restante no relógio.

O lugar onde o código roda não acaba. O import dispara de qualquer jeito e o processo desanexado começa de qualquer jeito, então o que ainda cabe a você escolher é o que esse processo encontra quando olha ao redor: uma máquina com o seu token do GitHub, o seu kubeconfig, a sua chave SSH e quarenta repositórios, ou uma VM Linux com um arquivo zip, um conjunto de marcadores e um socket de rede que só vai aonde você mandou.

Aceite o desafio. Use o agente de código que o README mandou não usar, e deixe que ele leia o server.js primeiro. Instale o Bromure Agentic Coding e dê ao zip uma máquina que não se incomoda.