科普 | TheDAO 攻击代码简介

孙峰   |     |   3392 次阅读

TheDAO 被攻击,攻击利用两个代码漏洞创建子合约提取了360万个以太币。
因为攻击者是通过splitDAO开始攻击。我们从splitDAO代码开始分析

  function splitDAO(
        uint _proposalID,
        address _newCurator
    ) noEther onlyTokenholders returns (bool _success) 

函数调用者只能是TokenHolder。不能是ether账户。这里没什么问题,符合预期

splitDAO的代码有点长,我们看到如下的代码

 uint fundsToBeMoved =
        (balances[msg.sender] * p.splitData[0].splitBalance) /
         p.splitData[0].totalSupply;
 if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
            throw;

在这里是计算需要支付的金额,然后调用createTokenProxy函数。我们需要记住这点。

再看如下代码块

  // Burn DAO Tokens
    Transfer(msg.sender, 0, balances[msg.sender]);
     withdrawRewardFor(msg.sender); // be nice, and get his rewards
     totalSupply -= balances[msg.sender];
     balances[msg.sender] = 0;
     paidOut[msg.sender] = 0;
     return true;

这段代码就有问题,他先调用了withdrawRewardFor,然后再对变量totalSupply,balances,paidOut赋值。这种方式可能会有RACE TO EMPTY攻击。
归纳来说,是当withdrawRewardFor调用时,balances,如果这个时候再去获取balances、paidOut的值是取了未更新前的值。

我们再往下看,withdrawRewardFor是非常不安全的。

  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))
            throw;
        paidOut[_account] += reward;
        return true;
    }

代码很短,一个判断再上一个payOut的调用。但是paidOut的值是在payOut调用后置的。这同样是一个很不好的习惯。

再往下。rewardAccount是ManagedAccount的合约,我们找到payOut的函数

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

_recipient.call.value()() 这个函数调用时,没有gas数量,这样更容易造成攻击。

我们现在可以建立一个攻击合约。

  1. 创建一个钱包,调用splitDAO多次,直到到达合约的gas limit,或者stack limit。
  2. 创建一个分割提案到一个新的钱包地址。
  3. 等待7天。
  4. 调用splitDAO

调用堆栈如下 (假设只调用了两次)

 splitDao
      withdrawRewardFor
         payOut
            recipient.call.value()()
               splitDao
                 withdrawRewardFor
                    payOut
                       recipient.call.value()()
    ```

    调用了两次splitDAO,withdrawRewardFor也调用了两次,并且两次都是成功。成功窃取ether。

    所以一定要先重置变量,再调用发送函数。

关于RACE To Empty。可参考http://vessenes.com/more-ethereum-attacks-race-to-empty-is-the-real-deal/


TheDAO合约源码地址为http://etherscan.io/address/0xbb9bc244d798123fde783fcc1c72d3bb8c189413#code
攻击者合约地址:http://etherscan.io/address/0x304a554a310c7e546dfe434669c62820b7d83490#internaltx
详细内容请参考:http://vessenes.com/deconstructing-thedao-attack-a-brief-code-tour/
 
4 人喜欢