По-перше, я збираюся допомогти вам зрозуміти простим способом, що таке атака повторного входу та як її можна запобігти, а потім я глибше занурюся в приклади коду, щоб показати, де є вразливі місця, який би був код зловмисника і, що найважливіше, я покажу вам найновіші перевірені методи захисту не лише одного, але й усіх розумних контрактів у вашому проекті.

Спойлер: якщо ви вже чули про модифікатор nonReentrant(), продовжуйте читати, оскільки ви збираєтеся відкрити кілька рядків нижче модифікатора globalNonReentrant() і шаблону перевірки-ефекти-взаємодії.



На зображенні вище ми маємо ContractA і ContractB. Тепер, як ви знаєте, смарт-контракт може взаємодіяти з іншим смарт-контрактом, як у цьому випадку ContractA може викликати ContractB. Отже, основна ідея повторного входу полягає в тому, що ContractB може передзвонити ContractA, поки ContractA все ще виконується.

Отже, як зловмисник може цим скористатися?

Вище ми маємо ContractA, який містить 10 ефірів, і ми бачимо, що ContractB зберіг 1 ефір у ContractA. У цьому випадку ContractB зможе використати функцію вилучення з ContractA та надіслати Ether собі назад, коли він пройде перевірку, де його баланс перевищує 0, щоб потім змінити його загальний баланс на 0.



Тепер давайте подивимося, як ContractB може використовувати повторне входження, щоб використовувати функцію вилучення та вкрасти всі ефіри з ContractA. По суті, зловмиснику знадобляться дві функції: attack() і fallback().

У Solidity запасна функція – це зовнішня функція без назви, параметрів або значень, що повертаються. Будь-хто може викликати резервну функцію: Викликаючи функцію, якої немає в контракті; Виклик функції без передачі необхідних даних; Відправка Ether без будь-яких даних до контракту.

Те, як працює повторне входження (дотримуймося стрілок крок за кроком), полягає в тому, що зловмисник викликає функцію attack(), яка всередині викликає функцію withdraw() з ContractA. Усередині функції вона перевірить, чи баланс ContractB перевищує 0, і якщо так, то продовжить виконання.



Оскільки баланс ContractB більше 0, він повертає 1 ефір і запускає резервну функцію. Зверніть увагу, що на даний момент ContractA має 9 ефірів, а ContractB вже має 1 ефір.



Далі, коли запасна функція виконується, вона знову запускає функцію виведення ContractA, знову перевіряючи, чи баланс ContractB перевищує 0. Якщо ви знову перевірите зображення вище, ви помітите, що його баланс все ще становить 1 ефір.



Це означає, що перевірка пройдена, і він надсилає інший ефір до ContractB, який запускає резервну функцію. Зауважте, що оскільки рядок, у якому ми маємо «balance=0», ніколи не виконується, це триватиме, доки весь ефір із ContractA не зникне.

___________

Давайте тепер подивимося на смарт-контракт, де ми можемо ідентифікувати повторне входження за допомогою коду Solidity.



У контракті EtherStore у нас є функція deposit(), яка зберігає та оновлює баланси відправника, а потім функція withdrawAll(), яка приймає всі збережені баланси одночасно. Будь ласка, зверніть увагу на реалізацію withdrawAll() де він спочатку перевіряє, що баланс перевищує 0, і одразу після цього знову надсилає ефір, залишаючи на кінець оновлення балансу відправника до 0.



Тут ми маємо контракт Attack, який використовуватиме повторне входження, щоб вичерпати контракт EtherStore. Проаналізуємо його код:

  • У своєму конструкторі зловмисник передасть адресу EtherStore, щоб створити екземпляр і таким чином мати можливість використовувати його функції.

  • Там ми бачимо функцію fallback(), яка буде викликана, коли EtherStore надсилатиме Ether до цього контракту. Всередині буде виклик виведення з EtherStore, доки баланс дорівнює або перевищує 1.

  • І всередині функції attack() у нас є логіка, яка використовуватиме EtherStore. Як ми бачимо, спочатку ми почнемо атаку, переконавшись, що у нас достатньо ефіру, потім внесемо 1 ефір, щоб мати баланс більше 0 в EtherStore і, отже, пройдемо перевірки перед тим, як почати виведення.

