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.jsestava 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_modulesdo 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.jscomo 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.
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.
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.