課程:《密碼與安全新技術專題》html
班級: 1892前端
姓名: 王子榛linux
學號:20189206nginx
上課教師:王志強程序員
論文名稱:SafeInit: Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities算法
會議名稱:ndss2017apache
做者:Alyssa Milburn、Herbert Bos、Cristiano Giffrida編程
未初始化值的使用仍然是C / C ++代碼中的常見錯誤。這不只致使未定義的和一般不指望的行爲,並且還致使信息泄露和其餘安全漏洞。數組
咱們都知道C/C++中的局部變量,在未初始化的狀況下,初值爲隨機值。緩存
以C++中局部變量的初始化和未初始化爲例:(int x;和int x = 0;)
編譯器在編譯的時候針對這兩種狀況會產生兩種符號放在目標文件的符號表中,對於初始化的,叫強符號,未初始化的,叫弱符號。鏈接器在鏈接目標文件的時候,若是遇到兩個重名符號,會有如下處理規 則:
一、若是有多個重名的強符號,則報錯。
二、若是有一個強符號,多個弱符號,則以強符號爲準。
三、若是沒有強符號,但有多個重名的弱符號,則任選一個弱符號。
簡單地說,未定義行爲是指C語言標準未作規定的行爲。編譯器可能不會報錯,可是這些行爲編譯器會自行處理,因此不一樣的編譯器會出現不一樣的結果,什麼都有可能發生,這是一個極大的隱患,因此咱們應該儘可能避免這種狀況的發生。
因爲C/C++不會像C#或JAVA語言,確保變量的有限分配,要求在全部可能執行的路徑上對它們進行初始化。因此,C/C++代碼可能容易受到未初始化的攻擊讀取。同時C/C++編譯器能夠在利用讀取未初始化的內存是「未定義行爲」時引入新的漏洞。
在本文中,提出了一種全面而實用的解決方案,經過調整工具鏈(什麼是工具鏈)來確保全部棧和堆分配始終初始化,從而減輕通用程序中的這些錯誤。 SafeInit在編譯器級別實現。
本文實現了:
在幾乎全部應用程序中,內存不斷被從新分配,所以被重用。
若是在使用以前不覆蓋這些數據,就會出現未初始化數據的問題,從而將舊數據的生命週期延長到新分配點以外。
內存也可能只是部分初始化; C中的結構和聯合類型一般是故意不徹底初始化的,而且出於簡單性或性能緣由,一般爲數組分配比存儲其內容所需的(最初)更大的大小。實際上,重用內存不只常見,並且出於性能緣由也是可取的。 當不清楚變量是否在使用以前被初始化時,惟一實用且安全的方法是在全部狀況下初始化它。
因爲未初始化數據而致使信息泄露的最明顯危險是直接敏感數據的泄露,例如加密密鑰,口令,配置信息和保密文件的內容。
現代軟件防護依賴於敏感元數據的保密性,同時,未初始化的值提供了指針公開的豐富資源。
例如:地址空間佈局隨機化(ASLR)之類的防護通常取決於指針的保密性,而且因爲這一般僅經過隨機化一個基地址來完成,所以攻擊者僅須要獲取單個指針以徹底抵消保護。 這樣的指針能夠是代碼,堆棧或堆指針,而且這些指針一般存儲在棧和堆上,所以未初始化的值錯誤提供了阻止這種信息隱藏所需的指針公開的豐富源。
未初始化數據致使的其餘漏洞容許攻擊者直接劫持控制流。常見的軟件開發的錯誤是:沒法在遇到錯誤時在執行路徑上初始化變量或緩衝區。
兩個例子分別是:
有些工具試圖在開發過程當中檢測未初始化變量,而不是試圖減輕未初始化的值錯誤,容許它們由程序員手動校訂。有些工具試圖在開發過程當中檢測它們,而不是試圖減輕未初始化的值錯誤,容許它們由程序員手動校訂。更重要的是,編譯器警告和檢測工具只報告問題,而不是解決問題。 這可能會致使錯誤和危險的錯誤。
函數堆棧幀:在堆棧中爲當前正在運行的函數分配的區域、傳入的參數。返回地址以及函數所用的內部存儲單元都存儲在堆棧幀中。
函數堆棧幀包含局部變量的副本,或具備被忽略的局部變量,同時還包含其餘局部變量和編譯器生成的臨時變量的溢出副本,以及函數參數,幀指針和返回地址。 鑑於堆棧內存的不斷重用,這些幀提供了豐富的敏感數據源。
現代編譯器使用複雜的算法進行寄存器和堆棧幀分配,這種方式減小了內存使用並改善了緩存局部性,但意味着即便在函數調用以前/以後清除寄存器和堆棧幀也不足以免全部潛在的未初始化變量。
下面的例子說明:內存重用了循環中的局部變量,doSomthing將在全部循環迭代中傳遞祕密值,因爲未初始化的局部變量,可是若是初始化了局部變量,只會在第一次doSomthing傳遞祕密值。
當C / C ++程序沒法遵循該語言強加的規則時,會發生未定義的行爲。在咱們討論的環境中,未定義行爲是指在代碼讀取未初始化的堆棧變量或者是未初始化的堆分配。
爲了實現最大數量的優化,特別是在可能從模板和宏擴展的代碼中,並最終被大部分丟棄爲沒法訪問,現代編譯器轉換利用了大規模的未定義的行爲。這樣的轉換能夠將未定義的值(以及所以也未初始化的值)解釋爲使得優化更方便的任何值,即便這使得程序邏輯不一致。
SafeInit經過強制初始化堆分配(在分配以後)和全部棧變量(不管什麼時候進入範圍)來減輕未初始化的值問題。這是經過修改編譯器直接在全部點插入初始化調用來完成的。爲了提供實用和全面的安全性,此工具必須在編譯器自己內完成。 只需在編譯過程當中傳遞額外的加固標記便可啓用SafeInit。
上圖是利用額外的編譯器傳遞,從而增長了必要的初始化
種簡單的初始化方法會致使過多的運行開銷,而咱們系統的一個重要元素是專門的強化分配器。 在許多狀況下,經過利用額外的信息並結合咱們的編譯器工具,能夠避免初始化問題。
能夠看到編譯器在得到C/C++文件後,編譯器前端將源文件轉換爲中間語言(IR),經過初始化、代碼優化結合現存編譯器的優化器,以後經過無效數據消除、強化分配器最後得到二進制文件。Safeinit在整個過程當中所添加的就是 初始化所有變量、優化以及強化分配器,來避免或緩解未初始化值。
SafeInit在首次使用以前初始化全部局部變量,做爲新分配變量的做用域處理。SafeInit經過修改編譯器編譯代碼的中間表示(IR),在每一個變量進入做用域後進行初始化(例如內置memset)。
SafeInit的強化分配器可確保在返回應用程序以前將全部新分配的內存清零。咱們經過修改現代高性能堆分配器tcmalloc來實現咱們的強化分配器。同時還修改了LLVM,以便在啓用SafeInit時未來自新分配的內存的讀取視爲返回零而不是undef。 同時,咱們在安全分配器中執行覆蓋全部堆分配函數以確保始終使用強化的分配器函數(對初始化堆分配是在分配以後進行強制初始化)。編譯器知道咱們的強化分配器正在使用中; 任何已分配內存的代碼都再也不使用未定義行爲,而且編譯器沒法修改或刪除
目的:可在提升效率和非侵入性的同時提升SafeInit的性能。優化器的主要目標是更改現有編譯器中可用的其餘標準優化,以消除任何沒必要要的初始化。
在正常執行期間不執行此代碼路徑,而且咱們不須要初始化緩衝區,直到咱們到達它將到達的路徑使用。
檢測初始化:檢測初始化數組(或部分數組)的典型代碼
字符串緩衝區:用於存儲C風格的以空字符結尾的字符串的緩衝區一般僅以「安全」方式使用,其中永遠不會使用超出空終止符的內存中的數據。傳遞給已知C庫字符串函數(例如strcpy和strlen)的緩衝區是「安全的」,優化器檢測到該緩衝區始終被初始化,能夠刪除掉該緩衝區的初始化代碼。
「無效存儲消除」(DSE)優化,它能夠刪除老是被另外一個存儲覆蓋而不被讀取的存儲。
LLVM中的局部變量是使用alloca指令定義的; 咱們的pass經過在每條指令以後添加對LLVM memset內部的調用來執行初始化。能夠保證清除整個分配,並在適當的時候轉換爲存儲指令。
經過修改現代高性能堆分配器tcmalloc來實現咱們的強化分配器。 只需清除在分配器返回指針以前,全部其餘堆分配爲零。還修改了LLVM,以便在啓用SafeInit時未來自新分配的內存的讀取視爲返回零而不是undef。 如上所述,這對於避免未定義值的不可預測後果相當重要。
經過將插入的memset調用移動到alloca的全部使用的主導點來實現咱們提出的用於堆棧初始化的下沉存儲優化。 在啓用優化的狀況下進行編譯時,clang將發出'lifetime'標記,指示局部變量進入範圍的點; 咱們修改了clang以在全部狀況下發出適當的生命週期標記,並在這些點以後插入初始化。
經過添加一個新的內部函數「initialized」來實現初始化檢測優化,該函數具備與memset相同的存儲殺死反作用,可是被代碼生成忽略。 經過擴展諸如LLVM的循環習語檢測之類的組件來生成這種新的內在函數,其中沒法用memset替換代碼,咱們容許其餘現有的優化傳遞利用這些信息而無需單獨修改它們。
咱們經過擴展示有的LLVM代碼實現了上述其餘優化,儘量減小咱們的更改。 咱們對只寫緩衝區的實現使用了D18714中的補丁(自合併以來),它爲writeonly屬性添加了基本框架。
咱們還基於D13363中的(拒絕)補丁實現了跨塊死區消除。 因爲性能迴歸,咱們爲小型商店(≤8字節)禁用此交叉塊DSE; 咱們還擴展了此代碼以支持刪除memset,並縮短此類存儲。
咱們的基準測試運行在(4核)Intel i7-3770上,內存爲8GB,運行(64位)Ubuntu 14.04.1。 禁用CPU頻率縮放,並啓用超線程。
基線配置:clang / LLVM的未修改版本,使用未修改的tcmalloc版本。
除了將它與SafeInit進行比較以外,咱們還提供了簡單方法的結果,它簡單地應用了咱們的初始化過程而沒有包含任何咱們提出的優化,使用一個簡單地將全部分配歸零的強化分配器。
咱們使用LTO和-O3在SPEC CPU2006中構建了全部C / C ++基準測試。 咱們使用參考數據集提供3次運行中值的開銷圖。
在沒有咱們的優化器的狀況下應用時,運行時開銷的(幾何)平均值爲8%。應用咱們的優化器能夠顯着下降剩餘基準測試的開銷,與咱們的基線編譯器相比,致使CINT2006的平均開銷爲3.5%。 CFP2006的結果相似,如圖14所示,平均開銷爲2.2%。
表I提供了每一個基準測試的allocas數量(表示局部變量的數量,偶爾的參數副本或動態分配)的詳細信息。 該表還提供了(剝離的)二進制大小; 在許多狀況下,初始化的影響對最終的二進制大小沒有任何影響,而且在最壞的狀況下它是最小的。#INITS是現有編譯器優化以後剩餘的大量初始化數量,而且咱們的優化器已經分別運行。
使用咱們的優化器做爲基線時的平均開銷爲3.8%
咱們經過使用兩個現代高性能Web服務器nginx(1.10.1)和lighttpd(1.4.41)來評估SafeInit的開銷,以減小計算密集度較低的任務。 咱們使用LTO和-O3構建了Web服務器。 因爲它們在咱們的1gbps網絡接口上使用時受到I / O限制,所以咱們使用環回接口對它們進行基準測試。
咱們使用apachebench重複下載4Kb,64Kb和1MB文件,持續30秒。 咱們啓用了流水線操做,使用了8個併發工做程序,並使用CPU功能爲apachebench保留了CPU核心。 咱們測量了10次運行中每秒請求中位數的開銷; 咱們沒有看到大量的差別。
使用咱們的工具鏈構建了最新的LLVM Linux內核樹。 咱們定製了構建系統,以容許使用LTO,從新啓用內置clang函數,並修改gold連接器以解決咱們在符號排序時遇到的一些LTO代碼生成問題。
因爲Linux內核執行本身的內存管理,所以它不會與用戶空間強化分配器連接; 咱們的自動加固僅保護局部變量。
下表提供了使用內核微基準測試工具LMbench的典型系統調用的延遲和帶寬選擇。 咱們運行了每一個基準測試10次,每次運行的預熱時間很短,迭代次數不少(100次),並提供中位數結果。 TCP鏈接是localhost,其餘參數是默認LMbench腳本使用的參數。
對於stat和open系統調用,咱們會產生大量開銷; 雖然咱們的優化器提供的性能獲得了很大程度的緩解,但這是值得關注的,咱們打算進一步研究它,以及fstat和(信號)保護故障,這是咱們所看到的開銷大於5%的惟一系統調用。
爲了評估應用於內核堆棧的SafeInit的實際性能,咱們使用SafeInit強化了nginx和內核,並將性能與在非強化內核下運行的非強化nginx進行了比較。 使用咱們上面討論過的發送文件配置,再次使用環回接口提供極端狀況,咱們分別觀察到1M,64kB和4kB狀況下的開銷分別爲2.9%,3%和4.5%。
爲了驗證SafeInit是否按預期工做,不只考慮了各類現實漏洞,例以下表中的漏洞,還建立了一套單獨的測試用例。 咱們手動檢查了爲相關代碼生成的bitcode和機器代碼,並使用咱們上面描述的檢測系統運行咱們的測試套件。 咱們還用valgrind來驗證咱們的硬化; 例如,咱們確認當使用SafeInit強化OpenSSL 0.8.9a時,來自valgrind的全部未初始化的值警告都會消失。
總的來講,咱們的SafeInit原型在LLVM中添加或修改的代碼少於2000行,包括一些調試代碼和基於第三方補丁的大約400行代碼。 雖然咱們的修改很複雜,但這是一個相對較少的代碼,每一個組件都應該是可單獨審查的; 爲了比較,咱們(單獨的)幀清除通道單獨超過350行代碼。
咱們的強化不會阻止程序在內部重用內存。例如,堆棧緩衝區能夠在同一個函數中重用於不一樣的目的,或者自定義內部堆分配器能夠重用內存而不清除它,例如咱們在PHP中看到的。 儘管可能使用啓發式方法或經過附加某種註釋來捕獲其中一些案例,但咱們認爲編譯器支持這種狀況並不現實也不合理。將變量清零可確保任何未初始化的指針爲空。 嘗試取消引用這樣的指針將致使錯誤; 在這種狀況下,咱們的緩解措施已將更嚴重的問題減小爲拒絕服務漏洞。
未初始化的數據漏洞繼續在現代C / C ++軟件中形成安全問題,而且確保不使用未初始化值的安全性並不像看起來那麼容易。 從簡單的信息披露到嚴重問題(如任意內存寫入,靜態分析限制以及利用未定義行爲的編譯器優化)等威脅相結合,使這成爲一個難題。
本文提出了一種基於工具鏈的強化技術SafeInit,它經過確保在使用前初始化全部局部變量和堆棧分配來減輕C / C ++程序中未初始化值的使用。經過使用適當的優化,咱們發現許多應用程序的運行時開銷能夠下降到能夠做爲標準強化保護應用的水平,而且這能夠在現代編譯器中實際完成。
本文經過在clang/LLVM編譯器架構上,經過修改代碼,實現了safeinit原型,在編譯C/C++源代碼時,傳遞一個標記便可使用safeinit實現優化編譯,緩解未定義變量。使用了強化分配器的safeinit能夠進一步優化代碼的同時,保證全部須要初始化的變量進行初始化,刪除多餘初始化代碼進行優化,這樣既保證緩解了未定義變量漏洞的威脅,同時與其餘現有方法相比,提高了性能。
選到這篇文章一開始沒有具體瞭解是涉及較爲底層的編譯器的內容,可是看完後,以爲其實有時候越是較爲底層的東西學習能夠幫助咱們更好地理解咱們利用編程語言實現的一些應用,能夠在之後編寫代碼時注意到一些以往可能注意不到的點,瞭解程序運行邏輯,因此選擇這篇文章也促使我瞭解了一些計算機系統和編譯原理中的內容,獲益匪淺。可是遺憾的是,文章中的編譯器並沒能復現出來,來對一些測試代碼進行編譯以更好了解其運行機制,我也會在之後繼續學習,爭取讀懂理解這篇文章的內容。