Оглавление
- "Введение"
- Реализация уязвимого контракта
- Отказ в обслуживании
- Возвратная атака
- Выводы
- "Использованная литература"
1. Введение
В этой статье мы моделируем 2 атаки на смарт-контракты блокчейна Ethereum: атаки типа «отказ в обслуживании» и атаки с повторным входом. Мы начнем с предыстории сетей Ethereum Mainnet и Testnet, смарт-контрактов и кошельков, а также языка программирования Solidity. Затем мы продолжаем внедрять и развертывать уязвимые смарт-контракты в Ropsten, тестовой сети Ethereum. Наконец, мы используем уязвимости точно так же, как это происходило в реальных контрактах в истории Ethereum. Мы объясняем весь поток атак, включая сами уязвимости, почему они возникают, как их использовать и, наконец, как уменьшить и избежать этих рисков.
Общие условия:
- Сеть Эфириума[1] — Блокчейн (например, Биткойн), который обеспечивает децентрализованное хранение и передачу ценности от однорангового узла к другому. Кроме того, он поддерживает выполнение кода и сохранение данных с использованием полной виртуальной машины Тьюринга Ethereum [2] (EVM).
- Mainnet —основная сеть Ethereum (реальное значение).
- Ropsten — тестовая сеть Ethereum.
- Транзакция — действие, инициированное кошельком для передачи стоимости или выполнения кода смарт-контракта.
- Эфир — собственная валюта, используемая в сети Ethereum.
- Газ — единица измерения, определяющая комиссию, уплачиваемую отправителем транзакции.
Аккаунты:
Участник сети Ethereum с шестнадцатеричным адресом длиной 20 байт. В Ethereum есть 2 типа учетных записей.
1. EOA (кошелек) — пара закрытого и открытого ключей, сгенерированных с использованием алгоритма цифровой подписи на основе эллиптических кривых (ECDSA) [3]. Эта пара представляет кошелек в сети Ethereum. Он называется внешней учетной записью, поскольку он принадлежит пользователю-человеку, использующему закрытый ключ.
- Закрытый ключ (256 бит) — должен храниться в секрете и представляет собой право собственности пользователя на кошелек и средства в нем. Этот ключ используется для подписи транзакций, отправленных этим кошельком.
- Открытый ключ — используется для получения общедоступного адреса кошелька, который открыт для сети Ethereum для таких взаимодействий, как получение средств, отправка средств, контроль доступа и другие различные взаимодействия внутри сети.
2. Смарт-контракт — учетная запись, которая также имеет исполняемый код и состояние, которые публично открыты для чтения и использования любым пользователем Ethereum. Код написан в байт-кодах, таких как ассемблер или байт-коды Java, и выполняется с использованием EVM. В отличие от EOA, теоретически смарт-контракты никому не принадлежат (практически обычно они принадлежат), и их адреса не являются производными от пары закрытый-открытый ключи.
- Уязвимости. Как и в любомкоде, написанном людьми, в программе могут быть преднамеренные и непреднамеренные ошибки и недостатки. Поскольку код в Ethereum является общедоступным и неизменяемым после развертывания, в краткосрочной перспективе это повышает возможность эксплуатации смарт-контрактов. Тем не менее, это улучшает общее качество кода экосистемы в долгосрочной перспективе, поскольку он подвергается массовому аудиту.
Разработка:
- Solidity [4] — язык программирования, такой как Java, который используется для программирования смарт-контрактов Ethereum. Код компилируется перед развертыванием байт-кодов EVM с использованием компиляторов .solc.
- Remix IDE [5] — простая браузерная IDE для Solidity и Ethereum.
- Etherscan.io[6] — обозреватель блоков, который позволяет исследовать блоки, транзакции, учетные записи и любую другую информацию в сети Ethereum с помощью веб-интерфейса и API.
- MetaMask [7] — расширение для браузера, предоставляющее пользователям Ethereum функции кошелька, такие как создание ключей, управление и резервное копирование, отправка транзакций, просмотр информации о кошельке, взаимодействие с узлом Ethereum и т. д.
- 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. Смягчение
- send() — один из способов снизить риск отказа в обслуживании — изменить
transfer()
наsend()
. В отличие отtransfer()
,send()
не генерирует исключение, а скорее возвращает true или false в качестве индикатора успеха. Однако это создает новый риск невозможности перевода средств пользователю из-за непредвиденной ошибки, в результате чего средства навсегда блокируются в контракте. Таким образом, фактически многие изsend()
используются какrequire(send())
. Это ведет себя как передача с точки зрения создания исключений. Другая проблема заключается в том, чтоtransfer()
иsend()
постоянно потребляют 2300 единиц газа. Это проблематично, поскольку стоимость газа для инструкций имеет тенденцию меняться и может повлиять на эффективность выполнения кода или даже привести к сбою из-за исключения «кончился газ». - call{value: amount}('') — это предпочтительное решение для передачи эфира [10] [11].
call()
используется для вызова любой функции контракта, а не только запасных вариантов. С одной стороны, таким образом, количество используемого газа не является постоянным, в отличие отsend()
. С другой стороны, он открывает новый вектор атаки под названием «Повторный вход». - Шаблон вывода средств — предполагает ленивую, а не энергичную реализацию. Уязвимость одного пользователя-злоумышленника, способного повлиять на всех других пользователей, участвующих в транзакции, может быть решена с помощью схемы снятия средств. Вместо того, чтобы отправлять эфир всем сотрудникам, в контракте будет храниться информация о том, сколько каждый сотрудник имеет право получить. Тогда каждый пользователь, который хочет получать свою зарплату, должен будет сам инициировать транзакцию и вывести только свою зарплату. Таким образом, ответственность за получение зарплаты лежит на сотруднике, и если кто-то решит действовать злонамеренно и отменить транзакцию, единственным затронутым пользователем будет он сам. Все остальные работники могут выводить свою зарплату самостоятельно. Мы реализуем этот шаблон в
withdrawSalary()
.
4. Повторная атака
4.1. Описание
Существуют риски в контрактах, вызывающих извне функции других контрактов, поскольку они не могут взять на себя поток управления. Повторный вход [12] — это когда вызываемый контракт вызывает обратный вызов вызывающего контракта до завершения первого вызова вызывающей функции. Например, свяжите функцию A a()
вызовами с контрактомBфункцией b()
, а b()
вызовет a()
до первого вызов завершен.
Вредоносные контракты могут использовать эту технику для изменения потока управления нежелательным образом, вызывая негативные последствия, такие как кража токенов. Эта уязвимость возникает в решении предыдущего раздела решения DoS с использованием call()
и шаблона отказа:
В обычном наивном потоке сотрудник инициализирует транзакцию withdrawSalary()
, чтобы получить свою зарплату. Сначала мы проверяем, что инициирующий адрес действительно является сотрудником и что он еще не снял свою зарплату. Затем мы переводим ему зарплату и отмечаем ее как выплаченную, чтобы он не смог вывести ее несколько раз.
4.2. Эксплуатация
Разворачиваем вредоносный контракт AttackerReentrancy и регистрируем его как контракт сотрудника: https://ropsten.etherscan.io/address/0x88c3c78e47840826363a933ea74048920ebc6146#code
Ход атаки:
- Мы отправляем небольшую часть ETH контракту AttackerReentrancy для вызова его функции
receive()
. - Функция
receive()
вредоносного контракта вызываетwithdrawSalary()
контракта Company жертвы. - Функция
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}('')
- Перечислить зарплату сотруднику. - Однако, прежде чем пометить сотрудника как снятого с зарплаты с помощью
hasWithdrawnSalary[msg.sender] = true
, вызываемаяreceive()
функция вредоносного контракта обращается кwithdrawSalary()
. Таким образом, приводит к повторному выполнению шага 2. - Шаги 2–4 выполняются снова, выводя все больше и больше ETH. Этот порочный круг выполняется 5 раз в соответствии с нашим кодом, в результате чего выводится 0,5 ETH. Это может быть реализовано для вывода всех контрактных средств жертвы (если мы предоставим достаточно газа для выполнения транзакции).
- Наконец, после последней итерации выполняется
hasWithdrawnSalary[msg.sender] = true
.
Реентерабельная withdrawSalary()
транзакция: https://ropsten.etherscan.io/tx/0x1f8e47f3bcf734a3c2a551d9ba405acd3417b05f49cf325519837ac3453e1bb5
4.3. Смягчение
- Нет записи после звонка. Если мы внимательно рассмотрим уязвимость, мы увидим, что злоумышленник мог несколько раз забрать свою зарплату. Это связано с тем, что контракт жертвы помечает сотрудника как
hasWithdrawnSalary[msg.sender] = true
после внешнего вызова вредоносного контракта. Таким образом, можно повторно войти в функцию до того, как состояние будет обновлено. Запись всех изменений состояния перед внешним вызовом функции в другие контракты снизит этот риск. Таким образом, во второй итерации флагhasWithdrawnSalary[msg.sender]
будет ужеtrue
, и выполнение будет отменено. - Reentrancy Guard. Функции повторного входа представляют высокий риск для смарт-контрактов, поскольку эта уязвимость может быть более сложной. Например, межфункциональный повторный вход [13] включает в себя несколько вызовов функций для использования повторного входа. Таким образом, лучший способ смягчить повторный вход — заблокировать его с помощью блокировки, такой как
nonreentrant
modifier OpenZeppelin [14].
5. Выводы
В этой статье мы рассмотрели две уязвимости смарт-контрактов Ethereum. Первый — это отказ в обслуживании, а второй — повторный вход. Мы разработали и развернули простые контракты жертвы и злоумышленника на Ropsten. Наконец, мы воспользовались уязвимостями и обсудили различные варианты снижения этих рисков.
6. Ссылки
- https://эфириум.org/ru/
- https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf
- https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
- https://docs.soliditylang.org/en/latest/index.html
- https://remix.ethereum.org/
- https://etherscan.io/
- https://metamask.io/
- https://swcregistry.io/docs/SWC-113
- https://blog.soliditylang.org/2020/03/26/fallback-receive-split/
- https://ethereum.stackexchange.com/questions/19341/address-send-vs-address-transfer-best-practice-usage/38642
- https://ethereum.stackexchange.com/questions/78124/is-transfer-still-safe-after-the-istanbul-update
- https://swcregistry.io/docs/SWC-107
- https://consensys.github.io/smart-contract-best-practices/known_attacks/#cross-function-reentrancy
- https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard
Список адресов
- Развертывание EOA: https://ropsten.etherscan.io/address/0x8020cc47e35cd4e291385c16fb587e270bda39e9
- Уязвимый контракт компании: https://ropsten.etherscan.io/address/0xef801ac273c1e42556d16a948f3926eed97481df#code
- Сотрудник 1 ЭОА: https://ropsten.etherscan.io/address/0xc1387017d4ae2cf3cc7da19f977fa74d85df0cdd
- Сотрудник 2 ЭОА: https://ropsten.etherscan.io/address/0x7dab537acb832738f020192a9cdb2b531fa1c599
- Контракт на DoS-атаку: https://ropsten.etherscan.io/address/0x4080c2acd8939e6b8db4f3dcc977aba22dd6a682#code
- Контракт на повторную атаку: https://ropsten.etherscan.io/address/0x88c3c78e47840826363a933ea74048920ebc6146#code