Атака на Bybit

21 февраля 2025 года Bybit потеряла примерно 1,5 млрд долларов с холодного мультиподписного кошелька Safe. Это крупнейшая кража в истории отрасли, и её стоит прочитать внимательно: почти всё там было правильно, кроме одной вещи.

Что произошло

Коротко, по публичным разборам:

  1. Злоумышленник скомпрометировал машину разработчика Safe{Wallet} и внедрил вредоносный JavaScript в бакет AWS S3, откуда раздавался фронтенд Safe{Wallet}. Код попал туда 19 февраля и сработал 21-го, будучи нацелен на конкретный Safe компании Bybit.
  2. Подписывающие Bybit открыли интерфейс и просмотрели транзакцию, которая выглядела обычной.
  3. То, что на самом деле уехало на их аппаратные кошельки, было не той транзакцией. Это был delegatecall, переписывающий masterCopy прокси Safe — слот 0 — и заменяющий всю реализацию аккаунта на реализацию атакующего.
  4. Подписывающие подтвердили. Подписи были действительными. Контракт сделал ровно то, что ему сказали.

Публично атрибуция указывает на активность, связанную с Северной Кореей (ФБР назвало кластер TraderTraitor).

Что не сломалось

  • Не контракты Safe. Они исполнили корректно подписанную инструкцию. Никакая ошибка в Safe не эксплуатировалась.
  • Не криптография. Каждая подпись была настоящей.
  • Не аппаратные кошельки. Устройства Ledger были в цепочке и всё равно подписали, потому что аппаратный кошелёк показывает то, что ему дали, а дали ему вредоносную нагрузку. Устройство, которое не умеет превратить delegatecall во что-то, что человек способен оценить, защищает ключ, а не решение.

Сломалось допущение, лежащее под любым интерфейсом кошелька: что экран, описывающий транзакцию, и байты, которые подписываются, — это одно и то же.

Почему это общий случай, а не курьёз

Каждая подпись, которую вы когда-либо сделали в веб-кошельке, опиралась на это допущение. Интерфейс собирает нагрузку, интерфейс рисует сводку, и ничто независимое не проверяет, что одно соответствует другому. Если код, раздающий этот интерфейс, подменят — скомпрометированный конвейер сборки, захваченный CDN, вредоносная зависимость, украденный ключ деплоя — сводка станет такой, какой захочет злоумышленник, а ваша подпись будет настоящей.

Именно на этот риск нацелена схема подписи в Vela. Не на фишинг. Не на утёкший ключ. На экран подписи, который вам лжёт.

Что с этим делает Vela

Понятная подпись — вплоть до calldata. Каждая транзакция переводится в читаемое намерение до подтверждения: сумма, получатель, что на самом деле делает вызов (ERC-7730). Вызов, который разобрать не удалось, помечается как неразбираемый, а не тихо рисуется как ни в чём не бывало. Нагрузка Bybit была delegatecall, подменяющим адрес реализации; именно такая вещь должна останавливать подписывающего намертво — и то, что её спрятали за дружелюбной сводкой, и есть причина, почему этого не случилось.

Независимый путь, способный проверить интерфейс. Vela делает страницу подписи без сборки и без зависимостей, которая сама отображает намерение и сама выполняет подпись WebAuthn — одна папка статических файлов, которую можно прочитать от начала до конца, раздавать самому или запустить как расширение браузера. Весь её смысл — быть вторым мнением, не разделяющим цепочку поставки основного приложения. Статус: сделана и протестирована, ещё не развёрнута. Когда выйдет — по желанию, и эта страница прямо скажет, когда это изменится.

Никакого контракта, который мы могли бы обновить. Нагрузка Bybit сработала, подменив реализацию аккаунта. Аккаунты Vela — это неизменённый Safe v1.4.1, и у Vela нет в них привилегированной роли: ни админского ключа, ни пути обновления, к использованию которого нас можно было бы принудить или взломать.

Новая биометрия на каждую подпись. Долгоживущего ключа сессии нет, а значит нет и окна, в котором что-то могло бы подписать за вас без вас.

Собственный хостинг как последняя опора. Приложение и все бэкенд-сервисы открыты. Если вы вовсе не хотите доверять нашему конвейеру сборки — поднимите свой; для этого класса атак это единственный ответ, не требующий доверять кому-либо.

Чего Vela не утверждает

Фронтенд Vela может быть скомпрометирован точно так же, как фронтенд Safe{Wallet}. Наш код не проходил аудит. Говорить иначе — ровно та разновидность заверений, которую этот инцидент должен был прекратить.

Что пытается сделать эта схема — сузить путь: сделать нагрузку читаемой, а не непрозрачной, убрать примитив обновления, на который опиралась атака, и дать вам способ проверить чем-то, что не является нами. Честный вывод: этот класс атак ослаблен устройством системы, а не устранён — а части, которые сделали бы его ещё жёстче, перечислены, незавершённые, в аудитах и известных проблемах.

Источники