萬(wàn) 明 劉嘉華 鄂龍慧 朱 江
(南京南瑞信息通信科技有限公司 江蘇 南京 210003)
現(xiàn)代編程語(yǔ)言通過部署類型系統(tǒng)來(lái)控制對(duì)象使用和錯(cuò)誤檢測(cè)[1]。類型系統(tǒng)可以分為靜態(tài)類型、動(dòng)態(tài)類型、強(qiáng)類型和弱類型。由于C/C++是靜態(tài)弱類型語(yǔ)言,允許通過默認(rèn)或強(qiáng)制的方式轉(zhuǎn)換變量的類型,因此只能依賴于編程人員良好的編程能力和習(xí)慣來(lái)避免在運(yùn)行時(shí)引入類型錯(cuò)誤,從而避免出現(xiàn)安全問題。然而編程人員水平參差不齊,且在開發(fā)過程中難免會(huì)有所疏忽,所以由類型錯(cuò)誤所引起的安全問題屢見不鮮。據(jù)統(tǒng)計(jì),微軟公司所有軟件已發(fā)現(xiàn)的代碼執(zhí)行漏洞中大約有75%是類型和內(nèi)存錯(cuò)誤[2]。市場(chǎng)占有率高達(dá)63.16%的谷歌瀏覽器被發(fā)現(xiàn)了存在于Histogram組件中的編號(hào)CVE-2017-5023的類型混淆漏洞,該漏洞允許遠(yuǎn)程攻擊者通過發(fā)送一個(gè)精心構(gòu)造的網(wǎng)頁(yè)來(lái)執(zhí)行受害者電腦上的任意代碼[3]。同年,在蘋果瀏覽器Safari內(nèi)核的webkit組件中也發(fā)現(xiàn)了編號(hào)為CVE-2017-2415的類型混淆漏洞,使得幾乎所有的蘋果設(shè)備都受到了威脅[4]。由此可見,類型轉(zhuǎn)換問題不能完全依賴于編程人員的編程技能和人為審計(jì)來(lái)規(guī)避,研發(fā)一個(gè)有效的針對(duì)性的檢測(cè)方法是很有必要的,這將在很大程度上幫助編程人員規(guī)避軟件中的安全隱患,增強(qiáng)軟件的健壯性。
為此,本文借鑒了動(dòng)態(tài)類型語(yǔ)言Python語(yǔ)言和Go語(yǔ)言中的類型系統(tǒng)[5],提出了一種針對(duì)C/C++隱式類型轉(zhuǎn)換產(chǎn)生的類型安全問題的編譯期檢測(cè)方法。該方法在編譯期作用于LLVM中間代碼,對(duì)代碼中的隱式類型轉(zhuǎn)換操作進(jìn)行檢測(cè),若匹配到相應(yīng)的模式即給出必要的提示或終止編譯過程。實(shí)現(xiàn)上,我們將該方法編譯為一個(gè)動(dòng)態(tài)鏈接庫(kù)文件,通過LLVM opt工具加載動(dòng)態(tài)鏈接庫(kù)來(lái)對(duì)代碼進(jìn)行類型轉(zhuǎn)換安全性檢測(cè)。
面對(duì)如今動(dòng)輒上百甚至數(shù)千萬(wàn)行代碼的重量級(jí)軟件,編譯器在處理一些類型轉(zhuǎn)換錯(cuò)誤時(shí)缺乏必要的提示,使得編程人員很難在代碼海洋里揪出這些不起眼的錯(cuò)誤。因此,人們開始著眼于構(gòu)建一個(gè)類型轉(zhuǎn)換錯(cuò)誤檢測(cè)工具,以幫助編程人員在開發(fā)過程中規(guī)避由類型問題引發(fā)的安全隱患。這些工具大體可以分為兩個(gè)類別:基于嵌入對(duì)象中的vtable指針的方法[6-9]和基于不相交元數(shù)據(jù)的方法[10-12]。
基于vtable指針的解決方案無(wú)須跟蹤活動(dòng)對(duì)象,但具有根本的局限性,即它們不支持不具有vtable的非多態(tài)類。UBSan[6]使用靜態(tài)強(qiáng)制轉(zhuǎn)換執(zhí)行顯式的運(yùn)行時(shí)檢查,從而有效地將其轉(zhuǎn)換為動(dòng)態(tài)強(qiáng)制轉(zhuǎn)換。為防止非多態(tài)類失敗,UBSan需要手動(dòng)將其列入黑名單。然而,C++編譯器中可用的現(xiàn)有類型檢查基礎(chǔ)結(jié)構(gòu)本質(zhì)上很慢(因?yàn)樗窃诩僭O(shè)只有很少的動(dòng)態(tài)檢查會(huì)被執(zhí)行且大多數(shù)檢查是靜態(tài)的前提下設(shè)計(jì)的),因此UBSan由于開銷過大僅被用作測(cè)試工具,而不是一個(gè)在線檢測(cè)工具。Clang控制流完整性(CFI)[8]旨在提高速度,但尚未發(fā)布性能數(shù)據(jù)。而且,像該組中的所有解決方案一樣,它不能支持非多態(tài)類。
基于不相交元數(shù)據(jù)的方法最早出現(xiàn)的是CaVer[10]。代替手動(dòng)列入黑名單的方式,CaVer使用不相交的元數(shù)據(jù)來(lái)支持非多態(tài)類。然而,由于元數(shù)據(jù)跟蹤效率低(尤其是在堆棧上)且檢查速度較慢,因此該方法開銷仍然過高,對(duì)于某些瀏覽器基準(zhǔn)測(cè)試,這種開銷高達(dá)100%。此外,該方法無(wú)法處理線程之間共享的堆棧對(duì)象且在對(duì)象分配范圍上表現(xiàn)較差,最終導(dǎo)致類型混淆檢測(cè)范圍減小。
TypeSan[11]實(shí)現(xiàn)了一個(gè)sanitizer編譯器組件,為程序添加了獨(dú)立設(shè)計(jì)的元數(shù)據(jù)類型來(lái)記錄類型跟蹤信息,在編譯期間為程序插入額外代碼片,這些代碼片段在運(yùn)行時(shí)跟蹤C(jī)++程序中所有cast操作相關(guān)的類型轉(zhuǎn)換操作,能夠檢測(cè)出C++中cast操作符使用不當(dāng)?shù)那樾巍EcCaVer相比,提升了3~6倍的性能,但是仍具有較大的時(shí)間開銷和內(nèi)存開銷(4~10倍基準(zhǔn)值)。
HexType[12]在TypeSan的基礎(chǔ)上,改進(jìn)了記錄類型跟蹤信息的元數(shù)據(jù),并嚴(yán)格區(qū)分了安全cast和不安全cast,對(duì)C++中的cast操作符有選擇地進(jìn)行跟蹤,做到了比TypeSan更高效的cast錯(cuò)誤檢測(cè)。但是,和TypeSan一樣仍然是給程序插入額外的代碼片段,在運(yùn)行時(shí)檢測(cè)類型轉(zhuǎn)換錯(cuò)誤中的其中一種情形(cast操作符的使用不當(dāng))。
綜上所述,現(xiàn)有的工具大多集中于檢測(cè)C++中cast操作符的使用不當(dāng),新出現(xiàn)的方法主要仍在改進(jìn)cast操作符使用不當(dāng)?shù)臋z測(cè)過程中的效率問題,面對(duì)更為普遍存在的隱式類型轉(zhuǎn)換,仍沒有一種有效的方法來(lái)檢測(cè)其是否安全合理。
類型轉(zhuǎn)換分為顯式類型轉(zhuǎn)換和隱式類型轉(zhuǎn)換。C++中,顯式類型轉(zhuǎn)換對(duì)應(yīng)四種cast轉(zhuǎn)換操作符:static_cast、dynamic_cast、const_cast和reinterpret_cast。四種不同的cast操作允許編程人員對(duì)變量進(jìn)行強(qiáng)制類型轉(zhuǎn)換以滿足開發(fā)過程中的不同需求。如果cast操作選擇不當(dāng)將會(huì)產(chǎn)生安全隱患。顯式類型轉(zhuǎn)換錯(cuò)誤示例如下:
1 class Parent
2 {
3 int x;
4 };
5 class Son : Parent
6 {
7 double y;
8 };
9 Parent *p=new Parent();
10 Son *s;
11 s=static_cast
12 s→y;
該示例通過子類指針s錯(cuò)誤訪問父類成員x。cast操作符將int類型轉(zhuǎn)換為double類型,使得子類s可以訪問比x更大的未初始化空間,造成圖1所示的越界讀。
圖1 cast操作使用不當(dāng)導(dǎo)致的越界讀
不同類型變量間,直接用賦值符“=”進(jìn)行賦值時(shí),右操作數(shù)不指明轉(zhuǎn)換的類型,則進(jìn)行的是隱式類型轉(zhuǎn)換。除了賦值操作,函數(shù)調(diào)用時(shí)形式參數(shù)的引用也存在隱式類型轉(zhuǎn)換,這類隱式類型轉(zhuǎn)換和賦值形式的隱式類型轉(zhuǎn)換有一點(diǎn)不同,函數(shù)調(diào)用時(shí)形式參數(shù)的隱式類型轉(zhuǎn)換可能會(huì)產(chǎn)生直接的安全問題。C/C++中一個(gè)熟知的內(nèi)存復(fù)制函數(shù)memcpy可以對(duì)變量進(jìn)行復(fù)制,由于memcpy函數(shù)的參數(shù)是指針變量,內(nèi)存拷貝操作沒有相應(yīng)的安全檢測(cè),有潛在的安全風(fēng)險(xiǎn)。像memcpy這樣能直接操作指針變量的函數(shù)有不少,在實(shí)際開發(fā)過程中也經(jīng)常出現(xiàn)由該情況引起的安全漏洞。本文提出的方法將著重解決該類問題。隱式類型轉(zhuǎn)換錯(cuò)誤示例如下:
1 struct Example1
2 {
3 int x;
4 void *func();
5 };
6 struct Example2
7 {
8 int x;
9 long y;
10 };
11 int main()
12 {
13 Example1 *e1=new Example1();
14 Example2 *e2=new Example2();
15 e2→x=0;
16 e2→y=0x1234;
17 memcpy(e1,e2,sizeof(Example1));
18 e1→func();
19 return 0;
20 }
該示例錯(cuò)誤地將int型變量賦值給了函數(shù)指針,調(diào)用函數(shù)e1->func()時(shí)變成了對(duì)0x1234這塊內(nèi)存的解引用,程序試圖去請(qǐng)求訪問0x1234地址的內(nèi)存,顯然地址0x1234在程序段中并不是一個(gè)合法的可讀地址,從而造成了內(nèi)存訪問違例(對(duì)應(yīng)linux下的segmentfault錯(cuò)誤)。
本文提出了一種針對(duì)C/C++隱式類型轉(zhuǎn)換產(chǎn)生的類型安全問題的編譯期檢測(cè)方法。該方法基于LLVM中間代碼檢索出程序代碼中的類型轉(zhuǎn)換語(yǔ)句,提取并比對(duì)變量類型以判斷該類型轉(zhuǎn)換是否會(huì)引入安全隱患。我們將該方法編譯為一個(gè)動(dòng)態(tài)鏈接庫(kù)文件,以作為L(zhǎng)LVM中間代碼的分析優(yōu)化器,由LLVM opt工具加載執(zhí)行。
LLVM[13]編譯器以其提供的優(yōu)化特性而出名,其中的代碼優(yōu)化階段允許開發(fā)人員利用LLVM提供的豐富的接口以實(shí)現(xiàn)自定義pass,也就是我們所說(shuō)的優(yōu)化器。LLVM架構(gòu)如圖2所示。
圖2 廣義LLVM架構(gòu)
如圖3所示,自定義的優(yōu)化器通過opt工具加載以在編譯期檢測(cè)類型轉(zhuǎn)換操作。opt工具又被稱為模塊化的LLVM優(yōu)化器和分析器,它接受LLVM IR(LLVM輸出的以.ll為拓展名的中間代碼)和LLVM字節(jié)碼(LLVM輸出的以.bc為拓展名的中間代碼)格式作為輸入,通過-load選項(xiàng)選擇要加載的優(yōu)化器。
圖3 編譯過程中檢測(cè)類型轉(zhuǎn)換
本節(jié)給出類型轉(zhuǎn)換安全的定義,并基于C/C++的一些語(yǔ)言特性,實(shí)現(xiàn)對(duì)類型轉(zhuǎn)換是否安全的一個(gè)判定算法。
C/C++中的類型可以分為基本類型、枚舉類型、void類型和派生類型,由于本文的關(guān)注點(diǎn)是類型轉(zhuǎn)換問題,因此只考慮基本類型和派生類型。
基本類型,即bool、char、short、int等。
派生類型,包括指針類型、數(shù)組類型、結(jié)構(gòu)類型、聯(lián)合體類型和函數(shù)類型。
不同數(shù)據(jù)類型可能占用不同的存儲(chǔ)空間,占用不用存儲(chǔ)空間的類型之間轉(zhuǎn)換存在越界讀的風(fēng)險(xiǎn)。本文對(duì)所有的數(shù)據(jù)類型按占用的存儲(chǔ)空間大小進(jìn)行分組(以64位機(jī)器為標(biāo)準(zhǔn)):
① 占用1字節(jié)類型:bool、char、unsigned char;
② 占用2字節(jié)類型:short、unsigned short;
③ 占用4字節(jié)類型:int、unsigned int、float;
④ 占用8字節(jié)類型:long、unsigned long、double、long int、long long;
⑤ 占用16字節(jié)類型:long double;
⑥ 指針類型(數(shù)組類型可以視為指向數(shù)組開始元素的指針);
⑦ 結(jié)構(gòu)體、聯(lián)合體等由多種類型復(fù)合而成的復(fù)雜數(shù)據(jù)類型。
typedef定義的類型別名,在編譯器做宏展開之后根據(jù)實(shí)際占用空間進(jìn)行分類。
此外,在考慮變量類型的同時(shí),還應(yīng)當(dāng)考慮其作用域。C/C++中可在以下區(qū)域聲明變量:
① 局部變量,在函數(shù)或代碼塊內(nèi)部聲明;
② 全局變量,在函數(shù)外部聲明;
③ 形參,在函數(shù)的參數(shù)定義中聲明。
局部變量只能在函數(shù)內(nèi)部被該函數(shù)或代碼塊內(nèi)的語(yǔ)句使用,在函數(shù)外部不可見。全局變量定義在函數(shù)外部,通常是在程序頂部,在整個(gè)程序的生命周期內(nèi)都是有效的,在任意函數(shù)內(nèi)部均可訪問。形式參數(shù)即函數(shù)參數(shù),可以作為函數(shù)內(nèi)部的局部變量使用。
變量在定義時(shí)就確定了作用范圍,不同的變量其生命周期也可能有所不同。本文為了對(duì)不同變量的作用域和生命周期進(jìn)行管理,確保能夠在必要的時(shí)候?qū)ψ兞窟M(jìn)行跟蹤,按定義變量的位置對(duì)變量的作用域和生命周期進(jìn)行了劃分:
① 外部變量:所有定義于函數(shù)外部的變量,包括函數(shù)和全局變量;
② 內(nèi)部變量:所有定義于函數(shù)內(nèi)部的變量,包括局部變量和形式參數(shù)。
之所以這樣處理,是因?yàn)榭紤]到帶返回值函數(shù)的函數(shù)返回值也可以在賦值操作的過程中作為賦值符號(hào)“=”的右值參與隱式類型轉(zhuǎn)換,也可以作為函數(shù)調(diào)用的參數(shù),所以從實(shí)際效果上看,帶返回值的函數(shù)實(shí)際上是一類特殊變量,變量類型由函數(shù)返回值(函數(shù))類型決定。由于C/C++語(yǔ)法規(guī)定函數(shù)不允許嵌套定義,即函數(shù)必須定義在其他任意函數(shù)的外部,因此從某種意義上帶返回值的函數(shù)可以當(dāng)作全局變量處理。基于此分析,本文在檢測(cè)過程中將函數(shù)和全局變量視為一類一起檢測(cè),省去了對(duì)函數(shù)和變量的區(qū)分操作,一定程度上提高了檢測(cè)效率。
基于LLVM的中間代碼可以提取出被測(cè)程序的變量類型信息,該類信息被寫入變量表。變量表的數(shù)據(jù)結(jié)構(gòu)如下:
//類型劃分
enum
{
SINGLE,//1字節(jié)
DOUBLE, //2字節(jié)
QUAR, //4字節(jié)
OCT, //8字節(jié)
HEX, //16字節(jié)
POINTER, //指針類型
COMPLEX, //復(fù)雜類型
}type;
//作用域劃分
#define OUTER 1 //外部變量
#define INNER 0 //內(nèi)部變量
struct
{
long nameHash; //對(duì)變量名Hash
int type;
int scope;
complex_type typeLayout; //復(fù)雜類型存儲(chǔ)對(duì)應(yīng)結(jié)
//構(gòu)樹,否則為null
}TypeInfo;
SINGLE、DOUBLE、QUAR、OCT、HEX、COMPLEX、OTHER分別對(duì)應(yīng)1字節(jié)、2字節(jié)、4字節(jié)、8字節(jié)、16字節(jié)、復(fù)雜類型和其他類型。某個(gè)劃分可能包含不止一種變量類型,這種劃分方式大大簡(jiǎn)化了C/C++中的變量類型數(shù)量,基于該簡(jiǎn)化模型可以更好地對(duì)類型轉(zhuǎn)換進(jìn)行歸類,提高檢測(cè)效率。
校驗(yàn)復(fù)雜數(shù)據(jù)類型間的轉(zhuǎn)換較基本類型而言更為復(fù)雜。為了比較兩種復(fù)雜數(shù)據(jù)類型的結(jié)構(gòu)一致性,本文將復(fù)雜數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)為一顆名為typeLayout的樹。該樹的數(shù)據(jù)結(jié)構(gòu)中包含了復(fù)雜數(shù)據(jù)結(jié)構(gòu)各個(gè)成員變量相對(duì)于起始內(nèi)存空間的偏移。復(fù)雜類型存儲(chǔ)樹數(shù)據(jù)結(jié)構(gòu)如下:
//復(fù)雜類型存儲(chǔ)樹節(jié)點(diǎn)
typedef struct LayoutNode
{
long memberHash;
int offset;
LayoutNode child;
}LayoutNode;
//復(fù)雜類型存儲(chǔ)樹
typedef struct typeLayout
{
int scope;
long complex_structure_name_hash;
LayoutNode root;
}typeLayout;
本文通過類型信息提取算法抽取出被測(cè)程序的類型信息。該算法以程序源碼文件為單位,遍歷函數(shù)中的每一條語(yǔ)句,如果在語(yǔ)句中發(fā)現(xiàn)類型定義,則將該處的類型信息(變量名稱、變量類型、作用域)以ExtractTypeInfo函數(shù)抽取出來(lái)。該函數(shù)的偽代碼如下:
1 ExtractTypeInfo(type):
2 if type is complex_type
3 NoteAsComplexType(type)
4 typeLayout = AnalyseComplexType(type)
5 CollectInfo(type, typeLayout)
6 else
7 CollectInfo(type, NULL)
在實(shí)際提取過程中,會(huì)出現(xiàn)這樣一種情形:在被測(cè)程序中有A、B兩個(gè)函數(shù),兩個(gè)函數(shù)中均存在變量名為example但類型不同的變量。由于在提取變量的過程中沒有對(duì)同名變量做區(qū)分,后出現(xiàn)的變量將會(huì)覆蓋前面出現(xiàn)的變量的類型,導(dǎo)致類型信息提取過程中的信息丟失。
為此,在提取變量類型的過程中,我們使用如下方法來(lái)處理不同函數(shù)有相同變量名的情況:在對(duì)變量名進(jìn)行Hash操作時(shí),先用該變量所在的位置和變量名進(jìn)行拼接組成一個(gè)新串,對(duì)拼接得到的新串再進(jìn)行Hash操作。這樣直接通過Hash值區(qū)別了不同作用域下的同名變量。
在占用不同存儲(chǔ)空間大小的變量間進(jìn)行類型轉(zhuǎn)換顯然是存在風(fēng)險(xiǎn)的,如將int型變量轉(zhuǎn)為double型變量,程序原本只能讀4字節(jié)的內(nèi)存空間,卻在轉(zhuǎn)換后讀取8字節(jié),會(huì)造成越界讀的安全問題。
對(duì)基本類型而言,我們定義了一個(gè)類型轉(zhuǎn)換操作表。該操作表以一個(gè)5×5大小的二維數(shù)組basic_conversion_set存儲(chǔ)。如表1所示,int型變量向double型變量轉(zhuǎn)換時(shí)是不安全的,因此basic_conversion_set[QUAR][OCT]=US(UNSAFE);而double型變量向int型變量轉(zhuǎn)換時(shí),雖然會(huì)有精度損失,但是并不算安全問題,因此basic_conversion_set[OCT][QUAR]=S(SAFE)。而對(duì)于同級(jí)別類型之間的轉(zhuǎn)換,主要考察其值域是否匹配,例如對(duì)于int型變量x,當(dāng)x=1時(shí),將其轉(zhuǎn)換為unsigned int是安全的;但是當(dāng)x=-1時(shí),該轉(zhuǎn)換是不安全的。特殊的,指針、整數(shù)和浮點(diǎn)值均可以隱式地轉(zhuǎn)換為bool類型:非0值轉(zhuǎn)換為1,0值轉(zhuǎn)換為0。
表1 基本類型類型轉(zhuǎn)換操作表
對(duì)指針類型而言,其指向內(nèi)存的某一塊區(qū)域,編程人員可以對(duì)該區(qū)域進(jìn)行操作。涉及指針類型的轉(zhuǎn)換:
① 一個(gè)指向任何對(duì)象類型的指針都可以轉(zhuǎn)換為void*類型;
② 兩個(gè)void*類型變量之間可以互相轉(zhuǎn)換,而且可以顯式地將void*轉(zhuǎn)換為另一個(gè)類型;
③ 除此以外的其他操作都是不安全的。
對(duì)復(fù)雜類型而言,可以將其視作多種數(shù)據(jù)類型組成的集合,其安全轉(zhuǎn)換必須滿足以下條件:
① 類型占用的內(nèi)存空間大小相同;
② 成員變量相對(duì)于起始地址的偏移相同;
③ 成員變量間的轉(zhuǎn)換是安全的。
基于上述分析,我們實(shí)現(xiàn)了類型轉(zhuǎn)換的安全性判定算法,且整個(gè)檢測(cè)過程執(zhí)行如下:遍歷程序中的每條語(yǔ)句,判斷語(yǔ)句中是否存在變量;如果語(yǔ)句中存在變量,進(jìn)一步判斷是對(duì)該變量的定義還是引用;如果是定義則執(zhí)行類型信息提取算法;如果是引用則執(zhí)行安全性檢測(cè)算法。該算法的偽代碼如下:
1 VerifySafety(statement)
2 {
3 if NotImplicitTypeConvertion(statement)
4 return
5 else
6 TypeInfo origin = GetOriginInfo(statement)
7 TypeInfo target = GetTargetInfo(statement)
8 if(size(origin.type)>size(target.type))
9 UNSAFE
10 else if(origin.type && target.type are BASIC)
11 CheckBasicType(origin, target, basic_conversion_set)
12 else if(origin.type || target.type is POINTER)
13 CheckPointerType(origin, target)
14 else if(origin.type || target.type is COMPLEX)
15 CheckComplexType(origin, target)
16 else
17 SAFE
18 }
該函數(shù)首先對(duì)語(yǔ)句進(jìn)行判斷,如果該語(yǔ)句包含類型轉(zhuǎn)換操作,那么進(jìn)一步提取語(yǔ)句中進(jìn)行類型轉(zhuǎn)換的兩個(gè)變量的類型信息。根據(jù)提取到的類型信息,先比較兩者的內(nèi)存空間占用,只有在內(nèi)存空間相同的情況下才進(jìn)行后續(xù)判斷。
本文基于LLVM編譯器實(shí)現(xiàn)對(duì)類型轉(zhuǎn)換的安全性檢測(cè),為了更方便地使用LLVM編譯環(huán)境,選擇了Ubuntu18.04操作系統(tǒng)。實(shí)驗(yàn)所用計(jì)算機(jī)的處理器為Intel Core i7- 4790 3.60 GHz 8核,內(nèi)存32 GB,編譯器選擇gcc 8.1.0,項(xiàng)目構(gòu)建選擇cmake 3.13.4,LLVM選擇了9.0.0。
本文從美國(guó)國(guó)家標(biāo)準(zhǔn)與技術(shù)研究院[14](National Institute of Standards and Technology,NIST)的軟件保障參考數(shù)據(jù)集[15](Software Assurance Reference Dataset,SARD)中選取了與類型轉(zhuǎn)換相關(guān)的四個(gè)子集以驗(yàn)證上述方法的有效性。SARD以CWE[16](Common Weakness Enumeration)列表為基準(zhǔn),為列表中出現(xiàn)的各種類型的安全性問題設(shè)計(jì)了大量的測(cè)試用例。SARD類型轉(zhuǎn)換測(cè)試集如下:
CWE194_Unexpected_Sign_Extension
CWE195_Signed_to_Unsigned_Conversion_Error
CWE196_Unsigned_to_Signed_Conversion_Error
CWE843_Type_Confusion
其中,CWE194和CWE843對(duì)應(yīng)不同數(shù)據(jù)類型之間互相轉(zhuǎn)換的情況,而CWE195和CWE196對(duì)應(yīng)相同數(shù)據(jù)類型有符號(hào)和無(wú)符號(hào)之間互相轉(zhuǎn)換的情況。文件夾下每個(gè)文件對(duì)應(yīng)該類型相關(guān)的一組測(cè)試用例,測(cè)試用例的正誤以函數(shù)名區(qū)分:good表示該例安全,應(yīng)當(dāng)通過;bad表示該例不安全,應(yīng)當(dāng)報(bào)警。
圖4 函數(shù)名區(qū)分測(cè)試用例正誤
從上述數(shù)據(jù)集中提取出的測(cè)試用例共計(jì)1 147個(gè),其中成功檢測(cè)1 126個(gè),錯(cuò)誤檢測(cè)21個(gè),準(zhǔn)確率為98.17%。我們對(duì)錯(cuò)誤的21個(gè)測(cè)試用例進(jìn)行分析,其中一部分是由于在分析建模時(shí),我們?yōu)榱撕?jiǎn)化分析,未考慮C/C++的存儲(chǔ)類,如auto等;另外一部分是由于測(cè)試用例中的成員變量類型相同,但是具有不同的排列順序。C/C++為提高內(nèi)存的讀寫效率,變量在存儲(chǔ)時(shí)遵守對(duì)齊機(jī)制,可以通過特定的宏來(lái)定義默認(rèn)的對(duì)齊值。結(jié)構(gòu)體、聯(lián)合體在對(duì)齊機(jī)制的作用下,成員變量的排列順序可能會(huì)影響結(jié)構(gòu)體占用的內(nèi)存空間大小。
如圖5所示,A結(jié)構(gòu)體和B結(jié)構(gòu)體的成員變量及其類型完全相同,但是實(shí)際上,A結(jié)構(gòu)體在64位機(jī)器下占用的內(nèi)存大小為16字節(jié),而B結(jié)構(gòu)體占用的內(nèi)存大小為24字節(jié)??紤]到這類情況的出現(xiàn),本文方法的準(zhǔn)確率實(shí)際上要高于98.17%。
圖5 結(jié)構(gòu)體成員變量的對(duì)齊方式
為幫助編程人員規(guī)避由類型轉(zhuǎn)換所帶來(lái)的安全風(fēng)險(xiǎn),本文提出了一種針對(duì)C/C++隱式類型轉(zhuǎn)換產(chǎn)生的類型安全問題的編譯期檢測(cè)方法。該方法在編譯期作用于LLVM中間代碼,通過編寫并加載自定義的優(yōu)化器來(lái)實(shí)現(xiàn)類型轉(zhuǎn)換的安全性檢測(cè)。自定義優(yōu)化器繼承了LLVM內(nèi)置的pass,依次遍歷程序源代碼中的每條語(yǔ)句,判斷語(yǔ)句中是否存在變量;如果語(yǔ)句中存在變量,則進(jìn)一步判斷是對(duì)該變量的定義還是引用;如果是對(duì)變量的定義,則提取變量的類型信息并保存;如果是對(duì)變量的引用,則依據(jù)源變量和目標(biāo)變量的類型判斷其是否符合安全規(guī)則。實(shí)驗(yàn)結(jié)果表明,本文提出的方法能夠有效地檢測(cè)C/C++中出現(xiàn)的隱式類型轉(zhuǎn)換操作的安全性。