鄧穎川,張 桐,劉維杰,王麗娜
(1.武漢大學 國家網絡安全學院空天信息安全與可信計算教育部重點實驗室,湖北 武漢 430040;2.螞蟻集團,浙江 杭州 310012)
使用C/C++編寫的程序可能包含安全漏洞??刂屏鹘俪止衾眠@些漏洞來篡改代碼指針,從而將程序的控制流轉移到目標代碼片段上。通過篡改多個代碼指針,攻擊者可以構造圖靈完備的代碼重用攻擊(Code-Reuse Attack),從而實現信息泄露或提權等目的。
針對控制流劫持攻擊而提出的防御機制嘗試確保間接函數調用時使用的函數指針為預期的值來阻止攻擊。指針完整性(Pointer Integrity,PI)[1-4]是其中具有代表性的一種,它維護了代碼指針和一部分數據指針的完整性。此外,控制流完整性(Control-Flow Integrity,CFI)[5-13]通過檢查間接控制流跳轉(Indirect Control-Flow,ICT)的目標是否符合預先分析得到的控制流圖(Control-Flow Graph,CFG),實現對前向間接控制流轉移的校驗,并將后向間接控制流轉移的校驗交給影子堆棧(Shadow Stack)[14]。
然而,即便有了這些防御措施,攻擊者依然能夠“修改”函數指針的值,使它指向一個預期之外的目標[15]。間接控制流轉移需要根據必要的運行時數據來選擇目標地址,這些運行時數據就是它的依賴,也就是對應函數指針的依賴。如果依賴被篡改了,那么控制流可能流向控制流圖中不同但是合法的路徑,而PI和CFI很難檢測到這類攻擊。例如,給定一個函數指針數組,其元素指向不同的函數。一個間接調用點從這個函數指針數組中根據索引取出對應元素作為跳轉目標。攻擊者可以通過將這個索引篡改為另一個合法的值,導致跳轉后執(zhí)行預期之外的函數。
文中引入了COLLATE,一個包含LLVM pass和運行時支持庫的工具鏈,用來保護函數指針及其依賴等控制相關數據的完整性。COLLATE首先使用靜態(tài)分析識別出控制相關數據,然后使用Intel 內存保護密鑰(Memory Protection Keys,MPK)(Intel Corporation.Intel(R) 64 and IA-32 Architectures Software Developer’s Manual,2016.https:∥software.intel.com/en-us/articles/intel-sdm),將它們和普通數據進行隔離,最后對程序進行插樁以允許合法的內存訪問操作。為了識別控制相關數據,COLLATE首先利用基于類型的分析來找出所有的函數指針和它們歸屬的對象(如果有的話)作為污點源(source)。然而,在某些情況下,一個通用指針(如void *)也可以指向一個函數。這意味著基于類型的分析得到的是不完整的結果。因此,首先利用上下文敏感且流敏感的指針分析SVF(Static Value-Flow Analysis Framework for Source Code.https:∥github.com/SVF-tools/SVF)來識別這些潛在的函數指針。然后,COLLATE將間接調用點標記為匯聚點(sink),并執(zhí)行污點傳播,收集污點指令及其操作數作為控制相關數據。
為了保護控制相關數據,COLLATE為它們的內存分配提供了一個受限的安全內存域Ms,而為其它數據提供了一個常規(guī)內存域Mn。對于控制相關數據中的全局和靜態(tài)變量,COLLATE首先將它們集中到可執(zhí)行文件中一個特殊的節(jié)(Section)中,然后在加載到內存中后將這個節(jié)映射到Ms中。對于棧分配,COLLATE首先在Ms中維持了一個額外的Separated Stack,然后使用它的棧分配指令替換了原本的棧分配指令。對于堆分配,COLLATE提供了一個定制的堆管理器來管理Ms中動態(tài)分配的控制相關數據。
此外,COLLATE利用指針分析來識別那些在程序執(zhí)行時預期修改控制相關數據的可信指令。通過使用專用的call gate,這些可信指令被授權訪問受保護的內存域。call gate在一條可信指令執(zhí)行前修改PKRU寄存器,授予它對Ms進行讀寫的權限,并在它執(zhí)行完成后立刻收回權限。
實現了COLLATE的原型系統(tǒng),包含一個LLVM pass和一個運行時支持庫。其中,LLVM pass負責控制相關數據的識別和插樁,動態(tài)庫提供了separated stack和heap的實現。系統(tǒng)保證了函數指針及其依賴的完整性,并將返回地址的保護交給影子堆棧(Shadow Stack)。
對COLLATE的有效性和性能進行了評估。該評估測試了COLLATE面對3個真實世界的CVE漏洞和一個虛表指針劫持攻擊[16]的測試集的防御效果。結果顯示,COLLATE能夠很好地防御控制流劫持攻擊。而在SPEC CPU 2006 benchmarks上的測試結果顯示,COLLATE引入的平均額外開銷約為10.2%,在Nginx上更是只約有6.8%,這種開銷是可以接受的。因此,解決方案提供了足夠的安全保證,并且具備了實用性。
綜上所述,筆者做出了以下三點貢獻。
(1) 提出了控制相關數據的概念,即函數指針及其依賴。它們可以被用來影響代碼指針的值,從而彎曲控制流。
(2) 構建了COLLATE的原型系統(tǒng),它有如下設計亮點:使用指針分析識別指向函數的通用指針;使用過程間靜態(tài)污點分析識別控制相關數據;將控制相關數據存儲到使用Intel MPK保護的安全內存域中。
(3) 使用SPEC CPU2006、Nginx和一些具有代表性的漏洞徹底評估了原型的有效性和性能。
文中把控制流劫持攻擊防御措施的傳統(tǒng)對手模型作為威脅模型,即假設強大但現實的場景:攻擊者可以首先利用內存漏洞訪問任意內存,然后控制代碼指針來劫持或彎曲控制流。威脅模型與現有的相關工作[2-3,17-18]是一致的。DEP/NX(Data Execution Prevention)被啟用,并且在系統(tǒng)中部署了一個影子堆?;虬踩褩?。筆者進一步假設所有的硬件(如Intel MPK)和操作系統(tǒng)都是可信的,這意味著針對它們的攻擊不予考慮。
COLLATE的目標是保證間接控制流轉移(ICTs)的目標符合預期。圖1是COLLATE工作流程的概覽。在獲取目標程序的LLVM bitcode文件后,COLLATE首先以函數指針和它們歸屬的對象作為污點源,ICTs作為污點匯聚點。接著,COLLATE根據提出的傳播規(guī)則執(zhí)行靜態(tài)污點傳播,從而識別出控制相關數據。然后,COLLATE在bitcode上進行插樁,將控制相關數據的內存分配到受保護的安全內存域Ms中,并僅允許可信指令對它們進行修改。最后,COLLATE將bitcode編譯為目標文件,并鏈接相應的運行時支持庫,從而生成加固后的可執(zhí)行文件。
圖1 COLLATE工作流程
COLLATE的第1步是識別控制相關數據。首先,使用一個混合分析方法找出函數指針和它們所屬的對象作為污點源。接著,創(chuàng)建用于識別函數指針的依賴的污點傳播規(guī)則。最后,在過程間數據流圖上執(zhí)行污點傳播,并收集所有被污染的對象作為結果。
污點源是指那些屬于函數指針或至少包含一個函數指針的內存對象。利用基于類型的分析來快速識別污點源,并利用指針分析的優(yōu)勢來對結果進行補充。基于類型的分析的任務是通過輕量級的類型匹配找到污點源。
基于類型的分析的第一步是識別污點源類型。借助LLVM類型系統(tǒng)的優(yōu)勢,可以遍歷程序中的所有類型,找到所有函數指針類型。污點源類型可能被嵌入到復合類型中(如結構體類型),此時它們都應當被視為污點源類型。注意,之前對于污點源類型的定義是遞歸的,這意味著需要遞歸地檢查每一個復合類型及其復合類型字段。
第一步是獲得所有內存對象的類型,并確定它們是否是污點源。LLVM IR提供了alloca指令來分配棧內存,并使用GlobalVariable類型來描述全局和靜態(tài)變量的內存分配,這使得可以通過預定義的API獲得內存對象的類型。然而,LLVM不能直接識別堆分配點,因為它被表示為一個函數調用,但缺乏類型信息。通常情況下,開發(fā)者會利用標準庫函數,如malloc和calloc,來為程序分配堆內存。注意到LLVM的優(yōu)化pipeline(如O1)包括一個名為tbaa的別名分析,它在元數據標簽中保留了Clang前端獲取的類型信息;這些元數據標簽被附加到讀/寫之前分配的堆內存的內存操作指令(如load)中。因此,COLLATE利用輕量級的use-def分析來找到堆內存對象最近的內存操作指令,并獲得存儲在tbaa標簽中的類型信息。有了所有對象的類型信息后,就可以通過類型匹配迅速識別污點源。
對基于類型的靜態(tài)分析所識別的污點來源可能是下近似的。即通用指針(如void *)也可能指向函數,而這是無法通過基于類型的分析識別的。因此遺漏的函數指針和依賴沒有得到保護,導致它們可以被攻擊者篡改。
如算法1所示,COLLATE利用指針分析來補全污點源集合。算法的輸入是通用指針集合,輸出是污點源集合。COLLATE用空集初始化額外的函數指針集合,用基于類型的分析結果初始化輸出(第①行)。對于通用指針集合中的每個元素,COLLATE找到其points-to set(第④行)。如果該指針是一個潛在的函數指針,COLLATE將其插入到函數指針集合中(第⑤~⑩行)。
算法1基于別名分析的污點源識別算法。
輸入:GPtrSet:程序中所有通用指針的集合
輸出:TaintSet:指向污點源的指針的集合
① ExFunPtrSet←?
② TaintSet ← typeBasedAnalysis()
③ foreach GPtr in GPtrSet do
④ GPTSet ← getPointToSet(GPtr)
⑤ foreach Target in GPTSet do
⑥ if Target isa Function then
⑦ ExFunPtrSet ← ExFunPtrSet∪{Target}
⑧ break
⑨ end
⑩ end
加入函數指針集合后,COLLATE首先使用后向數據流分析來找到該函數指針所在的內存對象(第行)。最后,COLLATE再次利用別名分析,找到所有作為污點源的內存對象(第~行)。
在識別污點源之后,COLLATE分別創(chuàng)建前向傳播規(guī)則以找到所有污點路徑和創(chuàng)建后向傳播規(guī)則以識別依賴。對于前向污點傳播,COLLATE提出以下規(guī)則:
(1) 前向污點分析從污點源開始,在間接函數調用點結束。
(2) 如果一條指令的任何操作數被污染,這條指令也會被污染。
通過應用規(guī)則(1)和規(guī)則(2),COLLATE確定了從污點源到間接調用點的所有污點路徑。每條污點路徑都由一組污點指令組成,描述了一個指針從創(chuàng)建到調用的生命周期。需要注意的是,在LLVM IR中,指令和它的結果在語義上是等價的,可以相互替換。
為了識別函數指針的依賴,COLLATE創(chuàng)建了如下后向傳播規(guī)則:
(3) 后向污點分析從(之前的)污點指令開始,在內存分配點結束。
(4) 如果一條指令被污染,那么它的所有操作數都會被污染。
(5) 如果一條內存操作指令被污染,那么它的指針操作數可能指向的所有目標都會被污染。
(6) 如果一條phi指令被污染了,那么它所對應的分支指令的條件也會被污染。
規(guī)則(3)和規(guī)則(4)描述了基本的后向污點傳播方向。 規(guī)則(5)介紹了內存操作指令的污點傳播規(guī)則,這些指令在值操作數和指針操作數之間傳輸數據。具體來說,以load指令為例:如果一條load指令被污染,COLLATE就會污染所有在其指針操作數的points-to set中的內存對象。
在LLVM IR中,phi指令用于實現靜態(tài)單賦值(Static Single Assignment,SSA)的Φ節(jié)點。當一個變量會根據控制流的路徑的不同(例如,根據if或else分支)被賦予不同的值時,它將被表示為一個Φ節(jié)點。因此,正如規(guī)則(6)所述,phi指令的值不僅取決于前面的基本塊,而且還取決于對應分支指令的條件。
為了進行污點傳播,COLLATE首先構建一個過程間的def-use鏈,然后根據前面提出的規(guī)則進行污點分析。
事實上,LLVM為每個函數構建了一個函數內的def-use鏈。因此,COLLATE首先用指針分析和類型匹配構建一個函數調用圖。函數調用圖表示了調用點和目標函數間的映射關系,將它們關聯起來。然后,COLLATE根據函數調用圖將現有的def-use鏈擴展為過程間的def-use鏈。具體來說,COLLATE以函數調用點的實參為def,目標函數的形參為use,為def-use鏈創(chuàng)建新的前向邊。同時,COLLATE將目標函數的返回值視為def,將函數調用點的結果值視為use以創(chuàng)建新的后向邊。
筆者用一個具體的例子來闡明污點傳播的過程。圖2中展示了一個函數的控制流圖。每個方框代表一個基本塊。基本塊之間的實線代表控制流,虛線表示兩個基本塊之間的支配(dominance)關系。例如,BB_1支配(dominates)BB_4,因為從入口節(jié)點到BB_4的每條路徑都必須經過BB_1。
圖2 一個函數內部的污點傳播過程
對于前向污點傳播,將fp_arr作為污點源(第⑦行),將間接控制流傳輸作為污點匯聚點(第行)。因此,總結出一條用實線下劃線標出的前向污點路徑,即:fp_arr(第⑦行)→%8(第⑦行)→%9(第⑧行)→%14(第行)→call(第行)。
后向污點傳播從之前的污點指令開始,被污染的%0(第⑦行)用方框進行了標注;它是這個函數的一個參數。此外,一條被污染的phi指令%14(第行) 使得對應的條件值%6(第⑥行)被污染,就像3.2節(jié)中規(guī)則(6)所描述的那樣。因此,COLLATE識別出另一條用虛線下劃線進行標識的后向污點路徑:%6 in br(第⑥行)→%6(第⑤行)→%5(第④行)→%4(第③行)→alloca(第①行)。最終,COLLATE將這些污點值視為控制相關數據。
為了將各種控制相關數據分配到受保護的內存域,COLLATE利用各種策略來處理不同的內存分配方式。
(1) 棧分配。COLLATE在Ms域中維護一個separated stack,以保存分配在棧中的控制相關數據。COLLATE模擬常規(guī)棧的操作,為局部變量分配和回收棧內存。具體來說,COLLATE在Ms域分配一個內存區(qū)域,并將separated stack的棧指針(即%sep_sp)初始化為該內存區(qū)域的高地址,如圖3所示。給定一個局部變量,COLLATE首先根據類型和對齊方式計算其大小。然后,COLLATE利用sub指令來調整%sep_sp的位置以分配??臻g。最后,sub指令的結果被轉換為原本的類型,可用于引用這個棧對象。為了回收棧幀,COLLATE在函數序言之后保存了%sep_sp的位置,并在所有的ret指令之前恢復%sep_sp的位置。
(2) 堆分配。對于動態(tài)分配的控制相關數據,COLLATE用一個專門的堆分配器取代了原來的glibc分配器。這個堆分配器將受保護的內存區(qū)域劃分為16字節(jié)的chunk,從低地址開始分配動態(tài)內存。在分配動態(tài)內存時,堆分配器會找到一個由連續(xù)的chunk組成的、符合請求大小的block,并返回該block的基址。為了找到可用的塊,COLLATE維護了一個空閑鏈表,將未分配的block連接成一個鏈表,如圖3所示??臻e鏈表記錄了每個block的大小和后續(xù)block的地址,有利于分配和刪除操作的實施。
圖3 Separated stack和heap的內存布局
(3) 靜態(tài)存儲區(qū)分配。COLLATE使用一個separated segment來保存靜態(tài)的控制相關數據。為了實現這一功能,COLLATE首先為它們創(chuàng)建一個特殊的ELF section,然后分別在該section的開始和結束處插入兩個填充變量,使得它按頁對齊。當可執(zhí)行文件被加載到內存中時,這兩個填充變量可以用來確定內存中segment的實際地址范圍。最后,在程序執(zhí)行前,COLLATE將該segment映射到Ms以進行保護。
可信指令是那些原本就會寫入控制相關數據的內存操作指令(例如,store)。對于每個可以寫入內存的內存操作指令,COLLATE利用指針分析來獲得其指針操作數的points-to set。如果points-to set中包含屬于控制相關數據的內存對象,COLLATE將該指令視為可信指令。在外部call指令的情況下,如果其參數的points-to set中包含屬于控制相關數據,COLLATE也將其視為可信指令。
COLLATE利用Intel MPK將進程空間分割成兩個內存域:普通數據的域Mn和受保護數據的域Ms。當CPU運行在Mn上下文時,屬于在域Ms的內存區(qū)域默認為只讀,以保證完整性。為了確保程序的正常運行,COLLATE用call gate來包裹可信指令,暫時允許它寫入受保護的內存區(qū)域。
call gate首先使用WRPKRU指令將PKRU_ALLOW_D1寫入PKRU寄存器(第①~⑤行),允許之后的代碼寫入Ms。PKRU_ALLOW_D1是一個宏,代表了Mn和Ms都可讀可寫時PKRU的值。call gate的匯編代碼如下所示:
① xor ecx,ecx
② xor edx,edx
③ mov PKRU_ALLOWED1,eax
④ ;Write PKRU ALLOW DI to PKRU,allow access domain 1
⑤ WRPKRU
⑥
⑦ ;Execute trusted instruction
⑧
⑨ xor ecx,ecx
⑩ xor edx,edx
接著,可信指令在對Ms有完全的權限的情況下被執(zhí)行(第7行)。在執(zhí)行完成后,call gate再次將PKRU_DISALLOW_D1寫入PKRU寄存器(第⑨~行),禁止之后的代碼寫入Ms。
關于call gate的安全性,一個合理的擔憂是,WRPKRU指令是否會被攻擊者利用。答案是否定的,因為控制流劫持攻擊必須首先篡改能夠影響控制流的數據,而COLLATE通過保護控制相關數據的完整性,使得它在源頭上就不可能做到。
筆者進行了實驗性的評估來展示COLLATE的效率和有效性。為了測試COLLATE的效率,將其應用于SPEC CPU 2006 benchmark和一個真實世界的應用程序上以測量它的開銷。為了測試COLLATE的有效性,使用3個CVE漏洞和CFIXX測試套件來測試它的防御效果。
在AWS EC2實例上進行實驗。 該實例的類型為c2n.xlarge,運行64 bit Ubuntu 20.04系統(tǒng),配備8核Intel Xeon Platinum 8124 MB CPU和16 GB RAM。利用wllvm(Whole-program LLVM.https:∥github.com/travitch/whole-program-llvm.)編譯源代碼,然后提取出LLVM bitcode作為COLLATE的輸入。然后,用COLLATE分析輸入的bitcode并進行插樁,生成轉換后的bitcode文件。最后,將bitcode編譯成目標文件,并和COLLATE的運行時支持庫鏈接起來,產生加固的可執(zhí)行文件。在編譯目標程序時,使用默認的優(yōu)化級別(SPEC CPU 2006為O2,Nginx為O1)和選項。
修改指令的數量如表1中第3列所示,絕大多數C編寫的benchmark中的插樁的指令少于CPI,甚至少于它的1/10。極少數情況下(bzip2)插樁的指令數量多于CPI。
這是由兩者設計上的不同導致的:CPI的權限檢查需要通過插入判斷函數,在軟件層面上判斷寫入內存時寫入的地址是否超出預期內存的邊界。因此它修改的指令中大部分都是這樣的判斷函數。而COLLATE使用MPK來保護數據,因此權限檢查是交由MPK自動進行的,這是在硬件層面上的檢查,因而不需要額外插樁。在一些benchmark上(如mcf和libquantum),沒有進行插樁。這是因為這些benchmark中不存在間接函數調用。
存在受保護數據的函數數量(FNprotected)如表1中第4列所示,筆者的工作同樣少于CPI。這一方面是因為保護的對象不同,CPI保護的是那些可能以內存不安全的形式訪問的內存對象,而COLLATE保護的是函數指針及其依賴;另一方面也是因為兩者判斷的方式不同,CPI僅僅是根據簡單的def-use關系判斷一個內存對象是否安全,沒有考慮通過指針訪問這個內存對象的情況,因而不夠準確;而COLLATE同時考慮了這兩種情況,通過使用指針分析更精確地確定需要保護的內存對象。當然,也找到許多不屬于任何函數的全局控制相關數據。
表1 控制相關數據的識別和插樁的結果
控制相關數據的數量(CRD)如表1第5列所示,控制相關數據的數量與ICT的數量呈正比。因為控制相關數據與函數指針密切相關。對于 h264ref和nginx來說,它們的控制相關數據的數量遠遠多于其他benchmark,因為它們有超過300個ICT。milc有不成比例的控制相關數據。這是因為它的4個ICT都用于回調,直接使用函數作為參數。所以,沒有任何指令會修改函數指針。
SPEC CPU 2006共包含了17個C/C++語言編寫的benchmark。在實驗中,測試了所有這些benchmark和一個真實世界的應用程序Nginx。測試結果展示在圖4中。
圖4 歸一化的性能開銷,average顯示的是不包括Nginx的平均開銷
如圖4所示,時間開銷總體上比CPI略大。在SPEC CPU 2006上的結果顯示,CPI的平均開銷約為3.7%,而COLLATE的平均開銷約為10.2%。筆者的開銷約是CPI的3倍。考慮到受保護數據的范圍,這種額外的開銷是可以接受的。CPI只保護函數指針和用于訪問函數指針的數據指針,而COLLATE保護函數指針和與函數指針的計算有關的所有數據。因此,只用2倍的額外開銷就提供了更全面的保護。此外,筆者發(fā)現平均開銷的差異有很大一部分是由bzip2帶來的。忽略bzip2后,COLLATE的平均開銷低至約6%,僅僅不到CPI的2倍。對于nginx,CPI的開銷約是6.4%,而COLLATE的開銷約是6.8%,僅增加了約0.4%。顯然,COLLATE在實際應用中的開銷比SPEC CPU 2006上的開銷要小,與CPI相差不大。綜上所述,COLLATE在保護了更多的對安全至關重要的數據,提供了更加全面的安全保障的情況下,性能開銷仍然保持在可接受范圍內。
很多情況下,盡管benchmark中應用了COLLATE,但是它的性能卻沒有降低,反而還略微提高了,如hmmer、namd等。這種現象的原因有兩個:一是由于應用了MPK,權限的切換僅需寫入專用的PKRU寄存器即可完成,使得修改后的指令增加的開銷很小;二是由于修改的指令原本的執(zhí)行頻率就相對較低。以hmmer為例,hmmer中COLLATE修改的指令約46%都位于hmmio.c中,用于IO操作。而相較于IO本身的開銷來說,COLLATE為指令本身增加的開銷幾乎可以忽略不計。同時,hmmer被修改的指令約有98%是標準庫函數調用,本身執(zhí)行頻率就遠低于store等寫指令。對于這些指令,與它們本身的開銷相比,COLLATE引入的開銷幾乎可以忽略不計。
盡管在大多數benchmark上COLLATE的開銷都很正常,但是,在bzip2上,COLLATE的開銷異常的高(約80%)。事實上,筆者發(fā)現,平均開銷上的很大一部分是由bzip2帶來的,在去除bzip2后,COLLATE的平均開銷減少到約6%。分析bzip2和COLLATE的工作機制后,得出原因如下。
bz_stream結構體中包含有函數指針類型(第⑥行)。因此,COLLATE將所有bz_stream結構體類型的數據都識別為需要保護的數據,使用MPK進行隔離,并且每一次對它的修改都需要使用call gate臨時授予權限。而bz_stream結構體保存了所有與壓縮相關的數據,它的字段包含用戶可見的全部數據。所以bzip2中修改這些數據的頻率特別高,再加上這些修改都是以字節(jié)為單位進行的(第,行),最終導致了bzip2 benchmark的開銷異常得高。
bzip2的bz_stream結構體類型和一條訪問其實例的指令如下:
① typedef struct {
② …
③ char *next_in
④ char *next_out
⑤ void *(*bzalloc)(void *,int,int)
⑥ …
⑦ } bz_stream
⑧ …
⑨ while (True) {
⑩ ∥type of s->stnn is bz_stream
為了證明COLLATE能夠提供足夠的安全保證,筆者使用真實世界的漏洞進行了測試。首先復現了2個針對FFmpeg的堆溢出漏洞(CVE-2016-10190和CVE-2016-10191),以及Nginx的1個棧溢出漏洞(CVE-2013-2028)。復現完成后,利用這些漏洞發(fā)起前向控制流劫持攻擊。最后,在FFmpeg和Nginx上應用COLLATE進行防御,并檢查攻擊是否被阻止。此外,還以相同的方式使用CFIXX test suite測試COLLATE在C++程序上的有效性。測試結果顯示,COLLATE成功檢測到了所有的控制流劫持攻擊。
CVE-2016-10190:這是一個由整數溢出引起的堆溢出漏洞。利用溢出來覆蓋read_packet實例中的函數指針AVIOContext,以劫持控制流。在COLLATE中,由于read_packet是一個函數指針類型,AVIOContext的實例被分配在Ms中。當攻擊者試圖通過堆溢出來修改它時,MPK會檢測到這種非法訪問,并報告錯誤。系統(tǒng)成功地檢測到了這種攻擊的發(fā)生。
CVE-2016-10191:這是一個堆溢出漏洞。利用溢出覆蓋RTMPPacket中的data指針構建任意寫漏洞,最后修改write_packet實例中的AVOutputFormat函數指針,實施控制流劫持攻擊。在COLLATE中,由于write_packet是一個函數指針類型,AVOutputFormat結構的實例被分配在Ms中。根據指針分析的結果,原本的data指針并沒有指向控制相關的數據。所以將data指針指向的數據寫入內存的指令不允許修改控制相關數據。因此,當通過任意寫入漏洞覆蓋 write_packet 指針時,MPK會檢測到這種非法訪問。
CVE-2013-2028:這是一個由整數溢出引起的棧溢出漏洞。利用溢出來覆蓋ngx_conf_t的實例中的函數指針handler,以劫持控制流。在COLLATE中,ngx_conf_t結構的實例將被分配到Ms中。當攻擊者試圖通過緩沖區(qū)溢出來覆蓋它時,COLLATE將檢測到這種非法訪問。
CFIXX test suite:這個測試套件演示了vtable指針被篡改的各種可能情況。總共有5種類型,即FakeVT、FakeVT-sig、VTxchg、VTxchag-hier和COOP。以VTxchag-hier的測試為例。VTxchag-hier攻擊將虛表指針修改為同一個類層次結構中另一個類的虛表指針。此時,虛函數調用的目標就變成了另一個類的虛函數,因為虛表指針被篡改了。在COLLATE中,虛表指針是控制相關數據,當程序調用構造函數創(chuàng)建對象時,在Ms處進行備份。當調用虛函數時,將虛表指針與它的備份進行比較。如果校驗失敗,則說明虛表指針被攻擊者篡改,COLLATE將終止程序并在檢查后報告錯誤。
分析哪些類型的攻擊可以被COLLATE阻止。
(1) 直接修改函數指針的攻擊,在進行修改時就會被發(fā)現并阻止。
(2) 修改控制相關數據(如函數指針數組的下標和虛表指針)的攻擊,由于這些數據受到保護,同樣會被阻止。
(3) 修改user identity data和decision-making data等關鍵數據的非控制數據攻擊無法被阻止,因為它們不在保護范圍內。
(4) DOP攻擊無法阻止,因為它完全不涉及控制流。
展望未來,COLLATE也許能夠防御(3)中的一部分,user identity data等數據經常用在分支語句中,并決定直接控制流轉移的目標。如果將直接控制流轉移納入考慮,那么這些數據就可以認為是控制相關數據,并用COLLATE來保護。
筆者大量使用了指針分析的結果,包括使用它來識別可信指令。然而指針分析的精度并不高,運行使用COLLATE保護后的SPEC CPU 2006 benchmarks,并記錄在運行時修改控制相關數據的指令。結果顯示,這些指令只占可信指令的1/5。如果能提高指針分析的精度,那么COLLATE的開銷將會進一步降低。也許未來可以結合靜態(tài)和動態(tài)指針分析來提高精度,或者寄希望于靜態(tài)指針分析的進步。
現在保護的粒度是整個內存對象,也就是說,即便函數指針依賴的只是結構體中的一個元素,依然要將整個結構體的內存分配到安全內存中。同時,即便是訪問這個內存中其它元素的指令,也必須被允許訪問受保護數據。如果能將保護的粒度降低到字段級別,那么就能進一步減少COLLATE的開銷。
在保護forward-edge control flow的過程中,筆者的工作和以往的相關工作有一些相似處和不同處。在這一節(jié)中,對相關工作進行簡要的介紹。
CFI是常用的控制流劫持攻擊的防御措施。早期的CFI是無狀態(tài)的(stateless)[5,10-11,13],它們通過靜態(tài)分析生成CFG,不考慮上下文信息。Intel更是在芯片上添加了對CFI的支持(Intel control-flow enforcement technology),這個硬件特性還被最新的研究用于內存隔離[19]。這些CFI無法防御控制流彎曲攻擊[15],因為它們的等價類(Equivalence Class,EC)很大。最近的上下文敏感的CFI利用上下文信息來減少EC的大小。PathArmor[12]在運行時使用last branch record (LBR)獲取最近的16個分支信息作為上下文。然而LBR的容量太小,導致PathArmor無法防御history flushing攻擊[20]。PITTYPAT[6]和μCFI[17]都利用Intel PT記錄的執(zhí)行路徑信息來縮小EC,μCFI還額外利用了約束數據(constraining data)。但是Intel PT的性能和存儲開銷很大,可能帶來實用性上的問題。此外,在靜態(tài)分析時,CFI-LB[21]使用函數調用點(call-site)作為上下文,OS-CFI[8]則選擇了函數指針和對象的起源(origin)。但是,CScan[9]的測試結果顯示它們縮小EC的實際效果和聲稱的存在差距。在上述研究聚焦于縮小EC時,最新的研究[7]認為這并不能很好地提高程序的安全性,并提出使用不同基本塊對攻擊者的有用程度作為新的衡量標準。
PI通過保護指針不被篡改來緩解攻擊。PointGuard[1]和CCFI[4]都對要保護的指針進行加密,并在使用時進行解密。但是,PointGuard通過異或進行加解密,只要獲取多個指針就能推測出密鑰。CCFI則沒有保護嵌入C的結構體中的指針。CPI[2]將函數指針及用于訪問它們的數據指針隔離到用信息隱藏(x86-64)或段寄存器(x86-32)保護的安全區(qū)域中。然而,信息隱藏(information hiding)已經被基于時間的側信道攻擊(timing side-channel attacks)[22]繞過,不再安全。ARMv8-A架構中增加了對指針認證 (Pointer Authentication,PA)的支持,并且已經被證明能夠有效地保護代碼指針和返回地址[23],甚至實現內存安全[24]。類似地,ZeR?[25]提出在ISA中增加一些專用于讀寫指針的指令以保護代碼和數據指針的完整性,并設計了一個新型的元數據編碼方案。
許多工作使用基于硬件的隔離來保護敏感數據或安全區(qū)域。xMP[26]使用Intel虛擬化技術(Intel VT-x)來保護它的不連續(xù)的xMP域,不過這需要對內核進行大量修改。Hodor[27]和ERIM[28]都使用MPK來實現進程內隔離,并分別在高吞吐率和高切換率的情況下取得了優(yōu)異的性能。VIP[18]采取了另一種思路,將安全敏感數據的值備份在MPK保護的HYPERSPACE中,并在使用時取出記錄進行驗證。為了提高性能,VIP中普通內存和HYPERSPACE是直接映射的,這導致內存開銷最大可約達103.1%。此外,VIP不能自動識別安全敏感數據,需要人工標注,主要用來加固其它安全措施。與之相比,能夠自動識別控制相關數據,并且沒有額外的內存開銷,只是改變了要保護的數據的分配位置。cryptoMPK[29]也能首先自動識別加密(crypto)相關的緩沖區(qū)和操作,然后在源代碼層面上將它們轉移到MPK保護的域中。TDI[30]則在隔離方面有更細的粒度,它將不同類型的內存對象隔離在不同的內存區(qū)域,并對這些區(qū)域的加載操作進行限制。PKRU-SAFE[31]將在混合語言環(huán)境中將安全語言和不安全語言的內存隔離開來,從而防止攻擊者通過攻擊不安全語言編寫的部分來破壞安全語言的安全保證。PKRU-SAFE只考慮堆內存,并使用動態(tài)分析識別需要保護的內存,而非COLLATE采用的靜態(tài)分析。這雖然避免了假陽性,卻會導致假陰性的出現,從而發(fā)生漏報。Jenny[32]則用MPK來保護自己的系統(tǒng)調用監(jiān)視器(syscall monitor)相關的代碼和數據,對系統(tǒng)調用進行過濾。
文中提出了COLLATE,一種新型的保證間接控制流轉移的目標為預期值的防御措施。COLLATE的關鍵思想是保護函數指針和它們的依賴不被篡改。實現了COLLATE的原型并評估了它的性能和有效性。評估結果表明,COLLATE在實際應用和測試套件中成功地阻止了控制流劫持,并將平均開銷保持在可接受范圍內。這證明COLLATE是有效的且是低開銷的,具有實用性。