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