1. 程式人生 > 其它 >【轉載】智慧合約安全事故回顧分析(1):The Dao事件

【轉載】智慧合約安全事故回顧分析(1):The Dao事件

首先需要說明的一點是,這個世界上沒有絕對安全的技術。在區塊鏈發展的十年裡,各種基於區塊鏈的數字貨幣引發的安全事故層出不窮,這些安全威脅主要來源有三個方面:

  1. 自身安全機制的問題,類似智慧合約。

  2. 生態安全問題,交易所,礦池,網站等等。

  3. 使用者安全問題,包括個人賬號密碼的洩露,被釣魚等。

作為普通的開發人員或者有一定程式設計知識的從業人員,我們首先應該確保的是自身安全機制沒有問題,當然這個“沒有問題”是一個相對的概念。智慧合約的安全為什麼這麼重要,這很大原因在於智慧合約程式設計和傳統程式設計的巨大區別:

  1. 智慧合約本身開發簡單,但是卻能夠儲存幾千萬到幾十億的的資產。

  2. 智慧合約部署的過程是一次共識的過程,如果部署以後發現了安全問題,不能通過傳統的打補丁或者升級的方式來避免。必須在設計和編碼的過程中處理好這些容錯和異常終止邏輯。

  3. 智慧合約的程式碼都是開放的,多任何人可見。這其中就包括了一些不懷好意的黑客,沒有傳統開發過程中的加密,訪問控制。

本系列希望通過對過往發生的一些安全事故的回顧,來提醒或者說警醒各位開發者,在開發的過程中,即便不能做到百分百安全,那麼起碼能做到“吸取前人的教訓”,避免已經發生過的安全事故再次發生。

本文介紹的是對以太坊影響深遠的The Dao 智慧合約漏洞事件

事件介紹

The Dao 是一個去中心化的自治風險投資基金,通過釋出的智慧合約來募集資金,參與者可以通過投票的方式來投資以太坊上的應用,如果盈利,參與者就能獲得回報。2016年6月17日,一名黑客發現了The Dao募資合約的漏洞,使得他可以無限的從合約中轉出資金,短短几小時,360萬的以太幣被轉出。這件事對以太坊的發展產生了巨大的影響,最後為了彌補使用者的損失V神智慧採用軟分叉的方式,即所有通過這個The Dao的合約來減少新增使用者餘額的方式都被視為無效。

漏洞原因

首先請讀者看一下合約中的程式碼,這端程式碼的業務邏輯是:如果使用者不同意其他使用者的投票,可以選擇分裂出去。簡單的說就是使用者拿錢給基金會投資,中間使用者如果反悔可以隨時退錢。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 //使用者選擇分裂出去呼叫的函式 function splitDAO(uint_proposalID, address _newCurator) noEther onlyTokenholders returns (
bool_success) {
// ... //利用平衡陣列計算應該轉移多少代幣 p是提案物件 uintfundsToBeMoved = (balances[msg.sender] * p.splitData[0].splitBalance) / p.splitData[0].totalSupply; if(p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) ==false) throw; // ... // Burn DAO Tokens Transfer(msg.sender, 0, balances[msg.sender]); withdrawRewardFor(msg.sender);// 轉移對應的金額給使用者 // XXXXX Notice the preceding line is critically before the next few totalSupply -= balances[msg.sender];// 相應變數更新 balances[msg.sender] = 0;// 餘額置為0 paidOut[msg.sender] = 0; returntrue; } function withdrawRewardFor(address _account) noEtherinternalreturns(bool_success) { if((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account]) throw; uintreward = (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account]; if(!rewardAccount.payOut(_account, reward))// XXXXX vulnerable throw; paidOut[_account] += reward; returntrue; } 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); returntrue; }else{ returnfalse; } }

  

上面的程式碼在瞭解業務很容易明白:

使用者提出分裂--》合約計算應該退給使用者的金額--》呼叫call函式傳送金額給使用者--》使用者的賬戶餘額歸為0,即先是呼叫splitDAO,splitDao中呼叫withdrawRewardFor,withdrawRewardFor中呼叫payOut執行轉賬。

乍一看沒什麼問題,講述黑客的攻擊手段之前,回顧一下solidity程式設計中的知識點:如果call函式的呼叫結果是true就一定是執行成功的嗎?答案是NO,因為有可能是執行了回撥函式。當呼叫call.value的時候,會把所有的gas傳送到合約地址上並執行預設函式。所以這個預設函式將會有足夠的gas執行任何操作,包括重新呼叫原合約的介面。本次攻擊的黑客正式利用了這一點。

攻擊手段

  1. 黑客先是通過自己建立了一個合約Child Dao,這個合約擁有一個回撥函式,這個函式的作用就是去呼叫The Dao中的splitDao。

  2. 黑客提交了splitDao,地址是Child Dao的地址,當然在此之前的操作都是合法的操作,滿足The Dao定義的呼叫splitDao的條件。

  3. 結合上面的程式碼,你會發現,開發者的程式碼先是在函式withdrawRewardFor中把金額退還給了使用者,然後在退出函式之後將使用者的餘額置為0。那麼如果攻擊者在withdrawRewardFor和餘額置空之間在此呼叫withdrawRewardFor,將會再次向攻擊者提交的地址轉移賬戶金額。結合剛才介紹的call函式知識點,聰明的讀者應該能夠想到攻擊的原理了。黑客利用了call函式的機制,在合約中再次呼叫轉賬申請,由於上一次轉賬申請的餘額還沒有更新,所以第二次也會成功。相當於在迴圈中的重複呼叫自己,程式設計中的遞迴。

如何防範

其實The Dao的開發者的漏洞程式碼在傳統的程式設計中沒有任何問題,傳統程式設計為了應對事務處理的結果,往往在轉賬之後進行餘額的更新,因為有可能因為網路等原因導致轉賬不成功,如果程式提前把使用者的賬戶餘額置為0則容易引發資料丟失的問題。本次The Dao事件的程式碼修復可以從多方面來考慮:

  1. 調整程式碼順序,在轉賬之前執行餘額減扣。

  2. 避免不可控的函式呼叫,黑客利用call函式fallback的呼叫機制來攻擊,這個場景其實在很多別的攻擊事件中也可能發生,後面介紹的DOS攻擊中黑客也利用了這一點。一方面應該避免這種方式呼叫,其實還應該避免在合約中直接使用轉賬操作,可以在設計的時候提供一個轉賬mapping,每個使用者可以提現金額的多少對應其中的key value,讓使用者主動去操作這個介面完成呼叫。因為合約主動呼叫本身就存在安全隱患,合約的許可權大於所有人。