​Автор: Сяо Бай

Редактировать: Лиз

Обзор фона

В экосистеме Ethereum механизм детерминированной генерации адресов контрактов предоставляет удобство для разработчиков, но одновременно вводит новые уязвимости. В этом выпуске мы проанализируем методы атаки и стратегии защиты, использующие операции CREATE и CREATE2 для развертывания различных контрактов по одному и тому же адресу в разное время. Ранее опубликованные статьи по безопасности смарт-контрактов можно найти в сборнике.

Предварительные знания

Сначала давайте ознакомимся с двумя правилами генерации адресов Ethereum:

1. CREATE

CREATE — это нативный код операции для динамического развертывания смарт-контрактов в виртуальной машине Ethereum (EVM). С момента создания генезис-блока Ethereum все развертывания контрактов зависят от этого механизма. Его основная особенность заключается в том, что генерация адреса зависит от nonce аккаунта развертывателя, поэтому адрес является недетерминированным (невозможно точно предсказать до развертывания).

Адрес контракта, созданного с помощью CREATE, определяется адресом развертывателя и nonce адреса:

2. CREATE2

CREATE2 — это новый код операции создания контрактов, введенный в Ethereum в результате хардфорка Константинополь (февраль 2019 года). В отличие от традиционного CREATE, перед развертыванием контракта участники вне цепочки могут заранее рассчитать адрес контракта, что делает возможным взаимодействие вне цепочки (например, каналы состояния) и сложные структуры контрактов.

Адрес контракта, созданного с помощью CREATE2, определяется следующими четырьмя параметрами:

На этом этапе, я уверен, что вы уже поняли два способа генерации адреса контракта в Ethereum; внимательные читатели могут задуматься, что делать, если рассчитанный адрес контракта уже существует? Об этом не нужно беспокоиться, в Ethereum, независимо от того, был ли адрес создан через CREATE или CREATE2, если он уже существует в цепочке (независимо от того, является ли это внешним аккаунтом EOA или аккаунтом контракта), EVM отклонит запрос на создание контракта. Вот два сценария конфликта адресов:

1. Целевой адрес — это внешний аккаунт (EOA)

  • Правило: если целевой адрес является существующим EOA (например, адресом кошелька пользователя), EVM отклонит запрос на развертывание контракта.

  • Результат: транзакция не удалась, Gas был потрачен, контракт не будет создан и никакие данные по этому адресу не будут перезаписаны.

2. Целевой адрес — это контрактный аккаунт

  • Правило: если целевой адрес является уже развернутым контрактным аккаунтом, EVM также отклонит запрос на развертывание.

  • Результат: транзакция не удалась, Gas был потрачен, код и данные хранилища оригинального контракта остаются неизменными.

Конечно, как и во всем, есть исключения; если целевой адрес является контрактом и был уничтожен через selfdestruct, в этот момент можно снова развернуть новый контракт по этому адресу.

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

Примеры уязвимостей

Анализ уязвимостей

Этот DAO контракт реализует основный механизм управления: Владелец проверяет и регистрирует адрес контракта предложения в массиве Proposals через функцию approve, любой пользователь затем может выполнить проверенное предложение через функцию execute. На вид строгий контроль доступа (только Владелец может проверять предложения) в сочетании с проверкой выполнения (предложение должно быть проверено и не выполнено), на самом деле имеет скрытую логическую уязвимость: адрес проверенного предложения может указывать на совершенно другой код контракта при выполнении. Атакующий может осуществить атаку в три этапа:

1. Разверните нормальный контракт, получите разрешение

Атакующий сначала развертывает контракт A, содержащий безвредную функцию executeProposal(), и добавляет адрес в список предложений через проверку Владелец.

2. Самоуничтожение оригинального контракта, захват адреса

Контракт A выполняет операцию самоуничтожения (selfdestruct), очищая код, после чего атакующий использует код операции CREATE2 для развертывания вредоносного контракта B (с логикой executeProposal()).

