攻擊Windows平臺NVIDIA驅動程序
前言
現代圖形驅動程序是十分復雜的,它提供了大量有希望被利用的攻擊面,可以使用具有訪問GPU權限的進程(比如Chrome的GPU進程)進行提權和沙箱逃逸。在這篇文章中,你們將看到如何攻擊NVIDIA內核模式的Windows驅動程序,以及在此期間我發現的一些bug。我的這項研究是Project Zero的一個20%項目的一部分,在此期間我總共發現了16個漏洞。
內核WDDM接口
圖形驅動程序的內核模式組件被稱為顯示微端口驅動程序。微軟的官方文檔為我們提供了一個很好的結構圖,總結了各個組件之間的關系:
在顯示微端口驅動程序 的DriverEntry()函數中,DRIVER_INITIALIZATION_DATA結構被由廠商實現的函數(實際上與硬件進行交互)的回調進行填充,該函數通過DxgkInitialize()傳遞給dxgkrnl.sys(DirectX的子系統)。這些回調要么由DirectX內核子系統調用,要么在某些情況下直接從用戶模式代碼調用。
DxgkDdiEscape
一個眾所周知的潛在漏洞的入口點是 DxgkDdiEscape 接口。它可以直接在用戶模式下被調用,并且可以接受任意數據,該數據以廠商指定的方式(本質上是IOCTL)解析和處理。在后文中,我們將使用術語“逃逸”來表示由DxgkDdiEscape 函數支持的特定命令。
截止寫作時,NVIDIA有著數量驚人的400多個逃逸,所以這里也是我花費了絕大部分時間的地方(這些逃逸中的絕大多數是否有必要處在內核空間中是一個問題):
- // (這些結構體的名稱是我命名的)
- // 表示一組逃逸代碼
- struct NvEscapeRecord {
- DWORD action_num;
- DWORD expected_magic;
- void *handler_func;
- NvEscapeRecordInfo *info;
- _QWORD num_codes;
- };
- // 有關特定逃逸代碼的信息
- struct NvEscapeCodeInfo {
- DWORD code;
- DWORD unknown;
- _QWORD expected_size;
- WORD unknown_1;
- };
NVIDIA為每一個逃逸都單獨實現了其私有數據(DXGKARG_ESCAPE 結構體中的pPrivateDriverData),格式為“頭部+數據”。頭部格式如下:
- struct NvEscapeHeader {
- DWORD magic;
- WORD unknown_4;
- WORD unknown_6;
- DWORD size;
- DWORD magic2;
- DWORD code;
- DWORD unknown[7];
- };
這些逃逸由32位代碼(上面NvEscapeCodeInfo結構體的第一個成員)標識,并根據它們的最高有效字節進行分組(從1到9)。
在處理每個逃逸代碼之前都會做一些驗證。具體來說,每個 NvEscapeCodeInfo 應當包含頭部后面的逃逸數據的預期大小。這將根據NvEscapeHeader中的大小來驗證,NvEscapeHeader自身又通過傳遞給 DxgkDdiEscape的PrivateDriverDataSize字段進行驗證。但是,預期大小有時可能為0(通常當逃逸數據為可變大小時),這意味著逃逸處理程序負責進行自身的驗證。這將導致一些bug(1,2)。
在逃逸處理程序中發現的大多數漏洞(總共13個)都是些非常基本的bug,例如盲目地向用戶提供的指針進行寫入操作,向用戶模式公開未初始化的內核內存以及不正確的邊界檢查。還有許多我發現的問題(例如OOB讀取)沒有報告出去,因為它們似乎沒有可以利用的地方。
DxgkDdiSubmitBufferVirtual
另一個有趣的入口點是DxgkDdiSubmitBufferVirtual函數,它首次在Windows 10和WDDM 2.0中被引入,主要用來支持GPU虛擬內存(而舊的 DxgkDdiSubmitBuffer / DxgkDdiRender 函數已被棄用)。這個函數相當復雜,并且還接受來自用戶模式驅動程序提交的每一個由廠商特定的數據。我在這里找到了一個bug。
其他
還有一些其他WDDM函數接受廠商特定的數據,但快速瀏覽后沒有發現任何有趣的東西。
暴露的設備
NVIDIA暴露了可由任何用戶打開的一些其他設備:
- \\.\ NvAdminDevice
似乎用于 NVAPI。很多ioctl處理程序似乎都會調用DxgkDdiEscape。
- \\.\ UVMLite {Controller,Process *}
可能與NVIDIA的“統一內存”相關。在這里找到1個bug。
- \\.\ NvStreamKms
作為GeForce Experience的一部分默認選擇安裝,但也可以在安裝期間選擇停用。不是很明白為什么這個驅動程序是必要的。在這里也發現了1個bug。
更多有趣的Bug
我發現的大多數bug是通過手動逆向和分析得到的,并且使用了一些自定義的IDA腳本。我還寫了一個模糊工具。最終結果成功得有點令人驚訝,這也說明了這些bug的簡單性。
雖然大多數bug相當無聊(缺乏驗證之類的簡單案例),但也有一些比較有意思。
NvStreamKms
此驅動程序使用 PsSetCreateProcessNotifyRoutineEx 函數注冊進程創建通知回調。該回調檢查系統上創建的新進程是否和先前通過發送IOCTL設置的映像名稱相匹配。
這個創建通知的例程包含一個bug:
(簡化的反編譯輸出)
- wchar_t Dst[BUF_SIZE];
- ...
- if ( cur->image_names_count > 0 ) {
- // info_是傳遞給例程的PPS_CREATE_NOTIFY_INFO
- image_filename = info_->ImageFileName;
- buf = image_filename->Buffer;
- if ( buf ) {
- filename_length = 0i64;
- num_chars = image_filename->Length / 2;
- // 通過掃描反斜杠來查找文件名
- if ( num_chars ) {
- while ( buf[num_chars - filename_length - 1] != '\\' ) {
- ++filename_length;
- if ( filename_length >= num_chars )
- goto DO_COPY;
- }
- buf += num_chars - filename_length;
- }
- DO_COPY:
- wcscpy_s(Dst, filename_length, buf);
- Dst[filename_length] = 0;
- wcslwr(Dst);
此例程通過向后搜索反斜杠('\')的方法從PS_CREATE_NOTIFY_INFO的ImageFileName 成員中提取映像名稱,然后使用 wcscpy_s 將其復制到堆棧緩沖區(Dst),但傳遞的長度是計算出的名稱長度,而不是目標緩沖區的長度。
即使 Dst 是大小固定的緩沖區,這也不能被視為一個直接溢出。因為它的大小大于255個wchar長度,并且對于大多數Windows文件系統路徑組件來說其不能超過255個字符。而因為ImageFileName 是規范化的路徑,所以掃描反斜杠在大多數情況下也是有效的。
然而,上述規則可以通過如下方式繞過:對于一個符合通用命名規約(UNC)的路徑,其規范化后保持以正斜杠('/')作為路徑分隔符(感謝James Forshaw向我指出這一點)。這便意味著我們可以得到一個“aaa / bbb / ccc / ...”形式的文件名從而引發溢出。
例如:
- CreateProcessW(L"\\\\?\\UNC\\127.0.0.1@8000\\DavWWWRoot\\aaaa/bbbb/cccc/blah.exe", …)
另一個有趣的關注點是,跟隨受損副本的wcslwr實際上并不限制溢出的內容(唯一的要求是有效的UTF-16編碼)。因為計算的filename_length不包含null終止符,所以wcscpy_s 會認為目的地太小,然后以在開始處寫入null字節的方式來清除目的地字符串(發生在內容復制到 filename_length 字節之后,因此溢出仍然發生)。這意味著 wcslwr是無用的,因為對 wcscpy_s的調用和一部分的代碼從來沒有工作過。
利用這個漏洞就不那么復雜了,因為驅動程序沒有使用堆棧cookie編譯過。在以前的漏洞中附加過一個本地特權提升漏洞利用程序,它配置了一個偽造的WebDAV服務器來利用漏洞(ROP,從主堆棧到用戶緩沖區,再次ROP來分配 讀寫執行內存,用來存放shellcode并跳轉進去)。
UVMLiteController中錯誤的驗證
NVIDIA的驅動程序還在 \\.\ UVMLiteController路徑中暴露了一個可以由任何用戶打開的設備(包括從沙箱中的Chrome GPU進程)。該設備的IOCTL處理程序直接將結果寫入Irp->UserBuffer中,作為將要傳遞給 DeviceIoControl 的輸出指針 (微軟的文檔中指出不要這樣做)。IO控制代碼指定使用METHOD_BUFFERED,這意味著在Windows內核檢查地址提供的范圍并將其傳遞給驅動器之前,用戶具有寫操作的權限。
然而,這些處理程序還缺少對輸出緩沖區的邊界檢查,這意味著用戶模式上下文可以通過任何任意地址傳遞值為0的長度(可以繞過ProbeForWrite的檢查), 這樣做的結果是創造出一個受限的Write-what-where情景(這里的“what“僅限于一些特定的值:包括32位0xffff,32位0x1f,32位0和8位0)。
在原始問題中附加了簡單的提權漏洞利用 。
遠程攻擊途徑?
考慮到已發現的bug數量如此之眾,我做了一個調查,是否可以在不必首先破壞沙盒進程的前提下,完全從遠程環境中訪問其中任意一個bug(例如通過瀏覽器中的WebGL或通過視頻加速)。
幸運的是結果似乎并非如此。但這并不令人驚訝,因為這里的易受攻擊的API是非常底層的,只有經過許多層才能訪問得到(對于Chrome而言,需要經歷libANGLE -> Direct3D運行時和用戶模式驅動程序 ->內核模式驅動程序),并且通常需要在用戶模式驅動程序中構造有效的參數才能調用。
NVIDIA的回應
發現的bug的性質表明NVIDIA仍有很多工作要做。他們的驅動程序包含的很多可能不必出現在內核中的代碼,而發現的大多數錯誤是非常基本的錯誤。事到如今,他們的驅動程序(NvStreamKms.sys)仍然缺乏非常基本的緩解措施(堆棧cookie)。
不過,他們的反應倒是快速且積極的。大多數bug在截止日期之前已經修復好了,并且他們自己內部也在做一些尋找bug的工作。他們還表示,他們一直在努力重構他們內核驅動程序的安全性,但還沒有準備好分享任何具體的細節。
時間線
補丁間隔
NVIDIA的第一個補丁,其中包括我報告的6個bug的修復,但是沒有在公告中詳細說明(發布說明稱作“安全更新“)。他們原本計劃在補丁發布后一個月再公布詳細信息。我們注意到了這一點并告訴他們這樣做并不恰當,因為黑客可以通過逆向補丁來找到之前的漏洞,而當大眾意識到這些漏洞細節的時候已經晚了。
雖然前6個bug修復后在30多天內都沒有發布修復的詳細信息,但剩余的8個bug的修復補丁發布后5天內就發布了細節公告。看上去NVIDIA也一直在嘗試減少這種差距,但是就最近的公告來看兩者的發布仍有很大的不一致性。
結論
鑒于內核中的圖形驅動程序所暴露出來的巨大攻擊面,以及第三方廠商的低質量代碼,它似乎是挖掘沙箱逃逸和特權提升漏洞的一個非常豐富的目標。GPU廠商應該盡快將其驅動代碼從內核中轉移出去,從而縮小攻擊面使得這種情況得以限制。