Атака на 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, подменяющим адрес реализации; именно такое
должно намертво останавливать подписанта — а сработала она как раз потому, что её
спрятали за дружелюбной сводкой.
Две вещи нужно назвать точно. dApp не может напрямую попросить у Vela delegatecall — запросы, которые может сделать страница, порождают обычные вызовы, —
поэтому сама нагрузка Bybit этим путём прийти не могла. А страница, которая просит
вызов из вашего Safe к нему самому, получает отказ, а не просто расшифровку: enableModule, addOwnerWithThreshold, swapOwner, setFallbackHandler, setGuard и остальные из этого семейства блокируются, когда адресат — ваш
собственный кошелёк, в том числе внутри пакета и внутри MultiSend; так же
блокируется любое звено с delegatecall, куда бы оно ни было направлено, и подпись
типизированных данных SafeTx. Кошелёк говорит, в каком вызове он отказал, и не
предлагает ничего подписывать. Любой из них, подписанный один раз, передал бы
аккаунт так же полностью, как нагрузка Bybit, — включённый модуль затем может сам
выполнить delegatecall. А если подменят код самой Vela, как подменили код Safe{Wallet}, расшифровка тоже будет принадлежать злоумышленнику — для этого и
нужен следующий пункт.
Независимый путь, который может проверить интерфейс. Trusted Signer от Vela — это страница из одного файла на sign.getvela.app, которая сама расшифровывает запрос и сама выполняет подпись WebAuthn, — страница, которую можно прочитать от начала до конца, пересобрать байт в байт или раздавать самому. Это второе мнение, которое не разделяет цепочку поставки с основным приложением: приложение только передаёт ему запрос, а то, что будет подписано, страница вычисляет сама. Статус: опубликован; настольное приложение и приложения для iPhone и Android используют его на устройстве, где вы выбрали его при создании кошелька или входе. Настольное приложение сравнивает страницу с версиями, которые поставляются вместе с ним, но несовпадение только записывает в журнал, а мобильные приложения пока ничего не проверяют.
У нас нет административной роли, которую можно потерять. Аккаунты Vela — это немодифицированный Safe v1.4.1, и у Vela нет в них
привилегированной роли: ни админского ключа, ни нашего пути обновления, к
использованию которого нас можно было бы принудить или который можно было бы
взломать. Но важно понимать, чего это не устраняет: примитив, которым
воспользовались атакующие Bybit, — подписанный владельцем delegatecall,
переписывающий реализацию аккаунта, — по-прежнему есть в каждом Safe, включая Safe
в Vela (собственные пакетные транзакции Vela используют delegatecall в MultiSend от
Safe). Для него нужна действительная подпись одного из ваших ключей. Защита от того,
чтобы вас уговорили её дать, — расшифровка выше и независимая проверка.
Новая проверка для каждой подписи. Каждая подпись требует подтверждения самим ключом — Face ID, отпечатком пальца, PIN-кодом или касанием и PIN-кодом на ключе безопасности. Долгоживущего сессионного ключа нет, а значит, нет и окна, в котором что-то могло бы подписать за вас без вашего участия.
Самостоятельное развёртывание как последний рубеж. Приложения и бэкенд-сервисы открыты. Если вы вовсе не хотите доверять нашему конвейеру сборки, соберите расширение или приложение сами и запустите нужные сервисы — руководство по самостоятельному развёртыванию проведёт через это шаг за шагом. Так наш конвейер сборки выпадает из цепочки доверия; но вы по-прежнему доверяете коду, который собираете, — поэтому прочитайте его.
Чего Vela не утверждает
Фронтенд Vela может быть скомпрометирован точно так же, как фронтенд Safe{Wallet}. Наш код не проходил аудит. Утверждать иное значило бы давать именно
те заверения, которым этот инцидент должен был положить конец.
Схема пытается сузить путь атаки: сделать нагрузку читаемой, а не непрозрачной, не держать административной роли, которой могли бы злоупотребить за вас, и дать вам способ проверить то, что вы подписываете, чем-то, что не является нами. Честный вывод: этот класс атак ослаблен устройством системы, но не устранён, а то, что сделало бы защиту ещё надёжнее, перечислено — незавершённым — в разделе Аудиты и известные проблемы.