闞哲
正交防線是調節操作系統的控制流。通過確保攻擊者無法將控制權轉移到他們選擇的代碼上,即使不刪除內存錯誤,也會使利用內存錯誤變得更加困難。最好的例子被稱為控制流完整性(CFI),現在許多編譯器工具鏈(如LLVM和微軟的VisualStudio),于2017年以ControlFlowGuard的名義并入Windows內核。
從概念上講,CFI非常簡單:確保代碼中的控制流始終遵循靜態控制流圖。例如,一個函數的返回指令應該只允許返回到它的調用站點,而使用C中的函數指針或C++中的虛函數的間接調用應該只能夠定位它應該能夠調用的合法函數的入口點。為了實現這種保護,可以標記間接控制轉移指令的所有合法目標(返回、間接調用和間接跳轉),并將這些標簽添加到特定于此指令的集合。在運行時,檢查指令即將進行的控制轉移是否為集合中的目標。否則,CFI會發出警報。
與ASLR一樣,CFI有多種“口味”,從粗粒度到細粒度,從上下文敏感到上下文不敏感。就像在ASLR中一樣,今天的大多數實現只采用最簡單、最粗粒度的保護。粗粒度CFI意味著為了性能而稍微放寬規則。例如,它不是將函數的返回指令限制為可能調用此函數的僅目標合法調用站點,而是可以針對任何調用站點。雖然不如細粒度CFI安全,但它仍然極大地限制了攻擊者的回旋余地,并且可以更快地在運行時檢查。
在現代機器上,某些形式的CFI甚至(或將要)得到硬件的支持。例如,英特爾控制流強制技術(CET)支持影子堆棧和間接分支跟蹤,以幫助強制實施退貨和前向控制傳輸的完整性(以非常粗粒度的方式)。ARM也不甘示弱,它提供了指針身份驗證,以防止對指針值進行非法修改———主要是通過使用指針的上位來存儲指針身份驗證代碼(PAC),其功能類似于加密指針值上的簽名(除非獲得正確的PAC,否則指針無效)。
遺憾的是,CFI只能通過破壞返回地址、函數指針和跳轉目標等控制數據來幫助抵御改變控制流的攻擊,但對非控制數據攻擊卻無能為力。例如,它無法阻止覆蓋當前進程的權限級別并將其設置為“root”的內存損壞(例如通過將有效用戶ID設置為root用戶的ID)。但是,如果對控制流的限制在實踐中如此成功,您可能想知道數據流是否也可以進行類似的限制。事實上,它們確實如此,這被稱為數據流完整性(DFI)。在DFI中,靜態地確定每個加載指令(即從內存中讀取的指令)存儲指令可能合法地產生了數據,并且標記這些指令并將這些標簽保存在一組中。在運行時,對于內存中的每個字節,記住該位置最后一個存儲的標簽。當遇到加載指令時,會檢查該地址的最后一個存儲是否在合法存儲集中,如果不是,會發出警報。與CFI不同,DFI在實踐中并未被廣泛采用,可能是因為其顯著的性能開銷。
大多數當前操作系統的主要防線之一是隱藏攻擊者可能感興趣的任何內容。具體來說,通過隨機化所有相關內存區域(在代碼、堆、全局數據和堆棧中)的位置,攻擊者將不知道在哪里轉移控制流,也無法發現哪個地址包含敏感數據等。術語地址空間布局隨機化(ASLR)是在PaX安全補丁發布時創造的,該補丁在2001年為Linux內核實現了這種隨機化。很快,類似的努力出現在其他操作系統中,第一個默認啟用ASLR的主流操作系統是2003年的OpenBSD和2005年的Linux。Windows和MacOS在2007年緊隨其后。然而,這些早期的實現只是隨機化了用戶程序中的地址空間,隨機化直到大約10年后才到達主要操作系統的內核。
如今,大多數實現都采用粗粒度隨機化:它們隨機化代碼、堆或堆棧的基本位置,但在這些區域中,每個元素都與基本元素處于固定偏移量。這很簡單,而且非常快,然而,一旦攻擊者設法通過信息泄漏獲得哪怕一個代碼指針,他們就會知道每條指令的地址。對于堆、堆棧等,也是如此,因此,毫不奇怪,這些信息泄漏是當今攻擊者高度重視的目標。
更細粒度的隨機化也是可能的。例如,可以在頁面級別或功能級別進行隨機化。如果在內存區域中打亂函數的順序,即使知道內核代碼的基礎對于攻擊者來說也是不夠的。事實上,我們可以更細粒度地洗牌基本塊、指令(可能帶有從不執行或無效的垃圾指令)甚至寄存器分配。許多細粒度隨機化技術都是以空間和時間開銷為代價的,例如,由于局部性和碎片化減少。
除了代碼之外,還可以對數據進行細粒度隨機化。例如,研究表明,堆棧上的堆分配、全局變量甚至變量都可以分散在內存中。當然,這樣做會產生性能和內存方面的成本。

