作者:隱形人真忙
作者博客:http://blog.csdn.net/u011721501/article/details/79450122

0x00 前言

最近關注了一下區塊鏈方面的安全,因此翻出來之前的DAO攻擊事件研究了一番,形成此文。

之后可能還會發一些其他的安全分析文章。

0x00 基礎知識

1.跨合約調用

智能合約之間的調用本質上是外部調用,可以使用message call或者創建智能合約對象的形式進行調用。

(1)使用message call

比如合約1調用合約2的某個方法:

bytes4 methodId = bytes4(keccak256("increaseAge(string,uint256)"));  
return addr.call(methodId,"jack", 1);  

(2)還原智能合約對象 如果已知合約的地址,可以通過如下方式獲取到合約對象:

Contract1 c = Contract1(AddressOfContract1) ;  
c.foo() ; //跨合約調用  
2.智能合約發送ETH

我們可以在智能合約中用代碼向某個地址(這個地址可以是人,也可以是智能合約)發送以太幣,比較常見的兩個方式是:

(1)調用send函數

比如:msg.sender.send(100)

(2)使用message call

msg.sender.call.value(100)()

這兩個方式不同的是發送的gas數量,gas就是執行opcode需要花費的一種幣,稱作為gas也特別形象。當調用send方法時,只會發送一部分gas,準確地來講,是2300gas,一旦gas耗盡就可能拋出異常。

而使用message call的時候,則是發送全部的gas,執行完之后剩余的gas會退還給發起調用的合約。

3.fallback函數

智能合約中可以有唯一的一個未命名函數,稱為fallback函數。該函數不能有實參,不能返回任何值。如果其他函數都不能匹配給定的函數標識符,則執行fallback函數。

當合約接收到以太幣但是不調用任何函數的時候,就會執行fallback函數。如果一個合約接收了以太幣但是內部沒有fallback函數,那么就會拋出異常,然后將以太幣退還給發送方。

下面就是一個fallback函數的代碼示例:

contract Sample{  
    function () payable{  
       // your code here  
    }  
}  

一般單純使用message call或者send函數發送以太幣給合約的時候,沒有指明調用合約的某個方法,這種情況下就會調用合約的fallback函數。

0x01 攻擊事件還原

我們先用簡單的模擬代碼來了解下整個攻擊過程。

首先是存在漏洞的智能合約代碼,Bank:

用戶可以通過addToBalance方法存入一定量的以太幣到這個智能合約,通過withdrawBalance方法可以提現以太坊,通過getUserBalance可以獲取到賬戶余額。

注意到這里是通過message call的方式來發送以太幣,所以在調用sender的fallback函數的時候我們就會有充足的gas來進行循環調用。如果是send的方式,gas只有2300,稍微一操作就會耗盡gas拋出異常,是不夠用來進行嵌套調用的。以下是不同操作所需要的gas數量:

出問題的是withdrawBalance方法,特別是在修改保存在區塊鏈的balances的代碼是放在了發送以太幣之后。 攻擊代碼如下:

這里的deposit函數是往Bank合約中發送10wei。withdraw是通過調用Bank合約的withdrawBalance函數把以太幣提取出來。注意看這里的fallback函數,這里循環調用了兩次Bank合約的withdrawBalance方法。

攻擊的過程如下:

(1)假設Bank合約中有100wei,攻擊者Attack合約中有10wei
(2)Attack合約先調用deposit方法向Bank合約發送10wei
(3)之后Attack合約調用withdraw方法,從而調用了Bank的withdrawBalance方法。
(4)Bank的withdrawBalance方法發送給了Attack合約10wei
(5)Attack收到10wei之后,又會觸發調用fallback函數
(6)這時,fallback函數又調用了兩次Bank合約的withdrawBalance,從而轉走了20wei
(7)之后Bank合約才修改Attack合約的balance,將其置為0

通過上面的步驟,攻擊者實際上從Bank合約轉走了30wei,Bank則損失了20wei,如果攻擊者多嵌套調用幾次withdrawBalance,完全可以將Bank合約中的以太幣全部轉走。

0x02 復現過程

給Bank合約100wei,給Attack合約10wei。

(1)部署Bank,分配100wei

(2)部署Attack

分配給Attack 10wei。

(3)調用Attack合約的deposit方法

(4)調用Attack合約的withdraw方法

(5)查看Attack合約的余額,變成了30wei,即竊取了20wei

0x03 DAO攻擊事件代碼分析

在DAO源碼中,有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)) //vulnerable  
    throw;  
  paidOut[_account] += reward;  
  return true;  
}  

這里調用了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)()) { //vulnerable  
      PayOut(_recipient, _amount);  
      return true;  
  } else {  
      return false;  
}  

而payOut中直接使用的是message call的方式發送以太幣,從而導致了嵌套漏洞。

0x04 總結

在編寫智能合約進行以太幣發送的時候,要使用send或者transfer,不要使用message call的方式,而send其實還是有些小問題,以后有時間再分析。DAO事件直接導致了以太坊硬分叉,分為ETH和ETC。可見,區塊鏈領域的安全不容忽視,因為其修復難度和所造成的影響都很高,畢竟是和錢打交道。


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