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