Автор: Сяо Бай
Редактировать: Лиз
Обзор фона
В экосистеме 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, подтверждайте, является ли целевой адрес доверенным, и существует ли риск вызова любого адреса.
Проверьте управление жизненным циклом предложений, например, есть ли механизм, предотвращающий изменение или замену предложений, например, блокировка состояния целевого адреса после утверждения.
