彎道超車老司機戲耍智能合約 | 成都鏈安漏洞分析連載第三期 —— 競態條件漏洞區塊鏈
引子:至道問學之有知無行,分溫故為存心,知新為致知,而敦厚為存心,崇禮為致知,此皆百密一疏。——清·魏源《庸易通義》
針對區塊鏈安全問題,成都鏈安科技團隊每一周都將出智能合約安全漏洞解析連載,希望能幫助程序員寫出更加安全牢固的合約,防患于未然。
引子:至道問學之有知無行,分溫故為存心,知新為致知,而敦厚為存心,崇禮為致知,此皆百密一疏。
—— 清·魏源《庸易通義》
卻說“DoS攻擊重現區塊鏈江湖,縝密防范助陣安全陣營”,例外判定合力數據結構的加固,亦使老牌勁敵DoS節節敗退。
此回:“重入”“競態”里應外合幣窮財盡,“交互”“限制”強強聯手鏈泰民安
區塊鏈的“高速公路”在川流不息的同時,卻也事故頻發。究其緣由,大批投資者涌入這個似乎暢通無阻,通向明日輝煌的康莊大道,都躍躍欲試一場“速度與激情”,展開對成功的追逐賽。未曾想,挑戰者中并非只有彼此,一襲黑衣,手段了得的選手大有人在,這些處心積慮的黑客總有辦法讓智能合約看似神通廣大,實則百密一疏。
這一回,我們將重點剖析競態條件漏洞的兩種形式,重入漏洞以及交易順序依賴漏洞。
事件回顧
2016年4月,完全自治,去中心化的項目DAO啟動,立刻成為最受歡迎的以太坊項目,然而在其發布之后,有開發者警告DAO的發起者,在splitDAO函數中潛伏著遞歸調用漏洞[1]。 2016年6月14日,DAO的項目方聲稱漏洞已被定位,資金和合約安全已受到保障。
然而就在3天之后,6月17日,黑客卻利用上述漏洞向DAO發起攻擊,360萬以太幣岌岌可危,超過6百萬美元的資金被源源不斷地被黑客暗度陳倉,著實來了一場“無間DAO”。
事件發生后,DAO負責人采取措施減緩了資金流失的速度,以太坊也在7月修改源碼幫助DAO轉移資金,嘗試奪回失竊資金,卻導致了以太坊的硬分叉[2]。
想要分析黑客如何對DAO的資金探囊取物,就不得不提到競態條件這個術語。
什么是競態條件
競態條件的官方定義是如果程序的執行順序改變會影響結果,它就屬于一個競態條件 [3]。
在智能合約中,競態條件漏洞被攻擊者利用后,攻擊者利用一個與存在漏洞合約平起平坐的外部合約競爭奪取控制權,改變該智能合約的行為。
用一個形象的比喻來說明,將智能合約理解成一條高速公路,所有函數和功能理解為車輛,原本的執行順序規定了車輛經過的順序,此時一名熟練的老司機,駕駛著GTR在彎道超車加塞,擾亂了整個道路的秩序,搶占了在道路中的領先地位,進而為所欲為,戲耍合約規則。
以太坊智能合約的特點之一是能夠調用和利用其它外部合約的代碼,調用外部合約主要存在的危險就是外部合約可以接管控制流,并對調用函數不期望的數據進行更改。這類漏洞有多種形式,我們在這里深度解析重入和交易順序依賴兩種。
競態條件漏洞分析及詳細修復建議
1. 重入漏洞(Reentrancy)
問題描述
合約通常用來處理 Ether,因此通常會將 Ether 發送給各種外部用戶地址。調用外部合約或將以太網發送到地址的操作需要合約提交外部調用。這些外部調用可能被攻擊者劫持,迫使合約執行進一步的代碼(即通過回退函數),包括回調自身。因此代碼執行"重新進入"合約。這種攻擊被用于上述臭名昭著的DAO 攻擊。
我們把存在漏洞的合約簡化成如下案例合約:
該合約有兩個函數:depositFunds()和withdrawFunds(),depositFunds()的功能是增加msg.sender的余額,withdrawFunds()的功能是取出msg.sender指定的數值為_weiToWithdraw的Ether。
現在,一個攻擊者創建了下列合約:
PS:注意此處由于重入攻擊造成了balances[msg.sender]溢出,強烈推薦所有數學運算都使用SafeMath進行,這個要點我們在第一期溢出漏洞中已經提到(敲黑板)。
我們來分析下該合約是如何進行重入攻擊的:
1、假設普通用戶向原合約(Reentrancy.sol)存入15 ether;
2、攻擊者部署攻擊合約(POC.sol),并調用setInstance()指向原合約部署地址;
3、攻擊者調用攻擊合約的depositEther()函數,預先向原合約預存1 ether,此時, 在原合約中,攻擊合約的地址有1 ether余額;
4、攻擊者調用攻擊合約的withdrawFunds()函數,該函數再調用原合約的withdrawFunds()函數,并傳參1 ether;
5、進入原合約,withdrawFunds()函數的第一行require(balances[msg.sender] >= _weiToWithdraw);,攻擊合約地址下余額為1 ether,等于_weiToWithdraw,條件滿足,進入下一行;
6、withdrawFunds()函數的第二行require(msg.sender.call.value(_weiToWithdraw)());,向msg.sender轉入_weiToWithdraw(此時是1 ether),由于msg.sender是合約地址,solidity規定向合約地址接收到ether時如果未指定其他有效函數,那么默認會調用合約的fallback函數,執行流進入攻擊合約,并調用攻擊合約的fallback函數,并且,因為是通過call.value()()方式發送以太幣,該方法會發送所有剩余gas;
7、進入攻擊合約的fallback函數,if判斷原合約余額,此時為16 ether,條件滿足,再次"重入"原合約的withdrawFunds()函數;
8、再次進入原合約的withdrawFunds()函數,因為balances[msg.sender] -= _weiToWithdraw;并未執行,所以此時攻擊合約地址仍有1 ether,第一個require條件滿足,執行到第二個require;
9、此后步驟6-8將一直重復,直到原合約余額少于1 ether或者gas耗盡;
10、最后進入原合約,執行balances[msg.sender] -= _weiToWithdraw;,注意,此處會從balances[msg.sender]中減去所有提取的ether,導致balances[msg.sender]溢出,如果此處使用SafeMath,可以通過拋出異常的方式避免重入攻擊。
最終的結果是攻擊者只使用了1 ether,就從原合約中取出了所有的ether。
漏洞修復
1、 在可能的情況下,將ether發送給外部地址時使用solidity內置的transfer()函數[4],transfer()轉賬時只發送2300 gas,不足以調用另一份合約(即重入發送合約),使用transfer()重寫原合約的withdrawFunds()如下;
2、 確保狀態變量改變發生在ether被發送(或者任何外部調用)之前,即Solidity官方推薦的檢查-生效-交互模式(checks-effects-interactions);
3、 使用互斥鎖:添加一個在代碼執行過程中鎖定合約的狀態變量,防止重入調用
接述事件回顧,重入在DAO攻擊中發揮了重要作用,最終導致了 Ethereum Classic(ETC)的分叉。有關The DAO原始漏洞的詳細分析,請參閱Phil Daian的文章。
2. 交易順序依賴攻擊
問題描述
與大多數區塊鏈一樣,以太坊節點匯集交易并將其形成塊。一旦礦工解決了共識機制(目前Ethereum的 ETHASH PoW),這些交易就被認為是有效的。解決該區塊的礦工也會選擇來自該礦池的哪些交易將包含在該區塊中,這通常是由gasPrice交易決定的。在這里有一個潛在的攻擊媒介。攻擊者可以觀察事務池中是否存在可能包含問題解決方案的事務,修改或撤銷攻擊者的權限或更改合約中的對攻擊者不利的狀態。然后,攻擊者可以從這個事務中獲取數據,并創建一個更高級別的事務gasPrice 并在原始之前將其交易包含在一個區塊中。
我們來看如下案例漏洞合約:
這個合約包含1000個ether,找到并提交正確答案的用戶將得到這筆獎勵。當一個用戶找出答案Ethereum!。他調用solve函數,并把答案Ethereum!作為參數。不幸的是,攻擊者可以觀察交易池中任何人提交的答案,他們看到這個解決方案,檢查它的有效性,然后提交一個遠高于原始交易的gasPrice的新交易。解決該問題的礦工可能會因攻擊者的gasPrice更高而先打包攻擊者的交易。攻擊者將獲得1000ether,最初解決問題的用戶將不會得到任何獎勵(合約中沒有剩余ether)。
漏洞修復
有兩類用戶可以進行這種的提前交易攻擊。用戶(修改他們的交易的gasPrice)和礦工自己(他們可以按照他們認為合適的方式重新排序交易)。一個易受第一類(用戶)攻擊的合約比一個易受第二類(礦工)攻擊的合約明顯更糟糕,因為礦工只能在解決一個區塊時執行攻擊,這對于任何針對特定區塊的單個礦工來說都是不可能的。在這里,我將列出一些與他們可能阻止的攻擊類別相關的緩解措施。
可以采用的一種方法是在合約中創建限制條件,即gasPrice上限。這可以防止用戶增加gasPrice并獲得超出上限的優先事務排序。這種預防措施只能緩解第一類攻擊者(任意用戶)的攻擊。在這種情況下,礦工仍然可以攻擊合約,因為無論gasPrice如何,他們都可以根據需要排序交易。
更可靠的方法是盡可能使用提交---披露方案(commit-reveal)。這種方案規定用戶使用隱藏信息(通常是散列)發送交易。在交易已包含在塊中之后,用戶發送一個交易解密已經發送的數據(披露階段)。此方法可防止礦工和用戶進行前瞻性交易,因為他們無法確定交易內容。然而,這種方法無法隱藏交易價值(在某些情況下,這是需要隱藏的有價值信息)。 ENS智能合約允許用戶發送交易,其承諾數據包括他們愿意花費的以太數量。然后,用戶可以發送任意值的交易。在披露階段,用戶退還了交易中發送的金額與他們愿意花費的金額之間的差額。
前事不忘,后事之師
DAO事件在當時區塊鏈行業轟動一時,損失之重,令無數投資人捶胸頓足,我們總結下來,為了防止類似的情況發生,開發者應注意以下幾點:
開發過程中注意查閱Solidity或者其他官方語言中是否已給出相關內置函數或者嚴謹的交互模式,如有應嚴格遵守,切不可異想天開;
勤于思考狀態變量有可能發生的意外,對有潛在問題的狀態變量應予以鎖定;
綜合運用gas限制以及披露方案,保障交易信息在合理的環節以合理的形式呈現。
區塊鏈時代的安全問題都帶有互聯網發展早期的影子,安全知識的遷移以及防范意識的提升將會是斬除隱患的利刃。
欲知后事,且看下回:底層函數調用險象環生 外部功能慎用防患未然
引用:
[1]:
https://courses.csail.mit.edu/6.857/2017/project/23.pdf
[2]:
http://baijiahao.baidu.com/s?id=1587206953375229861&wfr=spider&for=pc
[3]:
https://blog.csdn.net/Clifnich/article/details/78447524
[4]:
https://blog.sigmaprime.io/solidity-security.html#race-conditions
1.TMT觀察網遵循行業規范,任何轉載的稿件都會明確標注作者和來源;
2.TMT觀察網的原創文章,請轉載時務必注明文章作者和"來源:TMT觀察網",不尊重原創的行為TMT觀察網或將追究責任;
3.作者投稿可能會經TMT觀察網編輯修改或補充。