王 豫 , 高鳳娟 , 馬可欣 , 司徒凌云 , 王林章 , 陳碧歡 , 劉 楊, 趙建華, 李宣東
1(計算機軟件新技術國家重點實驗室(南京大學),江蘇 南京 210023)2(復旦大學 計算機科學技術學院,上海 200433)3(上海市數(shù)據(jù)科學重點實驗室(復旦大學),上海 200433)4(上海智能電子系統(tǒng)研究所(復旦大學),上海 200433)5(School of Computer Science and Engineering,Nanyang Technological University,Singapore)
信息物理融合系統(tǒng)(cyber-physical system,簡稱CPS)是計算過程和物理過程的融合系統(tǒng).它強調(diào)信息世界與物理世界的融合,強調(diào)對交互環(huán)境的實時監(jiān)測與控制.它通過與交互環(huán)境的實時交互來增加或擴展新的功能,以安全、可靠和實時的方式檢測或者控制一個物理實體.當前,CPS 系統(tǒng)被廣泛應用于重要基礎設施的監(jiān)測與控制、航天與空間系統(tǒng)、電力系統(tǒng)、鐵路系統(tǒng)和智能家居等諸多領域[1,2].CPS 系統(tǒng)中,不同異構組件的組合引起系統(tǒng)行為極為復雜[3].作為使命攸關與安全攸關的系統(tǒng),CPS 系統(tǒng)需要滿足實時性、安全性和可用性等特性.然而,CPS 系統(tǒng)的基本組件多由嵌入式系統(tǒng)構成,該系統(tǒng)主要是由以C/C++語言為代表的編程語言實現(xiàn).但是,由于開發(fā)者在C/C++語言程序中對于指針的錯誤使用,可能會導致CPS 系統(tǒng)行為不符合預期.其中,類似于緩沖區(qū)溢出、use-after-free 和double-free 的漏洞,會導致系統(tǒng)崩潰甚至執(zhí)行任意惡意代碼.在這3 種缺陷中,后兩個缺陷都是由垂懸指針引起的.
垂懸指針(dangling pointer)是由于指針或其別名所指向的內(nèi)存區(qū)域被釋放但指針本身未被置空.雖然垂懸指針未被訪問(解引用或釋放)時是安全的,但開發(fā)人員可能會無意中使用垂懸指針,在運行時導致潛在的可利用程序狀態(tài).攻擊者可以利用use-after-free 或double-free 漏洞來危害程序的正確性和安全性,如程序崩潰、信息泄露、權限提升,甚至執(zhí)行惡意代碼[4].自2008 年以來,此類漏洞的數(shù)量每年翻倍,并且由垂懸指針引起的漏洞越發(fā)普遍和危險[5].但是,由于復雜的函數(shù)調(diào)用和指針指向關系,開發(fā)人員很難杜絕垂懸指針.
當前,針對垂懸指針的方法有3 種.
· 首先,基于靜態(tài)分析的方法[4-6]一般有較高的誤報率和可擴展性問題(即僅適用于小規(guī)模程序);
· 其次,基于動態(tài)分析的方法[7-9]十分依賴于給定的一組測試用例,并且會面臨代碼覆蓋問題,可能有較高的漏報率.此外,這些方法通常需要大量的工作來監(jiān)控程序運行時的行為,從而導致過高的額外性能開銷(>100%);
· 最后,運行時防御方法[10,11]可以通過在運行時置空所有釋放內(nèi)存區(qū)域的指針來動態(tài)地保護垂懸指針,以免受攻擊.但是,這些方法需要額外的內(nèi)存和計算資源在運行時記錄和分析指針指向關系,然后根據(jù)指針指向關系置空潛在垂懸指針及其別名.因此,這些方法也會產(chǎn)生較高的額外性能開銷(>25%).
為了避免CPS 系統(tǒng)的中的垂懸指針被攻擊,開發(fā)者可以借助運行時防御方法,在運行時發(fā)現(xiàn)漏洞,從而避免該漏洞造成惡性影響.但是,當前主流[10,11]的運行時防御方法引入了較大的額外性能開銷.為了解決該問題,在本文中,我們提出了名為DangDone 的垂懸指針防御方法,通過在編譯時的程序轉換來定位潛在的垂懸指針,并防御use-after-free 或double-free 漏洞.確切地說,該方法先通過靜態(tài)方法分析潛在垂懸指針,然后通過在這些檢測出的指針(即潛在垂懸指針及其別名)和它們指向的內(nèi)存區(qū)域之間插入中間指針來保護每個潛在的垂懸指針.因此,將中間指針置空,能使?jié)撛诖箲抑羔樇捌鋭e名置空,消除垂懸指針的同時避免use-after-free 或double-free 漏洞造成的攻擊.
我們基于LLVM[12]實現(xiàn)了DangDone,該方法不需要開發(fā)人員的干預,并且能在編譯時自動運行.在實驗研究中,DangDone 成功防護了11 個開源項目中的use-after-free 和double-free 漏洞,體現(xiàn)出DangDone 的有效性.此外,我們使用SPEC CPU Benchmark[13]評估了DangDone 引入的運行時開銷.結果表明,運行時的開銷可以忽略不計(即平均約為1%).這使得它比目前的方法[10,11]更加有效.
總而言之,本文的創(chuàng)新貢獻如下.
· 提出基于靜態(tài)分析的垂懸指針檢測方法;
· 提出了一種通過程序轉換的輕量級方法來防御由垂懸指針引起的use-after-free 和double-free 漏洞;
· 開發(fā)了原型工具DangDone,通過實驗,體現(xiàn)DangDone 的有效性和高效率.
本文第1 節(jié)介紹垂懸指針及其相關漏洞.第2 節(jié)介紹DangDone 的方法.第3 節(jié)通過實驗評估DangDone 的有效性和性能,并討論DangDone 的局限性,第4 節(jié)回顧相關工作.第5 節(jié)對本文進行總結.
如果指針指向已被釋放的內(nèi)存區(qū)域(包括??臻g和堆空間內(nèi)存),則它是一個垂懸指針.如果一個垂懸指針永遠不被解引用,那么它是一個良性的垂懸指針;否則是一個不安全的垂懸指針[10].垂懸指針可以是堆空間垂懸指針或??臻g垂懸指針.如果指針指向一個對象并且該對象已被釋放,則指針變?yōu)槎芽臻g垂懸指針;如果指針指向棧變量并且棧變量超出其范圍,指針仍然指向棧上不再有效的地址,這時指針變?yōu)闂?臻g垂懸指針.圖1 顯示了堆空間垂懸指針的示例.在圖1(a)中,p1 和p2 在第8 行代碼后變成垂懸指針,因為在第8 行的內(nèi)存區(qū)域被釋放后p1 和p2 沒有置空.
Fig.1 An example of heap space dangling pointer圖1 堆空間垂懸指針示例
雖然垂懸指針分為棧空間指針和堆空間指針,但是本文主要關注堆空間指針,因為??臻g指針很難被利用[10].但是為了程序的正確性,DangDone 在必要的時候會轉換棧空間指針(在第2.3.1 節(jié)討論).堆空間中的垂懸指針是導致use-after-free 和double-free 漏洞的根本原因.釋放垂懸指針時會導致use-after-free 漏洞,并可能會導致不可預測的行為,因為內(nèi)存區(qū)域可能已被其他函數(shù)重用或存儲完全不同的數(shù)據(jù).如果垂懸指針指向的內(nèi)存區(qū)域被重復使用,則use-after-free 漏洞可能會泄漏關鍵信息.特權升級攻擊是通過與信息泄漏類似的方式實現(xiàn).即:垂懸指針指向的內(nèi)存區(qū)域用于檢查權限,并且該垂懸指針可以對內(nèi)存區(qū)域進行寫入.當函數(shù)指針使用垂懸指針時,攻擊者可以利用use-after-free 漏洞來執(zhí)行任意函數(shù).常見的劫持技術是擴散惡意內(nèi)容以覆蓋垂懸指針所指向的堆中的函數(shù)指針或?qū)ο蟮奶摫淼刂穂14].這種劫持技術在Linux 內(nèi)核中容易成功,因為內(nèi)核為了效率起見會更傾向于使用內(nèi)存分配的重用機制,這使得攻擊者更容易借助垂懸指針攻擊內(nèi)核[4].
use-after-free 漏洞的一個特例是double-free 漏洞,它是由兩次調(diào)用free函數(shù)引起的.double-free 漏洞可能導致內(nèi)存分配函數(shù)覆蓋存儲在塊中的內(nèi)存管理信息.用double-free 漏洞可能與利用use-after-free 漏洞一樣具有破壞性,可能導致遠程代碼執(zhí)行[15]等問題.
2.1.1 研究框架
DangDone 建立在對垂懸指針根本原因的觀察基礎上:指針在它們指向的內(nèi)存區(qū)域被釋放后沒有置空.為了將這些指針置空,我們可以在釋放內(nèi)存區(qū)域的語句之后插入置空操作.但是因為指針可能有許多別名,所以難以精確有效地對其進行分析.特別是靜態(tài)指針指向分析不能保證準確性;動態(tài)分析需要為每個指針操作設置跟蹤指令,會導致高開銷[7,10].因此,在指針被釋放后直接置空指針本身及其每個別名的方法不適用于實際的程序.為了解決這個問題,我們的主要想法是:在內(nèi)存區(qū)域和指向內(nèi)存區(qū)域的潛在垂懸指針之間插入一個中間指針,強制轉換中間指針的類型,使其對潛在的垂懸指針不可見,并使所有潛在垂懸指針指向中間指針.因此,在釋放內(nèi)存區(qū)域的語句之后對中間指針插入置空操作,將使所有潛在垂懸指針在執(zhí)行置空操作后指向NULL.此時,程序在嘗試解引用垂懸指針及其別名時會直接崩潰,而不會給攻擊者利用use-after-free 的機會.
基于上述觀察,我們提出了一種名為DangDone 的方法,通過編譯時的程序轉換自動消除垂懸指針.圖2 展示了DangDone 的框架,它將目標程序和配置文件的源代碼作為輸入,并返回受保護的二進制程序.配置文件給出內(nèi)存操作函數(shù)的信息(例如內(nèi)存分配、釋放和重新分配相關的函數(shù)名稱和參數(shù)信息).隨后進行缺陷檢測,以靜態(tài)地分析潛在垂懸指針.這些潛在垂懸指針將會作為輸入傳遞給程序轉換模塊.DangDone 的核心是通過程序轉換實現(xiàn)的,該轉換在 LLVM 中間表示層[16]上進行,將目標程序變換為受保護的二進制程序.如圖 2 所示,DangDone 先通過缺陷檢測模塊靜態(tài)地分析潛在垂懸指針,再根據(jù)其結果執(zhí)行指針傳遞和指針變換.在程序轉換時,DangDone 先通過指針傳遞查找由其他指針傳遞的指針,然后基于指針變換規(guī)則在指針變換模塊轉換這些指針.以這種方式,DangDone 的程序轉換將轉換與潛在垂懸指針具有指針指向關系的所有指針.即:若別名是傳遞指針的子集,則該潛在垂懸指針的所有別名都將被轉換.
具體來說,程序變換模塊為每個潛在垂懸指針執(zhí)行4 個步驟.
(1) 將分配的指針標識為潛在垂懸指針;
(2) 構造新的分配函數(shù),分配一個中間指針,將分配的內(nèi)存地址分配給中間指針,并返回中間指針;
(3) 用新的分配函數(shù)替換原來的分配函數(shù)(如malloc),轉換指針解引用指令,使內(nèi)存訪問與原始程序一致;
(4) 最后將被釋放的指針置為空.
Fig.2 Framework of Dangdone圖2 DangDone 的框架圖
DangDone 不需要別名分析或指針指向分析,因為所有與潛在垂懸指針相關的指針都將被轉換.通過這種方式,對潛在垂懸指針的防御被縮小到對中間指針的防御,這可以簡單地通過置空中間指針來實現(xiàn),因為潛在垂懸指針和它們的別名都指向那些中間指針.這些轉換由DangDone 自動完成,不需要用戶干預.轉換可以在源代碼層、中間層或二進制層進行.雖然當前DangDone 是在中間層實現(xiàn),但是為了便于演示,我們在文中使用源代碼級的示例.
2.1.2 示例
圖1(a)顯示了一個具有use-after-free 漏洞的程序.第10 行的漏洞源于第8 行的內(nèi)存釋放.它的源碼級轉換程序如圖1(b)第2 行~第12 行所示.這里,變量p1 是潛在垂懸指針,因為它指向由malloc分配的內(nèi)存區(qū)域,DangDone 將內(nèi)存分配函數(shù)替換為第14 行~第18 行定義的新分配函數(shù).新的分配函數(shù)包裝傳統(tǒng)的分配函數(shù),并在分配的內(nèi)存區(qū)域和用戶定義的指針之間插入一個中間指針.第6 行的指針傳遞語句p2=p1 表示p2 是p1 的使用點(也是別名),因此應轉換p2.結果,兩個指針都指向相同的中間指針.然后,DangDone 使用中間指針跟蹤原始指針的別名(第6 行),這樣指針p1 和p2 上的任何讀取或?qū)懭氲囊枚紝⑹紫仍L問中間指針*(char * *)p1,然后是*(char * *)p1 指向的內(nèi)存區(qū)域.由于庫函數(shù)strcpy和free的參數(shù)類型是char*,因此,參數(shù)分別由*(char * *)p1和*(char * *)p2 替換(第5 行、第8 行和第7 行、第10 行),以避免修改函數(shù)定義和維護原始程序的語義.DangDone在第9 行插入一個置空語句,以使指針*(char **)p1 置空,該指針的目標內(nèi)存區(qū)域被顯式釋放.因此在第10 行,由于p2 指向*(char * *)p1 且*(char * *)p1 指向NULL,*(char * *)p2 等于*(char * *)p1,程序?qū)⒈罎⒍皇怯|發(fā)useafter-free 漏洞以及給helloworld加上?.
圖3 和圖4 展示出了原始程序和圖1 中的變換程序的別名指向關系.這里,我們在圖3 中添加置空指針的操作(即內(nèi)存釋放后增加一個p1=null)以更好地說明最終的指向關系.在圖3 中,在使p1 置為NULL之后,p2 仍然指向被釋放的內(nèi)存.程序員應該在釋放對象時使所有別名無效,所以僅為被釋放的對象提供自動置空的方法不能消除垂懸指針.相反,在圖4 中,使兩個指針中的任何一個置空(即*(char * *)p1 或*(char * *)p2)都可以消除兩個垂懸指針.原因是p1 和p2 都指向相同的中間指針I(yè)P,并且置空被釋放的對象將消除垂懸指針.
Fig.3 Point-to relations for aliases in the original program in Fig.1圖3 圖1 中原始程序的別名指向關系
Fig.4 Point-to relation for aliases in the transformed program in Fig.1圖4 圖1 中修改后程序的別名指向關系
為清楚起見,在下文中,我們將用原始指針指代那些易受攻擊的指針,并以中間指針指代插入內(nèi)存區(qū)域和原始指針之間的指針.以DD開頭的函數(shù)表示這些函數(shù)是由DangDone 定義的.變量的使用點表示直接使用變量的指令.
DangDone 進行保守的靜態(tài)分析,以確定需要轉換的潛在易受攻擊的指針.此步驟減少了在其后的程序轉換中需要考慮的指針數(shù)目.實際上,這一步是可選的,因為DangDone 可以以效率為代價轉換程序中的所有指針.
靜態(tài)分析算法在算法1 中顯示,它主要作用是排除不是垂懸指針的指針.首先,它讀取配置文件以獲取內(nèi)存釋放和重新分配函數(shù)(系統(tǒng)的和自定義的)的列表(第1 行),因為這些函數(shù)返回的指針可能是垂懸指針.這些內(nèi)存釋放和重新分配函數(shù)也會用于程序轉換.然后分析程序的調(diào)用圖并獲得調(diào)用圖的逆拓撲排序(第2 行、第3 行),這是為了確保在函數(shù)的調(diào)用點中,被調(diào)用者始終是被分析過的.接下來,對于每個函數(shù),它獲取此函數(shù)的所有函數(shù)調(diào)用,并標識作為內(nèi)存釋放或重新分配的函數(shù)調(diào)用的參數(shù)的指針(第5 行~第9 行).這些指針可能是垂懸指針,因為它們涉及內(nèi)存釋放或重新分配.
在第2 個循環(huán)中,對于每個可疑指針,分析其別名,如果有一個指針的所有別名未都被置空,DangDone 將該指針標記為垂懸指針(第10 行~第16 行).在第11 行、第12 行,因為全局指針分析復雜,DangDone 直接將全局指針視為垂懸指針.第16 行的updateMemFuns用于分析過程間垂懸指針.這種類型的垂懸指針是由在被調(diào)用者內(nèi),作為參數(shù)的指針被釋放但沒有在該函數(shù)內(nèi)被置空引起的.為簡單起見,DangDone 將這些函數(shù)視為一種特殊類型的內(nèi)存釋放函數(shù).因此,DangDone 將在其他函數(shù)調(diào)用它們時檢查對應的實參是否在調(diào)用者內(nèi)被置空.第13行的別名分析基于Steensgard 的算法[17].最后,在第17 行、第2 行,它提取潛在垂懸指針的信息并輸出.所輸出的潛在垂懸指針的信息包括指針名稱、函數(shù)名稱、代碼行和文件名稱.開發(fā)人員可以進一步驗證這些指針,以減少誤報的數(shù)量.
算法1.保守的靜態(tài)分析.
在設計靜態(tài)分析時,更準確的靜態(tài)分析也是可行的.例如,像Andersens 算法[18]這樣更精確的別名分析可以進一步減少誤報.DangDone 也可以在沒有靜態(tài)分析的情況下轉換所有指針,目前的保守靜態(tài)分析會引入許多誤報,但可以避免漏報.沒有漏報的原因是垂懸指針的漏報來源于不精確的別名分析,而基于Steensgard 的別名分析是沒有漏報的.我們設計這種保守的靜態(tài)分析的原因是誤報只會增加開銷,而漏報可以繞過DangDone 的策略.因此在這里,我們嘗試通過排除不是垂懸指針的指針來減少開銷.
在現(xiàn)實世界的程序中,指針非常復雜.因此,我們研究了SPEC CPU Benchmark[13]測試中指針的使用統(tǒng)計數(shù)據(jù):27.3%的指針指向基本類型,41.7%的指針指向類或結構,16.3%的指針指向結構中的元素,6.4%的指針指向模板(包括用戶定義的模板和容器),8.3%的指針是二級指針.
此外,我們根據(jù)操作是否對指針和內(nèi)存產(chǎn)生副作用對操作進行分類:指針的副作用意味著指針指向不同的內(nèi)存區(qū)域;對內(nèi)存的副作用意味著內(nèi)存區(qū)域被改變.
· 對指針和內(nèi)存沒有副作用:例如指針傳遞和解引用;
· 僅對指針產(chǎn)生副作用:例如malloc或第三方用戶定義的分配函數(shù)以及指針運算;
· 副作用僅限于內(nèi)存中:例如free或第三方或用戶定義的釋放函數(shù);
· 對指針和內(nèi)存的副作用:例如,realloc或第三方或用戶定義的重新分配函數(shù).
基于上述統(tǒng)計和類別,我們通過圖1 和圖5 中的示例展示變換規(guī)則.其中,ele表示復雜結構的類型.
Fig.5 Examples for pointer modification rules圖5 指針變換規(guī)則的例子
2.3.1 指針傳遞和解引用的規(guī)則
我們首先介紹指針傳遞和解引用的變換規(guī)則,它們對指針和內(nèi)存沒有任何副作用.
規(guī)則1.DangDone 通過檢查被分配的內(nèi)存區(qū)域是否被訪問來區(qū)分指針傳遞和解引用.如果未訪問所分配的存儲區(qū),則視為指針傳遞;否則視為指針解引用.例如,指針傳遞包括指針賦值、函數(shù)參數(shù)和獲取指針的地址.
規(guī)則2.如果指針由潛在垂懸指針傳遞,則指針也是潛在垂懸指針.示例如圖1 中的第6 行,其中p2=p1 表示p2 也是潛在垂懸指針.
規(guī)則3.如果原始指針被解引用,DangDone 將指針轉換為二級指針,并將其使用替換為額外的解引用.此時,原始指針變成了二級指針,因為新的分配函數(shù)(參見圖1 的DDMalloc)與原來的分配函數(shù)不同.如圖1 中的例子所示,第10 行中的p2 由*(char **)p2 代替.附加的引用可以使原始程序和轉換程序之間的內(nèi)存保持一致.因此,如果解引用原始一級指針,DangDone 將用類型轉換和原始指針的雙重解引用替換原先的使用點.
對于變換后的程序,原始指針的指針傳遞不會改變,但是原始指針的解引用會被類型轉換和附加的解引用替換.因此,中間指針對于程序的其余部分是不可見的.
然后,我們將展示如何將這些規(guī)則應用于復雜的指針,包括過程間指針、函數(shù)指針、間接指針、結構中的指針和棧空間指針.
· 過程間指針
過程間指針可以是兩種類型:全局指針和作為函數(shù)參數(shù)傳遞的局部指針.全局指針的轉換規(guī)則同局部指針.
如果原始指針作為函數(shù)參數(shù)傳遞,DangDone 傳遞原始指針而不是指向參數(shù)的間接引用指針,以使指針指向關系保持與原始程序一致.例如:在圖5 中的第6 行有一個函數(shù)調(diào)用foo(p),p是潛在垂懸指針;并且轉換后的函數(shù)調(diào)用仍然是foo(p).但是,foo的參數(shù)p與原始語義不同.因此,如果函數(shù)的參數(shù)已經(jīng)轉換,則需要轉換相應的函數(shù)參數(shù).在這種情況下,還應轉換foo的參數(shù)以保持內(nèi)存訪問的一致性.然后,DangDone 搜索定位函數(shù)的所有使用點,以使它們的調(diào)用者的相應參數(shù)也被轉換.這里有一個例外是處理庫函數(shù)的參數(shù)時,DangDone 將參數(shù)替換為對原始指針的強制轉換和解引用(如圖1 中的第10 行).原因是當函數(shù)的參數(shù)改變時,這些參數(shù)的使用點需要進行相應的修改.因此,DangDone 不能轉換沒有源代碼的庫函數(shù).
· 函數(shù)指針
C/C++中的一個特殊結構是指向函數(shù)的指針,這些函數(shù)指針為攻擊者提供了一種通過利用use-after-free 漏洞來注入代碼的方法[4,19].據(jù)我們所知:盡管函數(shù)指針可以允許攻擊者執(zhí)行任意代碼,但函數(shù)指針不會直接導致use-after-free 或double-free 漏洞,但其他類型的指針(如指向結構的指針)確實會引發(fā)此類漏洞.因此,DangDone不處理函數(shù)指針.但是,轉換的指針可以用作函數(shù)指針的參數(shù).在這種情況下,定義滿足函數(shù)指針類型的函數(shù)將進行相應的轉換.
· 間接指針
我們將具有兩個或以上級別的指針稱為間接指針.如果我們對所有一級指針(即直接指針)進行保護,那么間接指針不會變成垂懸指針.但是,盡管間接指針不需要關心內(nèi)存區(qū)域,但DangDone 應該對它們進行轉換,因為它們可以通過兩個或多個解引用來訪問中間指針.因此,DangDone 會分析間接指針并在它們訪問中間指針時對它們進行轉換,使得訪問期望的內(nèi)存空間.
· 結構體中的指針
對于結構體中的指針(例如數(shù)組、結構、類和模板),它們的轉換遵循與其他指針相同的規(guī)則.不同之處在于解引用操作,因為訪問結構的方式是多種多樣的.為了使內(nèi)存訪問與原始程序保持一致,我們應該考慮內(nèi)存訪問的特性.數(shù)組中指針變換的一個例子如圖5 中的第8 行~第13 行所示.當libFun調(diào)用它時,DangDone 應轉換指針a1[i],因為它是一個庫調(diào)用.另一個顯示結構中指針轉換的例子如圖5 中的第14 行~第18 行所示.指針p→var被轉換為二級指針并解引用,以便libFun的參數(shù)與原始程序保持一致.
· ??臻g指針及常量地址
雖然我們專注于堆空間垂懸指針,但由于指針傳遞,??臻g指針也可能會被轉換.例如:指針在一個分支中,它指向的是分配的內(nèi)存;在另一個分支中,它指向的是一個棧變量.DangDone 通過為潛在垂懸指針傳遞的指針構造一個中間棧指針來轉換??臻g指針.中間指針的生命周期與它指向的內(nèi)存區(qū)域相同.因此,棧和堆空間指針的操作是一致的.在CPS 系統(tǒng)中,常量地址相較于常規(guī)應用程序更為常見.它是指針傳遞過程中其中一個別名被賦值成一個常量地址.針對常量地址的賦值,我們也應用相同的處理方式.示例代碼如圖6 所示.
Fig.6 Examples of stack space pointer and pointer from constant address圖6 ??臻g指針和常量地址例子
2.3.2 內(nèi)存分配函數(shù)的規(guī)則
現(xiàn)在我們介紹對指針和內(nèi)存有副作用的操作規(guī)則,以便在轉換后指針和內(nèi)存仍然保持一致.潛在垂懸指針被釋放或重新分配時,如果它們指向釋放的內(nèi)存,我們應該將它們置空.此外,還應考慮一個指針的多重分配.指針運算將在第3.4 節(jié)討論.
規(guī)則4(內(nèi)存釋放).為了防護潛在垂懸指針,如果釋放了一個內(nèi)存區(qū)域,DangDone 會中間指針插入置空語句.如果此時程序執(zhí)行到內(nèi)存釋放語句和置空語句后,原始指針及其別名都最終指向NULL.圖1 中的第9 行顯示了一個示例.由于所有別名都指向同一個中間指針,因此無論它們是顯式釋放還是隱式釋放,所有別名都將置空.從這個意義上講,DangDone 只需要為一次內(nèi)存釋放插入一個置空語句.
規(guī)則5(內(nèi)存重新分配).DangDone 對重新分配進行包裝,以便在重新分配函數(shù)返回的指針與傳遞給函數(shù)[11]的原始指針不同時,置空原始指針并為新返回的指針添加一個中間指針.這個規(guī)則是由于類似于realloc(·)的內(nèi)存分配函數(shù)可以增加或減少所分配的內(nèi)存空間.但是如果所需的內(nèi)存空間太大,則重新分配的地址可能與原始地址不同.所以DangDone 在所分配地址和原始地址不同時,置空原始指針并為新指針加入中間指針.
規(guī)則6(內(nèi)存分配和多重分配).對于每個潛在垂懸指針,我們用DangDone 定義的新內(nèi)存分配函數(shù)替換原來的內(nèi)存分配函數(shù)(包括堆和棧空間內(nèi)存).新的內(nèi)存分配函數(shù)在調(diào)用原內(nèi)存分配函數(shù)的同時,還分配了指向內(nèi)存區(qū)域的中間指針.然后返回中間指針的地址,而不是所分配的內(nèi)存區(qū)域的地址.請注意:此時原始指針變?yōu)槎壷羔?DangDone 將類型轉換為一級指針.使用新分配替換分配的目的是強制所有指針及其別名都指向中間指針.因此,該替換應該分析指針的級別(如是否是間接指針),除了在內(nèi)存區(qū)域和一級指針之間插入中間指針外,應避免在其他指針之間插入中間指針.例如,圖1 中的DDMalloc展示了這種新分配的過程.關于分析指針級別的例子,如例子程序char * *p=malloc(size_t);char *p=malloc(32),此轉換應只替換第2 個內(nèi)存分配(即malloc(32)).
多重分配是指一個程序分配了一個新的內(nèi)存區(qū)域并將其分配給一個已經(jīng)指向內(nèi)存區(qū)域的指針.處理方式依舊遵循規(guī)則6 以避免語義上的差異.換句話說,DangDone 不去區(qū)分某處的內(nèi)存分配是否是多重分配.但是,替換分配的副作用是內(nèi)存泄漏.分配引入的潛在內(nèi)存泄漏問題將在第3.4 節(jié)中討論.
2.4.1 指針傳遞
DangDone 跟蹤從原始指針傳遞的每個指針的指針傳遞,并遞歸地變換這些指針.它通過記錄原始指針和轉換原始指針的使用點來實現(xiàn).原始指針的指針指向結果存儲在它們相應的中間指針中,因此,指針保持著與原始程序相同的指針指向關系.
算法2 介紹了遞歸變換指針的過程.輸入程序是需要保護的代碼.DangDone 首先分析程序,找出指向已被分配的內(nèi)存區(qū)域(第1 行)的潛在垂懸指針.然后將潛在垂懸指針存儲在棧PointerStack(第2 行)中.對于棧頂部的每個指針,首先替換指針oriPointer(第5 行)的分配函數(shù)(若有)(第4 行).相反,如果它指向??臻g(第7 行),則為指針oriPointer(第6 行)插入一個中間指針,然后,DangDone 將原始指針標記為已修改以供進一步使用(第8 行).然后,DangDone 基于Def-Use Chains 和Use-Def Chains[16](第10 行)識別所有使用點(例如使用原始指針的指令).對于每個使用點,DangDone 通過函數(shù)getOtherHandSide獲得使用點(第11 行)也使用的指針otherPointer,該函數(shù)確定與原始指針相關的指針(例如別名和解引用).由于某些指令只有一個操作數(shù),因此otherPointer可以為NULL.如果存在otherPointer,則檢查otherPointer是否已經(jīng)轉換(第13 行);如果沒有,則將此指針存到PointerStack.請注意:如果未轉換otherPointer,則不會轉換此使用點;但當兩個指針都被標記為已修改時,它將被轉換.在添加潛在的潛在垂懸指針后,DangDone 使用第18 行的函數(shù)transformPointer來轉換指令.如果指令是指針傳遞,它將保持指向關系;如果指令中未引用指針,則它將替換為其他引用.詳細的轉換算法見第2.4.2 節(jié).由于DangDone 是在潛在垂懸指針使用到這些指針時變換這些指針,因此它不需要進行額外的別名分析.雖然遞歸變換可能會導致指針別名的大量轉換代碼帶來的額外開銷,但實驗表明這個開銷是很小的.
算法2.遞歸變換指針.
例:對于圖1 中的程序,PointerStack只包含一個指針p1,因為只有這個指針被顯式分配.DangDone 首先替換內(nèi)存分配函數(shù),以使原始指針p1 指向中間指針.然后搜索p1 的所有使用點,發(fā)現(xiàn)p1 有4 個使用點.因為其中3個沒有對應的操作數(shù),所以將他們傳遞到transformPointer.與之對應,由于p2=p1 有對應的操作數(shù)p2,并且發(fā)現(xiàn)該指針尚未被修改,即p2 未標記為已修改,因此,DangDone 將它存到PointerStack,以便之后的指針變換.注意:即使p2 不在初始的PointerStack中,DangDone 也可以保護這個指針,因為當p1 的轉換完成時,PointerStack上的頂部變量是p2,此時指令p2=p1 將被轉換.
2.4.2 指針變換
為清楚起見,算法3 僅顯示常見場景的轉換.輸入包括指令、指令中使用的原始指針;算法的輸出是轉換后的指令.首先對指令進行分析,確定操作類型.如果指令用于指針傳遞,則不需要轉換指令.這是因為轉換的一個目標是使內(nèi)存訪問與原始程序保持一致,并且一級指針的指針傳遞與二級指針相同.換句話說,對于指針傳遞,無論指針的級別如何,指向關系都是相同的.如果指令解引用原始指針或調(diào)用庫函數(shù),它將用另外的解引用替換原始指針的解引用(第1 行~第3 行).由于解引用指針的目的是訪問指針所指向的內(nèi)存區(qū)域,因此它添加了原始指針的額外的解引用,以使原始指針的內(nèi)容與原始指令想要訪問的內(nèi)容一致.大多數(shù)情況下,如果原始指針解引用一次,則只需要對原始指針進行兩次解引用.對于對指針或內(nèi)存有副作用的操作,轉換策略顯示在第4 行~第8行.如果指令釋放原始指針,則它插入指令將中間指針置空(第6 行).如果指令重新分配原始指針,它將使用DangDone 定義的新的重分配替換.它添加了置空語句,根據(jù)重新分配的指針是否等于原始指針(第8 行),將指針置為空并插入一個中間指針.
算法3.變換指針.
例:繼續(xù)圖1 中的程序.DangDone 分析到在轉換p1 時,p1 的3 個使用點被傳遞到這個階段.因為它使用*(char**)p1 作為中間指針,p1=malloc(32)轉換為p1=DDMalloc(32);strcpy(p1,“hello”)轉換為strcpy(*(char * *)p1,“hello”);free(p1)被free(*(char **)p1)替換,并插入額外的置空操作.在轉化p1 之后,p2 的3 種用途被傳遞到該階段.兩個函數(shù)調(diào)用的轉換類似.p2=p1 不需要變換,因為它屬于指針傳遞操作,并且兩個指針被標記為已修改.
該方法適用于源代碼、中間表示、甚至二進制.目前,為了提高效率和跨語言支持的能力,我們在基于LLVM[12]的中間表示(IR)級別實現(xiàn)了DangDone(https://bitbucket.org/fff000147369/nodang).DangDone 中的轉換被實現(xiàn)為LLVM Pass.LLVM Pass 執(zhí)行構成了編譯器的轉換和優(yōu)化[20].因此,DangDone 的核心流程是:通過Clang的AST 分析潛在垂懸指針;開發(fā)者選擇潛在垂懸指針用于被防護;Clang[21]將目標項目編譯為中間表示(LLVM IR[16]);DangDone 應用LLVM Pass 來轉換LLVM IR;Clang 將轉換后的LLVM IR 編譯為二進制.
我們對DangDone 的實驗旨在回答以下3 個研究問題.
(1) 防護能力:DangDone 是否有效地防護use-after-free 和double-free 漏洞?
(2) 運行時開銷:就時間和內(nèi)存而言,DangDone 是否有可接受的運行時開銷?
(3) 垂懸指針檢測效果:DangDone 的靜態(tài)分析是否有足夠的效果?
我們使用了 11 個真實的漏洞來評估 DangDone 的安全性,并使用 SPEC CPU Benchmark[13]來評估DangDone 的靜態(tài)分析效果和運行時開銷.SPEC CPU Benchmark 是一系列CPU 密集型程序的集合,著重于CPU、內(nèi)存以及編譯器的評估.實驗是在運行64 位Ubuntu 14.04 的8 核Intel Xeon CPU E5620(2.40GHz)和16 GB RAM 的PC 上進行的.使用LLVM 3.6.0 以無優(yōu)化(-O0)的方式編譯SPEC CPU Benchmark.并且為了展示獨立的實驗效果,靜態(tài)分析和代碼轉換模塊是各自單獨評估的.即,此次實驗的程序變換模塊將所有指針視為潛在的垂懸指針.
為了評估DangDone 對實際程序存在的漏洞的檢測預防措施,我們選擇在開源程序中并且有公開惡意輸入(如Exploit Database[22])的CVE 漏洞作為基準程序.選擇了11 個CVE 漏洞.這些漏洞的詳細信息列在表1 的前6 列中,包括CVE ID、受影響的程序和版本、漏洞類型以及使用的程序版本和缺陷位置.
Table 1 Evaluation results for real-world vulnerabilities表1 基于真實漏洞的評估結果
與此同時,為了評估DangDone 在CPS 系統(tǒng)中的有效性,我們選取了5 個開源CPS 系統(tǒng)中注入特定的缺陷,并借助DangDone 進行防護.所選取系統(tǒng)的相關信息見表2 的前3 列所示.所注入的缺陷信息如第4 列~第6 列所示.針對Use-after-free,注入缺陷的方式是在第6 列所指定的位置釋放指針,在下個使用點之前加入指針是否為空判斷語句.針對Double free,注入缺陷的方式是在指定位置插入free語句,并在插入的語句之前加入指針是否為空判斷語句.
Table 2 Evaluation results for real-world cyber-physical systems表2 基于CPS 系統(tǒng)的評估結果
實驗結果在表1 和表2 的最后兩列中報告,包括執(zhí)行惡意輸入時程序的行為以及由DangDone 轉換的程序在執(zhí)行惡意輸入后的行為.在表1 中,我們發(fā)現(xiàn)其中3 個程序,在其代碼被DangDone 保護后導致了段錯誤.原因是對空指針的解引用導致的.其余情況出現(xiàn)的原因則是因為在加入指針置空語句之后,原來的垂懸指針指向了NULL.而這些程序會判斷所使用的指針是否為NULL,如果發(fā)現(xiàn)為NULL則會進行相應處理,如直接退出或者報錯.在DangDone 轉換易受攻擊的程序之后,所有的漏洞都可以在執(zhí)行惡意輸入之后直接退出而不會被攻擊.在表2 中,由于注入的缺陷的特征,直接執(zhí)行有缺陷的結果都是程序崩潰,相比之下,借助DangDone 防護的程序則是能夠正常退出.因此,我們的實驗回答了RQ1,即DangDone 在防護垂懸指針引起的use-after-free 和double-free漏洞方面是有效的.并且,在實驗過程中,我們沒有發(fā)現(xiàn)DangDone 在指針變換時的誤報.
當DangDone 向目標程序插入新的指令時,它會增加目標程序的執(zhí)行時間和內(nèi)存消耗.理論上,如果程序的所有變量都是指針,并且除了指針解引用和指針傳遞之外沒有其他語句,那么最大運行時開銷接近100%.為了評估DangDone 對實際應用程序的運行時開銷,我們使用SPEC CPU Benchmark 分別運行3 次原始程序和轉換后的程序來測量時間開銷和內(nèi)存開銷.注意:由于編譯問題,沒有使用PerlBench 和Deall 這兩個基準程序.
圖7 顯示了DangDone 對特定CPU Benchmark 施加的時間開銷,這些Benchmark 的輸入是在Benchmark中名為“Ref”目錄中.我們使用幾何平均數(shù)計算平均開銷.平均而言,Dangdone 的時間開銷為 0.3%.這表明DangDone 引入了可忽略的運行時開銷.
圖7 還顯示了DangDone 引入的內(nèi)存開銷.我們通過當前內(nèi)存使用率的頻繁快照收集內(nèi)存使用率,并比較它們的最大內(nèi)存使用率.所有Benchmark 的內(nèi)存開銷都小于0.8%,這表明DangDone 引入的內(nèi)存開銷可以忽略不計.這是因為我們創(chuàng)建了很小的指針來跟蹤指針.內(nèi)存開銷由中間指針引入,因此,DangDone 引入的內(nèi)存開銷取決于內(nèi)存分配的數(shù)量.在大多數(shù)程序中,分配內(nèi)存的大小遠遠大于指針的大小,導致內(nèi)存開銷較低.
此外,我們在表3 中報告了規(guī)范CPU Benchmark 的轉換、指針覆蓋率和編譯統(tǒng)計信息.#m_ptr表示變換的指針數(shù),#m_ins表示變換的指令數(shù).由于指針可能有許多用途,因此變換過的指令的數(shù)量是一個更好的運行時開銷指標.結合表3 中的變換的指針、指令數(shù)和圖7 中的時間開銷,我們可以看到,增加的時間開銷和邊換的指針、指令數(shù)都是正相關的.此外,Dyn.Cov報告那些執(zhí)行代碼中由DangDone創(chuàng)建的所有指針中覆蓋的堆指針的比例.注意,我們使用動態(tài)覆蓋率是因為很難靜態(tài)地區(qū)分棧指針和堆指針.平均而言,DangDone 覆蓋了83%的堆指針,同時仍然保持著較低的時間開銷.編譯時,大多數(shù)Benchmark 的開銷可以忽略不計;平均而言,DangDone 在編譯時預防漏洞的時間開銷為0.9%,這表明DangDone 在編譯時引入的額外開銷也低.
Fig.7 Overhead for the SPEC CPU Benchmarks圖7 SPEC CPU Benchmarks 的運行時開銷
Table 3 Pointer coverage and compilation statistics for the SPEC CPU Benchmarks表3 基于the SPEC CPU Benchmarks 的指針轉換覆蓋率和編譯數(shù)據(jù)
總之,運行時開銷表明了DangDone 的正確性,因為SPEC CPU Benchmark 有結果比較機制,以確保結果與預先定義的結果相同.所以這部分實驗表明,DangDone 的方法在SPEC CPU Benchmark 不會對程序執(zhí)行有影響.同時,圖7 和表3 中的結果肯定地回答了RQ2,即:DangDone 對程序的開銷可以忽略不計,編譯的時間開銷也可以接受.
表4 展示了靜態(tài)分析的效果.該分析方法分為兩部分:讀取 AST 的時間和分析時間.為了提升效率,DangDone 通過動態(tài)讀取AST 以優(yōu)化內(nèi)存開銷.在該實驗中,我們依舊使用SPEC CPU 2006 Benchmarks,配置的動態(tài)讀取AST 的數(shù)目分別為1 000 和10 個.#ASTs和#KLOC是AST 的數(shù)目和程序行數(shù).針對這兩個AST 讀取數(shù)目的配置,我們分別統(tǒng)計他們的讀取時間,分析時間和內(nèi)存消耗.#Warnings是靜態(tài)分析報告中潛在垂懸指針的數(shù)目.在此次實驗中,我們只考慮free和delete這兩個內(nèi)存釋放函數(shù).FPRate是DangDone 在特定程序下的誤報率.誤報是通過人工檢查每個報告統(tǒng)計的.
我們可以看到:如果內(nèi)存足夠,DangDone 的靜態(tài)分析是輕量級的,并且它的平均分析速度是7KLOC/s 左右.盡管該靜態(tài)分析工具可能消耗4GB 的內(nèi)存,但所有的程序都可以在22s 內(nèi)進行分析.相比而言,當加載在內(nèi)存中的AST數(shù)被設為10 時,分析速度會降為1.8KLOC/s,但是內(nèi)存消耗會降到380Mb,幾乎所有現(xiàn)代PC 都可以提供該要求.此外,不同的程序消耗不同的內(nèi)存.一個原因在于AST的大小是不同的,例如有的AST大小為30MB 而有的為0.1MB.如果不限制內(nèi)存中加載的AST的數(shù)量,則遇到超大規(guī)模的項目時,很容易消耗所有內(nèi)存.
Table 4 Performance of static analysis for the SPEC CPU 2006 Benchmark表4 靜態(tài)分析方法在SPEC CPU 2006 Benchmark 上的性能
此外,為了評估靜態(tài)分析的誤報率,如果warning(告警)數(shù)小于50,則人工驗證所有warning;否則,隨機抽取50 個進行人工驗證.我們發(fā)現(xiàn),靜態(tài)分析的誤報率約為32.657%.此外,為了評估靜態(tài)分析的漏報率,我們使用Fortify SCA[23],Splint[24]以及Coverity[25]作為基準,共報告了106 處warning.具體來說,Fortify 報告了23 處warning,包括12 個double-free 和11 個use-after-free.Splint 和Coverity 分別報告了72 個和11 個warning.我們?nèi)斯を炞C這些warning并發(fā)現(xiàn)DangDone 的靜態(tài)分析部分也會報告所有warning,這說明DangDone 的靜態(tài)分析的漏報率為0.
綜上所述,表4 的結果可以回答RQ3,靜態(tài)分析可以通過減少潛在垂懸指針數(shù)量來減少DangDone 的時間開銷;這需要一定的時間開銷以及內(nèi)存開銷;并且具有可接受的誤報率和非常低的漏報率.
· 漏報
據(jù)我們所知:當應用到現(xiàn)實項目中時,DangDone 會引入漏報,即無法防御指針計算生成的指針.因為DangDone 試圖使所有別名指向中間指針,而指針算法不遵循此規(guī)則.例如,圖8 中的轉換程序創(chuàng)建一個生命周期與p2 相同的指針,然后將指針運算的結果賦給新創(chuàng)建的指針.最后,它將地址存儲到p2,使所有轉換的指針具有相同的級別.但是,第10 行的引用仍然是use-after-free.盡管DangDone 無法保護通過指針運算創(chuàng)建的潛在垂懸指針,但比起不轉換,不如對其進行轉換,因為我們?nèi)匀豢梢员Wo部分漏洞(例如指針的指針算法位于分支中時).在我們的實驗中,尚未發(fā)現(xiàn)與指針運算相關的垂懸指針.
· 間接賦值
別名可以通過間接賦值引入.對于存儲在模板中的原始指針(即用戶定義的模板和容器),我們遞歸地轉換模板中使用的所有指針(即模板的使用點).如果不轉換所有指針,會導致程序語義發(fā)生變化.
· 類型轉換
非指針類型變量之間的指針傳遞可能導致特殊別名,但是DangDone 可以識別這樣的指針傳遞;這些類型的指向關系可以被它們對應的原始指針跟蹤,它們的指針類型可以由DangDone 確定.
· 釋放中間指針
雖然DangDone 通過替換內(nèi)存分配函數(shù)分配了中間指針,但是不能通過替換內(nèi)存釋放函數(shù)釋放中間指針.這可能導致內(nèi)存泄漏.為了解決這個問題,我們通過靜態(tài)分析得到的保守的指針分析結果,在某個潛在垂懸指針及其別名中分析出生命周期最長的變量,確定其最后一次使用點所在的函數(shù).在該函數(shù)的返回之前,插入中間指針釋放語句.
· CPS 系統(tǒng)中的防護結果
雖然DangDone 針對use-after-free 和double free 漏洞的防護結果是導致程序崩潰,而CPS 系統(tǒng)中的可用性是一個重要指標,直接導致程序崩潰會影響其可用性,但是值得注意的是:在借助于來自CVE 的漏洞的實驗中,有一些程序能安全退出是通過檢測被使用的指針是否為NULL.所以在CPS 系統(tǒng)中,雖然我們能夠避免更嚴重的攻擊后果(如權限提升、執(zhí)行惡意程序等),但是依舊需要開發(fā)者有一定的容錯機制.例如,在指針的使用點需要檢測該指針是否為NULL:如果是,則進入錯誤處理.
Fig.8 Dangling pointer caused by pointer arithmetic圖8 指針計算導致的垂懸指針
我們改進了內(nèi)存分配函數(shù)來保護垂懸指針.Cling[26]替換了原來的內(nèi)存分配函數(shù),因為一些攻擊手段依賴于釋放內(nèi)存的重復使用,它會避免重復使用同一類型的內(nèi)存對象.DangDone 也用自定義的內(nèi)存分配函數(shù)替換了內(nèi)存分配函數(shù),但是Cling 的內(nèi)存分配函數(shù)側重于只復用特定被釋放的內(nèi)存,這段內(nèi)存的對象類型和釋放之前的對象類型一致.Cling 的時間開銷與DangDone 相似,但Cling 的內(nèi)存開銷略高一些.此外,Cling 不能處理由垂懸指針引起的所有類型的漏洞,例如無法防止由垂懸指針引起的信息泄漏.并且,該方法無法及時檢測到出現(xiàn)了useafter-free 等漏洞,相比之下,DangDone 在預防這類漏洞的同時,也能夠通過程序崩潰報告所出現(xiàn)的漏洞.Diehard[27]是一個運行時系統(tǒng),可以通過隨機化和復制來容忍內(nèi)存錯誤.可以通過內(nèi)存隨機化和復制來容忍內(nèi)存錯誤.它提供了一個近乎無限大的堆空間,使對象可以永遠不被釋放,并且各個對象可以無限遠地被分配.以此消除了垂懸指針,并且不易受到緩沖區(qū)溢出攻擊.但是空間消耗比傳統(tǒng)的內(nèi)存分配函數(shù)大,因為它需要至少兩倍的額外內(nèi)存空間去防止堆被破壞.Dieharder[28]通過改進隨機化操作擴展了這種方法,這使得開發(fā)變得更困難,并且降低了性能開銷.DieHarder 具有20%的幾何平均性能影響,并且該方法可能面臨能夠控制內(nèi)存分配的攻擊者的攻擊.相比之下,內(nèi)存開銷高于DangDone,少于DieHarder.
與動態(tài)方法或運行時保護相比,檢測垂懸指針的靜態(tài)方法相對少見.大多數(shù)靜態(tài)方法關注一組軟件缺陷,垂懸指針只是其中的一個.Musuvati 等人[29]實現(xiàn)一種新的模型檢查器,可以直接檢查C/C++程序,無需人工抽象系統(tǒng)行為.自動抽象模型是通過捕獲系統(tǒng)的所有行為來實現(xiàn)的[30].因為系統(tǒng)應該是一個面向行為的程序,這種技術的一個局限性是靈活性.Slayer[31]是一個驗證工具,用來證明系統(tǒng)代碼不存在內(nèi)存安全錯誤,例如垂懸指針解引用和double-free 問題.但是與我們的方法相比,它是一個重量級的工具,因為slayer 的可擴展性受到SMT 解算器的限制.
以檢測是否出現(xiàn)垂懸指針或其漏洞的方法可以分為兩種類型:一種關注use-after-free 漏洞;另一種是內(nèi)存錯誤檢測器,它還可以檢測垂懸指針或use-after-free 漏洞.Valgrind Memcheck[32]是一種內(nèi)存錯誤檢測器,可以檢測堆中的use-after-free 和double-free 漏洞.與purify[33]類似,這兩種工具都會產(chǎn)生很高的內(nèi)存和CPU 開銷(代價是2 倍~25 倍的性能損失),并且無法檢測到重新分配數(shù)據(jù)位置的垂懸指針.Mudflap[34]使用編譯工具在指針使用(解引用)時插入判斷內(nèi)存區(qū)域是否被合法引用的斷言.為了判斷內(nèi)存訪問的合法性,它還需要檢測來維護有效內(nèi)存對象的列表.然而,Mudflap 在SPEC CPU Benchmark 上的額外開銷范圍從2x~41x,在復雜的C++代碼中也有誤報率.一種類似于Mudflap 的方法是AddressSanitizer[35],它是一個基于Clang 的內(nèi)存錯誤檢測器.它使用影子內(nèi)存記錄應用程序內(nèi)存的每個字節(jié)是否可以安全訪問,并使用檢測工具檢查每個應用程序加載或存儲時的影子內(nèi)存.但是,為了性能考慮,影子內(nèi)存沒有映射到所有內(nèi)存分配函數(shù).該優(yōu)化導致漏報,如果訪問重新分配的內(nèi)存塊,將不會檢測到use-after-free.DangDone 會在釋放對象時置空垂懸指針,使這些漏洞無法利用.此外,由AddressSanitizer 所引入的減速是2 倍,這也高于DangDone.DangSan[36]方法則是側重于在多線程下高效率地通過日志檢測垂懸指針.它主要優(yōu)化了記錄指針指向、釋放信息所需要的時間,以此提高效率.
相比于RAII和C++11 及其之后版本中引入的智能指針(如shared_ptr),這兩種方法都能更方便地管理指針.但是相比之下,DangDone 有著更廣泛的適用性.這是因為一些系統(tǒng)如CPS 系統(tǒng)中,不能引入RAII 或智能指針.除此之外,DangDone 著重于預防和檢測垂懸指針,這是RAII 和智能指針無法直接支持的,需要開發(fā)者自行避免垂懸指針及其使用.
通過程序轉換[10,11,37-40]確??臻g安全錯誤的方法與我們的方法類似.為了減少空間安全誤差、提高效率,不同的轉換策略被提出.以前的方法存在一個或多個不足,很難被廣泛采用,例如運行時開銷高或需要對現(xiàn)有代碼進行很大更改.
DangNull[10],FreeSentry[11]和pSweeper[39]是最近并且與DangDone 最相似的預防措施,通過動態(tài)運行時檢查防止use-after-free 漏洞.DangNull 由兩個主要組件組成:靜態(tài)檢測和運行時庫.第1 個組件通過插入對跟蹤例程的調(diào)用來標識指針傳遞,以跟蹤指針和內(nèi)存對象之間的指針指向關系.所有內(nèi)存操作函數(shù)(如malloc和free)都會被替換,以跟蹤內(nèi)存對象和指針的生命周期.第2 個組件為靜態(tài)檢測函數(shù)提供運行時庫,以便動態(tài)跟蹤指針并將釋放的指針及置空其別名.基于SPEC CPU Benchmark 的評估顯示,DangNull 增加了80%的平均性能開銷,這比DangDone 的開銷高得多.FreeSentry 通過將對象鏈接回其指針來防止use-after-free 漏洞.在這些鏈接的幫助下,當對象容易受到垂懸指針的攻擊時,FreeSentry 可以使鏈接到已釋放對象的所有指針失效.為了實現(xiàn)這一點,freesentry 注冊了創(chuàng)建或修改指向新對象的指針的地址.釋放對象后,釋放函數(shù)將查找該對象所駐留的內(nèi)存區(qū)域的所有指針.如果這些指針仍然指向釋放的對象,則將指針置空;如果某些指針試圖對失效的內(nèi)存地址進行解引用,將導致程序崩潰.SPEC CPU Benchmark[13,41]的評估顯示:如果禁用??臻g保護,則平均開銷為25%;否則,平均開銷為 42%.pSweeper 則是借助另一個線程去掃描已有的指針,判斷這些指針是否是垂懸指針,實驗表明:pSweeper 的開銷可低至12.5%.
綜上所述,可以發(fā)現(xiàn),三者的開銷都比DangDone 高得多.但是DangDone 依賴于轉換規(guī)則,而例如DangNull等方法的規(guī)則則相對更加通用.例如:如果存在嵌套的struct結構,DangDone則還需要增加對應轉換規(guī)則.本文之前的工作[42]針對所有指針進行防護,但是實際程序它們不一定都是垂懸指針.所以為了減少不必要的轉換,我們加入靜態(tài)分析技術以減少所需要轉換的垂懸指針數(shù)量.相比之下,前期工作只能夠預防垂懸指針而不能通過靜態(tài)分析檢測垂懸指針.
CPS 系統(tǒng)在應用層面臨的攻擊主要通過軟件系統(tǒng)漏洞發(fā)起的,而在這方面的攻擊檢測、預防技術主要通過專門模塊(如入侵檢測模塊)實現(xiàn)[43,44].不同于常規(guī)軟件,CPS 系統(tǒng)可以有獨立的模塊檢測攻擊者的惡意輸入.如通過神經(jīng)網(wǎng)絡或支持向量機等分類器,采用數(shù)據(jù)挖掘、數(shù)據(jù)融合等方式對不同來源的數(shù)據(jù)進行分析.利用模式識別方法分析當前輸入[43].在攻擊檢測中,由于惡意輸入的特征一般會與常規(guī)輸入有著較大的差異,該方法也能夠在一定程度上檢測并預防use-after-free 或double free 攻擊.第2 類方法[44]則是側重于身份認證,該方法基于的前提是攻擊者無法獲得證明用戶身份的證據(jù),如密碼、認證證書等.相比于針對CPS 特征的攻擊防御方法,DangDone 則是側重于在代碼層面加固系統(tǒng)本身,避免CPS 系統(tǒng)內(nèi)部的缺陷、漏洞暴露出來.
在本文中,我們針對CPS 系統(tǒng)的垂懸指針問題提出了基于中間指針的垂懸指針預防方法.它先通過靜態(tài)和動態(tài)的方法檢測潛在的垂懸指針,再在編譯時通過程序轉換防止這些指針被利用.我們通過測試11 個實際漏洞和5 個CPS 系統(tǒng)中人工植入的漏洞評估了它在預防use-after-free 和double-free 方面的有效性,并使用SPEC CPU Benchmark 測試評估了運行時開銷.實驗表明了該方法的有效性和可以忽略不計的運行時開銷.在未來,我們計劃進一步檢測垂懸指針和未被發(fā)現(xiàn)的缺陷,如通過模糊測試的方式檢測垂懸指針.此外,與大多數(shù)現(xiàn)有的預防措施類似,都可能將use-after-free 和double-free 問題被轉換為空指針解引用問題,這也會影響CPS 系統(tǒng)的可用性,所以我們計劃通過容忍空指針解引用來增強該方法.