印桂生, 高樂, 莊園, 李俊
(1.哈爾濱工程大學 計算機科學與技術(shù)學院,黑龍江 哈爾濱 150001;2.國家工業(yè)信息安全發(fā)展研究中心, 北京 100040)
自2015年全球性去中心化應(yīng)用平臺以太坊[1]成立,以智能合約為基礎(chǔ)的去中心化應(yīng)用高速發(fā)展。智能合約被定義為一組具有特定規(guī)則的數(shù)字化協(xié)議[2],是運行在區(qū)塊鏈網(wǎng)絡(luò)上的應(yīng)用程序,其主要編寫語言為Solidity,被編譯后以字節(jié)碼形式在以太坊虛擬機(ethereum virtual machine,EVM)[3]中存儲執(zhí)行。隨著區(qū)塊鏈應(yīng)用的廣泛普及,智能合約被發(fā)現(xiàn)存在多種漏洞,導(dǎo)致了多起因合約漏洞被攻擊的事件,造成數(shù)千萬美元的損失[4]。例如2016年的去中心化自治組織(decentralized autonomous organization,DAO)[5]安全漏洞事件,區(qū)塊鏈業(yè)界眾籌項目The DAO被攻擊,黑客利用合約代碼中的可重入漏洞盜取資金池中的資產(chǎn),導(dǎo)致360萬以太幣流失,市值6 000萬美元。2017年,黑客利用Parity[6]多重簽名錢包合約中的委托調(diào)用漏洞,獲取錢包地址的所有權(quán)并轉(zhuǎn)移內(nèi)部資產(chǎn),導(dǎo)致價值上億美元資金被凍結(jié)。案例表明,智能合約若自身存在漏洞隱患,會嚴重威脅用戶信息和財產(chǎn)安全,甚至造成難以預(yù)估的損失。面對智能合約存在的安全問題,研究高效的合約漏洞檢測方法具有重要意義。
目前,智能合約漏洞檢測的主要方法有形式化驗證、符號執(zhí)行、模糊測試、污點分析等[7]。形式化驗證是通過數(shù)學推理邏輯和證明,檢查代碼功能正確性和屬性的安全性[8],保證一定范圍內(nèi)的絕對正確,但需要人工參與建模和推理過程,效率較低。符號執(zhí)行的核心思想是使用符號值代替具體的執(zhí)行程序[9],此方法能夠減少測試用例集達實現(xiàn)高覆蓋率,但也會出現(xiàn)路徑爆炸的情況。模糊測試是一種通過構(gòu)造非預(yù)期的輸入數(shù)據(jù)并監(jiān)視程序運行異常結(jié)果的軟件故障識別方法[10],其優(yōu)點在于測試速度快,消耗低,缺點在于所能涵蓋的系統(tǒng)行為有限,無法達到理想的路徑覆蓋率。這些檢測方法各有優(yōu)勢,但仍存在如下問題:1)目前近30%的智能合約沒有源碼[11], 而大部分智能合約漏洞檢測工具只支持合約源碼的檢測,面對沒有源碼或只有字節(jié)碼的合約無法實現(xiàn)漏洞檢測;2)現(xiàn)有靜態(tài)檢測[12]方法只針對單一函數(shù)調(diào)用行為進行建模,沒有構(gòu)建和分析合約的整體執(zhí)行流程;3)目前檢測方法多依賴于有限的專家規(guī)則,漏洞定義相對簡單,導(dǎo)致檢測結(jié)果的真陽性比例較低[13]。
針對上述問題,本文工作引入關(guān)鍵指令概念,提出一種基于關(guān)鍵路徑的漏洞檢測方法。該方法面向智能合約的二進制代碼,為不同漏洞定義關(guān)鍵指令及其檢測規(guī)則,通過構(gòu)建合約控制流程圖,生成基于關(guān)鍵指令的執(zhí)行路徑,采用規(guī)則匹配模型完成漏洞識別,有效地提高了智能合約安全檢測的準確度。
隨著智能合約的廣泛應(yīng)用,關(guān)于智能合約安全問題的研究也日益重要。NCC Group[14]總結(jié)出10種出現(xiàn)頻率較高的智能合約安全漏洞,分別為: 可重入、委托調(diào)用、時間戳依賴、Gas耗盡終止、訪問控制、未嚴格判斷不安全函數(shù)調(diào)用返回值、拒絕服務(wù)、可預(yù)測的隨機處理、短地址攻擊以及整數(shù)溢出。本文主要針對可重入、委托調(diào)用、時間戳依賴這3種最高頻的漏洞類型進行分析,將與其漏洞特征相關(guān)的EVM底層指令定義為關(guān)鍵指令,并根據(jù)不同漏洞的關(guān)鍵指令提出相應(yīng)的檢測規(guī)則。
智能合約的重要特點是調(diào)用外部合約函數(shù),通過外部調(diào)用,合約完成轉(zhuǎn)賬(即發(fā)送以太幣給外部賬戶)。若外部調(diào)用操作不當,極其容易被攻擊者利用,通過回退函數(shù)或者回調(diào)攻擊合約自身來盜取以太幣,從而造成用戶的損失[15]?;赝撕瘮?shù)沒有參數(shù)和返回值,在合約的調(diào)用中,如果沒有其他函數(shù)與給定的函數(shù)標識符匹配,那么回退函數(shù)就會執(zhí)行。此外,每當合約收到以太幣但沒有任何附帶數(shù)據(jù)時,回退函數(shù)也會執(zhí)行。攻擊者可通過編寫攻擊合約,調(diào)用受害合約,利用攻擊合約的回退函數(shù),循環(huán)調(diào)用受害合約的代碼??芍厝肼┒垂敉峭獠空{(diào)用被攻擊者劫持,迫使合約進一步執(zhí)行代碼,通過回退函數(shù)再次調(diào)用回退函數(shù)本身,最終實現(xiàn)單次或多次重入攻擊。DAO事件就是這類攻擊的典型例子。
可重入漏洞特點:在Solidity中一般有3種轉(zhuǎn)賬方法,分別是transfer()、send()、call()。由于以太坊執(zhí)行交易需要收取一定數(shù)量的費用(簡稱Gas[16]),當合約調(diào)用call()函數(shù)進行轉(zhuǎn)賬時,所有可用Gas會被傳遞,這使得攻擊者能夠多次回調(diào)受害合約。當某函數(shù)通過一系列調(diào)用調(diào)回自身時,此時可能發(fā)生可重入漏洞。針對可重入漏洞,本文定義關(guān)鍵指令如表1所示。
表1 可重入漏洞關(guān)鍵指令Table 1 Key instructions for reentrancy
根據(jù)上述關(guān)鍵指令,可重入漏洞的檢測規(guī)則具體定義為:
1)對于一個函數(shù)A,檢查函數(shù)調(diào)用A是否在源自調(diào)用A的調(diào)用鏈中出現(xiàn)了不止一次。即檢查EVM底層的CALL指令調(diào)用鏈;
2)檢查函數(shù)中存在call()調(diào)用且滿足value>0且Gas足夠多。即檢查CALL指令的第1個堆棧參數(shù)Gas以及第3個堆棧參數(shù)value;
3)資產(chǎn)記錄的改變,在實際轉(zhuǎn)賬后。即檢查算數(shù)邏輯指令與CALL指令出現(xiàn)的先后順序。
Solidity中有2個常用的內(nèi)置變量msg.sender和msg.data,前者表示合約調(diào)用者的地址,后者表示調(diào)用者傳入的數(shù)據(jù)。此外,Solidity中調(diào)用其他合約的方法,除了call()外,通過委托調(diào)用delegatecall()方法也可以實現(xiàn)智能合約之間的交互。與call()調(diào)用不同,delegatecall調(diào)用會修改調(diào)用者的存儲,且在其調(diào)用后msg.sender的值一直為原調(diào)用者的地址[17]。攻擊者通過自身合約的上下文環(huán)境調(diào)用其他合約的代碼,當delegatecall的參數(shù)設(shè)置為msg.data時,攻擊者一般通過構(gòu)造msg.data,實現(xiàn)調(diào)用受害合約的任何函數(shù)。正是由于這種委托漏洞,Parity合約損失了價值3 000萬美元的以太幣。
委托調(diào)用漏洞特點:當合約中存在委托調(diào)用,且委托調(diào)用的調(diào)用地址和調(diào)用字符序列由調(diào)用者傳入時(例如delegatecall的參數(shù)為msg.data),就會產(chǎn)生委托調(diào)用漏洞。針對委托調(diào)用漏洞,本文定義關(guān)鍵指令,如表2所示。
表2 委托調(diào)用漏洞關(guān)鍵指令Table 2 Key instructions for delegatecall
根據(jù)上述關(guān)鍵指令,委托調(diào)用漏洞的檢測規(guī)則具體定義如下:
1)檢查在當前合約的執(zhí)行過程中是否存delegatecall()調(diào)用,即檢查是否存在DELEGATECALL和SELFDESTRUCT指令;
2)檢查delegatecall()的調(diào)用地用的字符序列是否由調(diào)用者傳入,即檢查DELEGATECALL指令的參數(shù)中是否存在CALLDATALOAD以及CALLVALUE。
時間戳是一個唯一標識某一刻的時間字符序列[18]。以太坊中,區(qū)塊時間戳有很多用途,例如生成隨機數(shù)或用于條件語句中作為時間變量的判斷條件。然而,當調(diào)用轉(zhuǎn)賬函數(shù)且函數(shù)中條件判斷中存在時間戳引用時,礦工通過修改時間戳,可以產(chǎn)生特定需求的隨機數(shù),并且可通過對時間戳的控制,滿足有利于自身的條件,進而損害其他用戶的利益。
時間戳依賴漏洞特點:當調(diào)用轉(zhuǎn)賬函數(shù)時(transfer()、send()、call()),合約中的時間戳引用極易被惡意礦工所利用。為此,針對委托調(diào)用漏洞,本文定義關(guān)鍵指令如表3所示:
表3 時間戳依賴漏洞關(guān)鍵指令Table 3 Key instructions for timestamp
根據(jù)上述關(guān)鍵指令,時間戳依賴漏洞的檢測規(guī)則具體定義為:
1)檢查合約或函數(shù)中是否有block.number、now、block.timestamp等時間戳操作,即是否存在TIMESTAMP以及NUMBER指令;
2)檢查函數(shù)是否調(diào)用了send()或transfer()。即檢查是否存在CALL指令,且GAS≤2 300。
3)檢查函數(shù)存在call()調(diào)用且value>0。即檢查是否存在CALL指令,且第3個參數(shù)value>0。
目前,現(xiàn)有漏洞檢測方法主要面向智能合約的源代碼,僅有少數(shù)工作支持二進制代碼的安全檢測[19],這些方法沒有深入挖掘漏洞與EVM底層指令間的關(guān)系,導(dǎo)致真陽性比例較低。為解決此問題,本文提出一種基于關(guān)鍵路徑的智能合約漏洞檢測方法,根據(jù)漏洞特征為不同漏洞定義相應(yīng)的關(guān)鍵指令,生成可用于規(guī)則匹配的關(guān)鍵路徑,實現(xiàn)對智能合約字節(jié)碼高效的漏洞檢測。具體方案流程如圖1所示。首先,將合約二進制代碼反編譯生成合約控制流圖(control flow graph,CFG);其次,根據(jù)關(guān)鍵指令生成關(guān)鍵執(zhí)行路徑;最后,采用匹配規(guī)則方法對關(guān)鍵路徑進行漏洞判斷。
圖1 工作流程Fig.1 The overall workflow graph
為了更好地表示智能合約的二進制代碼,采用由數(shù)據(jù)依賴關(guān)系和控制關(guān)系組成的控制流程圖對其進行表示。構(gòu)建CFG首先反編譯合約的字節(jié)碼,生成EVM指令及參數(shù),EVM中常用指令如表4所示。
表4 EVM常用指令舉例Table 4 Examples of common instructions
CFG是由指令及其參數(shù)構(gòu)成的基礎(chǔ)塊組成,其中每個基礎(chǔ)塊以非跳轉(zhuǎn)指令開頭,以跳轉(zhuǎn)或終止指令(如STOP、JUMP、JUMPI、RETURN、REVERT、SELFDESTRUCT等)作為結(jié)束。合約二進制代碼先反編譯生成基礎(chǔ)塊,再根據(jù)各個基礎(chǔ)塊的跳轉(zhuǎn)關(guān)系,即解析基礎(chǔ)塊中的跳轉(zhuǎn)指令(JUMP和JUMPI)指令,將基礎(chǔ)塊連接起來,形成目標合約的控制流程圖。
如圖2所示,構(gòu)建CFG首先找到基礎(chǔ)塊間明顯的跳轉(zhuǎn)關(guān)系,例如,在基礎(chǔ)塊162和基礎(chǔ)塊694中找到2組跳轉(zhuǎn)指令PUSH2 0x2f2和JUMP,表示跳轉(zhuǎn)到地址0x2f2,即基礎(chǔ)塊754,它將基礎(chǔ)塊162、基礎(chǔ)塊694、基礎(chǔ)塊754構(gòu)成一個CFG子圖。同時,尚未計算的跳轉(zhuǎn)指令(JUMP和JUMPI)被標記為未解析狀態(tài)。其次,選擇一個CFG子圖中未解析的跳轉(zhuǎn)指令,推斷其跳轉(zhuǎn)目標的逆向指令集,執(zhí)行該指令以計算跳轉(zhuǎn)目標,并將該指令標記為已解析狀態(tài),最后添加到CFG中。由于新引入的跳轉(zhuǎn)關(guān)系可能導(dǎo)致構(gòu)建的CFG子圖出現(xiàn)新的跳轉(zhuǎn)指令,所以該子圖中跳轉(zhuǎn)指令都需再次標記為未解析狀態(tài),重復(fù)此過程,直到所有跳轉(zhuǎn)指令都被標記為已解析狀態(tài)。
圖2 CFG構(gòu)建Fig.2 The CFG construction phase
如圖2所示,基礎(chǔ)塊162、基礎(chǔ)塊694、基礎(chǔ)塊754構(gòu)成一個CFG子圖,當執(zhí)行到基礎(chǔ)塊754中最后一行的JUMP指令,出現(xiàn)2個逆向指令,分別為基礎(chǔ)塊694和基礎(chǔ)塊162中的2個PUSH2指令,說明此時基礎(chǔ)塊754引入2個新的跳轉(zhuǎn)關(guān)系:基礎(chǔ)塊754→基礎(chǔ)塊1435、基礎(chǔ)塊754→基礎(chǔ)塊1456,最終形成新控制流程圖。
對智能合約而言,其漏洞特點可以通過EVM關(guān)鍵指令體現(xiàn),如果攻擊者利用漏洞進行合約攻擊,那么會執(zhí)行一條或多條含有關(guān)鍵指令的路徑來完成。將關(guān)鍵路徑定義為包含關(guān)鍵指令的執(zhí)行路徑,若關(guān)鍵路徑與給定的某種漏洞規(guī)則匹配,則表明存在此漏洞風險。
對CFG的路徑探索是典型的靜態(tài)路網(wǎng)中求解路徑的問題,本文采用A*算法探索路徑[20],其中路徑代價定義為該路徑在CFG中遍歷的分支數(shù)?;舅悸肥菑拿款惵┒吹年P(guān)鍵指令集選擇一個指令,利用A*算法進行路徑探索,每一步執(zhí)行后,檢查是否仍然可以從當前路徑訪問關(guān)鍵指令集中至少一個其他剩余的指令,如果無法訪問,則放棄對該路徑的進一步探索,路徑生成示意圖如圖3所示。
圖3 路徑生成示意Fig.3 The critical-path generation phase
給定關(guān)鍵路徑后,即生成初步執(zhí)行路徑集合,將每條路徑進行規(guī)則匹配。此過程需要解析路徑中每個指令及其參數(shù),通過匹配相應(yīng)的漏洞規(guī)則,給出最終檢測結(jié)果。
1)可重入漏洞規(guī)則匹配。
依據(jù)可重入漏洞檢測規(guī)則,若要對初步生成的路徑進行規(guī)則匹配,需要解析路徑中的CALL以及算數(shù)運算指令。其次,檢查路徑中CALL指令調(diào)用鏈,并檢查CALL指令的第1個堆棧參數(shù)Gas是否大于2 300以及第3個堆棧參數(shù)value是否大于0。同時,檢查算數(shù)邏輯指令是否出現(xiàn)在CALL指令之后。若滿足以上規(guī)則,則提取該路徑作為關(guān)鍵路徑。
2)委托調(diào)用漏洞規(guī)則匹配。
對委托調(diào)用漏洞進行規(guī)則匹配,需要解析路徑中的DELEGATECALL等指令,即檢查路徑中是否存在DELEGATECALL指令且DELEGATECALL指令的參數(shù)中是否存在CALLDATALOAD以及CALLVALUE讀取的數(shù)據(jù)。若滿足以上規(guī)則,則提取該路徑作為關(guān)鍵路徑。
3)時間戳依賴漏洞規(guī)則匹配。
對時間戳依賴漏洞進行規(guī)則匹配,需要解析路徑中的TIMESTAMP等指令。即檢查路徑中是否存在TIMESTAMP以及NUMBER指令;同時檢查檢查是否存在CALL指令,且GAS≤2 300。最后檢查是否存在CALL指令,且第3個參數(shù)value>0。若滿足以上規(guī)則,則提取該路徑作為關(guān)鍵路徑。
對漏洞進行規(guī)則匹配需要解析指令及參數(shù),即對EVM指令進行符號化建模,完成EVM指令到符號表達式的轉(zhuǎn)換。EVM中有2類指令,一類是數(shù)據(jù)長度是固定值的指令,例如CALL等;另一類是長度可變的指令,例如CALLDATACOPY等。使用Z3[21]約束求解器對指令進行建模,具體過程如下:
1)對固定數(shù)據(jù)長度的指令建模,通過Z3的位向量為:
α′m[retOffset+i]←BitVector('instruction_name+i',8)
(1)
式中:αm表示內(nèi)存存儲;BitVector即Z3的位向量表達式;retOffset為指令返回數(shù)據(jù)的內(nèi)存地址;instruction_name是指令的名稱;i為數(shù)據(jù)長度,從0一直循環(huán)到需讀取的數(shù)據(jù)的總長度。每一次循環(huán),從內(nèi)存中讀取固定8 bit的數(shù)據(jù),直到循環(huán)結(jié)束。
2)對可變數(shù)據(jù)長度指令建模,通過Z3的If表達式來表示:
α′m[destoffset+i]←If(i i],αm[destoffset+i]) (2) 式中:EI為當前指令執(zhí)行的符號環(huán)境;destOffset、offset、length為可變長指令的數(shù)據(jù)地址,源數(shù)據(jù)地址,數(shù)據(jù)長度。If即Z3的If表達式,i為復(fù)制數(shù)據(jù)的長度,從0循環(huán)到length。 綜上,本文提出了基于關(guān)鍵路徑的智能合約漏洞檢測方法,在僅給定字節(jié)碼的情況下檢測合約是否存在可重入漏洞、委托調(diào)用漏洞以及時間戳依賴漏洞,依據(jù)漏洞特點為不同類別的漏洞定義關(guān)鍵指令及規(guī)則,構(gòu)建CFG分析合約執(zhí)行路徑,通過規(guī)則匹配完成漏洞檢測。 本文使用PC作為實驗環(huán)境,使用python語言實現(xiàn)提出的方法,具體操作系統(tǒng)及軟件配置見表5。從以太坊網(wǎng)站上獲取8 000份真實智能合約及其字節(jié)碼進行驗證,實驗結(jié)果表明本文方法具有高效的漏洞檢測能力。如表6所示,在294個真實漏洞中成功檢測到244個漏洞,包括15個可重入漏洞,9個委托調(diào)用漏洞以及220個時間戳依賴漏洞。 表5 實驗環(huán)境Table 5 Experimental environment configuration 表6 漏洞檢測結(jié)果Table 6 Vulnerability detection results 1)可重入漏洞。 在可重入漏洞方面,本文方法充分挖掘了可重入漏洞的底層特點,在8 000個合約中成功檢測到15個可重入漏洞(共有16個可重入漏洞),人工檢測證明僅有1個假陰性結(jié)果(即1份可重入漏洞合約沒有檢測到),檢測準確率高達93.75%。 2)委托調(diào)用漏洞。 在委托調(diào)用漏洞方面,本文方法成功檢測到9個漏洞(共有11個漏洞),其中8個真陽性結(jié)果和1個假陽性結(jié)果。此外,出現(xiàn)3個假陰性結(jié)果,檢測準確率為72.72%。存在假陰性結(jié)果是因為在委托調(diào)用過程中,有些合約的調(diào)用鏈過長,本文方法沒有捕捉到這種過長的調(diào)用關(guān)系。 3)時間戳依賴漏洞。 在時間戳依賴漏洞方面,本文方法成功檢測到220個漏洞(共有267個漏洞),其中218個真陽性結(jié)果和2個假陽性結(jié)果,檢測準確率為81.65%。此外,出現(xiàn)49個假陰性結(jié)果,原因在于本文定義時間戳依賴漏洞的檢測規(guī)則時,沒能充分考慮與時間戳有關(guān)數(shù)據(jù)調(diào)用鏈,會在后續(xù)工作中進一步改進。 最后,與2種現(xiàn)有方法進行比較:模糊測試方法ContractFuzzer[22]和靜態(tài)符號執(zhí)行方法Oyente[23],本文方法整體表現(xiàn)更優(yōu),如表7所示,由于Oyente不支持對委托調(diào)用漏洞的檢測,所以相應(yīng)結(jié)果用“—”表示。 表7 與ContractFuzzer和Oyente對比結(jié)果Table 7 Comparison with ContractFuzzer and Oyente 由表7可知,針對可重入漏洞和時間戳依賴漏洞,本文方法的準確度明顯高于對比方法,原因在于這兩類漏洞的特點比較明顯,且調(diào)用鏈數(shù)量小,相對于ContractFuzzer與Oyente定義的復(fù)雜檢測規(guī)則,基于關(guān)鍵路徑的檢測方法具備更高效的漏洞定位能力。 1)定義與漏洞相關(guān)的關(guān)鍵指令及檢測規(guī)則,通過反編譯字節(jié)碼構(gòu)建控制流圖,基于符號表達式建模,對關(guān)鍵執(zhí)行路徑進行規(guī)則匹配并完成漏洞檢測。 2)本文采用以太坊上真實智能合約數(shù)據(jù)進行實驗,結(jié)果表明本文方法優(yōu)于智能合約現(xiàn)有漏洞檢測工具,準確度提升近10%。 未來的研究主要考慮結(jié)合動靜態(tài)方法的漏洞檢測技術(shù),例如形式化驗證與模糊匹配方法相結(jié)合,亦可使用機器學習或深度學習實現(xiàn)智能合約的漏洞檢測。3 實驗結(jié)果
4 結(jié)論