Давайте напишем уязвимый код смарт-контракта, посмотрим, как работают атаки, и разберемся с методами предотвращения, чтобы исправить это.

Одной из особенностей смарт-контрактов ethereum является их способность вызывать и использовать код из других внешних контрактов.

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

Такого рода атаки использовались в печально известном взломе DAO.

Понимание уязвимости

Этот тип атаки может произойти, когда контракт отправляет эфир на неизвестный адрес. Злоумышленник может тщательно создать контракт на внешнем адресе, который содержит вредоносный код в резервной функции.

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

Термин «повторный вход» происходит от того факта, что внешний вредоносный контракт вызывает функцию на уязвимом контракте, и путь выполнения кода «повторно входит» в него.

Чтобы прояснить это, рассмотрим простой уязвимый контракт в EtherStore.sol, который действует как хранилище Ethereum, позволяющее вкладчикам снимать только 1 эфир в неделю:

Этот контракт имеет две публичные функции, depositFunds и withdrawFunds.

Функция depositFunds просто увеличивает баланс отправителя.

Функция withdrawFunds позволяет отправителю указать сумму wei для снятия.

Эта функция предназначена для успешной работы только в том случае, если запрошенная сумма для вывода меньше 1 эфира, а вывод средств не производился в течение последней недели.

Уязвимость находится в строке 17, где контракт отправляет пользователю запрошенное количество эфира.

Рассмотрим злоумышленника, который создал контракт в Attack.sol:

Как может произойти эксплойт?

Сначала злоумышленник создаст вредоносный контракт (скажем, по адресу 0x0… 123) с адресом контракта EtherStore в качестве единственного параметра конструктора.

Это инициализирует и указывает общедоступную переменную etherStore на контракт, который будет атакован.

Затем злоумышленник вызовет функцию attackEtherStore с некоторым количеством эфира, большим или равным 1 — давайте на данный момент предположим, что это 1 эфир.

В этом примере мы также предположим, что ряд других пользователей вложили эфир в этот контракт, так что его текущий баланс составляет 10 эфиров. Затем произойдет следующее:

  1. Строка 15 Attack.sol: Функция depositFunds контракта EtherStore будет вызываться с msg.value 1 эфира (и большим количеством газа). Отправителем (msg.sender) будет вредоносный контракт (0x0… 123). Таким образом, balances[0x0..123] = 1 ether.
  2. Строка 17 Attack.sol: Затем вредоносный контракт вызовет функцию withdrawFunds контракта EtherStore с параметром 1 эфира. Это позволит выполнить все требования (строки 12–16 контракта EtherStore), так как ранее снятие средств не производилось.
  3. Строка 17 EtherStore.sol: Контракт отправит 1 эфир обратно вредоносному контракту.
  4. Строка 25 Attack.sol: платеж по вредоносному контракту затем выполнит резервную функцию.
  5. Строка 26 от Attack.sol: Общий баланс контракта EtherStore был 10 эфиров, а теперь равен 9 эфирам, так что если утверждение прошло успешно.
  6. Строка 27 Attack.sol: резервная функция снова вызывает функцию EtherStore withdrawFunds и «повторно входит» в контракт EtherStore.
  7. Строка 11 EtherStore.sol: во втором вызове withdrawFunds баланс атакующего контракта по-прежнему составляет 1 эфир, поскольку строка 18 еще не выполнена. Таким образом, у нас все еще есть balances[0x0..123] = 1 ether. Это также относится и к переменной lastWithdrawTime. Опять же, проходим все требования.
  8. Строка 17 EtherStore.sol: Атакующий контракт забирает еще 1 эфир.
  9. Шаги 4–8 повторяются до тех пор, пока не исчезнет EtherStore.balance > 1, как указано в строке 26 в Attack.sol.
  10. Строка 26 от Attack.sol: Как только в контракте EtherStore останется 1 (или меньше) эфира, это выражение if завершится ошибкой. Это позволит выполнить строки 18 и 19 контракта EtherStore (для каждого вызова функции withdrawFunds).
  11. EtherStore.sol, строки 18 и 19: Балансы и сопоставления lastWithdrawTime будут установлены, и выполнение завершится.

Конечным результатом является то, что злоумышленник вывел все эфиры, кроме 1, из контракта EtherStore за одну транзакцию.

Профилактические методы

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

Первый — по возможности использовать встроенную функцию перевода при отправке эфира на внешние контракты. Функция передачи отправляет только 2300 газа с внешним вызовом, чего недостаточно для адреса назначения/контракта, чтобы вызвать другой контракт (т. е. повторно ввести контракт отправки).

Второй метод заключается в том, чтобы гарантировать, что вся логика, которая изменяет переменные состояния, происходит до отправки эфира из контракта (или любого внешнего вызова). В примере EtherStore строки 18 и 19 из EtherStore.sol должны быть помещены перед строкой 17.

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

Третий метод — ввести мьютекс, то есть добавить переменную состояния, которая блокирует контракт во время выполнения кода, предотвращая повторные вызовы.

Применение всех этих методов (использование всех трех необязательно, но мы делаем это в демонстрационных целях) к EtherStore.sol дает контракт без повторного входа:

Время историй

Атака DAO (децентрализованная автономная организация) была одним из основных взломов, которые произошли на ранней стадии разработки Ethereum.

На тот момент сумма контракта составляла более 150 миллионов долларов. Реентерабельность сыграла важную роль в атаке, которая в конечном итоге привела к хардфорку, создавшему Ethereum Classic (ETC). Хороший анализ эксплойта DAO можно найти в этом блоге.

Более подробную информацию об истории форка Ethereum, графике взлома DAO и рождении ETC в результате хардфорка можно найти в (ethereum_standards).