《我学区块链》—— 十四、以太坊安全之 太阳风暴

十四、以太坊安全之 太阳风暴

       2016年6月20日,以太坊用于开发智能合约的静态语言 Solidity,被发现了一个安全漏洞,它可以影响整个以太坊,而不仅仅是 DAO。

回顾DAO漏洞

       还记得DAO漏洞吗,下面再简单复习一下,已经很熟悉的小伙伴可以直接跳过此部分。DAO 漏洞的完整文章可以 点击这里

function splitDAO(uint _proposalID, address _newCurator) noEther onlyTokenholders returns (bool _success) {
    // ...
    // XXXXX Move ether and assign new Tokens. Notice how this is done first!
    uint fundsToBeMoved = (balances[msg.sender] * p.splitData[0].splitBalance) / p.splitData[0].totalSupply;
    if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
        // XXXXX This is the line the attacker wants to run more than once
        throw;
    // ...
    // Burn DAO Tokens
    Transfer(msg.sender, 0, balances[msg.sender]);
    withdrawRewardFor(msg.sender);  // be nice, and get his rewards
    // XXXXX Notice the preceding line is critically before the next few
    totalSupply -= balances[msg.sender];    // XXXXX AND THIS IS DONE LAST
    balances[msg.sender] = 0;   // XXXXX AND THIS IS DONE LAST TOO
    paidOut[msg.sender] = 0;
    return true;
}
function withdrawRewardFor(address _account) noEther internal returns(bool _success) {
    if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
        throw;
    uint reward = (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
    if (!rewardAccount.payOut(_account, reward))    // XXXXX vulnerable
        throw;
    paidOut[_account] += reward;
    return true;
}

function payOut(address _recipient, uint _amount) returns (bool) {
    if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
        throw;
    if (_recipient.call.value(_amount)()) { // XXXXX vulnerable
        PayOut(_recipient, _amount);
        return true;
    } else {
        return false;
    }
}

       最后一个函数 payOut 中,_recipient.call.value(_amount)() 这句会使用黑客地址的_recipient去调用黑客的 fallback 函数,而黑客会在 fallback 中再去调用 DAO 的 splitDAO 函数,进而源源不断的将代币转移到黑客的地址。

太阳风暴

       以太坊合约常规性的调用其他合约。这是社区鼓励采用的行为,愿景是智能合约在任何地方进行互动,并减少重复内容在链上的资源占用和gas消耗。结果是,当一个以太坊合约与另一个合约交互时,它会丧失对自己程序状态的控制。

       如果你使用Solidity的call调用功能,同时你自己的合约有一个可以被外部调用的函数可以修改状态,那么你无法对你的合约在调用外部函数之后的状态做任何假设。

       非常重要的一点,应区分这个漏洞和可重入性漏洞——一个已知的漏洞,并被用于攻击DAO。

太阳风暴vs可重入性

       可重入攻击(DAO漏洞)可简化成下面的形式:

/**
 * DAO 合约
 */
contract Dao {

    mapping(address => uint256) balances;

    function splitDAO() public {
        // addressH 为黑客地址,由于后面的 balances[addressH] = 0; 无法得到执行,导致 if 内容始终为 true
        if (balances[addressH] > 0)
            _recipientH.call.value(_amount)();
        // addressH 为黑客地址,但这一句却不会被执行到,因为上面一句 _recipientH.call.value(_amount)(); 会执行黑客的 fallback,
        //      而 fallback 又会从头调用 splitDAO.
        balances[addressH] = 0;
    }
}

/**
 * DAO 黑客合约
 */
contract DaoH {
    function() public {
        _recipientDAO.call("splitDAO", _proposalID, _newCurator); // _recipientDAO 为 DAO 合约地址
    }
}

       太阳风暴攻击的一种情况会是下面的形式

/**
 * SolarStorm A 合约
 */
contract SolarStormA {

    mapping(address => uint256) balances;

    function A (address _address) {
        if (balances[_address] > 0)
            _recipientB.call("B", ...arguments);

        if (balances[_address] > 0) // balances[_address] > 0 将不再成立
            ...
        else
            ...
    }

    function C (address _address) {
        balances[_address] = 0;
    }
}

/**
 * SolarStorm B 合约
 */
contract SolarStormB {
    function B (address _address) {
        _recipientA.call("C", addressB);
    }
}

即:

1 合约A,函数A调用合约B。

2 合约A有另外一个函数C,与函数A共享状态。

2 合约B 调用合约A,函数C。

所以,太阳风暴的一个更泛化描述是:

1 合约A调用任何外部合约

2 合约A有外部函数来修改状态(多数情况下都有)

总结

       1、类似太阳风暴的漏洞会冲击以太坊上的所有智能合约,而不仅仅是DAO。这是以太坊用于开发智能合约的类java-script语言 Solidity 的问题。

       2、可能在以太坊已经发布的合约中存在这种漏洞。开发者应当检查是否他们的合约具有脆弱性,并采取相应措施(转移资金,发布新合约)。
       3、开发者在未来的合约中,应当对外部调用极度谨慎。或能够避免外部调用,直到本问题被解决。

猜你喜欢

转载自blog.csdn.net/xuguangyuansh/article/details/81299126