張登記,趙相福,陳中育,童向榮
1.浙江師范大學(xué)數(shù)學(xué)與計算機科學(xué)學(xué)院,浙江金華321004
2.煙臺大學(xué)計算機與控制工程學(xué)院,山東煙臺264005
自2009年中本聰(Satoshi Nakamoto)引入比特幣(Bitcoin)[1]以來,去中心化的數(shù)字加密貨幣引起了廣泛關(guān)注。加密貨幣由用戶在網(wǎng)絡(luò)中公開管理,不依賴于任何可信的第3 方,其用戶可以通過運行一種共識協(xié)議來維護和共享數(shù)據(jù)賬本。比特幣的底層技術(shù)是區(qū)塊鏈,它在本質(zhì)上是一個可信的去中心化分布式賬本數(shù)據(jù)庫。比特幣是區(qū)塊鏈技術(shù)第1 個成熟的應(yīng)用,至今仍是主流加密貨幣之一,它標志著區(qū)塊鏈技術(shù)從理念進入實踐,促進了區(qū)塊鏈技術(shù)的進一步發(fā)展。
智能合約的概念最早由計算機科學(xué)家、密碼學(xué)家尼克·薩博(Nick Szabo)[2]于1994 提出,最初被定義為一套以數(shù)字形式定義的承諾,包括合約參與方可以在上面執(zhí)行這項承諾的協(xié)議,其設(shè)計初心是能夠把智能合約內(nèi)置到物理實體來實現(xiàn)靈活可用的智能資產(chǎn)。由于當(dāng)時計算手段落后且缺失可信執(zhí)行環(huán)境及其應(yīng)用場景,智能合約并未引起研究者的廣泛關(guān)注。區(qū)塊鏈技術(shù)的興起重塑了智能合約,解決了之前技術(shù)不成熟和應(yīng)用場景缺失等問題。
2013年年末,以太坊創(chuàng)始人Buterin 發(fā)表了以太坊白皮書[3],將智能合約應(yīng)用到區(qū)塊鏈中,拓寬了區(qū)塊鏈技術(shù)在除數(shù)字貨幣之外的應(yīng)用場景,開啟了區(qū)塊鏈2.0 時代。智能合約作為嵌入?yún)^(qū)塊鏈的合約代碼,同樣具有區(qū)塊鏈的一般特征,如分布式存儲、不可篡改等,其運行機制如圖1所示。一旦各方簽署智能合約,合約便以代碼的方式依附在區(qū)塊鏈數(shù)據(jù)上。智能合約封裝了預(yù)置響應(yīng)規(guī)則和觸發(fā)條件,其狀態(tài)由區(qū)塊鏈實時監(jiān)控;區(qū)塊鏈通過校驗外部的數(shù)據(jù)源來激活特定觸發(fā)條件,之后智能合約就會響應(yīng)[4]。
圖1 智能合約的運作機制Figure 1 Operating mechanism of smart contracts
以太坊是一個去中心化的虛擬機,依據(jù)用戶請求去執(zhí)行智能合約。合約一旦創(chuàng)建成功就不允許再次修改,通過合約地址標識每個合約。合約可以持有以太幣(Ether)和私有存儲空間。以太坊中智能合約的代碼是一種低級且基于堆棧的字節(jié)碼語言,在以太坊虛擬機(Ethereum virtual machine,EVM)上執(zhí)行。用戶可使用高級編程語言編寫智能合約,如Solidity[5]語言(語法接近于JavaScript),在以太坊虛擬機上可以編譯為二進制EVM 代碼。Solidity 是為實現(xiàn)智能合約而設(shè)計的,是面向?qū)ο蟮膱D靈完備的高級編程語言。使用Solidity 可以為投票、眾籌、盲拍等應(yīng)用場景編寫智能合約。隨著以太坊平臺的逐漸成熟,Solidity 也逐漸成為全球最受歡迎的智能合約開發(fā)語言之一。與此同時,智能合約的安全性問題也面臨著巨大挑戰(zhàn),但編寫安全無缺陷的智能合約是一項艱巨的任務(wù)。研究表明,早期部署在Ethereum 上的大部分智能合約是易受到攻擊的[6]。例如在2016年6月,著名眾籌公司DAO 因智能合約代碼存在安全漏洞而遭到黑客攻擊,導(dǎo)致超過360 萬Ether 與DAO 資源池分離[7]。攻擊者利用可重入漏洞遞歸調(diào)用splitDAO 函數(shù)將Ether 轉(zhuǎn)移到私人賬戶。之后對以太坊實施軟硬分叉才挽回了部分資金損失。2017年9月,以太坊多重簽名錢包Parity 出現(xiàn)嚴重安全漏洞,導(dǎo)致10 萬Ether 被盜[8],之后因其他漏洞造成百萬美元被凍結(jié)。2018年4月,BEC 智能合約出現(xiàn)重大安全漏洞,攻擊者利用存在整數(shù)溢出漏洞的代幣合約無限生成代幣,這一漏洞導(dǎo)致約9 億美元被盜。在2019年,EOS 公鏈爆發(fā)了超60 起典型攻擊事件,多數(shù)是因智能合約存在安全缺陷引起的,總損失超1 000 萬美元。2020年4月,黑客對DeFi 平臺Uniswap 的智能合約實施可重入攻擊,還對Lendf.Me 實施了類似攻擊。從技術(shù)上看,這兩起事件背后的主要原因是ERC777 與DeFi 智能合約不兼容,允許攻擊者通過注入其他惡意代碼來劫持交易,執(zhí)行非法操作。
智能合約本身持有金融屬性,可以存儲資產(chǎn)價值,于是利用合約也可以進行數(shù)字貨幣交易,因此保障智能合約的安全性至關(guān)重要。存在缺陷或漏洞的智能合約維護成本較高,原因之一是合約一旦部署成功就不允許再次修改。在合約上鏈之前,保障智能合約設(shè)計和編碼的安全性極其重要,若合約代碼存在重大安全漏洞就很難打上安全補丁。本文總結(jié)并分析了智能合約在編寫時易出現(xiàn)的安全漏洞問題;剖析了智能合約的安全漏洞原理,復(fù)現(xiàn)了合約漏洞的場景,給出了預(yù)防智能合約漏洞的安全策略;最后分析了現(xiàn)有的幾種合約漏洞安全檢測工具。
1.1.1 可重入漏洞原理
在以太坊上,智能合約之間可以相互進行外部調(diào)用。以太坊用戶可以執(zhí)行智能合約并將以太幣發(fā)送到接收賬戶地址,接收賬戶可以為外部賬戶,也可以為合約賬戶,因為兩類賬戶都可以進行轉(zhuǎn)賬等操作。當(dāng)用戶向以太坊的合約賬戶發(fā)送以太幣時,合約賬戶中的fallback() 函數(shù)將被激活。一旦向攻擊者設(shè)定好的合約地址進行轉(zhuǎn)賬,就要迫使執(zhí)行攻擊合約的fallback()函數(shù)。攻擊者在fallback() 函數(shù)中可添加惡意代碼,再次調(diào)用被攻擊者合約的轉(zhuǎn)賬代碼,以致造成用戶損失。比如之前震驚以太網(wǎng)的“The DAO 事件”就是黑客利用可重入漏洞攻擊合約的典型案例。
智能合約中可重入漏洞的定義如下:“任何從合約A到B的交互以及任何從A到B的以太幣轉(zhuǎn)移,都將控制權(quán)交給B,這使得B能夠在交互結(jié)束前回調(diào)A中的代碼”[9]。若合約C向攻擊者合約D進行轉(zhuǎn)賬將觸發(fā)D中fallback() 函數(shù)的惡意代碼,此時D在擁有足夠Gas情況下回調(diào)C就可以實現(xiàn)一次轉(zhuǎn)賬。在第1 次調(diào)用轉(zhuǎn)賬函數(shù)結(jié)束之前重復(fù)遞歸調(diào)用轉(zhuǎn)賬函數(shù),會對合約D造成極大的經(jīng)濟損失。
1.1.2 可重入漏洞場景復(fù)現(xiàn)
在轉(zhuǎn)賬或調(diào)用外部合約時通常發(fā)生可重入漏洞攻擊。Fund 合約簡單實現(xiàn)了存取款操作,用戶執(zhí)行合約中的Deposit() 函數(shù)會記錄其存款額度,同時可以通過Withdraw() 函數(shù)進行取款操作。這段合約代碼似乎在邏輯上沒有缺陷,但問題出現(xiàn)在取款轉(zhuǎn)賬時的call.value() 函數(shù)上。智能合約的漏洞與語法特性有一定的關(guān)聯(lián),合約調(diào)用send() 和transfer() 函數(shù)時只分配2 300 Gas 處理轉(zhuǎn)賬操作(只夠記錄幾條log),而call.value() 函數(shù)將余下的全部Gas 用于合約的外部調(diào)用。若轉(zhuǎn)賬目標是一個合約地址,則自動執(zhí)行該合約的fallback() 函數(shù);若攻擊者在其fallback() 函數(shù)中添加惡意的重復(fù)遞歸調(diào)用轉(zhuǎn)賬代碼,則導(dǎo)致代幣合約里的余額全部轉(zhuǎn)出,給被攻擊者合約造成不可估量的經(jīng)濟損失。
攻擊者創(chuàng)建Attack 合約并向Fund 合約存入一定數(shù)量的以太幣,之后調(diào)用Withdraw()函數(shù)取出存款,在Fund 合約向Attack 合約成功轉(zhuǎn)出存款后,將自動激活A(yù)ttack 合約的fallback() 函數(shù),于是再一次調(diào)用Fund 合約的Withdraw() 函數(shù)。此時,F(xiàn)und 合約還未對Attack 合約賬戶執(zhí)行清零操作,Withdraw() 函數(shù)依然成功調(diào)用,直至Fund 合約賬戶的余額全部轉(zhuǎn)出時循環(huán)結(jié)束,攻擊者完成攻擊。
1.1.3 預(yù)防可重入漏洞的安全策略
為了避免可重入安全漏洞的產(chǎn)生,在轉(zhuǎn)賬以太幣時需要仔細權(quán)衡call.value()、send() 以及transfer() 函數(shù)的使用。采用call.value() 函數(shù)執(zhí)行轉(zhuǎn)賬操作時存在可重入攻擊的安全風(fēng)險;而使用send() 和transfer() 函數(shù)能通過設(shè)置Gas 值預(yù)防可重入攻擊,因此編寫智能合約轉(zhuǎn)賬函數(shù)時提倡優(yōu)先選擇這種方式。然而,這種方式可能因Gas 不足導(dǎo)致合約調(diào)用fallback() 函數(shù)出現(xiàn)問題,可以通過push 和pull 機制平衡這兩種方式。對外部未知地址轉(zhuǎn)賬可用send() 或transfer() 方式,在合約內(nèi)轉(zhuǎn)賬可用call.value() 方式。
若使用call.value() 這樣的低級調(diào)用函數(shù),應(yīng)切換為checks-effects-interactions 模式[10]。在沒有完成所有內(nèi)部工作之前不進行合約的外部調(diào)用,即先更改合約的內(nèi)部狀態(tài)再執(zhí)行低級調(diào)用函數(shù)。如在Fund 合約中,應(yīng)先執(zhí)行第9 行代碼更改合約狀態(tài),再執(zhí)行第8 行代碼調(diào)用call.value() 函數(shù)。添加互斥鎖也可以避免重入調(diào)用,即設(shè)置一個在代碼執(zhí)行過程中能夠鎖定合約的狀態(tài)變量。
智能合約調(diào)用不信任的外部合約可能引發(fā)不可預(yù)知的風(fēng)險,因為外部調(diào)用可能在其合約內(nèi)執(zhí)行惡意代碼,造成不可控的安全威脅,所以應(yīng)盡可能避免外部調(diào)用。
1.2.1 整數(shù)溢出漏洞原理
在智能合約中,一個整型變量只能表示一定范圍的數(shù)值。例如:無符號整數(shù)uint8 存儲范圍為0~255,(uint8)255+1 出現(xiàn)上溢則被錯誤存儲為0,(uint8)0?1 出現(xiàn)下溢則被錯誤存儲為255。同樣有符號整數(shù)也會出現(xiàn)上溢或下溢的情況。
EVM 為數(shù)據(jù)類型指定了固定大小,當(dāng)數(shù)據(jù)超出指定范圍時會發(fā)生溢出現(xiàn)象,若被攻擊者利用會造成不可預(yù)知的損失。
1.2.2 整數(shù)溢出漏洞場景復(fù)現(xiàn)
在智能合約中對整型變量進行算術(shù)運算時,若忽略整型數(shù)值的表示范圍則極易發(fā)生數(shù)值溢出。IntIssues 合約中的第7 行代碼判斷賬戶余額是否大于取款額度,明顯存在整數(shù)溢出漏洞。無符號整型uint256 可表示的數(shù)值范圍為0~2256?1,若參數(shù)amount 數(shù)值大于賬戶余額(即取款多余賬戶余額)則導(dǎo)致整數(shù)下溢,于是繼續(xù)執(zhí)行第8 行轉(zhuǎn)賬代碼,造成了合約賬戶上的以太幣流失。因此,攻擊者可以利用該漏洞達到存入較少但獲取較多以太幣的目的。
1.2.3 預(yù)防整數(shù)溢出漏洞的安全策略
采用邏輯判斷取代算術(shù)運算操作,將代碼require(balances[msg.sender]>amount) 作為賬戶余額判斷語句,可以相應(yīng)地規(guī)避整數(shù)溢出漏洞的產(chǎn)生。
為了預(yù)防整數(shù)溢出,應(yīng)在算術(shù)運算邏輯前后進行驗證或使用由OpenZeppelin 提供的SafeMath 庫[11]。
1.3.1 DoS 攻擊漏洞原理
DoS 攻擊可以使用戶在短時間內(nèi)或在某些狀態(tài)下造成智能合約永久無法正常執(zhí)行的情況。DoS 或者使合約程序代碼出現(xiàn)邏輯錯誤,或者耗盡Gas 無法執(zhí)行后續(xù)操作[12]。攻擊方式有很多,通常導(dǎo)致智能合約不能正常運行或者不能提供正常服務(wù)請求。如在fallback() 函數(shù)添加revert() 函數(shù),回滾所有狀態(tài)使得合約無法繼續(xù)執(zhí)行;惡意變更合約變量會耗盡Gas 而導(dǎo)致合約中止執(zhí)行等。此漏洞與外部調(diào)用密切相關(guān),為了杜絕這種情況,既要正確處理任何外部調(diào)用拋出的異常行為,又應(yīng)避免循環(huán)調(diào)用行為。
1.3.2 DoS 攻擊漏洞場景復(fù)現(xiàn)
在智能合約中,部分函數(shù)的執(zhí)行依賴于外部調(diào)用,并且對外部調(diào)用返回結(jié)果沒有進行正確處理。非預(yù)期的返回結(jié)果或無返回結(jié)果,都可能產(chǎn)生安全隱患。KotET 合約是King of the Ether 合約的簡化版,設(shè)立一個King,玩家可以將以太幣發(fā)送給競選合約來參與王位的競選。如果參與者E出價1 Ether 成為King,參與者F出價2 Ether,合約將1 Ether 成功還給E后,F(xiàn)將成為當(dāng)前King。似乎邏輯合理,若E的地址是合約地址,將調(diào)用E合約的fallback()函數(shù),以致產(chǎn)生不可預(yù)知的安全隱患。若攻擊者在其fallback() 函數(shù)中添加如revert() 函數(shù)的攻擊代碼,則智能合約中止運行而使攻擊者永久成為King。
將智能合約控制權(quán)交付給某一變量,極易造成控制權(quán)壟斷。當(dāng)控制權(quán)擁有者丟失密鑰或處于非活躍狀態(tài)時,智能合約處于癱瘓狀態(tài)。
以太坊中制定每個區(qū)塊所能花費Gas 的最大限額,若超出則交易不能成功。一次性向所有人轉(zhuǎn)賬,Gas 很可能到達上限,有DoS 攻擊的風(fēng)險。攻擊者通過外部干預(yù)合約數(shù)據(jù)結(jié)構(gòu)(數(shù)組、映射等)大小,達到耗盡Gas 而終止合約運行的目的。
1.3.3 預(yù)防DoS 攻擊漏洞的安全策略
在第1 種和第2 種場景下,引入時間機制可以預(yù)防DoS 攻擊。在一定時間內(nèi)允許合約繼續(xù)執(zhí)行,若超時或操作失敗,合約就執(zhí)行已經(jīng)規(guī)定的另一種執(zhí)行策略,而非中止合約運行。
在第3 種場景下,建議優(yōu)先使用pull 機制化被動為主動,由用戶自己主動向智能合約申請退款,而非重復(fù)循環(huán)地使用一個被外部干預(yù)的數(shù)據(jù)結(jié)構(gòu)。
1.4.1 時間戳依賴漏洞原理
礦工通常將本地系統(tǒng)時間設(shè)置為區(qū)塊時間戳,如果智能合約中設(shè)定了時間戳作為觸發(fā)條件去執(zhí)行一些重要操作(如轉(zhuǎn)賬等),就意味著礦工可以通過操縱時間戳來破壞合約公平性,以致產(chǎn)生時間戳依賴漏洞。礦工接收到一個新區(qū)塊并進行有效性檢查后將檢查當(dāng)前區(qū)塊時間戳是否大于前一區(qū)塊時間戳,且距本地系統(tǒng)時間戳不應(yīng)超過900 s。因此,攻擊者可以控制新區(qū)塊時間戳的產(chǎn)生,從而操縱存在時間戳依賴漏洞的智能合約。
1.4.2 時間戳依賴漏洞場景復(fù)現(xiàn)
TheRun 合約的主要功能是利用依賴時間戳產(chǎn)生的隨機數(shù)來選出中獎?wù)遊13]。根據(jù)當(dāng)前時間戳選擇一個區(qū)塊(代碼5~6 行),以所選區(qū)塊的哈希作為隨機種子來選擇勝者。
礦工可以為區(qū)塊時間戳設(shè)定一個有利的特定值來影響與時間戳相關(guān)的條件或參數(shù)。在TheRun 合約中,前一區(qū)塊的哈希和區(qū)塊號以及其他參數(shù)(如Last_Payout)均已知,礦工根據(jù)已知參數(shù)能預(yù)先計算并設(shè)定時間戳,這樣random() 函數(shù)可以產(chǎn)生一個有利于礦工的結(jié)果。攻擊者可以完全操縱隨機種子的結(jié)果,從而控制智能合約任意選擇誰是勝者。
1.4.3 預(yù)防時間戳依賴漏洞的安全策略
編寫智能合約時應(yīng)注意:區(qū)塊時間戳不可用于產(chǎn)生隨機數(shù),或者說它不應(yīng)作為游戲判定勝負或改變合約重要狀態(tài)的決定性因素。在對時效性要求高的情況下,建議使用區(qū)塊號和平均產(chǎn)生區(qū)塊時間來估算時間。由于礦工不易操縱區(qū)塊號,以區(qū)塊號作為合約狀態(tài)條件將更加安全。
1.5.1 Tx.Origin 依賴漏洞原理
Tx.origin 和msg.sender 均為Solidity 智能合約中的全局變量,tx.origin 是回溯整個調(diào)用棧并返回最初發(fā)生調(diào)用的賬戶地址,而msg.sender 是當(dāng)前發(fā)生調(diào)用的賬戶或合約地址。在智能合約使用tx.origin 變量進行身份校驗時容易遭到與網(wǎng)絡(luò)釣魚相似的攻擊。因為tx.origin 可以將合約的控制權(quán)授權(quán)給攻擊者,所以攻擊者能夠獲得對合約內(nèi)資金的完全訪問權(quán)[15]。
1.5.2 Tx.Origin 依賴漏洞場景復(fù)現(xiàn)
UserWallet 合約使用tx.origin 驗證調(diào)用者是否為合約擁有者。依照正常邏輯,只有擁有UserWallet 合約的賬戶才能成功完成轉(zhuǎn)賬;而UserWallet 合約使用tx.origin 驗證,就把合約權(quán)限授權(quán)給其他合約賬戶。
攻擊者部署AttackWallet 合約,同時說服或引誘UserWallet 合約的擁有者與之交易。當(dāng)用戶合約發(fā)送給攻擊者合約以太幣時,自動觸發(fā)AttackWallet 合約的fallback() 函數(shù),之后調(diào)用UserWallet 合約的transferTo() 函數(shù)。由于交易最初由UserWallet 合約的擁有者發(fā)起,UserWallet 合約中的第7 行代碼身份核驗通過,于是正常執(zhí)行轉(zhuǎn)賬操作,完成攻擊。
1.5.3 預(yù)防Tx.Origin 依賴漏洞的安全策略
Tx.origin 變量避免用于智能合約的所有權(quán)授權(quán)。為預(yù)防Tx.Origin 依賴漏洞,建議采用msg.sender==owner 進行身份核驗,也就是只有當(dāng)合約擁有者直接調(diào)用合約時核驗才能通過。若要拒絕外部合約調(diào)用當(dāng)前合約,可用代碼require(tx.origin==msg.sender)進行權(quán)限約束。
1.6.1 交易序列依賴漏洞原理
交易序列依賴性(transaction-ordering dependence,TOD)是指用戶無法確定交易的順序。TOD 也稱為競爭條件問題,即區(qū)塊中不同的交易順序會導(dǎo)致區(qū)塊鏈處于不同的交易狀態(tài)。一個區(qū)塊包含若干交易,交易產(chǎn)生的同時區(qū)塊鏈狀態(tài)也會不斷更新。假設(shè)區(qū)塊鏈處于狀態(tài)σ,且新區(qū)塊中含有兩筆調(diào)用同一智能合約的交易(如交易Ti和Tj),此時用戶在執(zhí)行各自調(diào)用時不能確定區(qū)塊鏈中被調(diào)用合約處于何種狀態(tài)。例如:交易Tj的產(chǎn)生可以使區(qū)塊鏈從狀態(tài)σ轉(zhuǎn)換到,而交易Ti的實施是從狀態(tài)σ[α] 還是狀態(tài)σ′[α] 開始,依賴于交易Ti和Tj的序列[13],這就導(dǎo)致用戶所期望的合約狀態(tài)與執(zhí)行時的實際合約狀態(tài)可能存在差異。因此,只有礦工才能決定這些交易在區(qū)塊中的序列,并決定某個區(qū)塊中智能合約的更新狀態(tài)。
1.6.2 交易序列依賴漏洞場景復(fù)現(xiàn)
Puzzle 合約的主要功能是獎勵解決難題的答題者。用戶可以發(fā)起解答難題的交易,合約所有者可以提交減少獎勵的交易。若兩類交易同時提出,很有可能所有者提交的交易首先被處理,導(dǎo)致合約所有者花費較小的代價得到這個難題的答案,而解題者的獎勵將會損失。
假設(shè)交易Ti是由合約所有者提交更新答題獎勵的交易,交易Tj是由用戶提交有效解決方案獲得獎勵的交易,并且交易Ti和Tj發(fā)送到Puzzle 合約的時間大致相同。因為交易Ti和Tj幾乎是在同一時刻廣播到區(qū)塊鏈網(wǎng)絡(luò)上,所以下一個新區(qū)塊很有可能包括交易Ti和Tj。兩筆交易的序列決定著用戶從Puzzle 合約中獲得多少解題獎勵。用戶期望在提交解決方案后獲得所觀察到的獎勵金額,但如果新區(qū)塊先打包交易Ti,他獲得的實際獎勵很可能少于期望獎勵。
在MarketPlace 合約中,合約所有者(賣方)可以調(diào)用updatePrice() 函數(shù)頻繁更新商品價格,買方可以調(diào)用合約的buy() 函數(shù)下訂單來采購商品。若賣方和買方同時發(fā)起交易,則根據(jù)交易序列的不同,買方的購買請求可能通過也可能不通過。若賣方提升價格在前,買方出價msg.value 低于更改后quant*price,導(dǎo)致買方采購商品失敗。還可能存在更嚴重的情況,當(dāng)購買者提交采購請求時,有可能支付的實際金額比預(yù)期支付價格高得多。
1.6.3 預(yù)防交易序列依賴漏洞的安全策略
用戶和礦工都可以針對這種存在TOD 漏洞的智能合約進行攻擊。易受用戶攻擊的智能合約與易受礦工攻擊的智能合約相比更加糟糕,因為礦工只有在打包新區(qū)塊時才能進行攻擊,而針對特定區(qū)塊的單個礦工是無法完成攻擊的。用戶可以提高交易的GasPrice 獲得礦工對交易的優(yōu)先排序,進而攻擊智能合約。針對這種漏洞的攻擊有下列預(yù)防措施。
在合約中設(shè)置GasPrice 上限,可以防止用戶以增加GasPrice 的方式獲得超出上限的優(yōu)先打包權(quán),但這種預(yù)防措施只能緩解用戶攻擊。此時礦工仍然可以針對合約發(fā)起攻擊,因為無論用戶增加多少GasPrice,礦工都可以根據(jù)自己需求改變區(qū)塊內(nèi)的交易序列。
Commit-reveal 方案要求用戶使用如哈希加密等隱藏加密信息發(fā)起交易。若區(qū)塊已包含此交易,則用戶只需在披露階段另發(fā)起一筆交易來解密已加密的數(shù)據(jù),即在提交和披露兩階段針對信息進行加密與解密,這種措施可預(yù)防礦工和用戶進行前瞻性計劃內(nèi)交易,因為礦工和用戶無法知曉交易具體內(nèi)容。
在智能合約的開發(fā)過程中,除了需要針對智能合約謹慎設(shè)計和編碼外,還應(yīng)利用智能合約安全漏洞分析檢測工具來驗證合約的可行性與安全性,從而在一定程度上提升和保障智能合約的安全性。
Oyente[13]是第1 個基于Ethereum 社區(qū)進行研究的安全分析工具,是一個符號執(zhí)行工具,它直接與EVM 字節(jié)碼一起工作,而不訪問高級語言。Oyente 利用符號執(zhí)行來發(fā)現(xiàn)潛在的安全漏洞,其中包括時間戳依賴、交易序列依賴、異常處理和可重入漏洞等。該工具可以分析Solidity 和合約的字節(jié)碼,能夠減少假陽性結(jié)果的可能性。Oyente 采用模塊化設(shè)計,其總體架構(gòu)如圖2所示,把智能合約的字節(jié)碼和當(dāng)前Ethereum 的全局狀態(tài)作為輸入,經(jīng)過分析檢測后回應(yīng)合約是否存在安全漏洞,并向用戶輸出疑似安全漏洞的符號路經(jīng)。
圖2 Oyente 的總體架構(gòu)Figure 2 Overview architecture of Oyente
Securify[16]是一個自動化、可擴展的合約漏洞安全分析工具,雖然使用形式驗證,但也依賴于靜態(tài)分析檢查。Securify 首先將EVM 字節(jié)碼反編譯為靜態(tài)單一分配形式(static-single assignmentform,SSA)的無堆棧表示;反編譯后根據(jù)數(shù)據(jù)流和控制流的依賴關(guān)系推斷語義事實;獲得語義事實后檢查一組合規(guī)性和違反性的安全模式,以捕獲足夠的條件來證實安全屬性是否成立;Securify 若匹配到違反模式,就能標注含有漏洞指令的位置。Securify 針對合約檢測的主要執(zhí)行過程如圖3所示。它涉及的安全問題包括交易序列依賴、遞歸調(diào)用、不安全編碼模式、意外以太流和不可信輸入的使用等漏洞。Securify 除了分析字節(jié)碼和Solidity 外,還可以根據(jù)以太坊上的合約地址分析智能合約安全性。
圖3 Securify 的執(zhí)行流程Figure 3 Executing process of Securify
SmartCheck[12]是一個基于Web 的安全代碼分析工具。它會自動檢測智能合約的漏洞和不良做法,高亮顯示漏洞代碼,給出漏洞的解釋以及避免特定安全問題的可行性解決方案。SmartCheck 只分析Solidity 代碼,可以發(fā)現(xiàn)的一些嚴重漏洞包括DoS 攻擊、資產(chǎn)凍結(jié)、可重入、時間戳依賴、tx.origin 依賴和未檢查的外部調(diào)用等。此外,SmartCheck 還識別了許多嚴重程度較低的其他漏洞,比如編譯器版本不固定、未使用可見性修飾語(public、internal和external 等)、違反樣式指南和冗余函數(shù)等發(fā)出的警告。
Remix 是一個基于Web 的IDE,它允許Solidity 智能合約的編寫、部署和運行[17],集成了調(diào)試器和測試環(huán)境如測試網(wǎng)絡(luò)Ropsten、Rinkeby 等。Remix 也是一個合約漏洞安全檢測工具,通過分析Solidity 的代碼來減少編碼錯誤并識別合約潛在的漏洞,其安全分析原理依賴于形式驗證(演繹程序驗證、定理驗證)。它識別的一些漏洞包括tx.origin 依賴、時間戳依賴、區(qū)塊哈希使用、Gas 成本模式、可重入等漏洞。
Beosin-VaaS 是由中國成都鏈安研發(fā)的一個智能合約自動形式化驗證平臺,也是Beosin一站式區(qū)塊鏈安全服務(wù)平臺[18]的核心一環(huán),為區(qū)塊鏈企業(yè)提供智能合約的安全審計、虛擬資產(chǎn)追溯、隱私保護、安全咨詢等全方位的安全服務(wù)。VaaS 平臺采用了形式化驗證方法,有效檢測智能合約的常規(guī)安全漏洞和功能邏輯缺陷,從而顯著提高了智能合約的安全等級。檢測范圍包括智能合約的代碼規(guī)范檢測、函數(shù)調(diào)用檢測、業(yè)務(wù)邏輯安全檢測、溢出檢測和異常可達狀態(tài)檢測等。Beosin-VaaS 具有可定制化和可移植性等特點,不僅支持Ethereum、Fabric 等主流鏈平臺,還支持在EVM 環(huán)境下的其他智能合約公鏈和聯(lián)盟鏈平臺。Beosin-VaaS 擁有在線版和離線插件版,離線版的整個合約檢測過程均在本地執(zhí)行,消除了用戶對源碼泄漏問題的顧慮。
本文針對以上智能合約的預(yù)防安全策略進行了有效的實驗驗證。在預(yù)防可重入漏洞的安全策略中,提出了轉(zhuǎn)賬時使用send()或transfer()函數(shù)來避免可重入攻擊;若使用call.value()函數(shù),應(yīng)使用checks-effects-interactions 模式。就未使用預(yù)防策略情況下對智能合約發(fā)起的模擬攻擊而言,本文的攻擊合約成功執(zhí)行了6 次Withdraw() 函數(shù),造成被攻擊合約賬戶丟失的以太幣如下:
本文分別采用上述3 種預(yù)防策略部署智能合約,攻擊合約每次只成功執(zhí)行了1 次Withdraw() 函數(shù),并未多次遞歸調(diào)用Withdraw() 函數(shù),也未造成合約賬戶資產(chǎn)損失,表明所提出的安全策略能有效阻止可重入漏洞的發(fā)生。同時本文還使用SmartCheck、Remix 以及Beosin-VaaS 安全檢測工具來驗證已采用安全策略的智能合約是否檢測出可重入漏洞,如表1所示。
分析表1可知,若使用send() 和transfer() 的智能合約,則3 種檢測工具均顯示智能合約無可重入漏洞。SmartCheck 和Beosin-VaaS 未能識別checks-effects-interactions 模式,顯示使用了低級調(diào)用call() 函數(shù),合約可能存在可重入漏洞。本文通過模擬攻擊實驗證明了checks-effects-interactions 模式可以有效預(yù)防可重入性漏洞。
表1 3 種工具檢測可重入漏洞的預(yù)防策略Table 1 Three tools to detect prevention strategy of reentrancy vulnerability
在預(yù)防整數(shù)溢出漏洞的安全策略中,本文以邏輯判斷取代算術(shù)運算操作,就能有效避免因算術(shù)運算造成的整數(shù)溢出;若使用算術(shù)運算,應(yīng)在其邏輯前后進行驗證。若合約執(zhí)行加法運算,則需驗證和是否大于任意一個加數(shù),add() 函數(shù)如下:
選擇和大于2256的兩個整數(shù)來驗證安全策略,當(dāng)執(zhí)行到合約中add 函數(shù)時,發(fā)生整數(shù)溢出現(xiàn)象將中止執(zhí)行,具體情況如下:
call of test.add errored:VM error:revert.
The transaction has been reverted to the initial state.
Reason provided by the contract:“Addition overflow”.
可見上述安全策略將有效阻止整數(shù)溢出造成的嚴重損失。
智能合約是自執(zhí)行和自驗證的代理,一旦部署到區(qū)塊鏈上便不能更改。到目前為止,Ethereum 智能合約安全漏洞造成了巨大的經(jīng)濟損失,學(xué)術(shù)界和業(yè)界已將大量注意力轉(zhuǎn)向了智能合約安全性的研究。因此,迫切需要更全面的方法來確保智能合約的安全性和正確性,從而減少合約漏洞造成的損失。本文主要分析了Ethereum 智能合約幾種常見的安全漏洞,對漏洞產(chǎn)生原理和漏洞場景再現(xiàn)兩方面進行詳細介紹,并提出了合約漏洞的預(yù)防策略,最后介紹了幾種用于檢測這些漏洞的合約安全分析檢測工具。優(yōu)化智能合約檢測工具及算法在未來依然是重要的研究方向,這些工作將有助于形成一個更加安全可靠的以太坊智能合約環(huán)境。