Ethereum Development 101: Урок 4: Защита ваших смарт-контрактов от атак повторного входа и кражи владельца

Необходимое условие: эта статья является частью Ethereum Development 101, курса, предназначенного для обучения основным концепциям разработки, тестирования и развертывания смарт-контрактов в сети Ethereum.

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

Обзор

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

Это особенно актуально при переходе с одной технологии на другую. Например: если у вас есть опыт работы с javascript, маловероятно, что вас будет сильно беспокоить эксплуатация переполнения, но в Solidity с этим нужно бороться.

Мы собираемся рассмотреть некоторые недостатки, присущие Solidity: взломы с повторным входом и кражу логики владельца.

Повторные атаки

Что

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

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

Фото Брет Кавано на Unsplash

Как

Возьмите следующую функцию внутри уязвимого контракта target:

function withdraw() external {
  uint amount = balances[msg.sender];
  require(msg.sender.call.value(amount)());
  balances[msg.sender] = 0;
}

Предполагая, что balances является отображением адресов в целочисленные значения, функция выполняет следующие действия:

  • Получить amount эфира для отправки вызывающему абоненту.
  • Отправьте эту сумму звонящему
  • Установить баланс звонящего на ноль

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

Вот простая резервная функция в рамках нашего вредоносного контракта:

function() external payable {
  while(calls < 10){
    calls++;
    reentrancyContract.withdraw();
  }
}

Предположим, что calls — это переменная состояния, определенная как 0 в конструкторе. Когда целевой контракт пытается отправить эфир на вредоносный адрес, где находится этот контракт, вызывается эта резервная функция. Если calls меньше 10, функция withdraw() в целевом контракте будет вызвана снова. Это повторяется рекурсивно, пока не будет вызвано 10 раз, истощая контракт target в 10 раз больше, чем предполагалось.

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

Предотвращение повторных атак

Есть несколько способов защитить ваши контракты от реентерабельных атак.

Во-первых, используйте transfer() для отправки эфира. В предыдущих версиях Solidity контракты должны были использовать метод call(), в котором по умолчанию не устанавливался лимит газа. transfer(), с другой стороны, в настоящее время имеет ограничение в 2300 единиц газа, что достаточно для создания события. С таким ограничением любое сложное рекурсивное выполнение почти сразу же иссякнет.

Используйте шаблон Проверки→Эффекты→Взаимодействия. При написании кода для конфиденциальных операций сначала убедитесь, что вы:

  1. Выполнить проверки: require() операторов.
  2. Затем внесите изменения, влияющие на переменные состояния: balance -= withdrawAmount;.
  3. Затем, наконец, выполните взаимодействие: address.transfer(withdrawAmount).

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

Владелец Логика Кража

Что

Модификаторы функций позволяют разработчикам определять, кто может вызывать каждую функцию в контракте в зависимости от их привилегий. Хотя ключевые слова public и private являются общими для разработчиков других языков, Solidity также позволяет использовать пользовательские модификаторы.

Обычный шаблон модификатора — onlyOwner. Именно здесь переменная состояния owner записывает адрес, на котором развернут контракт. Любые функции с этим модификатором требуют, чтобы адрес вызывающей стороны был равен owner.

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

modifier onlyOwner {
  require(msg.sender == owner);
  _;
}

Функции, использующие этот модификатор, могут быть объявлены следующим образом:

function somethingImportant() public onlyOwner { … }

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

Как

OnlyOwner контракты часто имеют функцию смены владельца:

function setOwner(address _newOwner) public {
    owner = _newOwner;
}

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

Предотвращение кражи владельца

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

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

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

Вывод

Реентерабельность — самый громкий эксплойт из-за взлома DAO. Были предприняты шаги для снижения риска, такие как введение transfer() , но вам все равно нужно с осторожностью относиться к этому и кодировать соответствующим образом.

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

Используйте статические инструменты анализа и проверяйте свой код.

Пожалуйста, добавляйте любые вопросы в комментарии ниже. Если вы закончили этот урок, сразу переходите к следующему:



Или вернитесь на страницу курса Ethereum Development 101, чтобы выбрать другой урок: