微軟70的漏洞仍然是內(nèi)存安全問題的例子?|譯文
2021-08-14
以下是翻譯:
之前,我們討論了主動解決內(nèi)存安全問題的必要性。顯然,僅靠工具和指導無法防止此類漏洞。十多年來,內(nèi)存安全問題與 CVE(常見漏洞披露)的比率非常接近。我們相信使用內(nèi)存安全語言可以通過工具和培訓無法實現(xiàn)的方式緩解這種情況。
在本文中系統(tǒng)編程語言,我們將探討一些可以通過使用內(nèi)存安全語言來防止的 產(chǎn)品漏洞(經(jīng)過測試和靜態(tài)分析)的真實示例。
內(nèi)存安全
內(nèi)存安全是編程語言的一個特性。在具有內(nèi)存安全性的編程語言中,所有內(nèi)存訪問都是明確定義的。今天使用的大多數(shù)編程語言都是通過某種形式的垃圾收集來實現(xiàn)內(nèi)存安全的。但是,無法承受垃圾收集器繁重的運行時間的系統(tǒng)級語言(即用于構建其他軟件所依賴的底層系統(tǒng)的語言,例如OS內(nèi)核、網(wǎng)絡堆棧等)。通常不是內(nèi)存安全的。
微軟已經(jīng)修復并指定了 CVE 安全漏洞,大約 70% 的根本原因是內(nèi)存安全問題。雖然我們采取了緩解措施,包括嚴格的代碼審查、培訓、靜態(tài)分析等。
微軟 70% 的漏洞仍然是內(nèi)存安全問題
盡管很多有經(jīng)驗的程序員都可以編寫正確的系統(tǒng)級代碼,但很明顯,無論采取何種緩解措施,使用傳統(tǒng)的系統(tǒng)級編程語言編寫內(nèi)存安全代碼幾乎是不可能的。
接下來,讓我們看一些現(xiàn)實生活中使用沒有內(nèi)存安全保證的語言導致的安全漏洞的例子。
空間內(nèi)存安全
空間內(nèi)存安全是指確保所有內(nèi)存訪問都在訪問類型的邊界內(nèi)。為此,需要代碼來跟蹤這些大小并根據(jù)這些大小正確檢查所有內(nèi)存操作。
在控制流的極端情況下,可能會因為沒有考慮整數(shù)符號、整數(shù)提升或整數(shù)溢出的復雜性,可能會錯過檢查,或者可能會錯誤地執(zhí)行檢查。下面我們來看看Edge的這個例子,由(CVE-2018-8301):
[0] 處的檢查是正確的。但是,[1] 可以修改字符串的大小,使獲取的偏移量無效。這會導致在 [2] 處調(diào)用復制函數(shù)時產(chǎn)生與預期不同的偏移量,從而導致越界寫入。
此漏洞的修復方法很簡單:將“偏移檢查”移到更接近使用時間的位置。問題是這個錯誤很容易出現(xiàn)在復雜的代碼庫中,簡單的重構代碼也可能再次導致這個漏洞?,F(xiàn)代 C++ 提供了跨度來強制執(zhí)行數(shù)組訪問的邊界檢查。但是,不幸的是,這不是默認值,因此完全取決于開發(fā)人員使用 span。因此,在實踐中很難強制使用這種結構。
如果編程語言可以自動跟蹤和驗證大小,那么程序員就不再需要擔心正確執(zhí)行這些檢查,我們也可以確定這些問題不存在于我們的代碼中。
時間記憶安全
時間內(nèi)存安全是指確保指針在解除引用時仍指向有效內(nèi)存。
一個常見的模式是發(fā)布后使用。觸發(fā)此漏洞的方法是先引用一個內(nèi)部訪問并保存到本地指針系統(tǒng)編程語言,然后進行一系列復雜的操作,可能會釋放或移動內(nèi)存,導致本地指針中的引用失效,而然后在引用無效后取消引用。比如找到Edge的源碼示例(CVE-2017-8596):
這個錯誤的原因是太多復雜的API相互交互,程序員無法在整個代碼中強制對內(nèi)存的所有權。在 [0] 處,程序獲得指向該對象擁有的對象的指針。然后在[1]處,由于語言的復雜性,代碼需要執(zhí)行更多的代碼來獲取另一個變量。在[2]中,它將使用緩沖區(qū)和寬度,并使用指針的內(nèi)容創(chuàng)建一個新對象。
問題是:
該程序同時使用垃圾收集和手動內(nèi)存管理。垃圾收集器會跟蹤對象,但不知道是否有指向對象內(nèi)部的指針。由于可重入,JS程序可以修改狀態(tài)并清除在[1]處創(chuàng)建別名的指針的所有權。該漏洞類似于迭代器失效漏洞。當狀態(tài)被修改時,所有指向內(nèi)部狀態(tài)的指針都可能變成無效指針。但是在像瀏覽器這樣的復雜程序中,幾乎不可能使用靜態(tài)方法來確保不出現(xiàn)錯誤。問題的根源在于為指向可修改狀態(tài)的指針添加別名。 C 和 C++ 沒有相應的工具來防止這種錯誤。但是,我們建議始終使用“智能指針”來跟蹤內(nèi)存所有權。
數(shù)據(jù)競爭條件
當同一個進程中的兩個或多個線程同時訪問同一個內(nèi)存地址,并且至少有一次訪問是寫操作,并且線程不使用任何顯式的鎖操作來控制對內(nèi)存的訪問,則會發(fā)生數(shù)據(jù)競爭。在多線程訪問共享數(shù)據(jù)的情況下,保持空間和時間內(nèi)存安全變得更加困難且更容易出錯。即使未同步的內(nèi)存只共享了很短的一段時間,也有可能被其他線程修改數(shù)據(jù),而被修改的數(shù)據(jù)就是引用其他內(nèi)存地址的數(shù)據(jù)。這就是檢查時間/使用時間()漏洞的原因之一,會導致空間和時間內(nèi)存安全漏洞。
2018 年披露的漏洞表明了數(shù)據(jù)競爭可能帶來的影響。當虛擬機向主機發(fā)送特定消息時調(diào)用此代碼。這意味著可以并行調(diào)用它來處理其他控制消息和數(shù)據(jù)包。這是有問題的,因為控制消息的處理函數(shù)使用的信息被修改,沒有任何鎖定操作[0]。
以下代碼被多個控制消息處理函數(shù)使用,從中我們可以看到更新的信息是如何使用的:
由于訪問不同步,新緩沖區(qū)可能會被舊的 -> [1] 值使用,導致越界寫操作 [3]。
防止此類漏洞需要對多個線程訪問的數(shù)據(jù)結構進行鎖定操作,直到數(shù)據(jù)處理完成。但是,C++ 中沒有簡單的靜態(tài)檢查方法來強制執(zhí)行此操作。
我們該怎么辦
需要幾個不同的指標來解決本文中提出的問題。 C++ 中的“現(xiàn)代”結構(例如 span)至少可以防止某些類型的內(nèi)存安全問題,而其他現(xiàn)代 C++ 功能(例如智能指針)應盡可能使用。但是,現(xiàn)代 C++ 仍然不是一種完全內(nèi)存安全且完全沒有數(shù)據(jù)競爭的語言。更糟糕的是,這些功能的使用完全依賴于程序員“做正確的事情”,這幾乎不可能在大型、晦澀的代碼庫中強制執(zhí)行。 C++ 也沒有工具可以用安全的抽象來包裝不安全的代碼,這意味著雖然可以在本地強制執(zhí)行正確的編程習慣,但在 C 或 C++ 中構建安全組件將極其困難。
此外,軟件也應盡可能轉為完全內(nèi)存安全的語言,例如C#或F#,它們使用運行時檢查和垃圾收集來確保內(nèi)存安全。畢竟,除非必要,否則您不應該參與復雜的內(nèi)存管理。
如果您出于合理的原因(例如速度、控制和可預測性)使用 C++,則可以考慮轉向內(nèi)存安全系統(tǒng)編程語言。在下一篇文章中,我們將介紹為什么我們認為 Rust 是目前最合適的編程語言,因為它可以以內(nèi)存安全的方式編寫系統(tǒng)級程序。
原文: