Атака на 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, подменяющим адрес реализации; именно такое должно намертво останавливать подписанта — а сработала она как раз потому, что её спрятали за дружелюбной сводкой.

Две вещи нужно назвать точно. 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}. Наш код не проходил аудит. Утверждать иное значило бы давать именно те заверения, которым этот инцидент должен был положить конец.

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

Источники