3. Запуск выполнения, захват контроля

Когда пользователь вызывает execute, контракт выполнит код нового вредоносного контракта B через delegatecall. Поскольку delegatecall сохраняет текущий контекст контракта, атакующий может изменить состояние контракта DAO (например, изменить владельца) или перевести активы.

Теперь давайте посмотрим на конкретный процесс атаки в сочетании с атакующим контрактом.

Атакующий контракт

Процесс атаки выглядит следующим образом:

1. Алиса развернула DAO контракт.

2. Злой развертывает контракт DeployerDeployer, адрес которого DD.

3. Злой вызывает DD.deploy(), используя CREATE2 для развертывания Deployer по адресу D (фиксированный salt).

4. Вызовите D.deployProposal(), чтобы создать адрес контракта Proposal P, в это время nonce D равен 0.

5. Алиса утверждает адрес P.

6. Атакующий вызывает D.kill(), уничтожает D, в это время учетная запись адреса D очищается, включая сброс nonce на 0.

7. Атакующий снова вызывает DD.deploy(), повторно развертывает Deployer по адресу D (с теми же параметрами CREATE2).

8. Вызовите D.deployAttack(), поскольку в это время nonce D сброшен на 0 и адрес D не изменился, создаваемый адрес контракта Attack совпадает с предыдущим адресом контракта Proposal P.

9. В этот момент в массиве proposals DAO есть одно предложение, указывающее на адрес P, но теперь адрес P был повторно развернут как контракт Attack. Поэтому, когда вызывается execute(), выполняется код executeProposal() контракта Attack, в конечном итоге через delegatecall изменяя владельца DAO на msg.sender, то есть контракт Attack.

Принцип атаки можно обобщить так: злоумышленник использует возможность CREATE2 повторно развернуть контракт по тому же адресу, сначала развернув легитимный контракт Proposal и получив одобрение DAO. Затем он уничтожает контракт Deployer, повторно разворачивает тот же адрес Deployer, сбрасывает nonce этого адреса и в конечном итоге успешно развертывает вредоносный контракт по тому же адресу, что и предыдущее предложение. Поскольку адрес предложения, хранящийся в DAO, теперь указывает на вредоносный контракт, при выполнении предложения вредоносный код выполняется в контексте DAO, успешно изменяя владельца на злоумышленника.

Рекомендации по исправлению

В качестве разработчика:

  • При одобрении предложения необходимо не только записать адрес, но и зафиксировать хэш кода этого адреса, а также проверять хэш кода на согласованность во время выполнения.

  • Избегайте использования delegatecall для вызова внешних контрактов, если нет достаточных мер безопасности.

  • Рассмотрите возможность самоуничтожения контракта и повторного развертывания по тому же адресу, при внешнем вызове необходимо проверить, является ли внешний контракт доверенным; если необходимо вызвать незнакомый внешний контракт, рекомендуется проверить наличие механизмов защиты от самоуничтожения в контракте.

В качестве аудитора:

  • Определите, существует ли функция selfdestruct (самоуничтожение) в контрактах, вызываемых извне; если такая функция есть, необходимо быть осторожным с риском атаки, когда внешний контракт самоуничтожается и развертывает вредоносный код по тому же адресу.

  • Проверьте, использует ли способ развертывания контракта CREATE2, чтобы убедиться, что значение salt, используемое для генерации адреса контракта, достаточно случайно, чтобы предотвратить атаки, когда адрес контракта предсказуем и может быть заранее развернут злоумышленником.

  • Проверьте, проверяет ли DAO согласованность кода целевого адреса при выполнении предложения, например, сравнивая текущий хэш кода с хэшем во время утверждения.

  • Осторожно анализируйте сценарии использования delegatecall, подтверждайте, является ли целевой адрес доверенным, и существует ли риск вызова любого адреса.

  • Проверьте управление жизненным циклом предложений, например, есть ли механизм, предотвращающий изменение или замену предложений, например, блокировка состояния целевого адреса после утверждения.