puzzor@home:~$

Edu智能合约漏洞

EDU智能合约漏洞

2018年5月23日夜间,EDU智能合约被爆出严重漏洞,攻击者可以在不知道账户持有人私钥情况下转走任意账户下的数字货币。根据描述来看可能是在某些权限控制上出现了问题,于是便开始审计EDU的智能合约代码。

EDU和合约地址为0xa0872eE815B8dd0F6937386Fd77134720d953581,我们找到其合约代码如下:

pragma solidity ^0.4.18;

interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }
contract Token {
    /// 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 public 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) public 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) public 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) public 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 public returns (uint256 remaining);
    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}

/*
You should inherit from StandardToken or, for a token like you would want to
deploy in something like Mist, see HumanStandardToken.sol.
(This implements ONLY the standard functions and NOTHING else.
If you deploy this, you won't have anything useful.)

Implements ERC 20 Token standard: https://github.com/ethereum/EIPs/issues/20
.*/

contract StandardToken is Token {
    function transfer(address _to, uint256 _value) public returns (bool success) {
        // Prevent transfer to 0x0 address.
        require(_to != 0x0);
        // Check if the sender has enough
        require(balances[msg.sender] >= _value);
        // Check for overflows
        require(balances[_to] + _value > balances[_to]);

        uint previousBalances = balances[msg.sender] + balances[_to];
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        Transfer(msg.sender, _to, _value);
        // Asserts are used to use static analysis to find bugs in your code. They should never fail
        assert(balances[msg.sender] + balances[_to] == previousBalances);
        return true;
    }

    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) 	{
        /// same as above
        require(_to != 0x0);
        require(balances[_from] >= _value);
        require(balances[_to] + _value > balances[_to]);
        uint previousBalances = balances[_from] + balances[_to];
        balances[_from] -= _value;
        balances[_to] += _value;
        allowed[_from][msg.sender] -= _value;
        Transfer(_from, _to, _value);
        assert(balances[_from] + balances[_to] == previousBalances);

        return true;
    }

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

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

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

    mapping (address => uint256) balances; /// balance amount of tokens for address
    mapping (address => mapping (address => uint256)) allowed;
}

contract EduCoin is StandardToken {

    function () payable public {
        //if ether is sent to this address, send it back.
        //throw;
        require(false);
    }
    string public constant name = "EduCoinToken";   
    string public constant symbol = "EDU";
    uint256 private constant _INITIAL_SUPPLY = 15*10**27;
    uint8 public decimals = 18;         
    uint256 public totalSupply;            
    //string public version = 'H0.1';
    function EduCoin(
    ) public {
        // init
        balances[msg.sender] = _INITIAL_SUPPLY;
        totalSupply = _INITIAL_SUPPLY;
       
    }

    /* Approves and then calls the receiving contract */
    function approveAndCall(address _spender, uint256 _value, bytes _extraData) public returns (bool success) {
        tokenRecipient spender = tokenRecipient(_spender);
        if (approve(_spender, _value)) {
            spender.receiveApproval(msg.sender, _value, this, _extraData);
            return true;
        }
    }
}

在这么多函数中,我们重点去关注和转账相关的函数Transfer和TransferFrom。对于Transfer函数来说,在真正做转账之前做了比较完备的条件检查,比如检查目的地址、检查当前余额、检查是否发生了溢出,在条件检查均通过的情况下才会进行实际的转账。

而再来看transferFrom函数,transferFrom函数本身的用途是转出其他账户中属于自己的代币,然而这就需要判断在转账的源账户中到底有多少是属于自己的。通常情况下需要用

require(allowed[_from][msg.sender] >= _value)

进行判断。而在EDU的合约代码中我们并没有看到这样的检查而只是检查了目的地址不为0,from地址余额充足,目的地址余额不会发生溢出。当同时满足这些条件的时候便可以进行转账了,也就造成了未经授权转走任意账户的EDU代币

require(_to != 0x0);
require(balances[_from] >= _value);
require(balances[_to] + _value > balances[_to]);