【原創】智能合約安全事故回顧分析(1):The Dao事件

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

  1. 自身安全機制的問題,相似智能合約。數組

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

  3. 使用者安全問題,包括我的帳號密碼的泄露,被釣魚等。網絡

做爲普通的開發人員或者有必定編程知識的從業人員,咱們首先應該確保的是自身安全機制沒有問題,固然這個「沒有問題」是一個相對的概念。智能合約的安全爲何這麼重要,這很大緣由在於智能合約編程和傳統編程的巨大區別:app

  1. 智能合約自己開發簡單,可是卻可以存儲幾千萬到幾十億的的資產。函數

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

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

本系列但願經過對過往發生的一些安全事故的回顧,來提醒或者說警醒各位開發者,在開發的過程當中,即使不能作到百分百安全,那麼起碼能作到「吸收前人的教訓」,避免已經發生過的安全事故再次發生。ui

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

事件介紹

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

漏洞緣由

首先請讀者看一下合約中的代碼,這端代碼的業務邏輯是:若是用戶不一樣意其餘用戶的投票,能夠選擇分裂出去。簡單的說就是用戶拿錢給基金會投資,中間用戶若是反悔能夠隨時退錢。

//用戶選擇分裂出去調用的函數
function splitDAO(uint _proposalID, address _newCurator) noEther onlyTokenholders returns (bool _success) {
    // ...
    //利用平衡數組計算應該轉移多少代幣 p是提案對象
    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;
    // ...
    // 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;
    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;
    }
}

  

 

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

用戶提出分裂--》合約計算應該退給用戶的金額--》調用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,讓用戶主動去操做這個接口完成調用。由於合約主動調用自己就存在安全隱患,合約的權限大於全部人。

相關文章
相關標籤/搜索