Детальне вивчення атаки Reentrancy та ефективних методів її запобігання

## Що таке атака Reentrancy?

Атака reentrancy — це одна з найпоширеніших уразливостей у смарт-контрактах. Вона виникає, коли функція контракту викликає зовнішній контракт до того, як завершити свій стан. Зловмисник може використати це для багаторазового виклику функції та розкрадання коштів.

## Класична атака Reentrancy

Це відбувається, коли:

1. Контракт A надсилає кошти контракту B
2. Контракт B виконує код (fallback функція)
3. Контракт B знову викликає контракт A перед завершенням першого виклику
4. Баланс контракту A ще не оновлений

## Ефективні методи захисту

### 1. Блокування Mutex (Перевірка стану)
```solidity
bool locked = false;

modifier noReentrancy() {
require(!locked);
locked = true;
_;
locked = false;
}
```

### 2. Перевірка-Ефект-Взаємодія (CEI)
```solidity
// Оновити стан перед зовнішнім викликом
balance[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
```

### 3. OpenZeppelin ReentrancyGuard
```solidity
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

function withdraw() public nonReentrant {
// Безпечний код
}
```

### 4. Pull замість Push
```solidity
// Замість відправлення коштів користувачу
// Дозвольте йому витягти їх
mapping(address => uint256) public balances;

function withdraw() public {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
```

## Найкращі практики

- Завжди оновлюйте стан перед зовнішніми викликами
- Використовуйте перевірені бібліотеки, такі як OpenZeppelin
- Проводьте аудит безпеки смарт-контрактів
- Обмежуйте привілеї функцій
- Тестуйте сценарії атак

Розуміння та запобігання атакам reentrancy — критично важливо для розробників смарт-контрактів.

Атака повторного виклику (reentrancy) є однією з найсерйозніших загроз безпеці для смарт-контрактів. У цій статті ми розглянемо не лише принцип роботи reentrancy, а й комплексні методи захисту вашого проекту.

Щоб отримати повне уявлення, почнемо з базових понять, проаналізуємо код реальних атак і наостанок розглянемо три доведені техніки запобігання: від modifier 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 знову виконує перевірку — баланс ContractB ще 1 Ether, тому знову відправляє Ether і активує fallback().

Цей цикл повторюється, доки ContractA не витратить усі Ether. Саме тому reentrancy є такою небезпечною вразливістю.

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

Розглянемо приклад з контрактом EtherStore, що має дві основні функції:

  • deposit(): зберігає і оновлює баланс користувача
  • withdrawAll(): виводить усі кошти

Функція withdrawAll() виконує: перевірка балансу (>0), відправка Ether і оновлення балансу на 0.

Вразливість у тому, що відправка Ether відбувається перед оновленням балансу, що дозволяє зловмиснику викликати повторно цю функцію.

Контракт Attack створений для експлуатації цієї вразливості:

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

Послідовність атаки:

  1. Зловмисник викликає attack() з Ether
  2. attack() відправляє 1 Ether у EtherStore, створюючи баланс >0
  3. attack() викликає withdrawAll()
  4. EtherStore відправляє Ether → активується fallback()
  5. fallback() викликає withdrawAll() знову
  6. цикл повторюється, доки EtherStore не спорожніє

Три техніки захисту від reentrancy

Техніка 1: Використання модифікатора nonReentrant

Простий спосіб — додати модифікатор nonReentrant до функції, яку потрібно захистити. Це блокуватиме повторні виклики під час виконання.

Як працює:

  • при вході у функцію встановлюється блокування
  • будь-який повторний виклик у цей час буде відхилений
  • після завершення блокування знімається

Недолік: захищає лише одну функцію, не запобігає атакам між різними функціями.

Техніка 2: Модель Check-Effect-Interaction

Більш просунутий підхід — змінити порядок виконання коду:

  • Перевірка (Check): перевіряємо умови (require)
  • Ефект (Effect): оновлюємо стан (баланс)
  • Взаємодія (Interaction): викликаємо зовнішні контракти або відправляємо Ether

Приклад:

  • Замість спершу відправляти Ether, оновлюємо баланс, а потім виконуємо відправку. Це запобігає повторним викликам, оскільки баланс вже оновлено.

Техніка 3: GlobalReentrancyGuard

Для складних систем з кількома контрактами створюється глобальний захисник, що контролює reentrancy по всій системі.

Замість окремих змінних у кожному контракті, використовується один централізований захисник, що відслідковує стан системи і запобігає chain-reentrancy.

Приклад: якщо один контракт виконує перекази, а інший намагається викликати повторно функції, GlobalReentrancyGuard зупинить це.

Вибір відповідної техніки захисту

nonReentrant підходить, якщо:

  • потрібно захистити окрему функцію
  • не важливі витрати газу
  • не потрібно захищати міжфункціональні виклики

Check-Effect-Interaction — для оптимізації і захисту всього проекту:

  • зменшує витрати
  • підходить для складних контрактів

GlobalReentrancyGuard — для великих систем з кількома контрактами:

  • централізований контроль
  • запобігає chain-reentrancy

Комбінуйте ці методи залежно від потреб вашого проекту.

Висновок

Атака reentrancy — не неможлива для запобігання, якщо розуміти її механізм і застосовувати правильні техніки. Від простого модифікатора до глобального захисника — кожен інструмент має своє місце у системі безпеки смарт-контрактів.

Пам’ятайте: безпека — не опція, а обов’язок. Зрозумівши атаки reentrancy і застосувавши відповідні заходи, ви зможете створювати безпечні та ефективні смарт-контракти.

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

Переглянути оригінал
Ця сторінка може містити контент третіх осіб, який надається виключно в інформаційних цілях (не в якості запевнень/гарантій) і не повинен розглядатися як схвалення його поглядів компанією Gate, а також як фінансова або професійна консультація. Див. Застереження для отримання детальної інформації.
  • Нагородити
  • Прокоментувати
  • Репост
  • Поділіться
Прокоментувати
Додати коментар
Додати коментар
Немає коментарів
  • Закріпити