考慮到KASLR,尤其是粗粒度的KASLR,作為抵御內存錯誤攻擊的第一道防線,這不會離目標太遠。不幸的是,它也是一個非常薄弱的防御,許多出版物表明,通過從內存、側信道等泄漏數據和(或)代碼指針,KASLR可以相當容易地被破解。
Multics引入的最具革命性的想法之一是保護環的概念———一種特權的分層,其中內環(環0)是最有特權的,并且外環的特權最低。因此,不受信任的用戶進程在外環中執行,而直接與硬件交互的受信任和特權內核在環0中執行,其他環可用于或多或少特權的系統進程。
保護環通常采用硬件支持,這是當今大多數通用處理器提供的功能,盡管保護環的數量可能有所不同。例如,霍尼韋爾6180支持多達8個環,英特爾的x86支持4個,ARMv7支持3個(加上TrustZone的額外1個)和PowerPC的2個。但是,故事變得有點混亂,因為一些現代處理器也引入了更多不同的處理器模式;大多數常規操作系統只使用2個環:一個用于操作系統,一個用于用戶進程。
每當權限較低的代碼需要更多權限的函數時,它就會“調用”下環以請求將此函數作為服務執行。因此,只有受信任的特權代碼才能執行最敏感的指令或操作最敏感的數據。除非具有較少權限的進程誘騙更多特權的代碼執行它不應該做的事情(作為困惑的副手),否則環會提供強大的保護。Multics的最初想法是,環之間的轉換將通過實施嚴格控制和調解的特殊呼叫門進行。例如,外環中的代碼不能調用內環中的任何指令,而只能調用第一個調用的預定義入口點經過審查,看它及其參數是否違反任何安全策略。
雖然像x86這樣的處理器仍然支持呼叫門,但很少有操作系統使用它們,因為它們相對較慢。相反,用戶進程通過執行操作系統處理的軟件中斷(陷阱),或更常見的是通過特殊的高效操作過渡到操作系統內核(系統調用)。系統調用指令(如SYSCALL、SYSENTER、SVC、SCALL等,具體取決于體系結構)。許多操作系統將系統調用的參數放在一組預定義的寄存器中。與調用門一樣,陷阱和系統調用指令也確保在操作系統中的預定義地址繼續執行,代碼在其中檢查參數,然后調用相應的系統調用函數。
除了用戶進程調用操作系統外,大多數操作系統還允許內核調用用戶進程。例如,基于UNIX的系統支持操作系統用來通知用戶程序“有趣的事情”的信號:錯誤、過期的計時器、中斷、來自另一個的消息工藝等,如果用戶進程為信號注冊了一個處理程序,操作系統將停止進程的當前執行,將其所有處理器狀態存儲在進程堆棧上,形成所謂的信號,并在信號處理程序上繼續執行。當信號處理程序返回時,進程執行sigreturn系統調用,使操作系統接管,還原堆棧上的處理器狀態并繼續執行進程。
安全域(如操作系統內核和用戶空間進程)之間的邊界是檢查系統調用本身及其安全性參數的好地方。例如,在基于功能的操作系統中,內核將驗證功能,而在MINIX3等操作系統中,只允許特定進程進行特定調用,因此任何嘗試撥打不在預先批準列表中的呼叫將被標記為違規。同樣,基于Windows和UNIX的操作系統必須檢查許多系統調用的參數。例如,考慮常見的讀寫系統調用,通過這些調用,用戶請求將數據從文件或套接字讀取到緩沖區中,或者從緩沖區分別到文件或套接字中。在執行此操作之前,操作系統應檢查要寫入或讀取的內存是否實際歸進程所有。
執行系統調用后,操作系統將控制權返回給進程,操作系統還必須注意不要返回危及系統安全性的結果。例如,如果一個進程使用mmap系統調用請求操作系統將更多內存映射到其地址空間,則操作系統應確保它返回的內存頁不再包含來自另一個進程的敏感數據(如首先將每個字節初始化為零)。
零初始化問題可能非常微妙,如編譯器經常在數據結構中引入填充字節以對齊。由于這些填充字節在編程語言級別根本不可見,因此編譯器可能認為沒有理由將它們初始化為零。但是,當操作系統返回此類數據結構以響應系統調用并且單元化填充包含來自內核或其他進程的敏感數據時,就會發生安全違規。
即使是UNIX系統中的信令子系統也是一個有趣的安全案例。回想一下,sigreturn采用堆棧上的任何處理器狀態并恢復該狀態。假設攻擊者能夠破壞進程的堆棧并在堆棧上存儲虛假信號幀,如果攻擊者能夠觸發sigreturn,或可以設置整個處理器狀態(包括所有寄存器值),這樣在熟練的攻擊者手中就提供了一個強大的基元,被稱為sigreturn導向編程(SROP)。
如前所述,為此,添加了看起來像底部額外戒指的東西。由于在x86處理器上,術語ring0已成為“操作系統內核”的同義詞(以及“環”與“用戶進程”的同義詞),因此這種新的虛擬機管理程序環通常稱為ring-1。它還指示其各自虛擬機中的操作系統可以繼續在本地執行環0指令。嚴格來說,它的目的與原始戒指大不相同,雖然環-1這個名字已經卡住了,但它可能有點像用詞不當。
為了完整起見,應該提到的事情可能會變得更加復雜,因為一些現代處理器仍然具有其他模式。例如,x86提供所謂的系統管理模式(SMM)。當系統啟動時,固件控制硬件,并為操作系統接管系統做好準備。但是,啟用SMM后,固件會在向CPU發送特定中斷時重新獲得控制權。例如,固件可以指示每當按下電源按鈕時它都希望接收中斷,在這種情況下,常規執行將停止,固件將接管。例如,可以保存處理器狀態,執行任何需要執行的操作,然后恢復操作系統以進行有序關閉。在某種程度上,SMM有時被視為比其他環(環-2)低的水平。
英特爾甚至以英特爾管理引擎(ME)的形式增加了環-3。ME是一個完全自主的系統,現在幾乎存在于英特爾的所有芯片組中。它在單獨的微處理器上運行一個秘密且完全獨立的固件,并且始終處于活動狀態:在啟動過程中,當機器運行時,當它處于睡眠狀態時,即斷電。只要計算機連接到電源,就可以通過網絡與ME通信,如安裝更新。雖然非常強大,但它的功能在很大程度上是未知的,除了運行自己的小型操作系統,研究人員發現其中包含漏洞。主CPU附帶的附加處理器(無論是ME還是Apple的T2和Google的Titan芯片等相關處理器)都提出了一個有趣的點———在主CPU上運行的操作系統是否能夠滿足當今的安全要求?至少,這種趨勢似乎通過專用系統(硬件和軟件)來增強它的安全性。
上述許多功能都可以在大多數通用處理器體系結構中找到。然而,在物聯網或一般的嵌入式系統中不一定如此,并且通常使用定制的操作系統。簡單的微控制器通常沒有MMU,有時甚至沒有MPU、保護環或在常見操作系統中依賴的任何高級功能。系統通常很小(減少攻擊面),應用程序受信任(并可能經過驗證)。然而,設備的嵌入式性質使其很難檢查甚至測試其安全性,并且無論它們在安全敏感活動中發揮作用的何處,安全性隔離/遏制和調解的手段應由環境在外部強制執行。更廣泛的物聯網問題在網絡物理系統安全CyBOK知識領域得到解決。