防不勝防!在Firefox中觸發UAF漏洞
安全防護人員早已開發出各種方法來預防各類內存損壞漏洞。不過就算這樣,UAF漏洞也很難被防護,原因是它的攻擊面太多!由于它無法與源代碼中的任何特定模式相關聯,因此預防此漏洞類并非易事。在本文中,我將分析 Mozilla Firefox 中的一個UAF漏洞,該漏洞已被命名為 CVE-2022-26381。在撰寫本文時,Mozilla 錯誤條目 1756793 仍然不對公眾開放。
什么是UAF漏洞?
當訪問指向已釋放對象的指針時,就會發生UAF漏洞。它沒有任何意義!為什么程序員要釋放一個對象,然后再次訪問它?
這種情況的發生是由于當今軟件的復雜性。例如,一個瀏覽器有很多組件,每個組件都可以分配不同的對象。它們甚至可以互相傳遞這些對象以進行處理。當組件使用一個對象時,它可以釋放該對象,而其他組件仍然有一個指向該對象的指針。該指針的任何解除引用都可能導致UAF漏洞。
概念驗證
讓我們先來看看最小化的概念驗證,當在最新版本的Mozilla Firefox(97.0.1)上運行時,很有可能會崩潰。這就是 IDA 中崩潰時的示例。它發生在一個循環中:

它從內存中解除引用一個值,然后使用獲取的值進行間接調用(虛函數調用)。因此,這被認為是一個遠程代碼執行漏洞。在解除引用期間使用的“rax”寄存器的值特別有趣:0xE5E5E5E5E5E5E5E5。這是一個神奇的值,Firefox 使用它來“毒化”已釋放對象的內存,這樣從釋放對象獲取的值的解除引用將導致崩潰,因為這個值從來都不是有效的內存地址。這有助于檢測UAF的發生。
要分析UAF漏洞,就必須獲得有關釋放對象的更多信息,比如類型、大小、分配位置、釋放位置以及隨后使用的位置。在 Windows 上,這通常通過使用 GFlags 工具啟用高級調試功能來啟用各種全局標志來完成。具體來說,它可用于啟用 pageheap 并創建用戶模式堆棧跟蹤以在分配特定對象時捕獲堆棧跟蹤。不幸的是,這對 Mozilla Firefox 沒有幫助,因為 Firefox 有自己的內存管理機制,稱為 jemalloc。我們可以獲得有關該對象的更多信息的方法是在 ASAN 版本的 Firefox 上運行 PoC。你可以看到如下結果:
我們得到了很多信息。讓我們通過檢查對象的分配位置來進一步分解它:

讓我們通過查看源代碼(/builds/worker/checkouts/gecko/layout/svg/SVGObserverUtils.cpp 的第 1164 行)來進一步檢查這個問題。你可以下載Firefox 97.0.1的源代碼或使用在線版本(注意在線版本的行號可能不匹配,因為它會不斷更新):

這是它在編譯后發布版本中的樣子。因此對象大小為 0x70 (112) 字節,用于在滾動觸發的reflow期間存儲和跟蹤幀的屬性。
然后我們想知道它在哪里被釋放和重用。ASAN提供了一個很長的堆棧跟蹤。仔細觀察就會得到一個很好的提示。讓我們首先檢查當對象被釋放時的堆棧跟蹤:

現在是隨后使用該對象時的堆棧跟蹤:

當崩潰發生和啟動對象釋放時,我們可以在堆棧跟蹤中看到" mozilla::SVGRenderingObserverSet::InvalidateAll "函數。這也與 OnNonDOMMutationRenderingChange 函數內部的發布版本的崩潰點相匹配(它表示它已內聯在 xul!mozilla::SVGRenderingObserverSet::InvalidateAll 中)。我們現在可以做一個初步的有根據的猜測:當一個對象在“mozilla::SVGRenderingObserverSet::InvalidateAll”函數中循環處理時,就會到達一個釋放正在處理的對象的代碼路徑,從而觸發了一個UAF漏洞。
現在我們已經掌握了所有實施過程,就可以通過在Firefox發布的版本上運行PoC一步一步地驗證這個假設。
首先,要知道分配對象的地址,以便我們可以監控它。這可以通過設置一個斷點來輕松實現,該斷點在分配時會打印對象的地址:

然后,讓我們看看這些對象是如何在IDA中的“mozilla::SVGRenderingObserverSet::InvalidateAll”函數的循環中被處理的。我們將打印將要被處理的對象的地址,并在隨后的虛函數調用中設置了一個斷點:

我們運行 PoC,調試器在調用虛函數之前停止。如上所示,分配了兩個對象,這兩個對象將在循環中處理。首先,處理一個對象并調用“SVGTextPathObserver::OnRenderingChange”函數,最終釋放各種分配的對象,包括等待處理的第二個對象!

我們可以在下圖中清楚地看到這一點,這是在調用返回后立即拍攝的。正如你所看到的,在處理第一個對象時,第二個對象已經被釋放(并且被0xe5毒化):

在第二次迭代中,被釋放的對象被加載進行處理,導致有毒值的加載,并導致崩潰:

在針對發布版本運行 PoC 時,我們在解除引用 0xE5E5E5E5E5E5E5E5 期間發生了崩潰。但是,在 ASAN 版本中,它在寫入內存時崩潰。為什么有區別?原因如下:
在release(非asan)構建中,當釋放一個對象時,它的內存仍然是可訪問的(而不是未映射的),因此對該內存的任何讀取和寫入仍然可正常進行,而不會立即觸發崩潰。這就是為什么指令“mov byte ptr [rcx+8], 0”在上圖中沒有錯誤的執行。不過,崩潰可能會在更長的時間內發生。在本文的示例中,如果從釋放的對象中獲取一個值,然后解除引用,那么解除引用可能會導致崩潰。如果釋放的對象內容被上面看到的毒值覆蓋,則尤其如此。請注意,有可能根本不會發生崩潰,例如,如果只對釋放的對象進行讀取和寫入,而沒有對獲取的值進行任何解除引用操作,或者有毒的值被不相關的數據覆蓋。這意味著,如果我們對發布版本進行模糊處理,就有可能錯過一個漏洞。
另一方面,ASAN監視內存上的所有讀、寫和解除引用,可以盡快捕獲此類漏洞。這就是為什么推薦使用ASAN版本進行模糊測試的原因。
漏洞修復
UAF漏洞通常通過將原始指針轉換為智能指針或通過更正對象引用計數的管理來修復。這樣就可以通過更改引擎中處理連續幀循環的方式來修復它:

總結
開發人員已經花費了大量的精力來消除源代碼中與已知模式相關的漏洞,并且他們基本上成功地降低了這些漏洞的影響。然而,有一些類型的漏洞更難預防,UAF就是其中之一。確保對具有一百萬行代碼的軟件中的對象生命周期進行完美管理是極其困難的。