沈思豪, 解 達(dá), 宋 威
1(中國科學(xué)院 信息工程研究所 信息安全國家重點實驗室, 北京 100093)
2(中國科學(xué)院大學(xué)網(wǎng)絡(luò)空間安全學(xué)院, 北京 101408)
內(nèi)存安全問題一直是安全領(lǐng)域中經(jīng)久不衰的問題[1]. 從緩沖區(qū)溢出漏洞的利用開始, 到現(xiàn)在返回導(dǎo)向編程技術(shù)(return oriented programming, ROP)[1,2]、數(shù)據(jù)導(dǎo)向編程技術(shù)(data oriented programming, DOP)[3]攻擊的應(yīng)用, 基于內(nèi)存安全問題的攻擊手段在不斷地更新迭代. 相應(yīng)的, 也不斷有內(nèi)存安全防御方案被提出. 早期的內(nèi)存安全防御方案大多基于軟件, 如棧幀守護者(stack canary)[4], Ccured[5]等, 這些方案雖然能夠達(dá)到較好的防御效果, 但是性能開銷較大. 所以這些方案鮮有被應(yīng)用于商業(yè)處理器架構(gòu)之上. 近幾年來, 隨著RISC-V等開源處理器架構(gòu)的興起, 陸續(xù)有新的硬件輔助內(nèi)存安全方案被提出. 硬件支持使得防御方案的性能開銷降低. 商業(yè)處理器架構(gòu)逐漸產(chǎn)生了接納硬件輔助安全方案的趨勢.
然而目前, 無論是在工業(yè)界還是學(xué)術(shù)界, 都缺少對處理器內(nèi)存安全性能進(jìn)行評估的測試集. 現(xiàn)有的測試集, 如RIPE[6], CBench[7]等, 雖然能夠在特定的內(nèi)存安全性質(zhì)上進(jìn)行測試, 但是難以系統(tǒng)全面地反映處理器的內(nèi)存安全狀況; 此外, 對于x86系列以外的指令集架構(gòu)的支持也不盡如人意.
因此, 設(shè)計一個系統(tǒng)的, 完善的, 跨平臺的, 可拓展性強的硬件內(nèi)存安全測試集就顯得尤為重要. 為了解決這個問題, 我們提出了一個可拓展的內(nèi)存安全測試集框架, 在該框架下給出了大小為160個測例的初始版本測試集. 受限于可用硬件資源, 我們僅在x86-64和RISC-V64兩種指令集的不同平臺上對測試集進(jìn)行了測試. 初始版本的測試集涵蓋了內(nèi)存的時間、空間安全性, 內(nèi)存訪問控制, 指針完整性和控制流完整性等幾個方面的安全特性.
本文工作可開源獲取, 初始版本測試集可以在GitHub上找到: https://github.com/comparch-security/cpu-sec-bench.
內(nèi)存安全攻擊和防御手段其實處于相互競爭和相互促進(jìn)的關(guān)系中.
類似C/C++的編程語言在底層實現(xiàn)中缺乏內(nèi)存安全支持, 導(dǎo)致使用C/C++語言編寫的大量應(yīng)用程序和動態(tài)庫中包含緩沖區(qū)溢出、指針釋放后使用(useafter-free, UAF)等內(nèi)存時間和空間安全性漏洞.
代碼注入攻擊利用上述漏洞, 將預(yù)先構(gòu)造好的惡意代碼寫入堆棧等用戶數(shù)據(jù)區(qū)中, 將棧幀中保存的返回地址等代碼指針的值覆蓋為惡意代碼起始地址, 從而實現(xiàn)惡意的代碼執(zhí)行.
代碼注入攻擊要求攻擊者能夠?qū)阂獯a寫入用戶的可寫數(shù)據(jù)區(qū), 劫持程序的控制流指向惡意代碼, 保證惡意代碼能夠執(zhí)行. 針對這些前提條件, 一些經(jīng)典的內(nèi)存安全防御方案被提出: 棧幀守護者[8]在函數(shù)返回時檢測棧幀是否被緩沖區(qū)溢出覆蓋, 阻止攻擊者劫持棧幀中保存的返回地址; 數(shù)據(jù)執(zhí)行保護(data execution prevention, DEP)設(shè)置頁面可寫不可執(zhí)行屬性, 將可執(zhí)行頁和可寫頁分開, 保證攻擊者即使成功將惡意代碼植入用戶可寫數(shù)據(jù)區(qū), 也無法將其作為代碼執(zhí)行; 地址空間隨機化(address space layout randomization, ASLR)通過使用位置無關(guān)代碼(place-independent code, PIC),在程序加載時將加載基地址隨機化, 達(dá)到將程序執(zhí)行期間的數(shù)據(jù)和代碼地址隱藏的目的.
由于上述方案特別是數(shù)據(jù)執(zhí)行保護與地址空間隨機化的性能開銷低, 防御效果顯著, 所以得到了大范圍部署. 直接的代碼注入攻擊幾乎失效了. 但攻擊者仍然能夠訪問和調(diào)用程序自身和動態(tài)庫提供的數(shù)據(jù)和代碼. 更復(fù)雜的內(nèi)存安全攻擊手段被提出, 來繞過上述防御方案, 如返回導(dǎo)向編程技術(shù)、跳轉(zhuǎn)導(dǎo)向編程技術(shù)(jump-oriented programming, JOP)[9]、偽造對象導(dǎo)向編程技術(shù)(counterfeit-object oriented programming,COOP)[10]、數(shù)據(jù)導(dǎo)向編程技術(shù)等, 這些攻擊手段都有一個共同的特征, 即不注入外部代碼, 而是直接復(fù)用受害者程序和標(biāo)準(zhǔn)庫中的代碼完成攻擊. 此類攻擊稱為代碼復(fù)用攻擊.
返回導(dǎo)向編程技術(shù)是典型的代碼復(fù)用攻擊[11], 常用于構(gòu)造指令執(zhí)行序列, 完成對系統(tǒng)函數(shù)mprotect等的調(diào)用, 關(guān)閉數(shù)據(jù)執(zhí)行保護. 跳轉(zhuǎn)導(dǎo)向編程技術(shù)類似于返回導(dǎo)向編程記錄, 但在技術(shù)細(xì)節(jié)上存在不同.
返回導(dǎo)向編程技術(shù)攻擊會改變程序的正??刂屏?控制流完整性保護(control flow integrity, CFI)[12]通過在程序的間接跳轉(zhuǎn)前插入驗證代碼, 保證程序的所有間接跳轉(zhuǎn)都在程序編譯時靜態(tài)分析得到的控制流圖的范圍內(nèi). 根據(jù)控制流分析精度的不同, 控制流完整性保護具體可以分為細(xì)粒度和粗粒度兩類. 前者使用不同的特征值區(qū)分不同的合法跳轉(zhuǎn)地址; 后者則不對合法跳轉(zhuǎn)地址進(jìn)行區(qū)分.
傳統(tǒng)的控制流完整性保護實現(xiàn)通過二進(jìn)制分析完成, 重點保護間接跳轉(zhuǎn)指令, 但是并不對類型進(jìn)行檢查,無法對多態(tài)類對象的虛函數(shù)表指針提供保護. 偽造對象導(dǎo)向編程技術(shù)利用了這一點, 通過在用戶數(shù)據(jù)區(qū)偽造對象, 修改虛函數(shù)表指針, 復(fù)用其它多態(tài)類提供的虛函數(shù)表, 通過調(diào)用一系列虛函數(shù)完成攻擊.
偽造對象導(dǎo)向編程技術(shù)攻擊無法通過簡單的二進(jìn)制控制流分析實現(xiàn)的控制流完整性保護進(jìn)行檢測, 需要結(jié)合類型實現(xiàn)控制流完整性保護進(jìn)行防御. 典型的防御方案如代碼指針完整性保護(code pointer integrity,CPI)[13], 指針標(biāo)記(pointer tagging), 指針審計(pointer authentication)等. 在x86架構(gòu)下的GCC提供了虛函數(shù)表驗證(vtable verification, VTV)[14]特性, 用于驗證虛函數(shù)調(diào)用的正確性, 但是默認(rèn)沒有被GCC開啟.
數(shù)據(jù)導(dǎo)向編程技術(shù)[3]通過修改控制程序分支執(zhí)行的關(guān)鍵數(shù)據(jù)來進(jìn)行攻擊, 達(dá)到劫持程序控制流的目的.對于一些涉及敏感系統(tǒng)調(diào)用的程序, 數(shù)據(jù)導(dǎo)向編程技術(shù)攻擊同樣可以完成關(guān)閉數(shù)據(jù)執(zhí)行保護的操作.
應(yīng)對上述高級攻擊的內(nèi)存安全防御方案一般有兩種實現(xiàn)方式. 一種使用純軟件方式實現(xiàn), 主要在標(biāo)準(zhǔn)庫、內(nèi)核和編譯器的層面進(jìn)行安全增強, 很少或不依賴硬件提供支持, 導(dǎo)致性能開銷過高, 難以被大范圍部署. 例如, 代碼指針完整性保護雖然能夠防御大部分的代碼復(fù)用攻擊, 但是由于插入的驗證代碼過多, 導(dǎo)致性能開銷過高, 一直沒有被主流編譯器(GCC, LLVM)采納.
另一種方式使用硬件輔助的方法提供內(nèi)存安全支持. 相較于純軟件的實現(xiàn)方式, 硬件輔助的實現(xiàn)方式能夠?qū)Π踩桨高M(jìn)行加速, 大幅降低安全方案實現(xiàn)的硬件開銷. 典型的硬件輔助防御方案如Intel的Intel內(nèi)存保護拓展(memory protection extension, MPX), Intel 控制流保護拓展(controlflow enforcement technology,CET), ARM公司在ARM v8.3-A中增加的Arm指針審計(pointer authentication, PA), ARM v8.5-A中增加的內(nèi)存標(biāo)簽拓展(memory tagging extension, MTE).
為了比較不同處理器提供的內(nèi)存安全性, 我們需要一套測試集. 這套測試集: 1)相對完善, 能夠量化處理器的內(nèi)存安全性; 2)可移植性強, 能夠在多個處理器平臺上進(jìn)行測試.
然而, 現(xiàn)在學(xué)術(shù)界普遍使用的測試集大多只關(guān)注內(nèi)存安全的某個特定方面, 缺乏對內(nèi)存安全的整體把握和評估. 以經(jīng)典的內(nèi)存安全測試集RIPE為例: RIPE測試集[6]是一系列緩沖區(qū)溢出攻擊的集合. 它應(yīng)用廣泛, 常被用于測試控制流完整性保護防御方案. 每個測例都受5個變量控制: 緩沖區(qū)溢出位置、受攻擊的代碼指針類型、溢出攻擊的類型、惡意代碼和攻擊使用的函數(shù). RIPE使用枚舉窮盡各個變量組合的方法對緩沖區(qū)溢出做了相對完善的測試, 但是對于緩沖區(qū)溢出以外的漏洞涉及不多, 所以不適合作為一個完善的處理器內(nèi)存安全測試集.
在上述觀察的基礎(chǔ)上, 我們提出了一種處理器內(nèi)存安全測試框架, 并開源了一個基于該框架的內(nèi)存安全測試集. 內(nèi)存安全測試集的初始版本包括160項測例, 覆蓋了內(nèi)存的時空安全性(spatial and temporal safety)、內(nèi)存訪問控制、指針完整性、控制流完整性等方面. 不同類型的漏洞及其相應(yīng)的防御方案分別由對應(yīng)的測例進(jìn)行評測. 測試集已經(jīng)在x86-64和RISCV64兩種指令集架構(gòu)的幾個不同平臺上進(jìn)行了評估.
為了將測試范圍限制在內(nèi)存安全上, 我們作如下假設(shè): 1)攻擊者能夠控制用戶程序的輸入, 惡意利用程序的內(nèi)存安全漏洞如注入惡意代碼; 2)用戶程序包含可以被攻擊者所利用的內(nèi)存漏洞, 攻擊者可以利用漏洞實現(xiàn)對程序任意地址的讀寫; 3)我們還假設(shè)攻擊者的目的是攻擊用戶程序空間內(nèi)的數(shù)據(jù), 而不是內(nèi)核數(shù)據(jù); 4)此外, 我們也不考慮側(cè)信道攻擊, 如緩存?zhèn)刃诺馈⑺矐B(tài)執(zhí)行攻擊等.
處理器內(nèi)存安全測試集以平臺為對象進(jìn)行測試.平臺定義為測試集可以運行的環(huán)境. 它包括被測處理器以及運行于其上的操作系統(tǒng). 操作系統(tǒng)包括一個內(nèi)核以及若干運行時庫.
測試集假設(shè): 內(nèi)存安全是一系列內(nèi)存安全性質(zhì)的集合; 所有的內(nèi)存漏洞和漏洞利用都是由于對某些內(nèi)存安全性質(zhì)缺少檢查, 使得內(nèi)存中的值被惡意泄露或篡改. 根據(jù)上述假設(shè), 對內(nèi)存安全的評估可以被細(xì)化為一系列對內(nèi)存安全性質(zhì)的測試, 測試這些內(nèi)存安全性質(zhì)是否能夠被惡意利用. 如果一項測例成功完成執(zhí)行,說明平臺對該測例對應(yīng)的內(nèi)存安全性質(zhì)缺少檢查.通過的測例數(shù)和被安全檢查攔截的測例分布, 可以反映系統(tǒng)整體的內(nèi)存安全性.
測試集主要關(guān)注那些能夠被硬件輔助安全方案保護的內(nèi)存安全性質(zhì). 對于利用內(nèi)存漏洞展開的攻擊, 我們分析該攻擊破壞或利用了哪些內(nèi)存安全性質(zhì), 而不關(guān)注攻擊本身.
以緩沖區(qū)溢出攻擊為例. 緩沖區(qū)溢出攻擊是一種越過緩沖區(qū)邊界對值進(jìn)行修改的行為. 該行為破壞了緩沖區(qū)不應(yīng)該越界訪問的內(nèi)存安全性質(zhì). PUMP[15],AArch64 Address Sanitizer等硬件輔助的安全機制能夠?qū)彌_區(qū)越界訪問提供防護. 所以, 對于緩沖區(qū)溢出,測試集測試幾類常見的訪問越界行為, 這些行為可能不構(gòu)成完整的攻擊. 這些行為如果被攔截, 則說明平臺保護了緩沖區(qū)不應(yīng)越界訪問的性質(zhì).
然而, 由于測試集本身也是軟件, 也需要在系統(tǒng)環(huán)境下運行, 所以單憑測試集本身難以區(qū)分一個內(nèi)存檢查到底是通過硬件方式還是純軟件方式實現(xiàn)的. 所以需要保證測試覆蓋的內(nèi)存檢查是由平臺而不是第三方安全軟件實現(xiàn)的. 例如, 二進(jìn)制翻譯技術(shù)和專用的內(nèi)核安全補丁也能提供內(nèi)存安全防護, 但由于它們使用純軟件方式實現(xiàn), 所以應(yīng)當(dāng)排除在測試范圍之外.
測例主要覆蓋內(nèi)存的空間安全性、時間安全性、訪問控制、指針完整性、控制流完整性等5個方面.
3.3.1 空間安全性
空間安全性指內(nèi)存訪問總是落在正確的數(shù)據(jù)邊界和程序的可見域內(nèi)的性質(zhì). 任何數(shù)據(jù)邊界外或是可見域外的訪問都是不安全的. 典型的破壞空間安全性的攻擊是緩沖區(qū)溢出攻擊. 它是對緩沖區(qū)的越界訪問. 除了緩沖區(qū)以外, 棧幀、動態(tài)分配對象、全局變量、只讀數(shù)據(jù)等均存在越界訪問的風(fēng)險.
緩沖區(qū)溢出按照溢出方向可以分為上溢和下溢;按照緩沖區(qū)位置可以分為堆上溢出和棧幀溢出. 噴射攻擊是溢出攻擊的一種特殊應(yīng)用, 它將溢出位置和目標(biāo)位置之間的全部內(nèi)存都進(jìn)行填充, 插入指向目標(biāo)位置的跳轉(zhuǎn)代碼, 降低控制流劫持的難度.
對于緩沖區(qū)溢出的檢測和防御方法包括內(nèi)存檢測器(address sanitizer, Asan)[16]、內(nèi)存標(biāo)記[17]和重型指針(fat pointer)[18]; 對于棧幀越界訪問, 使用重型指針在棧幀粒度上保持?jǐn)?shù)據(jù)完整性[19]、在棧幀之間填充字節(jié)[20]、在棧幀粒度進(jìn)行數(shù)據(jù)隔離[21]都是有效的防御手段; 對于堆越界訪問, 部分防御方案將陷阱數(shù)據(jù)填充在對象之間[22], 或者在對象粒度上進(jìn)行邊界檢查.
測試集的空間安全性測例共98項, 主要使用兩種方式構(gòu)造緩沖區(qū)越界訪問: 1)通過合法的緩沖區(qū)指針和越界的地址偏移; 2)修改合法的緩沖區(qū)指針指向界外位置. 測試集對棧上、堆上、全局變量、只讀數(shù)據(jù)中的越界訪問都進(jìn)行了測試.
3.3.2 時間安全性
時間安全性指內(nèi)存中的數(shù)據(jù)訪問只發(fā)生在數(shù)據(jù)的生命周期之內(nèi). 任何發(fā)生在程序生命周期之前(未初始化數(shù)據(jù))和之后(釋放后使用)的訪問都是不安全的. 時間安全性的測例只關(guān)心一個生命周期外的訪問是否會發(fā)生, 不會針對具體的內(nèi)存分配算法進(jìn)行測試.
釋放后使用相關(guān)的漏洞主要包括空懸指針(dangling pointer), 未初始化變量等. 在堆上和棧上的空懸指針都會為程序帶來較大的安全隱患.
應(yīng)對空懸指針的防御方案包含空懸指針歸零[23]、解引用前檢查空懸指針[24]、阻止分配器在被釋放對象地址進(jìn)行重分配[25]、阻止未初始化數(shù)據(jù)訪問、細(xì)粒度??臻g隨機化[26]、內(nèi)存標(biāo)記等.
與時間安全性相關(guān)的測試共有13項, 主要檢測棧上或堆上的數(shù)據(jù)在棧幀或?qū)ο蟊会尫藕竽芊癖豢諔抑羔樌^續(xù)訪問. 此外, 還檢查平臺是否具有保證相同類型的對象在相同的內(nèi)存地址不被重新分配、函數(shù)每次調(diào)用時動態(tài)變化棧幀結(jié)構(gòu)等性質(zhì).
3.3.3 訪問控制
訪問控制性質(zhì)指限制了攻擊者內(nèi)存訪問能力的性質(zhì). 主要用來防御信息泄露攻擊. 程序的函數(shù)體代碼、全局偏移量表(global offset table, GOT)都是潛在的攻擊目標(biāo). 攻擊者在運行時讀取程序的函數(shù)體代碼, 檢索可能成為gadget的代碼片段, 用以構(gòu)造代碼復(fù)用攻擊.
全局偏移量表用于程序動態(tài)鏈接共享庫時檢索符號. 由于全局偏移量表表項在運行時動態(tài)更新, 所以需要存儲在可寫頁上. 攻擊者可以通過讀取全局偏移量表表項獲取動態(tài)庫函數(shù)在內(nèi)存中的地址, 造成信息泄露. 攻擊者也可以通過修改全局偏移量表來劫持庫函數(shù).
針對上述攻擊, 主要的防御技術(shù)包括地址空間隨機化, 防止攻擊者讀取可執(zhí)行頁的代碼隨機化、可讀不可執(zhí)行[27]等. 測試集的訪問控制測例共3項, 也圍繞這些防御技術(shù)展開, 主要檢查地址空間隨機化是否有效, 函數(shù)體代碼是否可讀, 以及全局偏移量表特定表項是否可讀等.
3.3.4 指針完整性
典型的控制流劫持攻擊和防御手段經(jīng)常圍繞指針展開. 攻擊的第1階段修改保存敏感數(shù)據(jù)的指針, 破壞了指針完整性; 第2階段使用被修改的指針劫持控制流, 破壞了控制流完整性.
指針完整性測例主要關(guān)注保存敏感數(shù)據(jù)的指針的安全性. 敏感數(shù)據(jù)指針包括函數(shù)指針、虛函數(shù)表指針和全局偏移量表.
函數(shù)指針通??煽截惖豢尚薷? 進(jìn)行算數(shù)運算的情況非常罕見. 函數(shù)指針一般通過指針審計和指針標(biāo)記[13]進(jìn)行保護. 虛函數(shù)表指針指向一張函數(shù)指針表,其中每個表項指向類型對應(yīng)的虛函數(shù). 對虛函數(shù)表指針的保護方案包括代碼指針完整性保護[13]、GCC VTV等.
測試集的指針完整性測例共5項, 主要檢測平臺是否允許函數(shù)指針拷貝和算術(shù)運算, 是否允許對虛函數(shù)表指針進(jìn)行讀取和修改、是否允許對全局偏移量表進(jìn)行修改等.
3.3.5 控制流完整性
控制流體現(xiàn)了程序動態(tài)執(zhí)行時指令間邏輯上的先后順序. 通過代碼指針調(diào)用完成的控制流跳轉(zhuǎn)稱為前向控制流; 通過返回地址完成的控制流跳轉(zhuǎn)稱為后向控制流. 前向控制流完整性指保護代碼指針解引用到合法的地址; 后向控制流完整性指保護返回地址不被惡意篡改.
控制流完整性相關(guān)的攻擊方式包括代碼注入攻擊、代碼復(fù)用攻擊等, 對于使用多態(tài)的程序還包括虛函數(shù)表劫持攻擊. 測試集的控制流完整性測例共41項,主要圍繞這些攻擊的典型防御方案如數(shù)據(jù)執(zhí)行保護,控制流完整性保護等進(jìn)行測試.
測試集的整體架構(gòu)如圖1所示. 測試樣例由兩部分組成: 平臺無關(guān)的測試邏輯, 與平臺相關(guān)的支持庫.
圖1 內(nèi)存安全測試集整體框架圖
每個測例為測試特定內(nèi)存安全性質(zhì)的C++程序;若某條性質(zhì)對應(yīng)的安全檢查缺失, 測例可以利用該漏洞完成測試邏輯并返回零值, 表示漏洞被成功利用; 否則測例返回非零值并提示測試失敗.
整個測試集的運行通過測試驅(qū)動控制. 測試驅(qū)動使用指定的編譯選項對測例進(jìn)行編譯, 運行測例并統(tǒng)計測例的運行結(jié)果, 得到測例通過數(shù)量的量化數(shù)據(jù).
測例中利用漏洞的惡意行為常常以匯編代碼的形式實現(xiàn). 這是為了防止編譯器優(yōu)化掉惡意行為. 這些匯編代碼在平臺相關(guān)的支持庫中. 測試邏輯是使用平臺無關(guān)的方式編寫的; 需要使用惡意代碼的部分通過調(diào)用平臺支持庫來完成.
測例的可移植性通過測試邏輯與支持庫的劃分實現(xiàn). 二者之間通過宏的定義和調(diào)用產(chǎn)生聯(lián)系. 支持庫中的匯編代碼使用宏定義的方式組織; 測試邏輯通過引用宏定義完成調(diào)用. 不同平臺的支持庫對相同的宏名稱提供定義, 使用平臺特定的匯編代碼實現(xiàn)相同的動作. 每個測例都會引用公共頭文件(include/assembly.hpp), 該文件對不同平臺支持庫的頭文件進(jìn)行了包裝,通過不同架構(gòu)的預(yù)定義宏進(jìn)行區(qū)分(如__x86_64、__riscv64), 編譯測試集時編譯器會根據(jù)預(yù)定義宏選擇正確架構(gòu)對應(yīng)的支持庫.
對于新增加的平臺或指令集架構(gòu), 只需要和其他支持庫對同樣的宏名稱進(jìn)行定義即可, 不需要修改測試邏輯; 對于新增加的測例, 需要將測試使用支持庫提供的宏實現(xiàn), 如果需要新增宏定義, 則需要在所有的支持庫中將對該宏進(jìn)行定義. 對于新的指令集架構(gòu)而言,目前只有約20個宏名稱需要被實現(xiàn). 綜上所述, 測試集具有良好的可拓展性.
下面以控制流完整性的測例call-instruction-instack為例, 描述測例的代碼結(jié)構(gòu)和測試流程.
測例call-instruction-in-stack用來測試將棧上地址作為目標(biāo)地址的情況下, 函數(shù)調(diào)用是否能夠成功執(zhí)行.測試邏輯的主要代碼如代碼清單1所示. 其中assembly.hpp和signal.hpp均為平臺支持庫的公共頭文件. 頭文件assembly.hpp負(fù)責(zé)提供FORCE_NOINLINE、CALL_DAT、FUNC_MACHINE_CODE等宏的宏定義. 頭文件signal.hpp負(fù)責(zé)提供異常處理所需的接口代碼. 部分平臺在檢測到違反內(nèi)存安全規(guī)則的操作時會拋出異常,signal.hpp提供將這些異常捕獲并產(chǎn)生特定返回值的代碼.
代碼清單 1. call-instruction-in-stack的測試邏輯#include "include/assembly.hpp"#include "include/signal.hpp"int gv = 1;int FORCE_NOINLINE helper(const unsigned char* m) {CALL_DAT(m);return gv;}int main(){unsigned char m[] = FUNC_MACHINE_CODE;… //異常處理初始化代碼int rv = helper(m);… //異常處理收尾代碼exit(rv);}
宏FORCE_NOINLINE的作用是使強制被修飾函數(shù)不生成內(nèi)聯(lián)代碼, 原因是內(nèi)聯(lián)代碼在部分平臺上會影響測例測試邏輯的正確性; 宏CALL_DAT(addr)的作用是將addr作為目標(biāo)地址, 進(jìn)行函數(shù)調(diào)用; 宏FUNC_MACHINE_CODE的作用是模擬函數(shù)體代碼, 使得CALL_DAT宏產(chǎn)生的函數(shù)調(diào)用一旦成功執(zhí)行, 后續(xù)指令能夠正常返回或是產(chǎn)生特定異常并被main函數(shù)中異常捕獲邏輯捕獲, 使得測例能夠正常退出.
宏CALL_DAT的具體實現(xiàn)為C擴展內(nèi)嵌匯編代碼, 所以不同的指令集架構(gòu)上, 實現(xiàn)各不相同. 在x86-64指令集架構(gòu)上其實現(xiàn)如代碼清單2所示, 在RISCV64指令集架構(gòu)上其實現(xiàn)如代碼清單3所示.
代碼清單 2. CALL_DAT宏的RISC-V64架構(gòu)實現(xiàn)#define CALL_DAT(ptr) asm volatile( "jalr ra, %0, 0;" : : "r"(ptr) : "ra" )代碼清單 3. CALL_DAT宏的x86-64架構(gòu)實現(xiàn)#define CALL_DAT(ptr) asm volatile( "call *%0;" : : "r" (ptr) )
如果需要將本測例移植到ARM AArch64架構(gòu), 則只需要在ARM AArch64指令集架構(gòu)的平臺支持庫和頭文件中實現(xiàn)對FORCE_NOINLINE、CALL_DAT和FUNC_MACHINE_CODE這3個宏的定義即可.
測例測試邏輯的核心部分在helper函數(shù). 如果helper函數(shù)中的CALL_DAT宏成功執(zhí)行, FUNC_MACHINE_CODE將會和main函數(shù)中的異常處理代碼結(jié)合, 將rv的值設(shè)置為0. 測例將以返回值0退出,表示測試通過. 如果CALL_DAT宏的執(zhí)行拋出異常,則main函數(shù)中的異常處理代碼會將拋出的異常轉(zhuǎn)換為非零返回值-1并退出程序.
對于x86-64架構(gòu), 我們使用一臺較舊的Intel i7-3770 CPU搭配Ubuntu 16.04操作系統(tǒng), 和一臺較新的Intel Xeon 8280 CPU搭配Ubuntu 18.04操作系統(tǒng)進(jìn)行測試.
對于RISC-V64架構(gòu), 我們使用SiFive公司的HiFive Unleashed和HiFive Unmatched兩款開發(fā)板進(jìn)行測試, 兩塊開發(fā)板分別基于SiFive公司的u540和u740 CPU, 操作系統(tǒng)均為SiFive公司提供的預(yù)編譯OpenEmbedded操作系統(tǒng).
我們在x86-64和 RISC-V64兩個ISA架構(gòu)的4個平臺上應(yīng)用測試集進(jìn)行了測試. 平臺列表如表1所示.
表1 內(nèi)存安全測試集運行平臺參數(shù)
4.2.1 不同平臺間安全性對比
為了保持一致性, 我們在不同平臺上統(tǒng)一使用操作系統(tǒng)提供的GNU g++編譯器, 使用相同的編譯選項“-O2 -std=c++11 -Wall”進(jìn)行編譯. 測試集結(jié)果的概要如表2所示.
表2 不同平臺成功執(zhí)行測試樣例數(shù)
時間安全性: Intel Xeon 8280、HiFive Unleashed、HiFive Unmatched平臺上, 部分堆上的釋放后使用相關(guān)測例運行失敗. Intel i7-3770平臺全部測例運行成功.說明除了Intel i7-3770平臺外, 其他各被測平臺都具有一定的內(nèi)存時間安全性防御能力. 通過調(diào)查原因發(fā)現(xiàn),這些平臺使用了較新版本的GLIBC. 后者采用了新的內(nèi)存分配算法, 在同一片內(nèi)存區(qū)域釋放后和分配前插入了垃圾內(nèi)容, 阻止了釋放后信息泄露以及偽造未初始化變量對象攻擊. 不過新的算法仍然能夠被強制在同一塊內(nèi)存區(qū)域重新分配相同類型的對象, 一些使用空懸指針的釋放后使用攻擊仍然有效. 此外, 各個被測平臺對棧上的釋放后使用同樣缺乏有效的安全檢查.
指針完整性: 在各個被測平臺上, 讀寫代碼指針和虛函數(shù)表指針的測例全部成功執(zhí)行. 雖然編譯器在編譯階段給出了指針?biāo)銛?shù)運算的警告, 但是指針?biāo)銛?shù)運算測例在各個平臺仍然能夠成功執(zhí)行. 修改全局偏移量表表項的測例在Intel Xeon 8280平臺上執(zhí)行失敗,但在其他平臺上成功執(zhí)行. 說明4個被測平臺中, 只有Intel Xeon 8280平臺默認(rèn)提供了部分指針完整性檢查.該檢查來自重定位只讀保護(relocation read-only,RELRO), 大多數(shù)Linux發(fā)行版都默認(rèn)提供了部分重定位只讀保護. 但是在HiFive Unmatched和HiFive Unleashed兩個平臺上重定位只讀保護并沒能覆蓋庫函數(shù)的入口.
訪問控制: 檢測發(fā)現(xiàn)Intel i7-3770平臺的地址空間隨機化測例成功執(zhí)行. 其他各被測平臺除了地址空間隨機化測例以外其余測例都成功執(zhí)行. 說明被測平臺中大部分都默認(rèn)開啟了地址空間隨機化保護, 但是缺乏對信息泄露的進(jìn)一步防御. 分析原因發(fā)現(xiàn), Intel i7-3770平臺編譯器默認(rèn)的編譯選項不支持生成位置無關(guān)代碼, 導(dǎo)致對用戶程序的地址空間隨機化無法使用. 增加“-pie -fPIE”選項后地址空間隨機化相關(guān)測例執(zhí)行失敗, 地址空間隨機化保護成功開啟.
空間安全性: 在4個被測平臺上, 所有98個測例都成功完成了測試. 說明被測平臺默認(rèn)提供的安全防護中缺乏對內(nèi)存越界訪問的安全檢查. 軟件上常使用address sanitizer來檢測越界訪問, 但是性能代價太高,只適合在開發(fā)階段使用, 無法部署到產(chǎn)品中. 硬件拓展如CHERI[28]、PUMP[15]等雖然實現(xiàn)了對越界訪問的檢查, 但是是以修改系統(tǒng)ABI、增加硬件開銷和性能開銷為代價的.
控制流完整性: 對于后向控制流劫持相關(guān)的測例,與返回導(dǎo)向編程技術(shù)相關(guān)的測例都成功執(zhí)行; 代碼注入攻擊的測例悉數(shù)被數(shù)據(jù)執(zhí)行保護攔截. 對于前向控制流劫持, 除了代碼注入攻擊的測例被數(shù)據(jù)執(zhí)行保護攔截, 其他類型攻擊相關(guān)的測例都成功執(zhí)行. 對于虛函數(shù)表保護, 替換虛函數(shù)表、偽造虛函數(shù)表的相關(guān)測例均成功執(zhí)行. 對部分使用新的內(nèi)存分配算法的平臺, 虛函數(shù)指針復(fù)用攻擊相關(guān)的測例執(zhí)行失敗, 調(diào)查原因發(fā)現(xiàn), 新的內(nèi)存分配算法在釋放對象時清零了虛函數(shù)表指針. 上述被測平臺都具有一定的控制流完整性防御能力.
總的來說, 在各個被測平臺上, 由默認(rèn)配置提供的安全防護并無太大區(qū)別. 各平臺默認(rèn)都沒有對空間安全性提供有效的保護; 在HiFive Unleashed和HiFive Unmatched平臺上由于配套的工具鏈和運行時庫增加了安全防護, 所以提供了更好的時間安全性保護. 地址空間隨機化和數(shù)據(jù)執(zhí)行保護雖然為各平臺提供了一定的內(nèi)存安全防護能力, 但覆蓋面較窄, 只能限制在特定的幾項內(nèi)存安全性質(zhì)上.
4.2.2 不同編譯器與編譯選項間對比
編譯器不同的編譯選項也提供了部分安全防護.在Intel Xeon 8280平臺上, 使用GCC 10.3.0和GLIBC 2.32對不同的編譯選項進(jìn)行了測試. 為了測試LLVM提供的控制流完整性保護防御機制, 也將LLVM13在Intel Xeon 8280平臺上進(jìn)行了測試.
很可惜, 由于工具鏈移植仍然不完整, RISC-V的GCC和LLVM沒有提供對VTV和CFI的支持, RISCV架構(gòu)的address sanitizer無法正常工作, 剩余的可用內(nèi)存安全選項測試得到的結(jié)果差別不大, 對判斷RISCV架構(gòu)平臺的內(nèi)存安全性意義不大, 所以我們將只對Intel Xeon 8280平臺的測試結(jié)果進(jìn)行討論.
我們按照功能將編譯器提供的安全方面的編譯選項分為幾組, 如表3所示.
表3 內(nèi)存安全相關(guān)不同編譯選項組
下面對表3中的選項組進(jìn)行解釋.
默認(rèn)選項: 只要求-O2優(yōu)化, 其他為編譯器默認(rèn)選項; RELRO: 開啟對全局偏移量表的全面保護; 棧保護:通過在棧中插入canary實現(xiàn)棧覆寫保護(stack smashing protection); VTV: GCC支持的虛函數(shù)表驗證特性,用于應(yīng)對偽造對象導(dǎo)向編程技術(shù)攻擊; CFI: LLVM支持的前向控制流攻擊防御機制; 全部防護: 對編譯器應(yīng)用上述支持的所有編譯選項; Asan: 開啟動態(tài)address sanitizer; 無防護: 關(guān)閉包括數(shù)據(jù)執(zhí)行保護在內(nèi)的所有防護, 包括內(nèi)核提供的地址空間隨機化等.
在默認(rèn)選項下, Intel Xeon 8280平臺使用GCC 10.3的通過測例數(shù)為142, 與使用平臺默認(rèn)的編譯工具相比, 全局偏移量表篡改可行性的測例通過, 但是有4個堆上釋放后使用的測例失敗, 原因是采用了新的GLIBC庫. 使用LLVM通過的測例數(shù)同樣為142, 不過由于LLVM生成的代碼默認(rèn)不開啟PIE選項, 并且在編譯時不允許代碼指針?biāo)阈g(shù)運算, 所以具體成功執(zhí)行的測例稍有區(qū)別.
在RELRO選項下, Intel Xeon 8280平臺下GCC編譯通過測例數(shù)減少了1, LLVM編譯通過測例數(shù)減少了2. 可見開啟RELRO選項對測試集涉及的內(nèi)存安全漏洞并不敏感.
測試結(jié)果如表4所示.
表4 Intel Xeon 8280平臺下不同編譯選項組測試集編譯運行通過測例數(shù)
開啟棧保護選項下, Intel Xeon 8280平臺下對測試集通過測例數(shù)幾乎沒有任何影響, 因為大多數(shù)返回導(dǎo)向編程技術(shù)攻擊都可以定位返回地址保存位置, 并在不觸碰canary的情況下能夠修改返回地址. 失敗的測例為偽造棧幀攻擊相關(guān)的測例.
VTV選項下, Intel Xeon 8280平臺下6項偽造對象導(dǎo)向編程技術(shù)相關(guān)測例都測試失敗. 不過將虛函數(shù)表替換為子類、父類的行為仍然沒有被攔截.
CFI選項下, Intel Xeon 8280平臺下幾乎沒有提供任何安全增強. 可能的原因是LLVM CFI要求在鏈接期間對所有的類定義都可見. 這需要使用靜態(tài)鏈接方式編譯. 而為了應(yīng)對編譯器優(yōu)化策略, 所有可執(zhí)行文件均以動態(tài)鏈接方式鏈接. 這導(dǎo)致鏈接時分析將對虛函數(shù)表指針和函數(shù)指針的修改操作識別為了合法操作.
全部防護選項下, Intel Xeon 8280平臺下GCC編譯測試集共有26項測例失敗; LLVM編譯測試集共有22項測例失敗.
開啟Asan選項下, Intel Xeon 8280平臺下GCC編譯測試集通過的測例數(shù)減少為8. 通過的測例僅包括兩項訪問控制測試(read-func和read-GOT)以及6項棧上釋放后使用攻擊測例. LLVM編譯測試集通過的測例數(shù)減少為21. LLVM編譯測試集中, 返回導(dǎo)向編程技術(shù)和偽造對象導(dǎo)向編程技術(shù)攻擊相關(guān)的測例都測試失敗, 但是跳轉(zhuǎn)導(dǎo)向編程技術(shù)攻擊相關(guān)的測例仍然成功執(zhí)行, 全局偏移量表表項修改可行性的測例也成功執(zhí)行. 不過LLVM編譯測試集中所有的釋放后使用相關(guān)測例都測試失敗, 包括被GCC Asan漏掉的棧上釋放后使用攻擊測例.
無防護選項下, Intel Xeon 8280平臺下GCC編譯測試集僅有5項測例失敗. 失敗測例均為堆上釋放后使用攻擊測例. LLVM編譯測試集除了沒有編譯通過的代碼指針?biāo)阈g(shù)操作測例之外, 結(jié)果與GCC編譯測試集相同. 結(jié)合上述各點, GCC編譯器與LLVM編譯器安全特性提供的內(nèi)存安全檢查大致相近, 只是在使用動態(tài)鏈接類型定義時LLVM的CFI安全特性未能發(fā)揮有效作用, 相比于GCC稍遜一籌. 不過, LLVM測試集中關(guān)于代碼指針?biāo)銛?shù)運算的測例沒有通過編譯, 而GCC測試集中只是給出了警告, 這也說明兩款編譯器對于內(nèi)存安全問題防護具有不同的側(cè)重點. 兩款編譯器提供的address sanitizer攔截了絕大多數(shù)的內(nèi)存安全惡意行為, 說明大多數(shù)的內(nèi)存安全性質(zhì)都依賴于內(nèi)存的空間安全性.
關(guān)于測試集, 早期的測試集主要用于測試計算機的計算性能. 19世紀(jì)70年代的LINPACK測試集用于測量計算機進(jìn)行線性代數(shù)數(shù)值計算的性能, 至今還用于超算的性能衡量中. Dhrystone[29]為衡量計算機普通整數(shù)運算提供了性能指標(biāo); CoreMark專注于微控制器的性能測量; SPEC測試集[30]則用于性能更強的通用計算機. PARSEC[31]則主要集中于衡量共享內(nèi)存和多線程應(yīng)用的性能.
在2005年, Kratkiewicz等[32]提出了使用構(gòu)造的小型緩沖區(qū)溢出攻擊測試現(xiàn)有的軟件防御方案.2006年, BASS[33]吸收了SPEC的思想, 將7個包含有不同種類內(nèi)存漏洞的測例綜合進(jìn)行安全性驗證, 同時提供了一個框架用于自動生成利用內(nèi)存漏洞攻擊. 據(jù)我們所知, BASS是最早的嘗試衡量計算機安全性的測試集; 然而該測試集的測試范圍只限定在幾個特殊的內(nèi)存空間漏洞上. RIPE[6]是當(dāng)前內(nèi)存安全領(lǐng)域應(yīng)用最為廣泛的安全測試集. 通過枚舉幾種攻擊方式的組合,RIPE能夠覆蓋850種緩沖區(qū)溢出攻擊和返回導(dǎo)向編程技術(shù)攻擊. 它也被用于衡量硬件輔助的控制流攻擊防御方案. 但是按照RIPE的方法覆蓋緩沖區(qū)溢出和返回導(dǎo)向編程技術(shù)攻擊就需要850項測例, 要對內(nèi)存安全進(jìn)行較全面的覆蓋可能難以實現(xiàn).
最近幾年出現(xiàn)了新的安全測試集設(shè)計. CONFIRM[34]是最近提出的用于衡量不同控制流完整性防御方案的兼容性和可用性的安全測試集, 但是缺少對于安全性的評估. CBench[7]對控制流完整性防御方案的實際效果進(jìn)行評估, 采用與BASS類似的設(shè)計, 共使用7個大類共18個包含漏洞的程序. 與本文工作相比, CBench使用完整的攻擊進(jìn)行測試, 而且集中在被測防御機制本身, 而不是實現(xiàn)這些機制的平臺, 另外, CBench也不支持跨平臺, 只能在x86-64架構(gòu)上運行.
由于目前我們可用的平臺支持的指令集架構(gòu)只包括Intel x86-64和RISC-V64, 測試集目前僅在這兩個指令集上進(jìn)行了測試, 對于其他指令集架構(gòu)的支持正在進(jìn)行中. 未來計劃增加對ARM AArch64和龍芯/MIPS指令集架構(gòu)的支持.
雖然主要測試目標(biāo)是處理器及相應(yīng)的指令集架構(gòu)的內(nèi)存安全水平, 但是測試集的執(zhí)行并不能脫離測試環(huán)境. 這也導(dǎo)致在測例不通過時, 有時較難區(qū)分具體是處理器的硬件防御機制起了作用, 還是操作系統(tǒng)、編譯器或標(biāo)準(zhǔn)庫的軟件防御機制起了作用.
上述問題向測試集引入了操作系統(tǒng)、編譯器和標(biāo)準(zhǔn)庫等無關(guān)變量. 一種消除這些無關(guān)變量的方法是將所有被測平臺都強制安裝特定的操作系統(tǒng)、編譯器和相同版本的標(biāo)準(zhǔn)庫. 這種方法雖然在理論上可行, 但是實踐的難度很大. 如果將測試環(huán)境縮小到只包含內(nèi)核與命令行工具的最小系統(tǒng), 在嵌入式平臺上比較容易實現(xiàn), 但是在一般的服務(wù)器和PC機上安裝最小環(huán)境則比較困難. 如果將測試環(huán)境規(guī)定為特定版本的操作系統(tǒng)發(fā)行版(如Ubuntu), 那么這一發(fā)行版并非能夠被所有被測平臺支持, 如Mac M1和其他眾多嵌入式平臺等.
基于上述原因, 我們不強制所有平臺運行特定的操作系統(tǒng)、編譯器和相同版本的標(biāo)準(zhǔn)庫, 而是默認(rèn)為某種發(fā)行版, 假定該發(fā)行版提供的測試環(huán)境足夠小. 我們將被測目標(biāo)的含義擴大為硬件平臺及其支撐的運行環(huán)境. 受控變量除了處理器和硬件平臺之外, 還包括平臺上運行的操作系統(tǒng)、編譯器和標(biāo)準(zhǔn)庫.
為了消除增加受控變量帶來的影響, 保證能夠正確的分析測試的結(jié)果, 在測試集的構(gòu)成上, 測例盡量使用不同的非零返回值去標(biāo)注不同位置和不同原因造成的測試失敗, 從而為判斷生效的防御類型提供線索.
此外, 我們也使用現(xiàn)有的平臺對編譯器提供的內(nèi)存安全標(biāo)志選項進(jìn)行了討論, 分析了主流編譯器提供的內(nèi)存安全防護的有效性. 操作系統(tǒng)和標(biāo)準(zhǔn)庫這些變量對內(nèi)存安全防御的影響也可以通過配置不同的內(nèi)核安全功能、標(biāo)準(zhǔn)庫版本進(jìn)行評估. 不過限于篇幅和工作量的關(guān)系, 這些評估現(xiàn)在還沒有展開.
我們設(shè)計了一套兼具綜合性和可移植性的內(nèi)存安全測試集框架. 初始的測試集包含160項測例, 覆蓋了內(nèi)存時空安全性、訪問控制、指針完整性和控制流完整性等幾個方面. 每一類漏洞及其相關(guān)的防御方案都被若干測例評估. 為驗證可用性, 我們將測試集在Intel x86-64和RISC-V64指令集架構(gòu)上進(jìn)行了評估. 我們的評估結(jié)果顯示, 雖然地址空間隨機化和數(shù)據(jù)執(zhí)行保護等防御方案對被測平臺提供了部分內(nèi)存安全保護,但大部分的內(nèi)存漏洞在部分處理器的默認(rèn)編譯器配置下仍然能夠被利用. 開啟額外的編譯器安全特性能夠抵御特定類型的內(nèi)存安全攻擊. 盡管address sanitizer作為調(diào)試工具不能用于生產(chǎn)環(huán)境中, 它在捕獲內(nèi)存安全攻擊上十分有效. 就相同平臺上的編譯器表現(xiàn)來看,LLVM和GCC能夠提供相近的內(nèi)存安全保護, 兩者對內(nèi)存安全保護的側(cè)重各有不同.
致謝
感謝郭雄飛提供的HiFive Unleashed開發(fā)板以及中國科學(xué)院軟件研究所PLCT團隊贈與的HiFive Unmatched開發(fā)板. 兩套硬件設(shè)施對我們在RISC-V架構(gòu)平臺上的測試起到了很大幫助.