Оглавление

  1. "Введение"
  2. Реализация уязвимого контракта
  3. Отказ в обслуживании
  4. Возвратная атака
  5. Выводы
  6. "Использованная литература"

1. Введение

В этой статье мы моделируем 2 атаки на смарт-контракты блокчейна Ethereum: атаки типа «отказ в обслуживании» и атаки с повторным входом. Мы начнем с предыстории сетей Ethereum Mainnet и Testnet, смарт-контрактов и кошельков, а также языка программирования Solidity. Затем мы продолжаем внедрять и развертывать уязвимые смарт-контракты в Ropsten, тестовой сети Ethereum. Наконец, мы используем уязвимости точно так же, как это происходило в реальных контрактах в истории Ethereum. Мы объясняем весь поток атак, включая сами уязвимости, почему они возникают, как их использовать и, наконец, как уменьшить и избежать этих рисков.

Общие условия:

  1. Сеть Эфириума[1] — Блокчейн (например, Биткойн), который обеспечивает децентрализованное хранение и передачу ценности от однорангового узла к другому. Кроме того, он поддерживает выполнение кода и сохранение данных с использованием полной виртуальной машины Тьюринга Ethereum [2] (EVM).
  2. Mainnet —основная сеть Ethereum (реальное значение).
  3. Ropsten — тестовая сеть Ethereum.
  4. Транзакция — действие, инициированное кошельком для передачи стоимости или выполнения кода смарт-контракта.
  5. Эфир — собственная валюта, используемая в сети Ethereum.
  6. Газ — единица измерения, определяющая комиссию, уплачиваемую отправителем транзакции.

Аккаунты:

Участник сети Ethereum с шестнадцатеричным адресом длиной 20 байт. В Ethereum есть 2 типа учетных записей.

1. EOA (кошелек) — пара закрытого и открытого ключей, сгенерированных с использованием алгоритма цифровой подписи на основе эллиптических кривых (ECDSA) [3]. Эта пара представляет кошелек в сети Ethereum. Он называется внешней учетной записью, поскольку он принадлежит пользователю-человеку, использующему закрытый ключ.

  • Закрытый ключ (256 бит) — должен храниться в секрете и представляет собой право собственности пользователя на кошелек и средства в нем. Этот ключ используется для подписи транзакций, отправленных этим кошельком.
  • Открытый ключ — используется для получения общедоступного адреса кошелька, который открыт для сети Ethereum для таких взаимодействий, как получение средств, отправка средств, контроль доступа и другие различные взаимодействия внутри сети.

2. Смарт-контракт — учетная запись, которая также имеет исполняемый код и состояние, которые публично открыты для чтения и использования любым пользователем Ethereum. Код написан в байт-кодах, таких как ассемблер или байт-коды Java, и выполняется с использованием EVM. В отличие от EOA, теоретически смарт-контракты никому не принадлежат (практически обычно они принадлежат), и их адреса не являются производными от пары закрытый-открытый ключи.

  • Уязвимости. Как и в любомкоде, написанном людьми, в программе могут быть преднамеренные и непреднамеренные ошибки и недостатки. Поскольку код в Ethereum является общедоступным и неизменяемым после развертывания, в краткосрочной перспективе это повышает возможность эксплуатации смарт-контрактов. Тем не менее, это улучшает общее качество кода экосистемы в долгосрочной перспективе, поскольку он подвергается массовому аудиту.

Разработка:

  1. Solidity [4] — язык программирования, такой как Java, который используется для программирования смарт-контрактов Ethereum. Код компилируется перед развертыванием байт-кодов EVM с использованием компиляторов .solc.
  2. Remix IDE [5] — простая браузерная IDE для Solidity и Ethereum.
  3. Etherscan.io[6] — обозреватель блоков, который позволяет исследовать блоки, транзакции, учетные записи и любую другую информацию в сети Ethereum с помощью веб-интерфейса и API.
  4. MetaMask [7] — расширение для браузера, предоставляющее пользователям Ethereum функции кошелька, такие как создание ключей, управление и резервное копирование, отправка транзакций, просмотр информации о кошельке, взаимодействие с узлом Ethereum и т. д.
  5. DeFi – децентрализованные финансы, объединяющее все финансовые приложения, развернутые на Ethereum и других смарт-блокчейнах с использованием смарт-контрактов.

