O ataque à Bybit e o caminho que ele usou

Em 21 de fevereiro de 2025, a Bybit perdeu cerca de US$ 1,5 bilhão de uma carteira fria multisig Safe. É o maior roubo da história do setor, e vale a pena ler com atenção, porque quase tudo nele estava correto, exceto uma coisa.

O que aconteceu

A versão curta, a partir das análises públicas pós-incidente:

  1. Um atacante comprometeu a máquina de um desenvolvedor da Safe{Wallet} e injetou JavaScript malicioso no bucket AWS S3 que servia o front-end da Safe{Wallet}. O código entrou em 19 de fevereiro e foi acionado em 21 de fevereiro, direcionado ao Safe específico da Bybit.
  2. Os signatários da Bybit abriram a interface e revisaram uma transação que parecia comum.
  3. O payload que de fato foi enviado para as carteiras de hardware deles não era essa transação. Era um delegatecall que sobrescrevia o masterCopy do proxy do Safe — o slot 0 —, trocando toda a implementação da conta pela do atacante.
  4. Os signatários aprovaram. As assinaturas eram válidas. O contrato fez exatamente o que mandaram.

A autoria foi atribuída publicamente a atividades ligadas à Coreia do Norte (o FBI apontou o grupo TraderTraitor).

O que não foi quebrado

  • Não os contratos Safe. Eles executaram uma instrução assinada de forma válida. Nenhum bug do Safe foi explorado.
  • Não a criptografia. Todas as assinaturas eram autênticas.
  • Não as carteiras de hardware. Havia aparelhos Ledger no processo, e eles assinaram mesmo assim — porque uma carteira de hardware mostra o que recebe, e o que ela recebeu foi o payload malicioso. Um aparelho que não consegue decodificar um delegatecall em algo que uma pessoa consiga avaliar protege a chave, não a decisão.

O que foi quebrado é a premissa por trás de toda interface de carteira: a de que a tela que descreve uma transação e os bytes que estão sendo assinados são a mesma coisa.

Por que esse é o caso geral, e não um acidente isolado

A maioria das assinaturas feitas numa carteira web se apoia nessa premissa. A interface monta o payload, a interface exibe o resumo, e nada independente confere se um corresponde ao outro. Se o código que serve essa interface for trocado — por um pipeline de build comprometido, uma CDN sequestrada, uma dependência maliciosa, uma credencial de deploy roubada —, o resumo passa a ser o que o atacante quiser, e a sua assinatura é real.

Esse é o risco para o qual o design de assinatura da Vela aponta. Não phishing. Não uma chave vazada. Uma tela de assinatura que está mentindo para você.

O que a Vela faz a respeito

Assinatura legível, até a calldata. Cada transação é decodificada numa intenção legível antes de você aprovar — valor, destinatário, o que a chamada faz de fato (ERC-7730). Uma chamada que não conseguimos decodificar é sinalizada como não decodificável, e não exibida discretamente como se estivesse tudo bem. O payload da Bybit era um delegatecall que trocava um endereço de implementação; é exatamente o tipo de coisa que deveria fazer um signatário parar na hora, e escondê-lo atrás de um resumo amigável foi o que impediu isso.

Duas coisas, para sermos exatos. Um dApp não consegue pedir à Vela um delegatecall diretamente — as solicitações que uma página pode fazer geram chamadas comuns —, então o próprio payload da Bybit não poderia chegar por esse caminho. E uma página que pede uma chamada do seu Safe para ele mesmo é recusada, não apenas decodificada: enableModule, addOwnerWithThreshold, swapOwner, setFallbackHandler, setGuard e o resto dessa família são bloqueados quando o destino é a sua própria carteira, inclusive dentro de um lote e dentro de um MultiSend, assim como qualquer trecho que carregue um delegatecall, seja qual for o alvo, e uma assinatura de dados tipados SafeTx. A carteira diz qual chamada recusou e não oferece nada para assinar. Qualquer uma delas, assinada uma única vez, entregaria a conta tão completamente quanto o payload da Bybit entregou — um módulo habilitado pode, depois, executar um delegatecall por conta própria. E, se o próprio código da Vela fosse trocado, como aconteceu com o da Safe{Wallet}, a decodificação também seria a do atacante — e é para isso que serve o próximo ponto.

Um caminho independente que pode conferir a interface. O Trusted Signer da Vela é uma página de arquivo único em sign.getvela.app que decodifica a solicitação e faz a assinatura WebAuthn por conta própria — uma página que você pode ler do começo ao fim, recompilar byte a byte ou servir você mesmo. Ele é uma segunda opinião que não compartilha a cadeia de suprimentos do app principal: o app só entrega a solicitação a ele, e a própria página calcula o que é assinado. Situação: publicado; os apps de desktop, iPhone e Android o usam num aparelho em que você o escolheu ao criar a carteira ou fazer login. O app de desktop compara a página com as versões que vêm com ele, mas só registra uma divergência no log, e os apps de celular ainda não fazem essa conferência.

Nenhum papel de administrador para perdermos. As contas da Vela são Safe v1.4.1 sem modificações, e a Vela não tem nenhum papel privilegiado nelas — nenhuma chave de administrador e nenhum caminho de atualização nosso que pudéssemos ser coagidos ou comprometidos a usar. Mas fique claro o que isso não elimina: a primitiva que os atacantes da Bybit usaram — um delegatecall assinado por um proprietário que reescreve a implementação da conta — continua existindo em todo Safe, inclusive nos da Vela (as transações em lote da própria Vela usam delegatecall para o MultiSend da Safe). Ela exige uma assinatura válida de uma das suas chaves. As defesas contra ser convencido a dar essa assinatura são a decodificação acima e a conferência independente.

Uma confirmação nova a cada assinatura. Toda assinatura exige a confirmação da própria chave — Face ID, digital, PIN, ou um toque e o PIN numa chave de segurança. Não existe chave de sessão de longa duração, então não existe uma janela em que algo possa assinar em seu nome sem você presente.

Auto-hospedagem como última linha de defesa. Os apps e os serviços de backend são de código aberto. Se você não quer confiar de jeito nenhum no nosso pipeline de build, compile a extensão ou um app você mesmo e rode os serviços de que precisar — o guia de auto-hospedagem mostra como. Isso tira o nosso pipeline de build da cadeia de confiança; você continua confiando no código que compila, então leia esse código.

O que a Vela não afirma

O front-end da Vela poderia ser comprometido do mesmo jeito que o da Safe{Wallet} foi. O nosso código não é auditado. Dizer o contrário seria exatamente o tipo de garantia que este incidente deveria ter enterrado.

O que o design tenta fazer é estreitar o caminho: tornar o payload legível em vez de opaco, não ter nenhum papel de administrador que possa ser usado contra você e dar a você uma forma de conferir com algo que não somos nós. O resumo honesto é que esse tipo de ataque é mitigado pelo design, não eliminado, e as partes que o endureceriam ainda mais estão listadas, inacabadas, em auditorias e problemas conhecidos.

Fontes