Взлом моста Nomad разбирали от первой подозрительной транзакции к одному параметру инициализации. Путь занял часы и состоял из четырёх шагов: найти исходную операцию, сравнить реализацию за прокси до и после обновления, прочитать аргументы вызова инициализации, проверить, выполняется ли инвариант проверки доказательства. На последнем шаге выяснилось, что корень доверия выставлен в ноль. Проверка доказательства после этого принимала любое сообщение.

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

Как выглядит схема со стороны жертвы

Мост работал четыре месяца без происшествий. Первого августа 2022 года вечером с контракта ушла первая нештатная операция. Дальше содержимое моста начали выводить десятки адресов подряд. Повторить операцию мог кто угодно: достаточно было взять уже прошедшую транзакцию и подставить в неё свой адрес получателя. Программировать не требовалось. По разборам аналитических компаний Elliptic и CertiK, опубликованным в первые дни после инцидента, вывод шёл волной из множества независимых адресов, а не одной рукой, и это с самого начала отличало историю от обычного адресного взлома.

Для держателя средств картина выглядела так. Баланс в мосте виден, вывести его нельзя. Интерфейс отдавал ошибки, а в сетевом обозревателе шла череда однотипных операций с одинаковой структурой вызова и разными адресами получателей, что в первые минуты читалось скорее как сбой интерфейса, чем как вывод средств. Остановить контракт команда смогла не сразу.

Отдельная часть этой истории — возвраты. Часть участников вывода обозначила себя как действующих в пользу проекта и вернула средства на указанный командой адрес после того, как была объявлена награда за возврат. Возврат был добровольным. Обеспечен он не был ничем. На такой исход не рассчитывают.

Признаки, по которым распознавалась причина

  • Все нештатные операции имели одинаковую структуру вызова и отличались только адресом получателя.
  • Доказательство в вызовах было нулевым либо произвольным и всё равно проходило проверку.
  • Адрес контракта не менялся: менялась реализация за прокси, что видно по истории обновлений.
  • Между обновлением реализации и первой нештатной операцией прошли месяцы — связь не бросалась в глаза.
  • Функция проверки корня возвращала истину для нулевого значения, что проверяется прямым чтением контракта.
  • Отчёт об аудите описывал не ту ревизию, которая работала в сети.
Инициализация прокси-контракта выполняется отдельным вызовом после развёртывания реализации. Аргументы этого вызова не видны в исходном коде и читаются только из транзакции инициализации. Именно там и оказался ноль.

Проверка за 60 секунд: что открыть и на что смотреть

Тот же порядок применим к любому мосту. Нужны обозреватель и отчёт об аудите. Шесть пунктов.

  1. Откройте адрес прокси и найдите историю смены реализации с датами.
  2. Посмотрите транзакцию инициализации и прочитайте переданные в неё аргументы.
  3. Проверьте чтением контракта, какие значения корня считаются подтверждёнными.
  4. Убедитесь, что нулевое значение корня подтверждённым не считается.
  5. Сверьте ревизию из отчёта об аудите с датой последнего обновления реализации.
  6. Посмотрите, у кого право обновления и есть ли таймлок между объявлением и вступлением в силу.
Схема из пяти шагов: находим первую нештатную операцию, сравниваем реализацию за прокси до и после обновления, читаем аргументы вызова инициализации, проверяем инвариант проверки доказательства, оцениваем масштаб по однотипным вызовам
Схема из пяти шагов: находим первую нештатную операцию, сравниваем реализацию за прокси до и после обновления, читаем аргументы вызова инициализации, проверяем инвариант проверки доказательства, оцениваем масштаб по однотипным вызовам

Второй шаг оказался решающим. По разбору компании CertiK, при обновлении реализации 21 апреля 2022 года в инициализацию был передан нулевой корень, и это значение попало в хранилище подтверждённых корней наравне с настоящими. Дальше логика работала штатно: функция проверяла, подтверждён ли корень, получала истину и пропускала сообщение. Ошибка была не в проверке, а в том, что ей дали проверять.

Четыре звена разбора и что дало каждое

Таблица показывает, какой вопрос решался на каждом шаге восстановления картины и какой именно признак позволял перейти к следующему шагу, не имея доступа ни к внутренней переписке команды, ни к её закрытому репозиторию, ни к объяснениям разработчиков.