2. Уязвимая реализация контракта

Мы реализуем один смарт-контракт с двумя уязвимыми функциями, одну для DoS и вторую для Reentrancy. Код контрактов предназначен для того, чтобы работодатель мог отправлять зарплату своим сотрудникам с помощью сети Ethereum. Реализация максимально проста, поэтому легче сосредоточиться и подчеркнуть важные ее части. Существует 2 типа субъектов: ownerконтракта (работодатель) и employeesего компании. Есть еще 2 функции, которые просты и понятны сами по себе.

Все смарт-контракты разрабатываются на Remix с использованием языка Solidity и компилируются в байт-коды EVM с помощью компилятора solc. Кроме того, смарт-контракты развертываются в сети Ropsten с помощью кошелька MetaMask, а исходный код контрактов проверяется в обозревателе блоков Etherscan.io (в противном случае видны только двоичные байт-коды). Для простоты все транзакции и развертывания инициируются с использованием одного и того же кошелька EOA: https://ropsten.etherscan.io/address/0x8020cc47e35cd4e291385c16fb587e270bda39e9.

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

2.1. Желаемая функциональность

После разработки, компиляции, развертывания, финансирования с помощью 5 Ether и проверки кода смарт-контракта с именем Company мы можем изучить его на etherscan.io по адресу: https://ropsten.etherscan.io/address. /0xef801ac273c1e42556d16a948f3926eed97481df#код»

Мы начинаем с выполнения логики, для которой был разработан контракт. Во-первых, владелец использует registerEmployee(address employee) для регистрации 2 EOA в качестве сотрудников. Затем мы отправляем этим сотрудникам их зарплату в 0,1 эфира, используя sendSalaries(). Все транзакции проходят успешно, как и ожидалось, и пока все отлично.

sendSalaries() транзакция: https://ropsten.etherscan.io/tx/0x5e87b6cefeb73f71eb23eb004362d70320117498f8001d1d59d0d90c9bc834b6

3. Атака отказа в обслуживании

3.1. Фон

Поскольку код смарт-контракта на Ethereum общедоступен и неизменен, злоумышленникам будет проще использовать уязвимость, а разработчикам будет сложнее (если не невозможно) исправить проблемы. Отказ в обслуживании [8] — это название атак, в результате которых атакуемые ресурсы не могут предоставлять свои услуги. Поскольку код неизменяем, можно использовать состояние контракта для использования DoS-уязвимости и атаки на контракт. Подобные атаки, которые мы представляем в этой статье, были выполнены на реальных контрактах.

Присмотревшись к функции sendSalaries(), мы увидим, что она перебирает зарегистрированных сотрудников и отправляет им 0,1 эфира с использованием transfer(0.1 ether). Для целевых счетов без кода, таких как EOA, transfer() просто перенесет значение. Однако для учетной записи смарт-контракта он будет выполнять функциюfallback() или функциюreceive() в более новых версиях компилятора [9]. Эти функции являются необязательными и имеют реализацию по умолчанию, которая просто принимает входящие средства.

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

3.2. Эксплуатация

Разворачиваем вредоносный контракт AttackerDos и регистрируем его как контракт сотрудника: https://ropsten.etherscan.io/address/0x4080c2acd8939e6b8db4f3dcc977aba22dd6a682#code

Теперь, когда владелец контракта Company отправляет транзакцию sendSalaries(), он отправляет зарплату всем 3 сотрудникам. 2 сотрудника — это предыдущие EOA, а 3 — наш злонамеренный контракт. Однако когда контракт Company пытается transfer(0.1 ether) подключиться к нашему вредоносному контракту AttackerDos, он вызывает свою receive() функцию.

Наша реализация receive() просто отменяет выполнение и отказывается получать входящие средства. Это приводит к тому, что функция transfer() генерирует исключение выполнения и отменяет всю транзакцию, что означает отмену транзакции. Это приводит к тому, что все 3 сотрудника не получают зарплату только из-за одного злонамеренного сотрудника. Учитывая, что код неизменяем и для сотрудников нет функции удаления, функция sendSalaries() всегда будет возвращаться. Поэтому он создает отказ в обслуживании для контракта Company, делая его бесполезным, а средства навсегда блокируются в контракте.

