Атака на Bybit
21 февраля 2025 года Bybit потеряла примерно 1,5 млрд долларов с холодного мультиподписного кошелька Safe. Это крупнейшая кража в истории отрасли, и её стоит прочитать внимательно: почти всё там было правильно, кроме одной вещи.
Что произошло
Коротко, по публичным разборам:
- Злоумышленник скомпрометировал машину разработчика
Safe{Wallet}и внедрил вредоносный JavaScript в бакет AWS S3, откуда раздавался фронтендSafe{Wallet}. Код попал туда 19 февраля и сработал 21-го, будучи нацелен на конкретный Safe компании Bybit. - Подписывающие Bybit открыли интерфейс и просмотрели транзакцию, которая выглядела обычной.
- То, что на самом деле уехало на их аппаратные кошельки, было не той
транзакцией. Это был
delegatecall, переписывающийmasterCopyпрокси Safe — слот 0 — и заменяющий всю реализацию аккаунта на реализацию атакующего. - Подписывающие подтвердили. Подписи были действительными. Контракт сделал ровно то, что ему сказали.
Публично атрибуция указывает на активность, связанную с Северной Кореей (ФБР назвало кластер 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}. Наш код не проходил аудит. Говорить иначе — ровно та разновидность
заверений, которую этот инцидент должен был прекратить.
Что пытается сделать эта схема — сузить путь: сделать нагрузку читаемой, а не непрозрачной, убрать примитив обновления, на который опиралась атака, и дать вам способ проверить чем-то, что не является нами. Честный вывод: этот класс атак ослаблен устройством системы, а не устранён — а части, которые сделали бы его ещё жёстче, перечислены, незавершённые, в аудитах и известных проблемах.