?

基于軟硬件協同的細粒度安全域隔離機制①

2024-02-13 12:25李亞偉章隆兵王劍
高技術通訊 2024年1期
關鍵詞:指針調用內存

李亞偉 章隆兵 王劍

(計算機體系結構國家重點實驗室(中國科學院計算技術研究所) 北京100190)

(中國科學院計算技術研究所 北京100190)

(中國科學院大學 北京100049)

現在的基礎軟件棧主要采用C/C ++這種非安全語言,這些語言為了方便靈活地操作底層的硬件,給程序員暴露了很多的細節,比如可以直接操作指針、隨意修改棧中的內容、無限制地使用內聯匯編等。這些編程語言的設計初衷是更加容易地與硬件交互,但是帶來易用的同時對系統造成了很大的安全性問題[1]。

對內存任意修改的非安全操作是造成現代各類安全問題的根源。目前各類攻擊(attacks)或漏洞(bugs),如Heartbleed[2]等,大都是內存安全相關的。為了解決這些安全問題,學術界提出了很多應對機制,主要有基于軟件的方法和基于硬件的解決方法?;谲浖膶崿F方式,如AddressSanitizer[3],使用編譯器在解析源程序時,動態地分析相關的敏感性操作(如指針的訪問、內存空間的分配等),增加程序對內存訪問的監管,減少容易產生安全問題的操作。StackGuard[4]和SoftBound[5]都是采用軟件來防護緩沖區溢出(buffer overflow)和邊界檢查(bound checking)的相關問題?;谲浖姆绞街饕庆`活性比較大,但其最大的問題在于該方式有嚴重的性能損耗[4-7]。

相比于軟件方式的不安全性,基于硬件的方式能夠最大程度地彌補軟件方式帶來的性能上的損耗。硬件的實現方式主要是利用底層硬件提供的模塊,配合相關的指令來完成安全性的檢查。如NX[8](no-execute),在頁表上增加不可執行位。Intel MPK(memory protection keys)[9]使用4 bit 的標記,將進程的內存空間分成了16 個相毗鄰的區域,來區分不同的安全域?;赟MEP(supervisor mode execution protection)[10-11]的方式是借助虛擬機,切換不同的特權級,從而達到隔離執行環境的目的,比如文獻[12,13]中的實現機制。而傳統的POSIX(portable operating system interface)方式就是采用操作系統提供的mprotect 系統調用,通過陷入內核特權,增加或者屏蔽頁面的屬性。

雖然增加硬件的支持能夠提供相對安全的執行環境,但還是存在一定的局限性。首先,基于硬件的方法不能細粒度地對程序進行有效的隔離。比如Intel 的MPK(memory protection keys)[9]和ARM 的TrustZone[14]只能提供少數的隔離空間,如果需要隔離較多的空間時,只能借助于來回切換空間,這造成了一定的損耗。其次是借助權限切換的方式,比如基于虛擬機和系統調用的方式,在一些應用場景比較快速時,延遲較大,性能下降明顯,如mprotect(20-50X)[15]和MPK(3-13.5X)[16]。

本文提出了一種更加細粒度的隔離方法,以函數調用為隔離邊界,能夠提供多達4 096 個相對獨立的安全空間,這些空間能夠毗鄰,也可以橫跨多個空間域,應用靈活。本文增加了新的用戶態ICall 與IRet 指令,這2 條指令除了正常的函數調用與返回的同時,還能夠切換執行環境。相比于其他的隔離方式,該方法切換迅速,不需要陷入特權態,從而有較高的性能。為了提供獨立的安全環境,在頁表項增加了12 bit 的隔離標志GFID(global function identifier)。每次內存分配時,都會根據全局的GFIDR(GFID register)寄存器設置頁表,執行流如果沒有權限則無法訪問其他的隔離空間。

為了能夠在隔離空間共享數據,本文提出了2種安全的策略,保證隔離域與非可信區的數據安全。這些策略都有硬件、編譯器以及輔助指令的支持,避免軟件惡意地修改,從而提高安全性。同時也嚴格地硬件隔離了程序執行的??臻g,防止指令流隨意修改其他棧中的敏感數據。用戶不需要大幅度修改源程序,只需要在程序函數調用時顯式地加入編譯屬性標志,編譯器會自動插入相關的指令流,以滿足不同的安全需求。

在模擬器Gem5[17]上實現了本文的原型設計,采用RISC-V 架構,處理器選擇O3CPU。為了評估本文的設計方案,主要做了安全評估與性能評估。安全評估采用NIST 測試樣例以及手動設計的關于跨域訪問的不安全操作代碼,實驗顯示本文方法能夠完全終止程序的執行。性能方面的評估,主要是采用SPECCPU2006 測試集,實驗結果顯示,本文的設計僅有3%的性能損耗。

1 相關工作

1.1 基于內核的技術

最開始提供隔離技術的是操作系統提供的進程隔離技術,進程之間彼此相互獨立,但這類實現有較高的延遲。比如基于輕量級上下文的實現lwCs(light-weight contexts)[18]、SMV (secure memory view)[19]以及嵌套內核的方法[20]。Mimosa[11]利用Intel 的TSX(transactional synchronization extensions)技術來保護密鑰,避免程序竊取和冷啟動攻擊。這些技術采用類似內核保護的技術切換私有的數據或者安全域。

1.2 基于虛擬化的方法

Dune[21]利用Intel 的VT-x X86 虛擬化技術實現進程內的隔離。SIM(secure in-VM monitoring)[22]在非安全的客戶端虛擬機中使用VT-x 來隔離安全的監視器。文獻[23]使用虛擬化技術提供隔離的沙箱機制,保證云端程序的安全性。這些方法都使用了相關的虛擬化技術,主要的代價開銷來源于頻繁的系統調用(陷入虛擬機)以及TLB(translation lookaside buffer) Miss??傮w來說基于虛擬化的方式實現的代價比較高。

1.3 基于可信執行環境的方法

這類的實現主要是Intel 的MPK 技術以及ARM的TrustZone。比如IMIX(in-process memory isolation extension)[24]和文獻[11,15],都是利用MPK 技術,擴展load/store 指令訪問安全區域。MPK 技術與本文的實現類似,都是通過增加頁表的一些保留位來區分不同的空間。但本文的設計是以函數的粒度來劃分和使用可信的空間,這與MPK 技術有著本質上的不同,具體的分析在第2 節詳細介紹。

2 設計

首先需要解釋的是進程內隔離的概念,這里所關注的隔離是指單個進程內部,有些函數的執行結果情況未知,比如調用了某個不安全的庫(在Linux中典型的.so 文件),或者執行某個有惡意篡改核心數據的代碼片段,本文需要提供一個相對安全的環境,保證調用不安全的函數之后,不影響本進程原有的敏感數據。下面詳細論述本文提出設計的目的、動機以及如何解決遇到的挑戰。

2.1 設計動機

為了保證執行流(代碼片段)擁有隔離的獨立環境,首先需要給代碼提供內存操作的能力,保證能夠將執行的結果保存。其次就是解決各個執行流之間的交互性問題,也就是如何實現數據的可訪問性與不可訪問性。因此為了保證程序的正確性與安全性,本文需要解決以下3 個主要的問題。問題1:如何定性地區分安全區與不可信區;問題2:如何保證隔離空間的獨立性;問題3:如何處理安全區與非安全區的數據傳遞問題。

2.2 可信區間的切換

為了保證執行的安全,需要嚴格管理非安全的操作權限。對于問題1,首先要確定程序遵守2 點規范:(1)可信區間需要謹慎使用不可信區間返回的數據;(2)不可信區間不能訪問可信區間的數據。這2 個規范保證程序之間執行的安全特性。其次是如何進入和退出可信區間。通常進入可信區間的方式主要有以下2 種。第1 種是通過權限切換,然后進入較高特權級,執行完相應的操作之后,再次返回。這樣做的好處是權限明顯,非安全區無法直接操作安全區的數據,設計上也比較容易實現。但存在的主要問題就是由于要進行權限切換,借助操作系統發生系統調用,頻繁的調用會使得系統的性能較差、開銷較大。

另外一種是構造一個虛擬的執行環境,然后通過調用VMcall 等相關指令進入虛擬機。好處是能夠獨立出一個進程并行執行,缺陷就是難以實現數據的共享,比如虛擬機需要返回數據,通常要借助其他的方式數據共享,每次調用都是一個比較耗時的處理。對于少數場景不太頻繁的切換來說,選擇類似虛擬機的方式比較明智。

為了實現細粒度的切換,使用更加簡單的方式,類似于普通的函數調用Call 指令與返回Ret 指令。進入和退出抽象成函數調用的方式,進入函數,則進入另一個隔離域;函數返回,則退出當前隔離域。不同于普通的系統調用或者虛擬機調用指令,這些都是切換特權的指令,需要陷入內核執行較為復雜的情況判斷。而ICall 和IRet 指令在執行函數調用的同時硬件也會準備隔離環境,僅需簡單的數條指令就可以完成。

2.3 獨立的執行空間

問題2 是實現隔離空間獨立的關鍵步驟,按照程序執行時內存的使用情況依次保證函數執行互不干擾。

首先,函數執行需要保證3 個區域的數據存取。第1 個區域是全局數據變量,這個保存在可執行文件(ELF)的數據段,因此在程序初始化的時候保證這部分數據可以訪問。第2 個主要的區域就是函數執行的??臻g,這個需要保存函數執行中的參數傳遞、臨時變量的存儲、運行中由于指令集寄存器有限而需要暫時保存在棧內存中的數據,以及比較關鍵的棧指針和函數返回地址也需要保存在棧中。第3個區域是在執行過程中動態地申請內存,比如通過malloc 等相關API(application programming interface)分配,最終還是系統調用mmap 或者brk 分配內存。

針對全局的數據訪問,處理的方式比較容易,就是在程序初始化的時候,設置可見的全局數據訪問范圍,底層硬件訪問方式在第3 節實現中具體說明。對于棧數據的訪問,本文的設計思路是:當前執行的執行流無法操作父棧中的數據,除非使用2.5 節中設定的數據共享策略,如果一個訪問不能滿足這2個條件,則需要觸發訪問異常。

動態內存分配為了更好地滿足對于即時編譯(just-in-time)這類應用的支持,本文設計了更加靈活的處理方式。這類應用需要提供獨立的執行環境,運行完即時銷毀。在通過malloc 分配內存時,會根據當前的GFIDR 寄存器的值來判斷是否需要分配新的頁。這個寄存器保存著全局隔離域的值,是一個64 位的值,這個值按照如下的方式更新:當使用2.2 節中的ICall 指令時,GFIDR 會自增1;當使用IRet 時,則自減1。這個寄存器是在程序初始化的時候,由操作系統提供的隨機值,無法用其他的指令讀取。在操作系統分配一個新的頁時,內核會在設置頁表時,在頁表的[61:50](這個位段稱為IPSD,isolation and protection status domain) 保存GFIDR 寄存器的低12 位。因此,動態內存區分成了4 096 個區域。

按照本文的設計思路,當訪存指令load、store 訪存時,在TLB 中做地址轉換的同時,還要判斷是否符合本文設計的實現規則。表1 是做轉換時主要的判斷原則。

表1 TLB 轉換時相應的權限

第1 種情況,訪問子函數產生內存的情況。由于子函數可能返回未知安全性的數據,強制無法訪問子函數申請內存的數據。如果需要訪問,則可以在傳遞函數的時候,直接傳遞有當前函數分配的內存的指針。此時的情況就轉化成了第3 種情況。對于第2 種情況,本文沒有任何的限制,這也是保證庫函數的主要原因。當前函數可以任意分配和使用當前函數的內存,無任何限制。第3 種情況涉及到數據的共享,通過2.4 節中的具體策略來約束數據的訪問權限。

上述針對全局數據、棧內數據以及動態申請內存的保護都在硬件上做了限制,防止執行流通過ROP(return-oriented programming)攻擊任意地被修改,這也是保證安全可靠的前提。具體的安全性將在安全示例中說明。

2.4 數據的共享

問題3 的處理是設計實現隔離環境的關鍵。針對上面的設計理念,如何在父函數給子函數傳遞數據的同時保證相對的獨立性是設計本身最主要的安全性之一。針對數據傳遞的特性,本文提出了2 個主要的約束,下面依次說明。

策略1 局部約束:父函數傳遞單一指針。

單一指針就是這個指針所指向的內存對象中都是元數據,沒有指針這類數據類型,這也是比較普遍的調用方式。如圖1 所示。

圖1 單一指針傳遞數據

圖1 中的結構體foo_obj 中只包含基礎數據,可以有嵌套的數據結構,但是這些數據結構中不能有指針變量。在被調用時,如圖中第14 行,傳遞給子函數時只需要傳遞指針。正確情況處理這種情況比較簡單,編譯器只需要在執行調用的時候,先將指針賦給a0 寄存器(以RISC-V 架構為例),再以a0為指針空間,產生ciprii 指令,確保將地址、空間大小及屬性在執行ICall 指令前生效,將這種情況認為是使用局部的約束,元數據全部是數據,不包含指針的情況。在子函數中,無法通過指針來訪問foo_obj 之外的地址空間,這就保證了與其他的地址空間的獨立性。

策略2 全局約束:父函數傳遞指針的指針。

當父函數傳遞給子函數的指針對指向的內存中還保存有指針的類型時,處理的情況有所不同。如圖2 中代碼所示。

圖2 父函數傳遞指針的指針

結構體foo_obj 中包含有指針數據,或者嵌套的結構體中包含有這類數據時,處理的方式有所不同。主要不同在于,當沒有指針的時候,在函數調用前只需要插入一條ciprii 指令。但是,有指針的時候,為了保證子函數能夠正常訪問父函數傳遞給它的數據,在產生ciprii 指令的同時,也要保證子函數能夠處理ptr->nextptr->data這類數據。因為編譯器處理的時候,首先要把nextptr 的值加載到寄存器,然后再根據結構體foo_obj 的空間排布,再去訪問data 這個成員變量。在這個過程中,要通過nextptr這個指針來訪問。但是在調用ICall 指令前就只確定了子函數能夠訪問父函數的空間,在處理這種情況前,首先要遍歷結構體,然后對于這種指針,需要額外產生相關的ciprii 指令,保證程序的正確性,這種約束稱為全局共享約束。

在保證安全性的同時,為了支持數據的共享策略,底層硬件需要提供約束檢查。函數調用時,主要是通過參數傳遞給子函數,然后子函數按照傳遞的參數來處理。根據前面的說明,在傳遞指針參數的時候,相當于為子函數提供可訪問的內存地址空間。按照2.3 節所述,需要硬件上提供可訪問的內存范圍。因此在底層提供上下文約束窗CSW(context strains windows),每次在函數調用時,如果函數傳遞的參數中有指針(內存空間)傳遞,需要有調用者建立可以訪問的地址窗口。除此之外,所有的訪問都是視為非法訪問。

需要注意的是CSW 和棧的行為相似,隨著函數的調用,當父函數進入子函數時,父函數的CSW 內容需要保存。當子函數退出到父函數時,需要恢復父函數的CSW 內容。進入和退出時的CSW 嚴格相同,這部分由硬件自行保存且讀取其他指令無法修改,這就保證了安全性。具體的硬件實現將在下一節中詳細地描述。

3 實現

本節主要討論實現的具體細節。首先描述軟件方面的設計,包括指令集的設計、編譯器的支持以及動態運行庫的相關支持。其次詳細地闡述底層設計實現的細節,包括內存檢查單元等。

3.1 指令集擴展

底層選用RISC-V 架構,使用32 bit 指令,主要包括以下3 類:

(1)進入和退出隔離域:ICall 和Iret;

(2)設置全局訪問和CSW 約束的指令類;

(3)打開和關閉,配置相關的指令類。

上述的3 類指令中,只有第3 類是特權級指令,用戶態無法使用。其他2 類都是用戶級指令。

上述第2 類,設置CSW 指令是實現數據共享的關鍵。它的指令格式如下所示:

其中a0 是傳遞子函數的參數,通常是指針。imm_attri 是14 位的立即數,前10 位是a0 指向的地址空間大小,后2 位是屬性。屬性包括只讀、可讀可寫。第2 條和第1 條相似,只是將其中的立即數先保存到寄存器中。這樣做的目的是,使其能夠處理超過10 位的地址空間,這主要用于提供更大的地址空間。

3.2 編譯器支持

采用Clang/LLVM 編譯組件,編譯器主要支持以下2 個方面。

(1)完成3.1 節中指令集的擴展,包括LLVM后端支持代碼的生成以及匯編器對特權級指令的支持,提供ICall 與IRet 的切換功能。

(2)由2.4 節所述,Clang 需要分析函數在調用時的行為,分析參數的類型。生成傳遞內存空間的大小以及相關的屬性。在傳遞指針的指針時,格外讀取指針相關元數據屬性,分析程序是否執行符合編程的安全性,反饋警告消息給開發者改進。

實現上在Clang 集成了CallIsolationGuardPass,主要是負責程序行為的分析、參數判斷,包括生成LLVMIntriscs、輔助后期程序進一步分析以及生成CSW 設置指令。其次增加了SwitchDomainPass,主要是根據調用點函數的屬性,生成相關的切換域指令。在函數emitPrologue 和emitEpilogue 中,設置棧的約束空間,保證子函數不會隨意訪問父類函數的內存空間,增加設計的安全性。

3.3 硬件支持

3.3.1 CSW表

上下文約束窗CSW 主要是安全域之間交互的訪問窗口。每次函數切換域時提供的指針參數以及相關的空間大小、屬性值都保存在這里。其具體的結構如圖3 所示。

圖3 CSW 表的底層結構

圖4 MCU 單元底層結構示意圖

當函數調用ICall 切換安全域之前,編譯器會分析進入函數時所需要的參數。假設有一個參數是傳遞指針,首先Clang 會產生一條指令:

cipriia0,imm_attri;

這條指令會在表項中生成一個項目,相關寄存器設置如圖3 所示。如果并非單一的指針參數,則還需要加載指針的指針相關的元數據,和上述基本相同。當編譯器分析完所有的參數時,釋放ICall 指令,使CSW 的指針指向新的表項。當使用IRet 時,則恢復原先的指向??紤]到設計最大支持4 096 個隔離域,而且傳遞的參數不等,因此選擇表的大小為128 項。如果CSW 表存滿,需要將之前的表項寫入內存中,當CSW 空的時候,從內存中讀取。

每次CSW 請求內存時,都會按照一個完整的棧幀保存。比如當前進入域切換時,傳遞了5 個參數,則需要一次性將5 個表項全部寫入或者從內存讀出。為了支持不等量的參數,存入內存時保證第1項必須是當前CSW 的個數,這可以在后期CSW 內存請求時進行預取。

為了保證設計的安全性,CSW 表在內存中保存的位置用戶態無法獲取,也無法修改。在內核創建進程的時候,會分配這樣的內存空間,這是保留的空間,僅內核可修改可讀取。如果CSW 內存請求時,發現此空間已滿,則會觸發例外程序,然后再分配內存。

3.3.2 內存檢測單元

內存檢測單元(memory check unit,MCU)主要負責訪存是否符合設計的安全約束,一旦出現違例,就會觸發異常處理。MCU 主要由4 個部分組成。

(1)GDM(global domain manager)單元,主要負責LSU(load store unit)單元產生虛擬地址時,將虛擬地址IPSD 域和當前的GFIDR 比較,結果按照表1的約束規則處理,其次負責初始化GFIDR 寄存器。

(2)CSWM(context strains windows manager)單元,主要負責維護CSW 表,如果需要內存請求,則向下一級Cache 發起訪存請求。其次維護CSW 指針,也負責監視ICall 和IRet 指令。內置CAM 表存儲當前CSW 的內容,比較是否命中。

(3)G&PM(global data and stack pointer manager)單元,主要負責監視當前棧幀的信息,同時維護全局地址空間可訪問表,這個表主要是用于給全局數據變量提供可訪問窗口,也可用于整個系統的地址空間信息隱藏。

(4)CCM(compare and check manager)單元,主要負責判斷是否滿足約束,如果出現違例,則觸發異常,這個單元也負責CSW 表的異常管理。

當load 或者Store 指令生成虛擬地址時,將此地址發送到GDM、CSWM 和G&PM 單元,如果符合其中的一項檢測,有些地址訪存會通過其中多項檢查。將比對結果發送到CCM 單元,如果最后符合約束規則可通過,通知ROB(reorder buffer)單元可以進行指令提交,否則產生異常,通知用戶。

3.3.3 檢測狀態機

每條指令進入LSU 單元時,也會進入MCU 的檢查隊列。每條訪存指令都有一個狀態的有限狀態機,配合完成安全檢查,如圖5 所示。

圖5 安全檢查狀態轉換示意圖

(1)Idle 狀態:load/store 指令進入MCU 單元隊列時,如果當前沒有其他檢查指令,則進入TLB 中的GDM 單元,狀態進入Dcheck 狀態。

(2)Dcheck 狀態:此時比對IPSD,如果成功則直接返回Success 狀態。否則進入Ccheck。

(3)Ccheck 狀態:接著會比對CSW 中的內容。如果命中,則返回Success 狀態,結束比對。如果比對失敗,則進入GPcheck 狀態。如果此時比對的內容不在內存中,則需要向內存發起訪問請求,進入Mreq 狀態。

(4)GPcheck 狀態:進入G&PM 單元進行檢查。此時,如果比對成功,則進入Success 狀態返回,沒有成功,則進入Failed 狀態。

(5)Mreq 狀態:這時等待CSW 內存請求,一直等待內存數據響應。讀完當前調用的CSW 后,返回Ccheck 狀態。

(6)Failed 狀態:到達此狀態說明前面的檢查都已經失敗,則發起異常請求給ROB。通知用戶這是一條違例的訪存指令。

(7)Success 狀態:表明此條指令是通過了檢查,結束檢查。將信息更新到ROB 中。

3.3.4 硬件優化

由于安全檢查模塊在訪存步驟的關鍵路徑上,模塊設計的優劣直接導致整個程序的性能下降。為了提高流水線的利用率主要做了以下的優化措施。

(1)流水線的優化

設計整體流水線如圖6 所示,load/store 流水線的主要步驟為:1)地址產生;2)訪問TLB 單元和Cache;3)數據對其檢查;4)寫回。

圖6 安全檢查底層流水線結構圖

如圖6 所示:1)在AG 階段輸出虛擬地址后,在進入隊列的同時,進入GDM、CSWM、G&PM 單元,將臨時信息保存;2)控制邏輯開始上節的狀態機控制邏輯,同時將各階段的信息傳遞給相關的單元;3)CSWM 單元由于是采用CAM(content-addressable memory)表來比較,為了減少單周期執行的時間,將其分為2 個流水階段,在TC 和DA 階段后產生結果;4)將各個單元比對的結果進入CCM,最后根據檢查結果繼續隊列中下一個指令操作。

(2)預取的優化

由于CSW 有可能需要在內存中保存,但是如果等到CSW 空或者滿的時候,將內容讀取或者寫入內存。由前面的分析可得,有限狀態機一直等到數據的讀寫完成,這樣會導致訪存指令一直在ROB 中等待完成,造成關鍵路徑的等待,因此采用延遲觸發的方式來處理。當快要滿或者將要空的時候,觸發內存邏輯,更早地完成操作。

為了配合延遲觸發,在保存CSW 項的時候做了一定的優化,保存的第0 項是CSW 項的數目以及參數掩碼,表示是哪幾個指針參數的相關屬性,之后通知請求邏輯完成數據的讀取。

3.3.5 內核和運行時的支持

為了輔助完成安全檢查,內核以及庫需要做以下的支持。

(1)內核支持:當發送PageFault 異常時,請求內存分配,而在調用mmap 或者brk 的時候會將GFIDR 寄存器的信息傳遞給底層內核vm_struct。當設置頁表表項(page table entry,PTE)時,將其保存在IPSD 域。

(2)調用系統malloc 等相關API 時,分配內存時,需要按照當前的GFIDR 寄存器來判斷是否需要合并分配的內存空間。如果不是,則重新分配新的分配槽。

(3)當程序在加載的時候,Ld.so 處理動態庫的依賴時,需要將動態庫可讀可寫的數據段設置成全局內存可見可訪問,這保證其在之后的執行時避免非法訪存。

4 安全示例

以常用的例子來演示安全機制是如何防護非安全域操作的,如圖7 的代碼片段所示。

圖7 安全示例代碼及棧幀視圖

假設main 函數是安全可信的,且在自己的棧內部保存了局部變量local_obj 以及申請的動態內存對象lptr 的指針。main 函數需要在調用untrust 時,在傳遞指針的同時,也切換了安全域。假設untrust函數是未知安全性的指令流,可能被劫持。圖8 的代碼段是正常的RISC-V 的反匯編代碼(省區無關代碼),包括安全防護后的匯編代碼。untrust 指令流通過操作local_obj 的指針,間接修改了main 棧內的數據,如圖7 中代碼所示。同理,假設foo_obj附近有重要的數據,通過foo_obj 的指針操作周圍的內存區,造成數據泄露。

圖8 匯編代碼示意圖

如果使用了使用安全防護,如圖8 所示,首先假設untrust 強制通過棧指針操作main 中保存的返回地址(圖7 中的棧排布),此時MCU 單元的G&PM模塊不能通過檢查,報告異常。假設untrust 想通過foo_obj 的指針越界操作main 的棧數據(圖7中的10400 處的代碼),此時安全檢查會在CSWM 內發現越界操作,將棧越界信息傳遞給CCM。如果通過lptr 的指針操作動態內存,則會在TLB 和GDM 中處理異常。最后綜合后將結果傳遞給ROB。

下面假設賦予untrust 更加極端的可能性,擁有Code-Reused 攻擊能力[21]。假設它可以跳轉到代碼10564(ICall 指令之前),此時a0 和a1 都被設置成目標的內存地址空間,Size 和屬性設置成攻擊范圍。此時如果跳轉到地址去,程序會再設置CSW 表,接著可以跳轉到攻擊者設計的位置去執行相關非法操作。但是,關鍵點在于,在執行函數main 的第1 條指令時,會在棧內保存當前函數的GFIDR,如圖中104f8 處指令代碼。然后在代碼10560 處比對保存的是否和當前GFIDR 值相等,如果不相等則觸發CRA 異常,通知上層用戶非法操作。

需要注意的是,ildt 指令和ICall 指令必須成對出現,并且ildt 指令不能出現在ciprii 類指令的前邊。如果檢查到惡意提前執行,也會報CRA 異常。ildt 指令執行完后,會在底層硬件置標值位。ICall指令執行前要檢查此標志,如果沒有也會報告硬件CRA 異常情況。

假設untrust 函數再試圖去操作main 棧中的數據,來修改之前main 保存的GFIDR 值。但是通過local_obj 和lptr 指針越界處理main 棧中的數據都會被MCU 單元捕獲,然后會進行異常處理。因此untrust 都是無法修改安全域保存的GFIDR 值。

5 評估

這部分討論如何全面評估安全機制。主要從下面3 個方面來分析。

(1)安全性:通過安全測試集驗證本文解決方案是否能夠保證數據的安全。

(2)性能評估:通過分析SPECCPU2006 來評估使用安全機制帶來性能上的損耗。

(3)實用性分析:主要是提供一些編程上的規范,指導開發者更加容易地使用機制。

5.1 實驗環境

5.1.1 硬件平臺

采用Gem5 模擬器作為硬件的實驗平臺,使用RISC-V64 位架構。中央處理器(central processing unit,CPU)模型選擇O3CPU,支持亂序執行。具體的配置選項如表2 所示。

表2 Gem5 模擬器配置

5.1.2 軟件環境

編譯器使用Clang/LLVM-12.0.1 版本[25],C++庫使用配套的版本,C 庫使用musl-libc 1.22 版本[26]。本文修改了其中malloc 的相關代碼,以支持安全機制。也修改了ld.so 程序,保證動態庫在加載的時候,能夠正常地初始化執行。本文并未在SPEC 中使用,只是測試了其基礎功能。內核使用Linux-5.6 版本,修改了異常處理部分代碼,能夠在PTE 設置的時候更新IPSD 域。增加了對MCU 產生異常的處理程序。其次在加載程序的時候,能夠隨機設置GFIDR,支持進程切換和開啟、關閉安全機制的功能。

5.2 安全性評估

對于安全的評估在第4 節中做了相關的說明,本節主要是通過一些有異常的程序來測試機制的安全性。

5.2.1 Juliet 安全測試集

Juliet 測試集是由NIST(美國國家標準技術研究院)收集的一些安全測試集,用C/C++語言編寫,多架構支持??偣灿?18 類安全問題,本文選取了以下幾類測試,結果如表3 所示。

表3 測試的Juliet 分類

將上述的8 類測試,逐個選取出來,編譯成獨立的ELF 可執行文件。測試結果中,關于緩沖區溢出的部分測試未通過,主要是因為這些測試緩沖區溢出只是針對當前的棧內溢出,并未涉及到新的函數調用,也就是說測試只在一個安全域中進行,因此實驗結果會全部通過。其他的關于函數調用(安全域切換)的測試,全部顯示捕獲了異常,程序中止。

5.2.2 偽造的測試集

手動編寫了幾大類的測試程序,都是按照不同的攻擊類型實現的非法訪問代碼,具體測試的細節如表4 所示。

表4 不同類型攻擊測試情況

5.3 性能評估

本節評估設計的性能損耗。首先微觀上分析可能存在損耗的方面,其次通過SPECCPU 2006 來宏觀測試性能損耗,最后分析本設計與其他安全機制的性能對比。

5.3.1 微觀底層硬件分析

由前面的分析可以看出,存在性能損耗的有以下幾個部分。

(1)本文設計中新增了的指令,主要包含ciprii類指令在建立CSW 的時候,會有多余的指令向其中寫入約束屬性,為安全域切換準備。

(2)CSW 表滿的時候,需要向內存寫入;當表空的時候,需要從內存中讀取。在此期間需要流水線等待完成才能寫回提交指令。

為了保證安全域CSW 表的設置不被惡意地修改,抵御Code-Reused 攻擊,首先要在函數的最開始保存GFIDR 寄存器的值,然后在ICall 指令之前重新讀取判斷是否遭受ROP 攻擊。

上述3 部分是底層可能帶來多余執行時間的環節。在3.3 節中,在每次訪存操作時,MCU 的安全檢查流水化在執行load/store 時同時執行,因此不會產生多余的流水線停頓。剩下的配置指令,保護使能和關閉安全機制這部分代碼做了進一步的安全防護,但是這部分代碼執行次數較少,因此損耗可忽略不計。

5.3.2 宏觀測試集

用SPECCPU 2006 整形測試集[27]評估安全機制帶來的性能損耗。編譯器使用Clang-12.01,C ++庫使用配套版本。編譯選項使用-O2 優化選項,將SPEC 測試集編譯靜態可執行文件。為了避免手動的設置安全域切換函數,隨機選擇函數作為切換點。這部分由動態編譯器確定,選取大概20%的函數作為測試函數??偣策x取了11 個測試程序,400.perlbench 執行結果異常,在統計過程中去掉。采用熱啟動的方式,讓程序先執行200 萬條指令,然后再統計執行2 億條指令的執行情況。

圖9 展示了使用安全機制后與原始執行的時間對比,實驗結果全部采取歸一化處理。從圖中可以看出,實現的安全機制在SPEC2006 的平均性能損耗為2.83%,這些性能損耗的增加主要在于多余的管理指令和ciprii 指令,這些指令流水化后的執行只需要1 個時鐘周期。其次是為了保存設計的安全性,在防御Code-Reused 攻擊的時候,首先要在函數開始處產生一個類似store 的指令,在執行敏感程序的時候,類似load 的操作,然后執行對比判斷,相當于多增加了3 條指令:load、store 和bneq。綜上所述,這些都會使性能降低。

實驗中445.gobmk 這個測試程序性能損耗最大,達到了6.55%。分析發現,這個程序中大量使用了在2.4節中的全局共享數據,主要是傳遞了指針的指針這類變量。傳遞的元數據中每一個指針變量,會產生一個load 指令,并且也會產生一個ciprii 指令。當大量使用的時候會造成上述情況,導致程序的性能損耗加大。

為了對比提出的安全機制設計與其他設計的性能損耗情況,選取了與本文設計相似的幾個安全機制,在同樣的測試集SPEC2006 下,對比性能損耗情況,具體如下所示。

(1)AddressSanitizer[6]:Clang 的安全保護擴展。

(2)CPI/CPS(code-pointer integrity/code-pointer separation)[28]: 用于保護指針完整性,還提供了安全棧保護敏感數據。

(3)MPK[29]:Intel 用于安全隔離的機制。

(4)ARM PA:ARM 的指針認證機制。

圖10 展示的是與其他安全機制的性能對比??梢钥闯?在使用軟件的方式AddressSanitizer 時,性能損耗最大,將近60%。這主要是Clang 在每次進行訪存的時候,都要進行一次檢查訪存是否符合,這樣一來每次訪存相當于變成了好幾條指令,性能損耗最大。其次CPI 也是軟件的方式實現,但是它只是監管程序中所有的代碼指針(比如函數指針、返回地址等),因此損耗比較低。IntelMPK 機制的性能損耗僅有25%左右,它只提供了一套能夠隔離的機制,但還是要用其他的輔助手段保證它的安全性,比如保護敏感數據,抵御Code-Reused 類的攻擊,這樣性能損耗也提高了。ARM的指針認證只是針對于返回地址的,保護返回地址不被惡意修改,因此性能損耗最低。

圖10 與其他機制的性能損耗比較

5.4 可用性指導

(1)靈活易用的編譯選項。在Clang 中提供了編譯屬性_attribute_((calliso)),在聲明函數的時候顯式地指出,在需要切換安全域的地方直接調用,編譯器會自動分析參數,生成合適的指令。本文還增加了其他的輔助性編譯選項,用于增加設計的安全性。

(2)可信空間的選擇問題。一般如果使用第三方庫的時候不確定其安全性,可以采用此套安全機制。但是以下場景不建議使用,比如使用庫函數malloc 分配內存的時候,因為返回時子函數返回的內存空間已經被CSW 回收,如果此時再使用則會導致訪問異常。

6 結論

本文提出了細粒度安全域隔離硬件機制,提供了一套完整的解決方案,能夠為可信環境提供安全有效的保障,底層硬件能夠提供多達4 096 個獨立的隔離域。同時為了防止威脅性最大的Code-Reuse的攻擊,本文提供了有效的防護機制,能夠有效阻止這類攻擊,使得隔離機制更加安全可靠。在SPEC CPU 2006 上的測試結果顯示,本文的安全機制性能損耗僅有3%。

猜你喜歡
指針調用內存
外部高速緩存與非易失內存結合的混合內存體系結構特性評測
核電項目物項調用管理的應用研究
“春夏秋冬”的內存
LabWindows/CVI下基于ActiveX技術的Excel調用
為什么表的指針都按照順時針方向轉動
基于系統調用的惡意軟件檢測技術研究
基于改進Hough變換和BP網絡的指針儀表識別
ARM Cortex—MO/MO+單片機的指針變量替換方法
基于內存的地理信息訪問技術
利用RFC技術實現SAP系統接口通信
91香蕉高清国产线观看免费-97夜夜澡人人爽人人喊a-99久久久无码国产精品9-国产亚洲日韩欧美综合