Я пояснив вище в прикладі ContractA і ContractB крок за кроком, як буде працювати код, тож тепер давайте підсумуємо, як це буде. Перш за все зловмисник викличе атаку(), яка всередині викличе drawAll() з EtherStore, який потім надішле ефір до резервної функції контракту Attack. І там він почне повторне входження та виснажить баланс EtherStore.

Отже, як ми можемо захистити наші контракти від атак повторного входу?

Я збираюся показати вам три методи профілактики, щоб повністю захистити їх. Я збираюся розповісти, як запобігти повторному входу в одній функції, перехресній функції повторного входу та перехресному контракту повторного входу.



Перший спосіб захисту окремої функції – використання модифікатора під назвою noReentrant.

Модифікатор – це особливий тип функції, який використовується для зміни поведінки інших функцій. Модифікатори дозволяють додавати додаткові умови або функції до функції без необхідності переписувати всю функцію.

Що ми тут робимо, так це блокування контракту під час виконання функції. Таким чином він не зможе повторно ввести одну функцію, оскільки йому потрібно буде пройти через код функції, а потім змінити змінну заблокованого стану на false, щоб знову пройти перевірку, виконану в запиті.

___________



Другий метод полягає у використанні шаблону «Перевірки-ефекти-взаємодії», який захистить наші контракти від повторного входу між функціями. Чи можете ви помітити зміни в оновленому контракті EtherStore вище?

Щоб глибше зануритися в шаблон Check-Effects-Interaction, рекомендую прочитати https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html





Вище ми бачимо порівняння між вразливим кодом із зображення ліворуч, де баланс було оновлено після надсилання ефіру, який, як показано вище, потенційно ніколи не міг бути досягнутий, і праворуч те, що було зроблено, це переміщення балансів [ msg.sender] = 0 (або ефект) відразу після require(bal > 0) (перевірка), але перед надсиланням ефіру (взаємодія).

Таким чином ми гарантуємо, що навіть якщо інша функція отримує доступ до withdrawAll(), цей контракт буде захищено від зловмисника, оскільки баланс завжди оновлюватиметься перед надсиланням ефіру.

Шаблон створено https://twitter.com/GMX_IO

Третя техніка, яку я вам покажу, — це створення контракту GlobalReentrancyGuard для захисту від повторного входу між контрактами. Важливо розуміти, що це стосується проектів із кількома контрактами, які взаємодіють один з одним.

Ідея тут така ж, як і в модифікаторі noReentrant, який я пояснював у першій техніці, він вводить модифікатор, оновлює змінну, щоб заблокувати контракт, і не розблоковує його, доки не закінчить код. Велика різниця тут полягає в тому, що ми використовуємо змінну, збережену в окремому контракті, яка використовується як місце для перевірки, чи була введена функція чи ні.







Я створив тут приклад без фактичного коду та лише з назвами функцій для довідки, щоб зрозуміти ідею, оскільки, з мого досвіду, це може допомогти візуалізувати ситуацію більше, ніж просто написати її словами.

Тут зловмисник викликає функцію в контракті ScheduledTransfer, яка після виконання умов надсилає вказаний ефір до контракту AttackTransfer, який, таким чином, увійде в резервну функцію і, отже, «скасує» транзакцію з точки контракту ScheduledTransfer. переглядати і все ж отримувати Ефір. І таким чином він буде починати перегляд до вичерпання всіх ефірів із ScheduledTransfer.

Що ж, використання GlobalReentrancyGuard, про яке я згадував вище, дозволить уникнути такого сценарію атаки.

__________________

Twitter @TheBlockChainer, щоб знайти більше щоденних оновлень про смарт-контракти, безпеку Web3, Solidity, аудит смарт-контрактів тощо.

__________________