王 沖,孫 毅,仵 俊
(南京航空航天大學(xué) 計(jì)算機(jī)科學(xué)與技術(shù)學(xué)院,江蘇 南京 211100)
C語言常用于諸如操作系統(tǒng)、嵌入式軟件系統(tǒng)等對性能要求較高的系統(tǒng)的編寫。然而C語言本身缺乏對內(nèi)存安全性檢測的相關(guān)功能,因此使用其編寫的程序可能存在較為嚴(yán)重的內(nèi)存安全性漏洞[1-5]。動態(tài)分析[6-8]是目前常用的對程序進(jìn)行內(nèi)存安全檢測的方法,目前常用的實(shí)現(xiàn)方法有二進(jìn)制代碼插樁、中間代碼插樁、源代碼插樁[9-10]等。
二進(jìn)制代碼插樁是對可執(zhí)行程序進(jìn)行插樁,優(yōu)點(diǎn)是不需要源代碼就可以對程序進(jìn)行動態(tài)分析;中間代碼插樁是對編譯后的代碼插樁,可以利用優(yōu)化,減少不必要的插樁;源代碼插樁是指對源碼上進(jìn)行修改,添加行為監(jiān)代碼,對程序進(jìn)行檢測,優(yōu)點(diǎn)是可以獲取源代碼中的位置,準(zhǔn)確地報(bào)告錯誤信息。
為了能更準(zhǔn)確有效地檢測程序的錯誤并能將錯誤的變量信息準(zhǔn)確地反饋給用戶,該文采用了源代碼插樁技術(shù)進(jìn)行插樁,并在基于指針技術(shù)[11-13]的基礎(chǔ)上,借助開源編譯器Clang和C++語言實(shí)現(xiàn)了內(nèi)存安全分析工具M(jìn)ovec,完成其對大規(guī)模C程序的內(nèi)存安全性檢測,并通過實(shí)驗(yàn)進(jìn)行了驗(yàn)證,表明該內(nèi)存分析工具對大規(guī)模程序的內(nèi)存檢測是有效且高效的。
基于指針的內(nèi)存安全性檢測技術(shù)的主要思想是對程序中的所有指針變量構(gòu)造一個指針元數(shù)據(jù),用來記錄該指針的內(nèi)存狀態(tài)、上下界以及指向當(dāng)前內(nèi)存的指針的個數(shù)。然后,當(dāng)指針賦值或者以函數(shù)參數(shù)傳遞的時候,更新這個指針的元數(shù)據(jù),用來保持?jǐn)?shù)據(jù)的一致性。最后,在對指針進(jìn)行解引用或者通過指針對內(nèi)存進(jìn)行讀寫時,根據(jù)指針元數(shù)據(jù)中記錄的內(nèi)存狀態(tài)信息,來判斷該次內(nèi)存訪問是否是合法的,從而檢測出內(nèi)存的安全性。
采用源代碼插樁實(shí)現(xiàn)基于指針內(nèi)存安全性檢測的過程分為三個部分:一是對指針變量定義進(jìn)行插樁以初始化元數(shù)據(jù),對指針變量賦值進(jìn)行插樁來更新元數(shù)據(jù)的信息;二是在指針解引用的時候來檢查該指針?biāo)玫膶ο蟮脑獢?shù)據(jù);三是對函數(shù)定義進(jìn)行插樁以初始化函數(shù)參數(shù)的元數(shù)據(jù)、計(jì)算存儲返回值的元數(shù)據(jù)。然后,對函數(shù)定義生成一個包裝函數(shù),該包裝函數(shù)用來對程序檢測并傳遞指針元數(shù)據(jù)。接著,對原函數(shù)調(diào)用重命名,并插入元數(shù)據(jù),然后將原函數(shù)調(diào)用重定向到其包裝函數(shù)來完成檢測。
目前的源代碼插樁工具對程序的插樁一般有兩種模式,一種是單文件插樁模式,一種是項(xiàng)目插樁模式。單文件插樁模式適用于一些文件數(shù)量比較少的情況。對于項(xiàng)目插樁模式,目前常采用的方法是使用搜索后綴的方法將文件中所有的.c和.h文件進(jìn)行搜索,然后將所有的文件添加到插樁列表中,把每個.c和.h文件都當(dāng)成一個翻譯單元進(jìn)行解析插樁,插樁完成后將新的文件生成到目標(biāo)文件夾中。這種方法在對大規(guī)模的程序進(jìn)行插樁時過于簡單,會導(dǎo)致如下問題:一對每個.c和.h文件進(jìn)行搜索,會將一些不必要的文件進(jìn)行搜索并插樁,增加了項(xiàng)目插樁時間;二是當(dāng)文件編譯命令中使用了-D定義了宏或者使用-I頭文件目錄時,這種項(xiàng)目插樁的方式獲取到的語法樹會和原語法樹完全不同,導(dǎo)致插樁錯誤;三是當(dāng)項(xiàng)目中的頭文件出現(xiàn)一個不完整文件時,將該文件當(dāng)作一個完整的翻譯單元處理時,無法獲取其完整的語法樹,導(dǎo)致程序插樁失敗。
對于問題一和問題二,該文利用編譯數(shù)據(jù)庫的概念,對源代碼插樁工具的項(xiàng)目插樁模式進(jìn)行改進(jìn)。編譯數(shù)據(jù)庫是在項(xiàng)目實(shí)際編譯過程中對編譯器調(diào)用的監(jiān)控記錄,其中包含了每個文件在編譯時的編譯選項(xiàng)。利用編譯數(shù)據(jù)庫獲取待插樁文件的存儲路徑和該文件對應(yīng)的編譯指令,構(gòu)造出每個文件的原始編譯命令,從而在對文件進(jìn)行解析時獲取到的語法樹和原始語法樹是一致的。同時,通過編譯數(shù)據(jù)庫,可以獲取一個可執(zhí)行文件的所有的依賴文件,不需要進(jìn)行.c和.h的搜索,降低了程序插樁的時間。
對于問題三,該文提供的解決方法是將不完整頭文件擴(kuò)展到源文件中,不再對該頭文件進(jìn)行單獨(dú)插樁。因此,該文提供了一個頭文件擴(kuò)展算法,該算法可以將指定的文件進(jìn)行擴(kuò)展,當(dāng)遇到該文件時,不對其插樁,同時將其內(nèi)容擴(kuò)展到所有引入該頭文件的文件中。當(dāng)對程序進(jìn)行內(nèi)存安全性檢測時,由于系統(tǒng)庫文件中的接口是編譯器提供的標(biāo)準(zhǔn)接口,不需要對其進(jìn)行插樁檢測,所以該文提供的頭文件擴(kuò)展算法對所有的系統(tǒng)庫文件不進(jìn)行擴(kuò)展,這不僅減少了對程序的插樁時間,也減少了代碼的膨脹率。同時,該文提供的算法還支持不擴(kuò)展用戶指定的頭文件。
該算法的主要思想是:首先,創(chuàng)建一個文件輸出流,然后利用Clang前端接口創(chuàng)建一個原始詞法解析器,該解析器只解析當(dāng)前主文件中的內(nèi)容,然后當(dāng)解析到#include指令時,在頭文件列表中查找該include的文件標(biāo)識,然后判斷該文件是否是系統(tǒng)庫文件,若不是,則將其內(nèi)容寫入到輸出流,同時遞歸地調(diào)用本方法去繼續(xù)擴(kuò)展頭文件中引入的頭文件。若是系統(tǒng)庫文件則保持不變,繼續(xù)解析下一個#include命令。其中,頭文件列表是當(dāng)讀取的文件發(fā)生切換時記錄的,它通過Clang提供的PPCallbacks中的FileChanged()回調(diào)函數(shù)記錄,每當(dāng)文件發(fā)生切換,記錄該文件的ID、類型等信息。當(dāng)一個文件中所有的#include指令的內(nèi)容擴(kuò)展完成后,再將#include指令后的內(nèi)容寫入到輸出流,最后寫回到原文件中,從而實(shí)現(xiàn)對頭文件的擴(kuò)展。具體實(shí)現(xiàn)如圖1所示。
圖1 頭文件擴(kuò)展算法
基于指針的內(nèi)存安全性動態(tài)分析技術(shù)對包含指針參數(shù)或返回值為指針類型的函數(shù),需要對其插樁包裝函數(shù),用來初始化函數(shù)參數(shù)和返回值變量的指針元數(shù)據(jù)。對函數(shù)定義生成其包裝函數(shù)定義,然后在其函數(shù)調(diào)用中重命名該方法,將其定位到包裝函數(shù)以完成內(nèi)存檢測。但是由于庫函數(shù)的定義在系統(tǒng)頭文件中,無法根據(jù)其定義生成包裝函數(shù)。通常,內(nèi)存分析工具會提供常用庫函數(shù)的包裝函數(shù),但是當(dāng)程序調(diào)用的庫函數(shù)較多或者使用了第三方庫時,內(nèi)存分析工具無法提供所有的庫函數(shù)的包裝函數(shù)。若沒有提供包裝函數(shù)的庫函數(shù),則會對其進(jìn)行插樁,此時會因?yàn)檎也坏桨b函數(shù)定義而導(dǎo)致編譯失敗。針對這類問題,該文提供的解決方法是:首先,對于一個函數(shù),判斷其是否是庫函數(shù),然后判斷該函數(shù)的包裝函數(shù)工具是否提供,若提供了其包裝函數(shù),則對該庫函數(shù)進(jìn)行插樁,若不提供,則不對該庫函數(shù)進(jìn)行插樁。
因此該文給出一個庫函數(shù)判斷算法,該算法的思想根據(jù)是庫文件是存儲在系統(tǒng)特定位置下,通過判斷一個函數(shù)所引用的聲明的文件是否在當(dāng)前工作目錄中,來判斷該函數(shù)是否為庫函數(shù)。具體的實(shí)現(xiàn)如圖2所示。
圖2 庫函數(shù)判斷算法
當(dāng)判斷一個函數(shù)是庫函數(shù)后,此時需要判斷函數(shù)是否需要插樁,該文利用Clang獲取用戶文件語法樹,然后通過函數(shù)聲明與定義訪問函數(shù)VisitFunctionDecl記錄下每一個函數(shù)名,將其傳遞給插樁模塊,配合系統(tǒng)提供的包裝函數(shù)列表,完成函數(shù)是否需要插樁的判定。
對于結(jié)構(gòu)體指針解引用,需要獲取該指針指向區(qū)域的上界和下界。對于指向命名結(jié)構(gòu)體的指針變量如struct st *ptr,它指向區(qū)域的上界和下界分別為ptr和ptr+sizeof(struct st)。但是對于匿名結(jié)構(gòu)體,無法獲取它的名字,所以sizeof的括號中缺少結(jié)構(gòu)體名字,導(dǎo)致插樁后的程序出現(xiàn)編譯錯誤,如:struct {int a; int b;} *ptr;對*ptr插樁后獲取的下界ptr+sizeof(struct(anonymous struct at /home/a.c:3:1)。
對于該問題,該文提供的解決方法是:對匿名結(jié)構(gòu)體添加一個唯一的ID,在使用sizeof獲取匿名結(jié)構(gòu)體變量類型的時候,使用該ID構(gòu)造函數(shù)的名字,通過該名字確定結(jié)構(gòu)體類型的大小。使用AST上該結(jié)構(gòu)體定義節(jié)點(diǎn)的地址作為ID。在結(jié)構(gòu)體定義時添加有該ID構(gòu)造的名字,然后在訪問該結(jié)構(gòu)體變量時獲取該變量的結(jié)構(gòu)體定義節(jié)點(diǎn),并獲取其地址,從而保證了構(gòu)造的ID是唯一的并且是一致的。具體的實(shí)現(xiàn)算法如圖3所示。
圖3 匿名結(jié)構(gòu)體插樁算法
當(dāng)一個指針無效時,需要對該指針的元數(shù)據(jù)進(jìn)行清除,以節(jié)省空間和時間。在循環(huán)結(jié)構(gòu)中包含break語句和continue語句,switch分支結(jié)構(gòu)中包含break語句,這些語句會改變程序的執(zhí)行流程,所以需要對break語句和continue語句進(jìn)行重寫,來實(shí)現(xiàn)對程序的指針元數(shù)據(jù)的清除,具體重寫的規(guī)則如下:
循環(huán)中的break替換為:{bc_flag_LOOP_BLOCK_ID=1;goto PRFlbl_THIS_BLOCK_ID;}。
continue語句替換為:{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時表示循環(huán)中的break語句,值為2時表示continue語句,bc_flag_SWIT_BLOCK_ID表示switch語句中的break語句。LOOP_BLOCK_ID表示該循環(huán)語句塊的ID,SWIT_BLOCK_ID表示語句switch語句塊的ID,lbl_THIS_BLOCK_ID插入在該語句塊最后用來清除在該語句塊內(nèi)定義的元數(shù)據(jù),然后再根據(jù)bc_flag判斷執(zhí)行流程。
在對break的替換的時候,需要考慮一些復(fù)雜結(jié)構(gòu),如循環(huán)中嵌套switch結(jié)構(gòu)或switch語句中嵌套循環(huán)結(jié)構(gòu),此時插樁時需要對該break語句進(jìn)行判斷來實(shí)現(xiàn)不同的替換。針對該問題,該文提出的解決方法是:對于一個break語句,在插樁前需要記錄它的父語句塊PBS,在進(jìn)行函數(shù)插樁時記錄循環(huán)結(jié)構(gòu)語句塊LBS和switch語句塊SBS,如果不存在循環(huán)結(jié)構(gòu)或switch結(jié)構(gòu)體,則LBS和SBS為空。然后通過比較break語句父語句塊PBS和LBS、SBS的關(guān)系,判斷出break語句是屬于循環(huán)結(jié)構(gòu)還是屬于switch分支結(jié)構(gòu),從而根據(jù)對應(yīng)的方法對break語句替換,以保證程序在清除完元數(shù)據(jù)之后能正常運(yùn)行。具體的算法如圖4所示。
圖4 break語句插樁算法
該文所述的對大規(guī)模C程序的應(yīng)用理論在內(nèi)存動態(tài)分析Movec上進(jìn)行了實(shí)現(xiàn)。該工具實(shí)現(xiàn)采用的是基于Clang編譯器來對源代碼進(jìn)行檢測邏輯的插樁,插樁過后的代碼仍然是標(biāo)準(zhǔn)C程序。同時,保證了改進(jìn)過的Movec能正常地插樁和檢測大規(guī)模C程序。其架構(gòu)如圖5所示。
圖5 Movec架構(gòu)
在對大規(guī)模程序內(nèi)存安全性進(jìn)行分析時,Movec的輸入是待檢測項(xiàng)目和一個編譯數(shù)據(jù)庫文件,即JSON文件,輸出是插樁完整的項(xiàng)目Movec對該JSON進(jìn)行解析,并構(gòu)造出完整的文件編譯規(guī)則,將其傳遞給C解析器,構(gòu)造每個文件的抽象語法樹。最后通過AST visitor對語法樹進(jìn)行訪問,在語法樹上獲取需要插樁的節(jié)點(diǎn)位置,通過Clang提供的SourceManager接口和Rewriter接口實(shí)現(xiàn)內(nèi)容的獲取和重寫,完成對包裝函數(shù)的插樁改進(jìn)實(shí)現(xiàn),對匿名結(jié)構(gòu)體的插樁實(shí)現(xiàn)以及對break語句改進(jìn)的實(shí)現(xiàn),完成對項(xiàng)目的源代碼插樁。將該文提出的插樁改進(jìn)規(guī)則應(yīng)用到Movec工具上,使其能有效地對大規(guī)模C程序進(jìn)行插樁,并對其進(jìn)行動態(tài)內(nèi)存分析。
基于上面介紹的算法,將其在Movec上進(jìn)行了實(shí)現(xiàn)。本節(jié)將介紹優(yōu)化后的Movec對大規(guī)模程序分析的有效性和高效性。
為了驗(yàn)證改進(jìn)部分插樁規(guī)則后工具的有效性,將Movec應(yīng)用到Mibench標(biāo)準(zhǔn)測試集上。實(shí)驗(yàn)平臺為64位的Ubuntu16.04操作系統(tǒng),處理器為Intel(R) Core(TM) i5-7200U CPU 2.70 GHz,內(nèi)存是8.00 GB,編譯器為gcc4.8.2。
選取了其中8個大規(guī)模的測試集進(jìn)行實(shí)驗(yàn),并與SoftBoundCets[14]、ASan[15]、Valgrind[16]進(jìn)行了對比。通過實(shí)驗(yàn)表明,Movec可以正確地對這8個大規(guī)模的測試集進(jìn)行安全檢測。Movec和ASan在blowfish、jpeg、rijndael和rsynth中檢測出了錯誤,但是Movec還檢測出了ASan未檢測出的錯誤,如在blowfish中的數(shù)組訪問越界錯誤:
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則對5個測試集無法正常插樁,并且其余三個沒有檢測出錯誤。Valgrind正常對程序檢測,但未發(fā)現(xiàn)任何錯誤。
通過結(jié)果表明,Movec對大規(guī)模程序的檢測是有效的,且沒有發(fā)生漏報(bào)和誤報(bào)。
本節(jié)將Movec與內(nèi)存檢測工具SoftBoundCets、ASan、Valgrind進(jìn)行性能對比。從Mibench中選取了規(guī)模較大的8個測試集進(jìn)行對比驗(yàn)證,考慮到誤差,選用了三次實(shí)驗(yàn)結(jié)果去平均值的方式。實(shí)驗(yàn)結(jié)果如表1所示。
表1 運(yùn)行時間對比結(jié)果
綜合表中數(shù)據(jù)可以看出,SoftBoundCet由于使用了靜態(tài)分析,其在gsm和blowfish(l)優(yōu)于Movec,但它僅僅只能在其中三個測試集中運(yùn)行成功;Valgrind采用的二進(jìn)制代碼插樁,雖然可以成功運(yùn)行在大規(guī)模C程序上,但運(yùn)行時間遠(yuǎn)遠(yuǎn)超過Movec;ASan在gsm和lame上的性能優(yōu)于Movec,但是當(dāng)在檢測出錯誤的測試集中(如blowfish、jpeg、rijndael、rsynth),Movec的性能是好于ASan的。Movec還可以設(shè)置在發(fā)現(xiàn)錯誤后繼續(xù)運(yùn)行,可以檢測出整個程序中可能存在的內(nèi)存錯誤,而ASan和SoftBoundCets在發(fā)生錯誤后立即終止,導(dǎo)致后面的錯誤無法正常檢測。
由以上分析結(jié)果可以看出,改進(jìn)后的Movec不僅能夠正確地在所有Mibench上運(yùn)行,而且在有效性和高效性上都是優(yōu)于其他工具的,是一個可靠的大規(guī)模C程序內(nèi)存安全分析工具。
對大規(guī)模C程序進(jìn)行動態(tài)內(nèi)存分析時可能出現(xiàn)的問題進(jìn)行了描述,并給出了相應(yīng)的解決方法,然后將其在內(nèi)存動態(tài)分析工具M(jìn)ovec上進(jìn)行了實(shí)現(xiàn),使其能對大規(guī)模C程序進(jìn)行內(nèi)存安全性檢測。通過實(shí)驗(yàn),表明Movec不僅能有效地對大規(guī)模C程序進(jìn)行檢測,同時在綜合性能上是更優(yōu)的。在接下來的工作中,將繼續(xù)優(yōu)化其對大規(guī)模程序檢測的運(yùn)行時間,例如結(jié)合靜態(tài)分析,以減少對程序不必要的插樁和檢測。