控制流劫持攻擊是當前較為主流的攻擊方式之一,包括ROP、JOP等等。相應的緩解、防禦措施則包括數據執行保護DEP、棧保護、地址隨機化ASLR、控制流完整性CFI等等。以上措施是大家比較常見和常用的,也有許多人寫了大量的相關文章進行分析。現在我想給大家介紹很少有人提及的一種防禦措施——代碼指針完整性CPI,希望對大家有所裨益,錯漏之處,還請大家不吝斧正。CPI即code-pointer integrity,是由Volodymyr Kuznetsov(可譯作庫茲涅佐夫,很帶感有沒有,推薦大家去看一看他的視頻,那英語更帶感)等人於2014年提出的一種防禦控制流劫持攻擊的機制。核心思想是將進程占用的內存劃分為安全區(safe region)和常規區(regular region),並基於硬件(也有軟件方式,但不常用)對兩個區域做了隔離。安全區只能存放敏感指針和元數據(metadata,用來描述敏感指針指向對象的值,地址上下界,以及分配的時序id,如下圖所示),同時對安全區的訪問要麼是在編譯時證明安全的,要麼是通過運行時安全檢查的。對常規區的訪問沒有任何特殊之處。 對進程內存有完全的控制權,但不能修改代碼段可以對任意地址 進行讀寫不能干涉程序加載這樣的假設已經大大高估了攻擊者的能力,同時也 能保證代碼插樁的進行和區域隔離的實施。靜態分析時將會對敏感指針進行識別。如果一個指針的類型是敏感的,那麼這個指針就是敏感的。敏感類型主要包括: 泛型指針(void*, char*, 在定義結構體或類以前就聲明的指針) 用戶自定義的敏感類型 (如存儲有操作系統UID信息的結構體) 所有在編譯或運行是隱式生成的代碼指針(返回地址,C++虛函數表,setjmp 緩存)另外,所有對敏感指針進行操作的指令也需要被識別,主要包括:由于敏感指針類型里包含char*這樣的泛型指針,所以靜態分析時會高估靜態指針數量,為了減少開銷,CPI將作為libc字符串函數參數和指向常量的char*指針認為是不敏感的。插樁時將會在安全區和常規區都分配空間給敏感指針,但同時只能有⼀個是有效的。這樣做能夠解決內存布局改變帶來的兼容性問題,同時也能避免類似void*這樣的指針的敏感性發生改變帶來的問題。這樣的方案能夠按照指針在常規區的位移來計算其在安全區的相應地址。靜態分析過程中已經找到了對敏感指針進行操作的指令,插樁將會對其進行針對性的改寫,創建新的或者是直接沿用之前已有的元數據。其中,對敏感指針進行讀取和存儲的指令將會由CPI機制設計的指令代替以將敏感指針從安全區取出或者是存入安全區。call指令和ret指令的保護將會通過安全棧(safe stack)來進行。敏感指針的每次解引用都要進行插樁,以便在運行時檢查元數據來檢測該指針是否安全。泛型指針在安全區和常規區都占有內存,若不是敏感指針,則將其元數據中的下界設置為大於上界,這樣一來,訪問元數據時就會認定為非法訪問,從而轉為訪問常規區進行相應操作。在此架構下,CPI依賴於硬件段保護,使得安全區只能通過特定、專用的段寄存器訪問,實際上是將該寄存器當做程序裝載器來使用。考慮如下代碼片段:此處使用gs寄存器來實施隔離,而syscall指令往往也要訪問該寄存器,所以CPI改寫指令,禁用了相關操作。此架構下段保護已無法保證,但仍可以基於信息隱藏完成隔離,因為常規區中的地址不會指向安全區(否則就是敏感指針),信息也就不會泄露(作者認為這是一個事實,但有趣的是,這其實只是一個不總是成立的假設)。該架構下48bit(Linux內核將x86-64的進程地址空間定義為「48 bit – 1 protect page」)的地址空間也可以做到防止暴力破解,攻擊者在試圖破解之後程序可能會崩潰(這是另外一個作者認為是事實的假設)。其他架構可以使用地址隨機化或者是software fault isolation(翻譯沒有英文那麼準確,姑且用其英文名)。敏感指針只占很少一部分,且空間分布高度分散。為了節省內存,採用哈希表、多級查找表,或者是將地址作為下標的數組來實現。為了減少開銷和降低複雜度,CPI特別引入了安全棧機制。因為棧上存放的返回地址等數據需要被頻繁訪問。絕大多數棧上數據的訪問能在編譯時就驗證其安全性,無需運行時來檢查,且大都是通過esp寄存器加上棧內偏移來訪問的。因此,CPI把所有這些被證明是安全的對象存放到安全區的某片區域,稱為安全棧。如果函數中的內存對象(全局或局部變量,動態分配的內存塊等)需要檢查,則在常規區給他們分配⼀塊獨立的棧幀。安全棧的實施同樣需要進行靜態分析和插樁,靜態分析得安全棧需要包含哪些對象,插樁完成相應結構的填充。另外安全棧還進行了運行時支持(runtime support),為每一個線程分配常規區上的棧,要麼作為線程庫的一部分,要麼用來干預線程的創建和銷毀。為了更高的性能,弱化版本的CPI——Code-Pointer Separation(CPS)被提出。相比CPI,CPS對敏感指針的認定有所放鬆,只識別代碼指針,指向代碼指針的指針不再是敏感的。同時,代碼指針只指向控制流目標(control flow destination)。控制流目標是代碼段的某個位置,包括函數入口和返回地址等等。這樣的機制可以防止偽造代碼指針,但是無法阻止攻擊者對代碼指針的讀寫。同時,由於控制流目標是精確的某一個位置,無需上下界等信息,CPS的敏感指針是在編譯時就能確定的,是靜態的,不用再分配id(當卸載共享庫時情況有所不同,需要特殊處理)所以無需引入元數據,使得安全區範圍縮小了,同時對代碼指針的訪問次數也大大減少。為了驗證該機制的正確性,作者做了形式化的證明。水平有限,我嘗試為大家梳理一下。首先,定義運行時環境為E,它是一個三元組,為以下形式:S表示將某變量映射到其類型和地址,Mu是常規區的某一地址,存儲着一個值,記作v,Ms是安全區某一地址,存儲着某值和其上下界信息v(b,e),b為下界,e為上界,或者被標記為none。v(b,e)和v表示安全,OK代表操作成功,OutOfMem、Abort代表錯誤。lu和ls是有某個左表達式產生的位置(location)。左表達式lhs包括某變量,結構體成員,指針解引用。右表達式包括整數、函數地址,左表達式、左表達式地址、指針大小、為右表達式分配內存情況。表示將右表達式分配至安全區或是常規區,可能還伴有運行時環境的轉換(E和)。表示執行命令c,得到結果r,可能伴有運行時環境的轉換(E和)。表示分配i單位內存,得到某位置上下界l、l+i,同時返回結果r。以上三個式子表示對敏感指針的操作(如解引用等)將會被判定,返回結果r。是對讀寫操作進行判定。對從安全區讀出的值進行檢查,如果與元數據信息(上下界)相符,則可以進行後續操作,否則返回Abort(錯誤)。對寫入安全區的數據也進行檢查,通過之後返回OK。表示任何經由常規區對安全區的訪問都是非法的,返回Abort。處理了泛型指針的敏感性會動態變化的情形。由於泛型指針在安全區和常規區均占有內存,讀取時先在安全區標記為none,然後從常規區直接取值。寫操作是類似的,直接寫入常規區,然後將安全區標記為none,成功返回OK。對於常規區的操作無需任何干涉,也不會產生任何問題,直接返回OK。函數的直接調用是很簡單的,如果函數指針位於安全區(ls),則返回OK,位於常規區(lu)則返回Abort。至此,對于敏感指針操作狀態的情況已經討論完畢,其判斷路徑是完備的,CPI機制的正確性也就證明了。為了驗證有效性,CPI基於RIPE(runtime intrusion prevention evaluator,運行時入侵防禦檢測,能自動生成溢出攻擊代碼,有興趣的可以看一下https://github.com/johnwilander/RIPE),進行了測試,成功進行了防禦。並對新出現的幾種能繞過DEP、ASLR、CFI等機制的攻擊也實現了防禦。CPI基於SPEC CPU2006標準,對100多個軟件包進行了測試,結果如下:可以看到,CPI,尤其是CPS的表現還是相當不錯的。同時,在WEB平台上也做了測試(FreeBSD + Apache + SQLite + mod_wsgi + Python +Django),結果如下:CPI進行了形式化的證明,證明了其能百分百防禦控制流劫持攻擊,這一點是毋庸置疑的。但正如前文所提到的,CPI依賴於兩個實際上是假設的「事實」,這就為針對它的攻擊(至少是針對某一種具體實現方式的攻擊)提供了可能。欲知後事如何,請聽下回分解。【技術分享】Python安全 - 從SSRF到命令執行慘案【技術分享】ARM 架構—探究繞過NX的一種方式Ret2ZP
鑽石舞台 發表在 痞客邦 留言(0) 人氣()