Неудачная sendSalaries() транзакция: https://ropsten.etherscan.io/tx/0x6677bd867765a30f2ae8bb1c6263e33b89154066da74e0ee3818112b0369435a

3.3. Смягчение

  1. send() — один из способов снизить риск отказа в обслуживании — изменить transfer() на send(). В отличие от transfer(), send() не генерирует исключение, а скорее возвращает true или false в качестве индикатора успеха. Однако это создает новый риск невозможности перевода средств пользователю из-за непредвиденной ошибки, в результате чего средства навсегда блокируются в контракте. Таким образом, фактически многие из send() используются как require(send()). Это ведет себя как передача с точки зрения создания исключений. Другая проблема заключается в том, что transfer() и send() постоянно потребляют 2300 единиц газа. Это проблематично, поскольку стоимость газа для инструкций имеет тенденцию меняться и может повлиять на эффективность выполнения кода или даже привести к сбою из-за исключения «кончился газ».
  2. call{value: amount}('') — это предпочтительное решение для передачи эфира [10] [11]. call() используется для вызова любой функции контракта, а не только запасных вариантов. С одной стороны, таким образом, количество используемого газа не является постоянным, в отличие от send(). С другой стороны, он открывает новый вектор атаки под названием «Повторный вход».
  3. Шаблон вывода средств — предполагает ленивую, а не энергичную реализацию. Уязвимость одного пользователя-злоумышленника, способного повлиять на всех других пользователей, участвующих в транзакции, может быть решена с помощью схемы снятия средств. Вместо того, чтобы отправлять эфир всем сотрудникам, в контракте будет храниться информация о том, сколько каждый сотрудник имеет право получить. Тогда каждый пользователь, который хочет получать свою зарплату, должен будет сам инициировать транзакцию и вывести только свою зарплату. Таким образом, ответственность за получение зарплаты лежит на сотруднике, и если кто-то решит действовать злонамеренно и отменить транзакцию, единственным затронутым пользователем будет он сам. Все остальные работники могут выводить свою зарплату самостоятельно. Мы реализуем этот шаблон в withdrawSalary().

4. Повторная атака

4.1. Описание

Существуют риски в контрактах, вызывающих извне функции других контрактов, поскольку они не могут взять на себя поток управления. Повторный вход [12] — это когда вызываемый контракт вызывает обратный вызов вызывающего контракта до завершения первого вызова вызывающей функции. Например, свяжите функцию A a() вызовами с контрактомBфункцией b(), а b() вызовет a() до первого вызов завершен.

Вредоносные контракты могут использовать эту технику для изменения потока управления нежелательным образом, вызывая негативные последствия, такие как кража токенов. Эта уязвимость возникает в решении предыдущего раздела решения DoS с использованием call() и шаблона отказа:

В обычном наивном потоке сотрудник инициализирует транзакцию withdrawSalary(), чтобы получить свою зарплату. Сначала мы проверяем, что инициирующий адрес действительно является сотрудником и что он еще не снял свою зарплату. Затем мы переводим ему зарплату и отмечаем ее как выплаченную, чтобы он не смог вывести ее несколько раз.

4.2. Эксплуатация

Разворачиваем вредоносный контракт AttackerReentrancy и регистрируем его как контракт сотрудника: https://ropsten.etherscan.io/address/0x88c3c78e47840826363a933ea74048920ebc6146#code

