王 沖,孫 毅,仵 俊
(南京航空航天大學(xué) 計(jì)算機(jī)科學(xué)與技術(shù)學(xué)院,江蘇 南京 211100)
C語(yǔ)言常用于諸如操作系統(tǒng)、嵌入式軟件系統(tǒng)等對(duì)性能要求較高的系統(tǒng)的編寫。然而C語(yǔ)言本身缺乏對(duì)內(nèi)存安全性檢測(cè)的相關(guān)功能,因此使用其編寫的程序可能存在較為嚴(yán)重的內(nèi)存安全性漏洞[1-5]。動(dòng)態(tài)分析[6-8]是目前常用的對(duì)程序進(jìn)行內(nèi)存安全檢測(cè)的方法,目前常用的實(shí)現(xiàn)方法有二進(jìn)制代碼插樁、中間代碼插樁、源代碼插樁[9-10]等。
二進(jìn)制代碼插樁是對(duì)可執(zhí)行程序進(jìn)行插樁,優(yōu)點(diǎn)是不需要源代碼就可以對(duì)程序進(jìn)行動(dòng)態(tài)分析;中間代碼插樁是對(duì)編譯后的代碼插樁,可以利用優(yōu)化,減少不必要的插樁;源代碼插樁是指對(duì)源碼上進(jìn)行修改,添加行為監(jiān)代碼,對(duì)程序進(jìn)行檢測(cè),優(yōu)點(diǎn)是可以獲取源代碼中的位置,準(zhǔn)確地報(bào)告錯(cuò)誤信息。
為了能更準(zhǔn)確有效地檢測(cè)程序的錯(cuò)誤并能將錯(cuò)誤的變量信息準(zhǔn)確地反饋給用戶,該文采用了源代碼插樁技術(shù)進(jìn)行插樁,并在基于指針技術(shù)[11-13]的基礎(chǔ)上,借助開(kāi)源編譯器Clang和C++語(yǔ)言實(shí)現(xiàn)了內(nèi)存安全分析工具M(jìn)ovec,完成其對(duì)大規(guī)模C程序的內(nèi)存安全性檢測(cè),并通過(guò)實(shí)驗(yàn)進(jìn)行了驗(yàn)證,表明該內(nèi)存分析工具對(duì)大規(guī)模程序的內(nèi)存檢測(cè)是有效且高效的。
基于指針的內(nèi)存安全性檢測(cè)技術(shù)的主要思想是對(duì)程序中的所有指針變量構(gòu)造一個(gè)指針元數(shù)據(jù),用來(lái)記錄該指針的內(nèi)存狀態(tài)、上下界以及指向當(dāng)前內(nèi)存的指針的個(gè)數(shù)。然后,當(dāng)指針賦值或者以函數(shù)參數(shù)傳遞的時(shí)候,更新這個(gè)指針的元數(shù)據(jù),用來(lái)保持?jǐn)?shù)據(jù)的一致性。最后,在對(duì)指針進(jìn)行解引用或者通過(guò)指針對(duì)內(nèi)存進(jìn)行讀寫時(shí),根據(jù)指針元數(shù)據(jù)中記錄的內(nèi)存狀態(tài)信息,來(lái)判斷該次內(nèi)存訪問(wèn)是否是合法的,從而檢測(cè)出內(nèi)存的安全性。
采用源代碼插樁實(shí)現(xiàn)基于指針內(nèi)存安全性檢測(cè)的過(guò)程分為三個(gè)部分:一是對(duì)指針變量定義進(jìn)行插樁以初始化元數(shù)據(jù),對(duì)指針變量賦值進(jìn)行插樁來(lái)更新元數(shù)據(jù)的信息;二是在指針解引用的時(shí)候來(lái)檢查該指針?biāo)玫膶?duì)象的元數(shù)據(jù);三是對(duì)函數(shù)定義進(jìn)行插樁以初始化函數(shù)參數(shù)的元數(shù)據(jù)、計(jì)算存儲(chǔ)返回值的元數(shù)據(jù)。然后,對(duì)函數(shù)定義生成一個(gè)包裝函數(shù),該包裝函數(shù)用來(lái)對(duì)程序檢測(cè)并傳遞指針元數(shù)據(jù)。接著,對(duì)原函數(shù)調(diào)用重命名,并插入元數(shù)據(jù),然后將原函數(shù)調(diào)用重定向到其包裝函數(shù)來(lái)完成檢測(cè)。
目前的源代碼插樁工具對(duì)程序的插樁一般有兩種模式,一種是單文件插樁模式,一種是項(xiàng)目插樁模式。單文件插樁模式適用于一些文件數(shù)量比較少的情況。對(duì)于項(xiàng)目插樁模式,目前常采用的方法是使用搜索后綴的方法將文件中所有的.c和.h文件進(jìn)行搜索,然后將所有的文件添加到插樁列表中,把每個(gè).c和.h文件都當(dāng)成一個(gè)翻譯單元進(jìn)行解析插樁,插樁完成后將新的文件生成到目標(biāo)文件夾中。這種方法在對(duì)大規(guī)模的程序進(jìn)行插樁時(shí)過(guò)于簡(jiǎn)單,會(huì)導(dǎo)致如下問(wèn)題:一對(duì)每個(gè).c和.h文件進(jìn)行搜索,會(huì)將一些不必要的文件進(jìn)行搜索并插樁,增加了項(xiàng)目插樁時(shí)間;二是當(dāng)文件編譯命令中使用了-D定義了宏或者使用-I頭文件目錄時(shí),這種項(xiàng)目插樁的方式獲取到的語(yǔ)法樹(shù)會(huì)和原語(yǔ)法樹(shù)完全不同,導(dǎo)致插樁錯(cuò)誤;三是當(dāng)項(xiàng)目中的頭文件出現(xiàn)一個(gè)不完整文件時(shí),將該文件當(dāng)作一個(gè)完整的翻譯單元處理時(shí),無(wú)法獲取其完整的語(yǔ)法樹(shù),導(dǎo)致程序插樁失敗。
對(duì)于問(wèn)題一和問(wèn)題二,該文利用編譯數(shù)據(jù)庫(kù)的概念,對(duì)源代碼插樁工具的項(xiàng)目插樁模式進(jìn)行改進(jìn)。編譯數(shù)據(jù)庫(kù)是在項(xiàng)目實(shí)際編譯過(guò)程中對(duì)編譯器調(diào)用的監(jiān)控記錄,其中包含了每個(gè)文件在編譯時(shí)的編譯選項(xiàng)。利用編譯數(shù)據(jù)庫(kù)獲取待插樁文件的存儲(chǔ)路徑和該文件對(duì)應(yīng)的編譯指令,構(gòu)造出每個(gè)文件的原始編譯命令,從而在對(duì)文件進(jìn)行解析時(shí)獲取到的語(yǔ)法樹(shù)和原始語(yǔ)法樹(shù)是一致的。同時(shí),通過(guò)編譯數(shù)據(jù)庫(kù),可以獲取一個(gè)可執(zhí)行文件的所有的依賴文件,不需要進(jìn)行.c和.h的搜索,降低了程序插樁的時(shí)間。
對(duì)于問(wèn)題三,該文提供的解決方法是將不完整頭文件擴(kuò)展到源文件中,不再對(duì)該頭文件進(jìn)行單獨(dú)插樁。因此,該文提供了一個(gè)頭文件擴(kuò)展算法,該算法可以將指定的文件進(jìn)行擴(kuò)展,當(dāng)遇到該文件時(shí),不對(duì)其插樁,同時(shí)將其內(nèi)容擴(kuò)展到所有引入該頭文件的文件中。當(dāng)對(duì)程序進(jìn)行內(nèi)存安全性檢測(cè)時(shí),由于系統(tǒng)庫(kù)文件中的接口是編譯器提供的標(biāo)準(zhǔn)接口,不需要對(duì)其進(jìn)行插樁檢測(cè),所以該文提供的頭文件擴(kuò)展算法對(duì)所有的系統(tǒng)庫(kù)文件不進(jìn)行擴(kuò)展,這不僅減少了對(duì)程序的插樁時(shí)間,也減少了代碼的膨脹率。同時(shí),該文提供的算法還支持不擴(kuò)展用戶指定的頭文件。
該算法的主要思想是:首先,創(chuàng)建一個(gè)文件輸出流,然后利用Clang前端接口創(chuàng)建一個(gè)原始詞法解析器,該解析器只解析當(dāng)前主文件中的內(nèi)容,然后當(dāng)解析到#include指令時(shí),在頭文件列表中查找該include的文件標(biāo)識(shí),然后判斷該文件是否是系統(tǒng)庫(kù)文件,若不是,則將其內(nèi)容寫入到輸出流,同時(shí)遞歸地調(diào)用本方法去繼續(xù)擴(kuò)展頭文件中引入的頭文件。若是系統(tǒng)庫(kù)文件則保持不變,繼續(xù)解析下一個(gè)#include命令。其中,頭文件列表是當(dāng)讀取的文件發(fā)生切換時(shí)記錄的,它通過(guò)Clang提供的PPCallbacks中的FileChanged()回調(diào)函數(shù)記錄,每當(dāng)文件發(fā)生切換,記錄該文件的ID、類型等信息。當(dāng)一個(gè)文件中所有的#include指令的內(nèi)容擴(kuò)展完成后,再將#include指令后的內(nèi)容寫入到輸出流,最后寫回到原文件中,從而實(shí)現(xiàn)對(duì)頭文件的擴(kuò)展。具體實(shí)現(xiàn)如圖1所示。

