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

Спойлер: если вы уже слышали о модификаторе nonReentrant(), продолжайте читать, потому что вы вот-вот обнаружите несколько строк под модификатором globalNonReentrant() и шаблон проверки-эффекты-взаимодействия.



На изображении выше у нас есть ContractA и ContractB. Теперь, как вы знаете, смарт-контракт может взаимодействовать с другим смарт-контрактом, как в этом случае ContractA может вызывать ContractB. Итак, основная идея повторного входа заключается в том, что ContractB может выполнить обратный вызов ContractA, пока ContractA все еще выполняется.

Итак, как злоумышленник может этим воспользоваться?

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



Давайте теперь посмотрим, как ContractB может использовать реентерабельность, чтобы использовать функцию вывода и украсть все эфиры у ContractA. По сути, злоумышленнику понадобятся две функции: Attack() и Fallback().

В Solidity резервная функция — это внешняя функция, не имеющая ни имени, ни параметров, ни возвращаемых значений. Любой может вызвать резервную функцию следующим образом: Вызов функции, которой нет в контракте; Вызов функции без передачи необходимых данных; Отправка эфира без каких-либо данных в контракте.

Принцип работы повторного входа (давайте шаг за шагом будем следовать стрелкам) заключается в том, что злоумышленник вызывает функцию Attack(), которая внутри вызывает функцию Withdraw() из ContractA. Внутри функции она проверит, превышает ли баланс ContractB 0, и если да, то продолжит выполнение.



Поскольку баланс ContractB больше 0, он отправляет 1 эфир обратно и запускает резервную функцию. Обратите внимание, что в этот момент у ContractA есть 9 эфиров, а у ContractB уже 1 эфир.



Затем, когда резервная функция выполняется, она снова запускает функцию вывода средств ContractA, снова проверяя, превышает ли баланс ContractB 0. Если вы еще раз проверите изображение выше, вы заметите, что его баланс по-прежнему равен 1 эфиру.



Это означает, что проверка проходит и он отправляет еще один эфир в ContractB, что запускает резервную функцию. Обратите внимание: поскольку строка, в которой указано «balance=0», никогда не выполняется, это будет продолжаться до тех пор, пока весь эфир из ContractA не исчезнет.

___________

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



В контракте EtherStore у нас есть функция deposit() , которая сохраняет и обновляет балансы отправителя, а затем функция withdrawAll() , которая принимает все сохраненные балансы одновременно. Обратите внимание на реализацию метода вывода всех(), где он сначала проверяет требование, чтобы баланс был больше 0, а сразу после этого снова отправляет эфир, оставляя на конец обновление баланса отправителя до 0.



Здесь у нас есть контрактная атака, которая будет использовать реентерабельность для истощения контракта EtherStore. Разберем его код:

  • В своем конструкторе злоумышленник передаст адрес EtherStore, чтобы создать экземпляр и получить возможность использовать его функции.

  • Там мы видим функцию fallback() , которая будет вызываться, когда EtherStore отправит Ether на этот контракт. Внутри он будет вызывать вывод средств из EtherStore, пока баланс равен или больше 1.

  • А внутри функции Attack() у нас есть логика, которая будет использовать EtherStore. Как мы видим, сначала мы инициируем атаку, убедившись, что у нас достаточно эфира, затем вносим 1 эфир, чтобы баланс в EtherStore был больше 0, и, следовательно, проходим проверки перед тем, как начать вывод средств.

Выше я объяснил в примерах ContractA и ContractB шаг за шагом, как будет работать код, поэтому теперь давайте подведем итоги того, как это будет. Прежде всего, злоумышленник вызовет функцию Attack(), которая внутри вызовет функцию выводаAll() из EtherStore, которая затем отправит Ether в резервную функцию контракта атаки. И там он начнет повторный вход и опустошит баланс EtherStore.

Итак, как мы можем защитить наши контракты от атак повторного входа?

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



Первый метод защиты отдельной функции — использование модификатора noReentrant.

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

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

___________



Второй метод – использование шаблона "Проверки-Эффекты-Взаимодействия", который защитит наши контракты от межфункционального повторного входа. Можете ли вы заметить в обновленном контракте EtherStore выше, что изменилось?

Чтобы глубже погрузиться в шаблон Check-Effects-Interaction, я рекомендую прочитать https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html. 





Выше мы видим сравнение уязвимого кода на изображении слева, где баланс был обновлен после отправки эфира, который, как показано выше, потенциально никогда не может быть достигнут, и справа то, что было сделано, это переместить балансы[ msg.sender] = 0 (или эффект) сразу после require(bal > 0) (проверка), но перед отправкой эфира (взаимодействие).

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

Шаблон создан https://twitter.com/GMX_IO

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

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







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

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

Что ж, использование GlobalReentrancyGuard, о котором я упоминал выше, позволит избежать такого сценария атаки.

__________________

В Твиттере @TheBlockChainer вы найдете больше ежедневных обновлений о смарт-контрактах, безопасности Web3, Solidity, смарт-контрактах аудита и многом другом.

__________________