Глубокое понимание атак Reentrancy и эффективные методы защиты

Атака Reentrancy — одна из наиболее серьёзных уязвимостей в смарт-контрактах Ethereum. Давайте разберёмся, как она работает и как её предотвратить.

**Что такое Reentrancy?**

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

**Классический пример — уязвимость DAO:**

```solidity
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);

(bool success, ) = msg.sender.call{value: amount}("");
require(success);

balances[msg.sender] -= amount;
}
```

Здесь баланс обновляется ПОСЛЕ отправки средств. Злоумышленник может перехватить управление и вызвать `withdraw` снова.

**Эффективные методы защиты:**

1. **Паттерн Checks-Effects-Interactions (CEI):**
```solidity
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;

(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
```

2. **Блокировка Mutex:**
```solidity
bool locked = false;

modifier noReentrancy() {
require(!locked, "Нет повторных вызовов");
locked = true;
_;
locked = false;
}

function withdraw(uint amount) public noReentrancy {
// Код функции
}
```

3. **ReentrancyGuard от OpenZeppelin:**
```solidity
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Safe is ReentrancyGuard {
function withdraw(uint amount) public nonReentrant {
// Защищённый код
}
}
```

4. **Паттерн Pull вместо Push:**
```solidity
function withdrawBalance() public {
uint balanceToWithdraw = balances[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: balanceToWithdraw}("");
require(success);
}
```

**Ключевые рекомендации:**

- Всегда обновляйте состояние ДО внешних вызовов
- Используйте проверенные библиотеки защиты
- Проводите аудиты безопасности
- Тестируйте сценарии reentrancy
- Рассмотрите использование pull-механизмов для платежей

Понимание этих принципов критично для разработки безопасных DeFi приложений.

Атака повторного вызова (reentrancy) является одной из самых серьезных угроз безопасности для смарт-контрактов. В этой статье вы узнаете не только о механизме работы reentrancy, но и о комплексных методах защиты вашего проекта.

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

Что такое атака типа reentrancy?

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

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

Например, ContractA держит 10 Ether, из которых ContractB уже отправил 1 Ether. Обычно, при запросе на вывод, контракт проверяет баланс (>0), отправляет Ether и устанавливает баланс в 0. Но при reentrancy этот процесс можно повторять несколько раз до обновления баланса.

Механизм атаки reentrancy

Злоумышленник использует два элемента: функцию attack() и fallback().

Функция fallback — особая функция в Solidity, вызываемая автоматически, если:

  • вызывается несуществующая функция
  • отправляются данные без указания функции
  • передается Ether без данных

Теперь пошагово:

Шаг 1: злоумышленник вызывает attack(), внутри которой вызывает вывод средств из ContractA.

Шаг 2: ContractA проверяет баланс ContractB, если он >0, отправляет 1 Ether и активирует fallback.

Шаг 3: В этот момент баланс еще не обновлен — контракт продолжает выполнение функции вывода.

Шаг 4: Вызов fallback() активируется и сразу вызывает повторный вызов функции вывода ContractA.

Шаг 5: ContractA снова проверяет баланс — он еще не обновлен, и отправляет еще 1 Ether, активируя fallback.

Этот цикл продолжается, пока ContractA не истощит все Ether. Поэтому reentrancy так опасна.

Анализ конкретного кода атаки

Рассмотрим пример смарт-контракта EtherStore с двумя функциями:

  • deposit(): сохраняет и обновляет баланс пользователя
  • withdrawAll(): выводит все средства и сбрасывает баланс

Функция withdrawAll() сначала проверяет баланс (>0), отправляет Ether, затем устанавливает баланс в 0. Уязвимость в том, что отправка происходит до обновления баланса.

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

  • принимает адрес EtherStore
  • содержит fallback(), вызывающую withdrawAll() при наличии Ether
  • функцию attack() для начала атаки

Процесс атаки:

  1. Злоумышленник вызывает attack() с достаточным количеством Ether
  2. attack() отправляет Ether в EtherStore, создавая баланс >0
  3. attack() вызывает withdrawAll()
  4. EtherStore отправляет Ether, активируя fallback() Attack
  5. fallback() вызывает withdrawAll() снова, так как баланс еще не обновлен
  6. цикл повторяется, пока EtherStore не опустошится

Три метода защиты от reentrancy

Метод 1: Использование модификатора nonReentrant

Простое решение — добавить модификатор nonReentrant к функциям, которые требуют защиты. Он блокирует повторные вызовы функции, пока она не завершится.

Работает так:

  • при входе в функцию устанавливается блокировка
  • при выходе — снимается блокировка
  • любые повторные вызовы внутри этого периода отклоняются

Недостаток: защищает только одну функцию, не предотвращает цепочку атак между разными функциями.

Метод 2: Модель Check-Effect-Interaction

Более продвинутый подход — менять порядок операций:

  • Check: проверка условий (например, баланс)
  • Effect: обновление состояния (баланса)
  • Interaction: вызов внешних контрактов или отправка Ether

Пример опасного кода:

require(balance > 0);
payable(msg.sender).transfer(balance);
balance = 0;

Безопасный порядок:

require(balance > 0);
balance = 0;
payable(msg.sender).transfer(balance);

Обновление баланса происходит до отправки Ether, что предотвращает повторные вызовы.

Метод 3: Глобальный Reentrancy Guard (GlobalReentrancyGuard)

Подходит для сложных систем с несколькими контрактами. Создается центральный контракт-замок, который отслеживает состояние reentrancy для всей системы.

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

Выбор подходящего метода

  • Используйте nonReentrant, если нужно защитить отдельные функции без сложных взаимодействий.
  • Применяйте Check-Effect-Interaction для оптимизации газа и защиты всей логики.
  • В сложных системах с множеством контрактов используйте GlobalReentrancyGuard.

Лучше всего комбинировать эти методы в зависимости от конкретных требований проекта.

Итог

Атака reentrancy — не неизбежна, если понять ее механику и правильно применить защитные меры. От простого модификатора до глобального замка — каждый инструмент важен для обеспечения безопасности смарт-контрактов.

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

Для получения обновлений по безопасности Web3, Solidity, аудиту смарт-контрактов и другим вопросам следите за @TheBlockChainer в Twitter.

Посмотреть Оригинал
На этой странице может содержаться сторонний контент, который предоставляется исключительно в информационных целях (не в качестве заявлений/гарантий) и не должен рассматриваться как поддержка взглядов компании Gate или как финансовый или профессиональный совет. Подробности смотрите в разделе «Отказ от ответственности» .
  • Награда
  • комментарий
  • Репост
  • Поделиться
комментарий
Добавить комментарий
Добавить комментарий
Нет комментариев
  • Закрепить