圖1 頭文件擴(kuò)展算法
基于指針的內(nèi)存安全性動(dòng)態(tài)分析技術(shù)對(duì)包含指針參數(shù)或返回值為指針類型的函數(shù),需要對(duì)其插樁包裝函數(shù),用來(lái)初始化函數(shù)參數(shù)和返回值變量的指針元數(shù)據(jù)。對(duì)函數(shù)定義生成其包裝函數(shù)定義,然后在其函數(shù)調(diào)用中重命名該方法,將其定位到包裝函數(shù)以完成內(nèi)存檢測(cè)。但是由于庫(kù)函數(shù)的定義在系統(tǒng)頭文件中,無(wú)法根據(jù)其定義生成包裝函數(shù)。通常,內(nèi)存分析工具會(huì)提供常用庫(kù)函數(shù)的包裝函數(shù),但是當(dāng)程序調(diào)用的庫(kù)函數(shù)較多或者使用了第三方庫(kù)時(shí),內(nèi)存分析工具無(wú)法提供所有的庫(kù)函數(shù)的包裝函數(shù)。若沒(méi)有提供包裝函數(shù)的庫(kù)函數(shù),則會(huì)對(duì)其進(jìn)行插樁,此時(shí)會(huì)因?yàn)檎也坏桨b函數(shù)定義而導(dǎo)致編譯失敗。針對(duì)這類問(wèn)題,該文提供的解決方法是:首先,對(duì)于一個(gè)函數(shù),判斷其是否是庫(kù)函數(shù),然后判斷該函數(shù)的包裝函數(shù)工具是否提供,若提供了其包裝函數(shù),則對(duì)該庫(kù)函數(shù)進(jìn)行插樁,若不提供,則不對(duì)該庫(kù)函數(shù)進(jìn)行插樁。
因此該文給出一個(gè)庫(kù)函數(shù)判斷算法,該算法的思想根據(jù)是庫(kù)文件是存儲(chǔ)在系統(tǒng)特定位置下,通過(guò)判斷一個(gè)函數(shù)所引用的聲明的文件是否在當(dāng)前工作目錄中,來(lái)判斷該函數(shù)是否為庫(kù)函數(shù)。具體的實(shí)現(xiàn)如圖2所示。

