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:
- 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 daSafe{Wallet}. O código entrou em 19 de fevereiro e foi acionado em 21 de fevereiro, direcionado ao Safe específico da Bybit. - Os signatários da Bybit abriram a interface e revisaram uma transação que parecia comum.
- O payload que de fato foi enviado para as carteiras de hardware deles não era
essa transação. Era um
delegatecallque sobrescrevia omasterCopydo proxy do Safe — o slot 0 —, trocando toda a implementação da conta pela do atacante. - 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
delegatecallem 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.