區塊鏈安全————DAO攻擊事件解析

FLy_鵬程萬里發表於2018-06-18
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。可見,區塊鏈領域的安全不容忽視,因為其修復難度和所造成的影響都很高,畢竟是和錢打交道。


相關文章