Шаг разбора Что открывали Что это давало
Поиск исходной операции Историю транзакций по адресу моста за вечер инцидента Время начала и структуру вызова, одинаковую у всех последующих операций
Сравнение реализаций за прокси Историю смены реализации по адресу прокси с датами Дату обновления, после которого поведение контракта изменилось
Чтение аргументов инициализации Транзакцию инициализации новой реализации Нулевое значение корня доверия, переданное параметром
Проверка инварианта Функцию проверки подтверждённых корней прямым чтением Подтверждение, что нулевой корень считается действительным
Оценка масштаба Все однотипные вызовы за период инцидента Число участвовавших адресов и итоговую сумму вывода
На 14 октября 2026 года по открытым разборам инцидента картина такая: обновление реализации — 21 апреля 2022 года, вывод средств — с 21:32 UTC 1 августа по 05:49 UTC 2 августа 2022 года, оценки ущерба — от 156 до 190 млн долларов в зависимости от момента оценки и курса, число участвовавших адресов — порядка трёхсот. Добровольно возвращено около 33 млн долларов к середине августа 2022 года.

Разброс в оценках ущерба не ошибка, а следствие метода. Часть оценок считалась по курсам на момент вывода, часть — по остатку в мосте до инцидента. Указывайте основание. Это правило для любых сумм по инцидентам.

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

Пошаговые действия, если деньги уже ушли

Возврат не гарантирован никем. Порядок ниже сохраняет доказательства. Делайте по шагам.

  1. Зафиксируйте хеши своих транзакций в мост, адреса и время по UTC.
  2. Сохраните страницу интерфейса и объявления проекта: формулировки меняются в первые сутки.
  3. Снимите с адреса действующие разрешения на расходование токенов.
  4. Выведите остатки из связанных контрактов того же проекта, не дожидаясь разъяснений.
  5. Подайте заявление через интернет-приёмную МВД или в отделение по месту жительства, приложив распечатку операций и расчёт суммы в рублях на дату события.
  6. Сохраните адреса получателей из нештатных операций: они входят в материалы аналитических разборов.
Объявленная проектом награда за возврат не является обязательством вернуть средства держателям. В истории Nomad часть выведенного вернулась добровольно, часть не вернулась. Планировать на основе такого исхода нельзя.

Как закрыть дыру, чтобы не повторилось

Выводы из разбора переносятся на любой мост. Их пять. Ни один не требует кода.

  1. Проверяйте дату последнего обновления реализации, а не дату на обложке отчёта об аудите.
  2. Читайте аргументы инициализации: нулевые значения в параметрах доверия — сигнал к остановке.
  3. Считайте мост местом транзита, а не хранения: остаток в нём держится минутами, не месяцами.
  4. Проверяйте наличие таймлока на обновление и читайте очередь изменений.
  5. Распределяйте суммы так, чтобы отказ одного моста не затрагивал основную часть капитала.

Чек-лист

  • История смены реализации по адресу прокси открыта, даты выписаны.
  • Аргументы транзакции инициализации прочитаны, нулевых значений в параметрах доверия нет.
  • Функция проверки подтверждённых корней прочитана напрямую из контракта.
  • Ревизия из отчёта об аудите сопоставлена с датой последнего обновления.
  • Владелец права обновления и задержка таймлока установлены.
  • Остаток в мосте ограничен суммой одного перевода.
  • Подписка на события обновления реализации настроена.

Частые вопросы

Аудит у проекта был. Почему он не помог?

Потому что проверялась не та ревизия. Отчёт описывает конкретный снимок кода, а уязвимый параметр появился при обновлении реализации, прошедшем мимо повторной проверки. Разрыв между проверенным и развёрнутым кодом в отраслевых разборах называют дрейфом аудита: документ настоящий, выводы относятся к другому коду.

Почему в выводе участвовали сотни адресов?

Операция повторялась копированием. Уязвимость снимала проверку доказательства, поэтому для вывода хватало взять уже прошедшую транзакцию и заменить в ней адрес получателя. Навыков это не требовало, и число участников выросло за часы. Такой характер инцидента делает идентификацию получателей задачей для следствия, а не для пострадавшего.

Как проверить, нет ли той же ошибки в другом мосте?

Прочитайте аргументы транзакции инициализации и проверьте функцию, которая отвечает за подтверждённые корни. Нулевое значение среди подтверждённых — прямой признак. Дополнительно сверьте дату последнего обновления реализации с датой отчёта об аудите: расхождение в месяцы означает, что проверенного кода в сети уже нет.

Можно ли было заметить раньше?

Да, и это главный вывод разбора. Нулевой корень доверия читался из контракта публично всё время между обновлением и инцидентом, то есть больше трёх месяцев подряд, и увидеть его мог любой, кто вызвал бы функцию проверки с нулевым аргументом. Отсутствовал не доступ к данным, а привычка проверять инвариант после обновления.

Вердикт

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

Разборы инцидентов и предупреждения публикуем в Telegram-канале Cryptium.