圖2 庫(kù)函數(shù)判斷算法
當(dāng)判斷一個(gè)函數(shù)是庫(kù)函數(shù)后,此時(shí)需要判斷函數(shù)是否需要插樁,該文利用Clang獲取用戶文件語(yǔ)法樹(shù),然后通過(guò)函數(shù)聲明與定義訪問(wèn)函數(shù)VisitFunctionDecl記錄下每一個(gè)函數(shù)名,將其傳遞給插樁模塊,配合系統(tǒng)提供的包裝函數(shù)列表,完成函數(shù)是否需要插樁的判定。
對(duì)于結(jié)構(gòu)體指針解引用,需要獲取該指針指向區(qū)域的上界和下界。對(duì)于指向命名結(jié)構(gòu)體的指針變量如struct st *ptr,它指向區(qū)域的上界和下界分別為ptr和ptr+sizeof(struct st)。但是對(duì)于匿名結(jié)構(gòu)體,無(wú)法獲取它的名字,所以sizeof的括號(hào)中缺少結(jié)構(gòu)體名字,導(dǎo)致插樁后的程序出現(xiàn)編譯錯(cuò)誤,如:struct {int a; int b;} *ptr;對(duì)*ptr插樁后獲取的下界ptr+sizeof(struct(anonymous struct at /home/a.c:3:1)。
對(duì)于該問(wèn)題,該文提供的解決方法是:對(duì)匿名結(jié)構(gòu)體添加一個(gè)唯一的ID,在使用sizeof獲取匿名結(jié)構(gòu)體變量類型的時(shí)候,使用該ID構(gòu)造函數(shù)的名字,通過(guò)該名字確定結(jié)構(gòu)體類型的大小。使用AST上該結(jié)構(gòu)體定義節(jié)點(diǎn)的地址作為ID。在結(jié)構(gòu)體定義時(shí)添加有該ID構(gòu)造的名字,然后在訪問(wèn)該結(jié)構(gòu)體變量時(shí)獲取該變量的結(jié)構(gòu)體定義節(jié)點(diǎn),并獲取其地址,從而保證了構(gòu)造的ID是唯一的并且是一致的。具體的實(shí)現(xiàn)算法如圖3所示。

圖3 匿名結(jié)構(gòu)體插樁算法
當(dāng)一個(gè)指針無(wú)效時(shí),需要對(duì)該指針的元數(shù)據(jù)進(jìn)行清除,以節(jié)省空間和時(shí)間。在循環(huán)結(jié)構(gòu)中包含break語(yǔ)句和continue語(yǔ)句,switch分支結(jié)構(gòu)中包含break語(yǔ)句,這些語(yǔ)句會(huì)改變程序的執(zhí)行流程,所以需要對(duì)break語(yǔ)句和continue語(yǔ)句進(jìn)行重寫,來(lái)實(shí)現(xiàn)對(duì)程序的指針元數(shù)據(jù)的清除,具體重寫的規(guī)則如下:
循環(huán)中的break替換為:{bc_flag_LOOP_BLOCK_ID=1;goto PRFlbl_THIS_BLOCK_ID;}。
continue語(yǔ)句替換為:{bc_flag_LOOP_BLOCK_ID=2;goto PRFlbl_THIS_BLOCK_ID;}。
switch中的break替換為:{bc_flag_SWIT_BLOCK_ID=1;goto PRFlbl_THIS_BLOCK_ID;}。
其中bc_flag_LOOP_BLOCK_ID值為1時(shí)表示循環(huán)中的break語(yǔ)句,值為2時(shí)表示continue語(yǔ)句,bc_flag_SWIT_BLOCK_ID表示switch語(yǔ)句中的break語(yǔ)句。LOOP_BLOCK_ID表示該循環(huán)語(yǔ)句塊的ID,SWIT_BLOCK_ID表示語(yǔ)句switch語(yǔ)句塊的ID,lbl_THIS_BLOCK_ID插入在該語(yǔ)句塊最后用來(lái)清除在該語(yǔ)句塊內(nèi)定義的元數(shù)據(jù),然后再根據(jù)bc_flag判斷執(zhí)行流程。
在對(duì)break的替換的時(shí)候,需要考慮一些復(fù)雜結(jié)構(gòu),如循環(huán)中嵌套switch結(jié)構(gòu)或switch語(yǔ)句中嵌套循環(huán)結(jié)構(gòu),此時(shí)插樁時(shí)需要對(duì)該break語(yǔ)句進(jìn)行判斷來(lái)實(shí)現(xiàn)不同的替換。針對(duì)該問(wèn)題,該文提出的解決方法是:對(duì)于一個(gè)break語(yǔ)句,在插樁前需要記錄它的父語(yǔ)句塊PBS,在進(jìn)行函數(shù)插樁時(shí)記錄循環(huán)結(jié)構(gòu)語(yǔ)句塊LBS和switch語(yǔ)句塊SBS,如果不存在循環(huán)結(jié)構(gòu)或switch結(jié)構(gòu)體,則LBS和SBS為空。然后通過(guò)比較break語(yǔ)句父語(yǔ)句塊PBS和LBS、SBS的關(guān)系,判斷出break語(yǔ)句是屬于循環(huán)結(jié)構(gòu)還是屬于switch分支結(jié)構(gòu),從而根據(jù)對(duì)應(yīng)的方法對(duì)break語(yǔ)句替換,以保證程序在清除完元數(shù)據(jù)之后能正常運(yùn)行。具體的算法如圖4所示。

圖4 break語(yǔ)句插樁算法
該文所述的對(duì)大規(guī)模C程序的應(yīng)用理論在內(nèi)存動(dòng)態(tài)分析Movec上進(jìn)行了實(shí)現(xiàn)。該工具實(shí)現(xiàn)采用的是基于Clang編譯器來(lái)對(duì)源代碼進(jìn)行檢測(cè)邏輯的插樁,插樁過(guò)后的代碼仍然是標(biāo)準(zhǔn)C程序。同時(shí),保證了改進(jìn)過(guò)的Movec能正常地插樁和檢測(cè)大規(guī)模C程序。其架構(gòu)如圖5所示。

圖5 Movec架構(gòu)
在對(duì)大規(guī)模程序內(nèi)存安全性進(jìn)行分析時(shí),Movec的輸入是待檢測(cè)項(xiàng)目和一個(gè)編譯數(shù)據(jù)庫(kù)文件,即JSON文件,輸出是插樁完整的項(xiàng)目Movec對(duì)該JSON進(jìn)行解析,并構(gòu)造出完整的文件編譯規(guī)則,將其傳遞給C解析器,構(gòu)造每個(gè)文件的抽象語(yǔ)法樹(shù)。最后通過(guò)AST visitor對(duì)語(yǔ)法樹(shù)進(jìn)行訪問(wèn),在語(yǔ)法樹(shù)上獲取需要插樁的節(jié)點(diǎn)位置,通過(guò)Clang提供的SourceManager接口和Rewriter接口實(shí)現(xiàn)內(nèi)容的獲取和重寫,完成對(duì)包裝函數(shù)的插樁改進(jìn)實(shí)現(xiàn),對(duì)匿名結(jié)構(gòu)體的插樁實(shí)現(xiàn)以及對(duì)break語(yǔ)句改進(jìn)的實(shí)現(xiàn),完成對(duì)項(xiàng)目的源代碼插樁。將該文提出的插樁改進(jìn)規(guī)則應(yīng)用到Movec工具上,使其能有效地對(duì)大規(guī)模C程序進(jìn)行插樁,并對(duì)其進(jìn)行動(dòng)態(tài)內(nèi)存分析。
基于上面介紹的算法,將其在Movec上進(jìn)行了實(shí)現(xiàn)。本節(jié)將介紹優(yōu)化后的Movec對(duì)大規(guī)模程序分析的有效性和高效性。
為了驗(yàn)證改進(jìn)部分插樁規(guī)則后工具的有效性,將Movec應(yīng)用到Mibench標(biāo)準(zhǔn)測(cè)試集上。實(shí)驗(yàn)平臺(tái)為64位的Ubuntu16.04操作系統(tǒng),處理器為Intel(R) Core(TM) i5-7200U CPU 2.70 GHz,內(nèi)存是8.00 GB,編譯器為gcc4.8.2。
選取了其中8個(gè)大規(guī)模的測(cè)試集進(jìn)行實(shí)驗(yàn),并與SoftBoundCets[14]、ASan[15]、Valgrind[16]進(jìn)行了對(duì)比。通過(guò)實(shí)驗(yàn)表明,Movec可以正確地對(duì)這8個(gè)大規(guī)模的測(cè)試集進(jìn)行安全檢測(cè)。Movec和ASan在blowfish、jpeg、rijndael和rsynth中檢測(cè)出了錯(cuò)誤,但是Movec還檢測(cè)出了ASan未檢測(cè)出的錯(cuò)誤,如在blowfish中的數(shù)組訪問(wèn)越界錯(cuò)誤:
void BF_set_key(key, int len, unsigned char* data){
unsigned char * end=&(data[len]);}
unsigned char ukey[8];
BF_set_key(&key,8,ukey);
而SoftBoundCets則對(duì)5個(gè)測(cè)試集無(wú)法正常插樁,并且其余三個(gè)沒(méi)有檢測(cè)出錯(cuò)誤。Valgrind正常對(duì)程序檢測(cè),但未發(fā)現(xiàn)任何錯(cuò)誤。
通過(guò)結(jié)果表明,Movec對(duì)大規(guī)模程序的檢測(cè)是有效的,且沒(méi)有發(fā)生漏報(bào)和誤報(bào)。
本節(jié)將Movec與內(nèi)存檢測(cè)工具SoftBoundCets、ASan、Valgrind進(jìn)行性能對(duì)比。從Mibench中選取了規(guī)模較大的8個(gè)測(cè)試集進(jìn)行對(duì)比驗(yàn)證,考慮到誤差,選用了三次實(shí)驗(yàn)結(jié)果去平均值的方式。實(shí)驗(yàn)結(jié)果如表1所示。

表1 運(yùn)行時(shí)間對(duì)比結(jié)果
綜合表中數(shù)據(jù)可以看出,SoftBoundCet由于使用了靜態(tài)分析,其在gsm和blowfish(l)優(yōu)于Movec,但它僅僅只能在其中三個(gè)測(cè)試集中運(yùn)行成功;Valgrind采用的二進(jìn)制代碼插樁,雖然可以成功運(yùn)行在大規(guī)模C程序上,但運(yùn)行時(shí)間遠(yuǎn)遠(yuǎn)超過(guò)Movec;ASan在gsm和lame上的性能優(yōu)于Movec,但是當(dāng)在檢測(cè)出錯(cuò)誤的測(cè)試集中(如blowfish、jpeg、rijndael、rsynth),Movec的性能是好于ASan的。Movec還可以設(shè)置在發(fā)現(xiàn)錯(cuò)誤后繼續(xù)運(yùn)行,可以檢測(cè)出整個(gè)程序中可能存在的內(nèi)存錯(cuò)誤,而ASan和SoftBoundCets在發(fā)生錯(cuò)誤后立即終止,導(dǎo)致后面的錯(cuò)誤無(wú)法正常檢測(cè)。
由以上分析結(jié)果可以看出,改進(jìn)后的Movec不僅能夠正確地在所有Mibench上運(yùn)行,而且在有效性和高效性上都是優(yōu)于其他工具的,是一個(gè)可靠的大規(guī)模C程序內(nèi)存安全分析工具。
對(duì)大規(guī)模C程序進(jìn)行動(dòng)態(tài)內(nèi)存分析時(shí)可能出現(xiàn)的問(wèn)題進(jìn)行了描述,并給出了相應(yīng)的解決方法,然后將其在內(nèi)存動(dòng)態(tài)分析工具M(jìn)ovec上進(jìn)行了實(shí)現(xiàn),使其能對(duì)大規(guī)模C程序進(jìn)行內(nèi)存安全性檢測(cè)。通過(guò)實(shí)驗(yàn),表明Movec不僅能有效地對(duì)大規(guī)模C程序進(jìn)行檢測(cè),同時(shí)在綜合性能上是更優(yōu)的。在接下來(lái)的工作中,將繼續(xù)優(yōu)化其對(duì)大規(guī)模程序檢測(cè)的運(yùn)行時(shí)間,例如結(jié)合靜態(tài)分析,以減少對(duì)程序不必要的插樁和檢測(cè)。