三天竟然爆发两起大漏洞事件!我们来教你如何跳过以太坊的坑
三天竟然爆發兩起大漏洞事件!我們來教你如何跳過以太坊的坑
2018年04月26日 00:00:00閱讀數:1314“現在進入你還是先行者,最后觀望者進場才是韭菜。”美圖董事長蔡文勝在三點鐘群中的預言一語成讖。在4月22日,隨著BEC智能合約漏洞的爆出,一行代碼蒸發了6447277680人民幣。然而時隔三天,SMT的智能合約又爆出漏洞,SMT在火幣Pro的價格下跌近20%。一時間,無論先行者還是準“韭菜”,都慘遭收割。
區塊鏈做為一款能與價值交互的產品,難免不被人們神化。理性地分析一下,程序中的漏洞總是不可避免的,很難保證代碼百分百不出錯,即使大公司也只能通過發布測試版本來降低漏洞出現的概率。今天讓我們來看看智能合約的初創者——以太坊智能合約都有什么“坑”,并且怎么寫代碼才不被坑。
作者 | ConsenSys Diligence
譯者 | Guoxi
4月25日上午,火幣Pro發布公告,虛擬幣SMT項目方反饋25日凌晨發現其交易存在異常問題,經初步排查,SMT的以太坊智能合約存在漏洞。火幣Pro也同期檢測到TXID為0x0775e55c402281e8ff24cf37d6f2079bf2a768cf7254593287b5f8a0f621fb83的異常。受此影響,火幣Pro暫停所有幣種的充提幣業務。當天,截止暫停交易,SMT在火幣Pro的價格下跌近20%。
而這類漏洞不是第一次發生了,距離上一次發生僅隔了三天。
在4月22日,BEC出現異常交易,據分析,BEC 智能合約中的 batchTransfer 批量轉賬函數存在漏洞,攻擊者可傳入很大的 value 數值,使 cnt *value 后超過 unit256 的最大值使其溢出導致 amount 變為 0。
簡單的說,BEC的某一段代碼忘記使用safeMath方法,導致系統產生了整數溢出漏洞,利用該漏洞,黑客可以通過轉賬手段生成大量原本合約中不存在的代幣,并將這些“無中生有”的代幣在市場進行拋售。
由于黑客轉出的代幣數量遠遠超過BEC發行數量70億枚,加之由此引發的恐慌拋售,BEC的64億市值瞬間幾乎歸零。
在這兩起事件之后 PeckShield 團隊利用自動化系統掃描以太坊智能合約并對它們進行分析。結果發現,多個 ERC-20 智能合約都存在 BatchOverFlow 安全隱患。若不做好嚴格的代碼審計和安全防護,億級資金的損失只在一瞬間。那怎么才能避免這種情況發生呢?以下是已發現的智能合約攻擊方式,為了資產安全,你必須知曉并在寫智能合約時避開這些漏洞。
競態條件引發的2種漏洞
競態條件(race condition)就是指設備或系統出現不恰當的執行時序,而得到不正確的結果。
在執行智能合約時調用外部合約有很大的風險,因為這個外部合約可以接管你當前合約的控制流程,惡意的外部合約可能會更改你合約中的關鍵數據,這對當前合約造成的影響是巨大的。兩個合約繞來繞去,是不是聽起來很拗口?通俗地給你解釋一下。
設想一下當你在轉賬時突然有個人出現在你的面前,打斷了你的操作并趁你不注意修改了你的轉賬信息,當你發現錢款轉錯人后已為時已晚。這種漏洞有很多種表現形式,它也是史上最大智能合約漏洞事件——The DAO的“罪魁禍首”。The DAO事件造成了價值6000萬美元的以太坊被盜,且6000萬美元的損失是按當時17.5美元的以太坊價格估算得出的,這也導致了以太坊當時的硬分叉。
漏洞一:函數可重入性
可重入性(Reentrancy)一般可以理解為一個函數在同時多次調用,例如操作系統在進程調度過程中,或者單片機、處理器等的中斷的時候會發生重入的現象。
這個漏洞第一種可能出現的情況是:在調用其他函數的操作完成之前,這個被調的函數可能會多次執行。這可能會導致智能合約中的幾個函數以破壞性的方式進行交互。
因為用戶的余額一直沒有被置0,直到函數執行的結束。第二次(之后一次)調用其他函數的操作仍會成功,并且會一次一次地取消對賬戶余額的置0操作。The DAO事件中以太坊被盜就是因為攻擊者執行了這樣的操作。
解決方案,在給出的示例中,為了避免碰到這個漏洞,我們的解決方案是:使用函數send()而不是函數call.value()(),這將阻止任何外部代碼的執行。
但是如果無法避免要調用外部函數時,防止這種攻擊的下一個簡便方法就是確保在你調用外部函數時已完成所有要執行的內部操作。
請注意,如果你有另一個函數也調用了withdrawBalance(),那么它也可能會受到相同的攻擊,因此你必須將這種調用不可信合約的函數視為不可信函數,接下來我會進一步討論潛在的解決方案。
漏洞二:跨函數的競態條件
攻擊者也可以對共享相同狀態的兩個不同函數進行類似的攻擊。
在這種情況下,攻擊者可以在代碼執行到調用withdrawBalance()時調用transfer() 函數,由于他們的余額在此時還未被置0,所以即使他們已經收到退款,他們也還能轉移通證,這個漏洞也被用在了The DAO事件中。
同樣的原理,同樣的注意事項。注意在這個例子中,這兩個函數都是同一個智能合約的組成部分,同樣的,當多個合約共享同一狀態時,這幾個合約之間也可能會出現這個漏洞。
由于競態條件可能發生在多個函數之間,甚至是多個智能合約之間,所以旨在防止重入現象的解決方案都是明顯不夠的。
解決方案,這兒有兩種解決方案,一是我們建議先完成所有的內部工作,然后再調用外部函數;二是使用互斥鎖。
1.首先第一種解決方案,先完成所有的內部工作,然后再調用外部函數。如果你在編寫智能合約時仔細地遵循這個規則,那么就可以避免出現競態條件。但是,你不僅需要注意避免過早地調用外部函數,還要注意這個外部函數調用的外部函數,例如,下面的操作就是不安全的。
盡管函數getFirstWithdrawalBonus()不直接調用外部的合約,但在函數withdraw()中的調用足以使其進入競態條件之中。因此,你需要將函數withdraw()視為不可信函數。
除了修復漏洞使這種重入現象變得不可能外,還要標記出不可信的函數。這種標記要注意一次次的調用關系,因為函數untrustedGetFirstWithdrawalBonus()調用了不可信函數untrustedWithdraw(),這意味著調用了一個外部的合約,因此你必須將函數untrustedGetFirstWithdrawalBonus()也列為不可信函數。
2.第二中解決方案是使用互斥鎖。即讓你“鎖定”某些狀態,后期只能由鎖的所有者對這些狀態進行更改,如下所示,這是一個簡單的例子:
如果用戶在第一次調用結束前嘗試再次調用withdraw() 函數,那么這個鎖定會阻止這個操作,從而使運行結果不受影響。這可能是一種有效的解決方案,但是當你要同時運行多個合約時,這種方案也會變得很棘手,以下是一個不安全的例子:
這種情況下攻擊者可以調用函數getLock()鎖定合約,然后不再調用函數releaseLock()解鎖合約。如果他們這樣做,那么合約將被永久鎖定,并且永遠不能做出進一步的更改。如果你使用互斥鎖來防止競態條件,你需要確保不會出現這種聲明了鎖定但永遠沒有解鎖的情況。在編寫智能合約時使用互斥鎖還有很多其他的潛在風險,例如死鎖或活鎖。如果你決定采用這種方式,一定要大量閱讀關于互斥鎖的文獻,避免“踩雷”。
有些人可能會反對使用競態條件這個術語,因為以太坊并沒有真正地實現并行性。然而,邏輯上不同的進程爭奪資源的基本特征仍然存在,所以同樣的漏洞和潛在的解決方案也同樣適用。
交易順序依賴與非法預先交易導致的漏洞
交易順序依賴(Transaction-Ordering Dependence,TOD)
非法預先交易(Front Running)非法預先交易是經紀人從客戶交易中獲利的一種不道德做法。在手中持有客戶交易委托的情況下搶先為自己的賬戶進行交易。
以下是區塊鏈固有的不同類型的競態條件:在區塊內部,交易本身的順序很容易受到人為操控。
由于在礦工挖礦時,每筆交易都會在內存池中待一段時間,因此可以想象到交易被打包進區塊前會發生什么。對于去中心化的市場,可更改的交易順序會帶來很多的麻煩。比如市場上常見的買入某些代幣的交易。而防范這一點十分地困難,因為它會涉及到合約中具體的實現細節。例如,在去中心化市場中,由于可以防止高頻交易,故批量拍賣的效果更好。另一種解決方法就是采用預先提交方案的機制,別著急,后面我會詳細介紹這個機制的細節。
漏洞三:時間戳依賴
請注意,區塊的時間戳可被礦工人為操縱,所以要留意時間戳的所有直接和間接使用。
還有很多與時間戳相關的注意事項,編程前一定要認真學習。
整數的上溢和下溢導致的漏洞
想象一個很簡單的轉移通證的場景:
如果你的賬戶余額達到了以太坊中最大的無符號整型值(2^256),那么你的余額再增加就無法表示了。因為平時遇到這種現象進位就可以了,但在這里無符號整型值只有256位,進位的第257位是不顯示的,所以你沒有猜錯,當你進位后你的余額就會回到0。在計算機科學中這種現象就叫做整數的上溢。
當然了,這種現象也不太常見,因為它需要同時保證你真的有這么多余額,你的智能合約中還沒考慮到上溢問題。考慮一下這個無符號整型值是否有機會達到這么大一個數字,再考慮一下這個無符號整型值如果改變當前數值,以及誰有權做出這樣的改變。如果智能合約中任何用戶都可以調用函數來更新這個無符號整型值,那么這個智能合約就會很容易受到攻擊。如果只有管理員可以做出更改,那么它才可能是安全的。如果合約中規定用戶的賬戶余額每次只能增加1,那么這個合約可能也很安全,因為現在還沒有可行的方法讓你短時間內達到這個限制。
賬戶余額達到最大時再增加就會被清零,你會瞬間從最富有的人變成最窮的人。不知你有沒有想到可以從最窮的人變成最富有的人?沒錯,下溢也是這個道理,如果這個無符號整型值小于0,那么它需要向前借位,而借的那一位并不顯示,所以你的余額就會下溢達到最大值。
看到這里,你一定要小心使用像8位,16位和24位的無符號整型值,因為8位無符號整型值最大僅可以表示255,所以相比之下它們更容易達到最大值而發生溢出現象。
對待溢出現象請千萬小心,之前有程序員整理了20個智能合約中上溢和下溢的場景。
漏洞四:存儲操作中的深度下溢
Doug Hoyte在2017年的以太坊黑客比賽中提出了這個漏洞,這也讓他獲得了比賽中的榮譽。這個想法很有意思,因為它引起了人們對C類語言下溢如何影響以太坊編程語言Solidity的擔憂。這是一個簡化了的版本:
一般來說,如果不經過keccak256哈希計算(當然,這是不現實的),變量manipulateMe的存儲位置就不會被影響。但由于動態數組是按順序存儲的,如果攻擊者想要改變manipulateMe這個變量,他只需要這樣做:
調用函數popBonusCode()來實現下溢。(請注意,以太坊編程語言Solidity并沒有內置的pop函數。)
計算變量manipulateMe的存儲位置。
使用函數modifyBonusCode()修改和更新變量manipulateMe的值。
實際上,人們都知道這種數組存在的漏洞。但如果這樣的數組被掩埋在更復雜的智能合約架構之下,誰又能輕易發現呢?這樣它就可以任意地對變量進行惡意篡改。
解決方案,在考慮使用動態數組時,使用一個容器式的數據結構是一種不錯的選擇。Solidity CRUD的第1部分和第2部分文章詳細介紹了這個漏洞。
意外恢復導致的漏洞
漏洞五:利用交易失敗,促使意外恢復
考慮一個簡單的拍賣合同:
當智能合約準備給商品原主人付款時,如果付款失敗,它將恢復。這意味著一個惡意的投標人可以在拍下商品的同時確保給商品原主人的付款總是付款失敗。這樣他們可以阻止其他人調用bid()函數,成為商品的新主人。如前所述,為了資金安全,建議拍賣時建立一個預授權方式的付款合約。
另一個例子是當智能合約通過數組的迭代向用戶付款時,例如給眾籌合約的支持者退款。通常要確保每筆付款都成功,如果哪一筆付款失敗了,則會恢復,重新付款。問題是如果一筆付款失敗了,那么你要恢復整個付款系統。這意味著如果哪一筆付款卡住了,這次迭代付款永遠都不會完成,因為一個地址出錯,所有人都拿不到這筆錢。
解決方案,這里我們的建議是使用預授權方式付款。
區塊燃料限制導致的漏洞
漏洞六:利用區塊燃料上限引發漏洞
你可能已經注意到了前一個例子中的另一個問題:如果要一次性地支付給所有人,你可能會遇到達到區塊中燃料上限的情況。每個以太坊的區塊都只能處理一定的最大計算量,如果你試圖超過這個限制,那么你的交易將會失敗。
即使沒有黑客故意攻擊你,這都是一個問題。如果攻擊者能夠操控你所需的燃料,情況就會變得更加糟糕。在前面的例子中,攻擊者可以添加一堆地址,每個地址都需要很少量的退款。因此,加上給攻擊者地址退款使用的燃料,可能會導致超過區塊燃料上限,從而阻止退款交易的發生。
解決方案,我們推薦使用預授權方式付款來解決這個漏洞。
如果你絕對需要遍歷未知大小的數組,那么你應該規劃一下應該把它們分到多少個區塊中,每個區塊需要多少筆交易。這樣你只需要留意現在進行到哪個區塊中的交易了,出錯后僅需從當前區塊開始恢復,如下所示:
你需要確保在等待payOut()函數的下一次迭代時處理的其他交易不出現錯誤。所以只有在絕對必要的時候再使用這種模式。
強行給智能合約中加入以太幣導致的漏洞
漏洞七:強行給智能合約中加入以太幣,引發程序邏輯漏洞
原則上,我們可以將以太幣強制發送到智能合約中而不觸發回退函數。當給回退函數加入重要功能或計算智能合約的收支平衡時,這是一個重要的考慮因素。請看下面這個例子:
這個智能合約的邏輯似乎不允許對智能合約付款,以防發生一些“不好的事情”。但是還是存在一些方法可以強制將以太幣送到合約中,使智能合約的余額大于0。
智能合約中的自毀方法允許用戶向指定的受益人發送任意數量的以太幣,而這個自毀方法并不會觸及合約的回退功能。
在部署一個智能合約之前,可以預先算出合約的地址并將以太幣發送到該地址。
解決方案,智能合約的開發者應該意識到以太幣可以被強制送到智能合約中,并應該相應地設計智能合約邏輯。一般情況下,需要假設無法限制智能合約的資金來源。
對已被棄用的協議進行攻擊導致的漏洞
漏洞八:利用已被棄用的協議進行攻擊
這些攻擊由于以太坊協議的改變或以太坊編程語言solidity的改進而不能使用。在這里記錄僅供參考,不做過多說明。
總結
以上是生活随笔為你收集整理的三天竟然爆发两起大漏洞事件!我们来教你如何跳过以太坊的坑的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TensorFlow中文社区论坛 发布上
- 下一篇: 以太坊完整工作原理和运行机制!