Ход атаки:

  1. Мы отправляем небольшую часть ETH контракту AttackerReentrancy для вызова его функции receive().
  2. Функция receive() вредоносного контракта вызывает withdrawSalary() контракта Company жертвы.
  3. Функция withdrawSalary() выполняет 2 проверки безопасности, завершается успешно и передает 0,1 ETH:
    3.1. isEmployee[msg.sender] - Адрес для звонка должен быть сотрудником.
    3.2. !hasWithdrawnSalary[msg.sender] - Работник еще не вывел заработную плату.
    3.3. payable(msg.sender).call{value: 0.1 ether}('') - Перечислить зарплату сотруднику.
  4. Однако, прежде чем пометить сотрудника как снятого с зарплаты с помощью hasWithdrawnSalary[msg.sender] = true, вызываемаяreceive() функция вредоносного контракта обращается к withdrawSalary(). Таким образом, приводит к повторному выполнению шага 2.
  5. Шаги 2–4 выполняются снова, выводя все больше и больше ETH. Этот порочный круг выполняется 5 раз в соответствии с нашим кодом, в результате чего выводится 0,5 ETH. Это может быть реализовано для вывода всех контрактных средств жертвы (если мы предоставим достаточно газа для выполнения транзакции).
  6. Наконец, после последней итерации выполняется hasWithdrawnSalary[msg.sender] = true.

Реентерабельная withdrawSalary() транзакция: https://ropsten.etherscan.io/tx/0x1f8e47f3bcf734a3c2a551d9ba405acd3417b05f49cf325519837ac3453e1bb5

4.3. Смягчение

  1. Нет записи после звонка. Если мы внимательно рассмотрим уязвимость, мы увидим, что злоумышленник мог несколько раз забрать свою зарплату. Это связано с тем, что контракт жертвы помечает сотрудника как hasWithdrawnSalary[msg.sender] = true после внешнего вызова вредоносного контракта. Таким образом, можно повторно войти в функцию до того, как состояние будет обновлено. Запись всех изменений состояния перед внешним вызовом функции в другие контракты снизит этот риск. Таким образом, во второй итерации флаг hasWithdrawnSalary[msg.sender] будет уже true, и выполнение будет отменено.
  2. Reentrancy Guard. Функции повторного входа представляют высокий риск для смарт-контрактов, поскольку эта уязвимость может быть более сложной. Например, межфункциональный повторный вход [13] включает в себя несколько вызовов функций для использования повторного входа. Таким образом, лучший способ смягчить повторный вход — заблокировать его с помощью блокировки, такой как nonreentrantmodifier OpenZeppelin [14].

5. Выводы

В этой статье мы рассмотрели две уязвимости смарт-контрактов Ethereum. Первый — это отказ в обслуживании, а второй — повторный вход. Мы разработали и развернули простые контракты жертвы и злоумышленника на Ropsten. Наконец, мы воспользовались уязвимостями и обсудили различные варианты снижения этих рисков.

6. Ссылки

  1. https://эфириум.org/ru/
  2. https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf
  3. https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
  4. https://docs.soliditylang.org/en/latest/index.html
  5. https://remix.ethereum.org/
  6. https://etherscan.io/
  7. https://metamask.io/
  8. https://swcregistry.io/docs/SWC-113
  9. https://blog.soliditylang.org/2020/03/26/fallback-receive-split/
  10. https://ethereum.stackexchange.com/questions/19341/address-send-vs-address-transfer-best-practice-usage/38642
  11. https://ethereum.stackexchange.com/questions/78124/is-transfer-still-safe-after-the-istanbul-update
  12. https://swcregistry.io/docs/SWC-107
  13. https://consensys.github.io/smart-contract-best-practices/known_attacks/#cross-function-reentrancy
  14. https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard

Список адресов

  1. Развертывание EOA: https://ropsten.etherscan.io/address/0x8020cc47e35cd4e291385c16fb587e270bda39e9
  2. Уязвимый контракт компании: https://ropsten.etherscan.io/address/0xef801ac273c1e42556d16a948f3926eed97481df#code
  3. Сотрудник 1 ЭОА: https://ropsten.etherscan.io/address/0xc1387017d4ae2cf3cc7da19f977fa74d85df0cdd
  4. Сотрудник 2 ЭОА: https://ropsten.etherscan.io/address/0x7dab537acb832738f020192a9cdb2b531fa1c599
  5. Контракт на DoS-атаку: https://ropsten.etherscan.io/address/0x4080c2acd8939e6b8db4f3dcc977aba22dd6a682#code
  6. Контракт на повторную атаку: https://ropsten.etherscan.io/address/0x88c3c78e47840826363a933ea74048920ebc6146#code