驅動程序中的每一個漏洞本質上都是Windows內核中的一個漏洞,因為每個驅動程序都共享內核的內存空間。擁有了在內核中運行代碼、從模型寄存器讀寫或複制特權訪問令牌的能力實際上是擁有了系統。本文將介紹在WDM驅動程序中發現漏洞的方法,然後通過kAFL利用內核模糊。大多數漏洞似乎都在WDM或KMDF中。
在本博客的每一部分中,我們都將從基礎開始,比如熟悉相關的API和數據結構。
WDMWindows驅動程序模型(WDM)是最古老的,也是最常用的驅動程序框架。每個驅動本質上都是一個WDM驅動;較新的框架WDF (Windows Driver framework)封裝了WDM,簡化了WDM的開發過程,解決了WDM的多種技術難題。在檢查WDM驅動程序時,我們關心的主要事情是如何與它們通信;幾乎驅動程序中的每個漏洞都涉及到一些從非特權用戶到驅動程序本身的通信。
在示例中,這是名為“testy”的驅動程序的入口點:
經典的DriverEntry代碼,注意,對IoCreateDevice的調用沒有FILE_DEVICE_SECURE_OPEN標誌
這段代碼是每個WDM驅動都有的DriverEntry函數的普通架構。第一個參數是DriverObject結構指針,用於設備創建和調度例程初始化。接下來,驅動程序有MajorFunction成員,它是一個函數指針數組,用於為不同的事件分配調度例程。此外,我們還有將在下一節中介紹的關鍵設備創建例程。
設備創建和初始化驅動程序首先通過調用IoCreateDevice 創建設備,這將在對像管理器中創建一個DEVICE_OBJECT。在Windows 中,設備對象表示驅動程序處理I/O 請求的邏輯、虛擬或物理設備。所有這些聽起來都不錯,但如果我們希望它從普通用戶的角度進行交流,這還不夠;為此,我們調用IoCreateSymbolicLink,它將在對像管理器中創建一個DoS 設備名稱,使用戶能夠通過該設備與驅動程序進行通信。但是,有些設備沒有正常的名稱;它們具有自動生成的名稱(在PDO 中完成)。對於沒有經驗的檢測人員來說,它們可能看起來很奇怪,所以如果你在你最喜歡的設備中第一次看到它們,請查看軟件,並在設備名稱列中查看8 位十六進制。這些設備可以像其他所有命名設備一樣進行交互。
展示WinObjEx 設備命名空間
在設備創建例程中要注意的最重要的事情是程序員是否為設備分配了ACL 以及DeviceCharacteristics 的值。
不幸的是,IoCreateDevice方法不允許程序員指定任何ACL,這是不好的。因此,開發人員必須在註冊表或驅動程序的ini文件中定義一個ACL。如果他們不能這樣做,任何用戶都可以訪問設備。然而,使用IoCreateDeviceSecure方法可以緩解這種情況。
除此之外,我們還需要查看第五個參數,即DeviceCharacteristics 。如果DeviceCharacteristics 的值沒有與0x00000100 或FILE_DEVICE_SECURE_OPEN 進行OR 運算,我們可能會面臨安全漏洞(除非我們討論文件系統驅動程序或任何支持名稱結構的驅動程序)。這背後的原因是Windows 對待設備的方式;每個設備都有自己的命名空間。設備命名空間中的名稱是以設備名稱開頭的路徑。對於名為\Device\DeviceName 的設備,其命名空間由“\Device\DeviceName\anyfile”形式的任何名稱組成。
如圖1所示,沒有FILE_DEVICE_SECURE_OPEN標誌的IoCreateDevice調用意味著設備ACL不應用於打開設備命名空間內文件的文件請求。換句話說,即使我們在通過IoCreateDeviceSecure或其他方式創建設備時指定了強ACL,該ACL也不會應用於打開文件請求。結果,我們並沒有真正得到我們想要的,所以使用\Device\testydrv 調用CreateFile 會失敗,但使用“\device\testydrv\anyfile”調用會成功,因為IoManager 沒有應用設備ACL到創建請求(因為它假設它是一個文件系統驅動程序)。對於初學者來說,它被認為是一個值得修復的漏洞。此外,這將導致非管理員用戶嘗試讀/寫設備,執行DeviceIoControl 請求等等,這通常是你不希望非管理員用戶做的事情。
更好的用戶保護我們可以通過調用IoCreateDeviceSecure(或WdmlibIoCreateDeviceSecure;它是相同的函數),使用安全描述符防止非管理員用戶打開設備句柄,並在創建例程中使用FILE_DEVICE_SECURE_OPEN值。這也將為我們省去在註冊表中聲明設備權限的麻煩,就像我們在IoCreateDevice 中需要的那樣。
我們應該如何創建設備
從尋找漏洞的角度來看,我們應該列舉系統中每一個可能的設備,然後嘗試用GENERIC_READ | GENERIC_WRITE打開它,這允許我們過濾掉不能與之通信的設備。
調度方法創建設備很好,但僅僅與驅動程序通信是不夠的,還需要IRP。驅動程序代表IoManager 接收IRP、I/O 請求數據包以用於特定觸發器。例如,如果應用程序嘗試打開設備句柄,IoManager 將調用分配給驅動程序對象的相關調度方法。因此,它允許每個驅動程序為其創建的每個設備支持多個不同的MajorFunction。大約有30 種不同的MajorFunction。如果算上已棄用的IRP_MJ_PNP_POWER,每個都代表不同的事件。我們將只關注其中兩個MajorFunction 方法,並添加關於其餘方法的簡短描述,這是我們在尋找漏洞時應該注意的地方。
基本的驅動程序調度表分配
調用IRP_MJ_CREATE在我們深入研究最有趣的目標之前,即IRP_MJ_DEVICE_CONTROL,我們將從IRP_MJ_CREATE 開始。每個內核模式驅動程序都必須在驅動程序調度回調函數中處理IRP_MJ_CREATE。驅動程序必須實現IRP_MJ_CREATE,因為沒有它,你將無法打開設備或文件對象的句柄。
正如你可能猜到的,當你調用NtCreateFile 或ZwCreateFile 時會調用IRP_MJ_CREATE 調度例程。在大多數情況下,它將是一個空存根,並根據設備的ACL 返回一個帶有請求的DesiredAccess 的句柄。
典型的DistpachCreate強制方法
但是,在某些情況下,會涉及更複雜的代碼,即使你滿足設備的ACL 標準,你也可能會收到類似STATUS_INVALID_PARAMETER 的狀態漏洞,因為你在調用NtCreateFile 時使用了不正確的參數。
不幸的是,這表明你不能盲目打開設備,希望通過DeviceIoControl與驅動程序通信;你首先需要了解它的預期參數。通常,DispatchCreate 需要一些ExtendedAttributes(不能為此使用常規CreateFile)或特定文件名(除了設備名稱)。因此,我們必須訪問DispatchCreate 方法。
顯示檢查是否存在名為“StorVsp-v2”的擴展屬性以及值字段的長度是否為0x19 字節長。因此,驅動程序是StorVsp.sys
除了打開句柄之外,你還可以在DispatchCreate中查找漏洞。函數變得越複雜,內存分配和釋放漏洞的可能性就越高,特別是因為DispatchCreate並不經常被檢查。
我們在尋找驅動程序中的漏洞時採取的一般方法是:
枚舉每個設備對象:
嘗試使用最寬鬆的DesiredAccess 打開它;
如果失敗,檢查狀態碼;如果不是STATUS_ACCESS_DENIED,你可能仍然可以通過做一些手動工作並更改一些參數來打開句柄;
通過遵循這個簡單的算法,我們將擁有一個包含大約70 個設備的列表,我們可以從非管理員的角度與之交談。當然,這個數字會因不同的Windows 設備而異,因為OEM 驅動程序和許多類型的軟件也會安裝驅動程序。
使用ioctls控制設備雖然ioctls很少讓你完全控制設備/驅動程序,但它實際上是應用程序與驅動程序通信的方式。驅動程序可以創建兩種ioctl調度例程:
設備控制方法的典型用法
唯一重要的方法是TestyDispatchIoctl,因為我們不能用任意參數發起對IoBuildDeviceIoControlRequest或IIoAllocateIrp的調用,這是觸發IRP_MJ_INTERNAL_DEVICE_CONTROL主函數的函數。如果是,那是因為內部調度方法很少經過適當的測試。
與DriverObject的任何調度方法一樣,它從IoManager接收兩個參數。
WDM驅動程序中的每個調度方法共享相同的函數簽名
第一個是我們對其執行CreateFile 操作的設備對象,第二個是指向IRP 的指針。從漏洞研究的角度來看,IRP 封裝了用戶數據和我們並不真正關心的許多其他內容。我們在這里關心的主要是從用戶模式發送哪些參數。如果我們看一下NtDeviceIoControlFile 的簽名,我們可以猜測在尋找驅動程序中的漏洞時我們關心哪些字段:
DeviceIoControl API
這種方法的主要問題是輸入/輸出緩衝區、它們的長度和Ioctl代碼本身。我們從Ioctl代碼開始,它是一個充當說明符的32位數字;它描述了緩衝區和長度如何被使用/複製到內核,所需的DesiredAccess(當你打開一個設備句柄時)和一個函數指示器。示例如下:
FileTest.exe工具的圖像,顯示了32 Ioctl編號的位域
我們可以看到ioctl代碼是0x1000,翻譯過來就是:
DeviceType:FileDevice_0:它與我們無關;
Function:0:與我們無關;
Method:METHOD_NEITHER:它與我們相關,因為它描述了imanager如何將數據傳輸到內核;
Access:FILE_ANY_ACCESS:它與我們相關,因為它定義了你需要對句柄擁有的所需訪問權限。如果你沒有正確的訪問權限,那麼IoManager 將不允許調用發生並返回AccessDenied。有四個不同的值:
FILE_ANY_ACCESS:無論DesiredAccess 參數如何,你始終擁有設備句柄;
FILE_READ_DATA:你使用GENERIC_READ 請求了一個句柄並獲得了一個有效的句柄;
FILE_WRITE_DATA:你使用GENERIC_WRITE 請求了一個句柄並獲得了一個有效的句柄;FILE_READ_DATA | FILE_WRITE_DATA:不言自明;你需要這兩種權利;
在\Device\VfpExt 的句柄上運行此DeviceIoControl 請求將導致BSoD,無論你的權限級別如何,在理解了圖3中的Method字段之後,我們將看到其中的原因。
Method/TransferTypeMethod/TransferType被稱為萬惡之母,這聽起來有些誇大其詞,但不幸的是,事實確實如此。傳輸類型的方法,即ioctl 32位數中的兩個最低有效位,指示IoManager 在內核中引用參數(緩衝區和長度)的方式。與訪問字段一樣,有四個不同的選項:
(1)METHOD_NEITHER,兩個位都是打開的:IoManager 是惰性的,不對緩衝區及其長度進行檢查。緩衝區不會復製到驅動程序並駐留在用戶模式下。因此,用戶可以隨意操縱緩衝區的長度並釋放/分配他們的頁面,這將導致許多糟糕的事情:系統崩潰和權限提升,除非正確探測緩衝區。如果你看到一個驅動程序沒有探測緩衝區,而是使用METHOD_NEITHER,那肯定存在漏洞。
(2) METHOD_BUFFERED,沒有一個位是打開的:IoManager將輸入/輸出緩衝區及其長度複製到內核,這使得它更加安全,因為用戶不能隨意換出緩衝區或更改它們的內容和長度。之後,輸入/輸出緩衝區指針被分配給IRP。
(3) METHOD_IN_DIRECT和(4)METHOD_OUT_DIRECT兩個位中的一個是打開的:這兩個非常相似;imanager會像METHOD_BUFFERED那樣分配輸入緩衝區。對於輸出緩衝區,IoManager探測緩衝區並檢查虛擬地址在當前訪問模式下是否可寫或可讀。然後,它鎖定內存頁並將指針傳遞給IRP。
讓我們看看驅動程序如何訪問用戶模式緩衝區並查看一個快速漏洞,它說明了在驅動程序中沒有進行適當的安全檢查的漏洞。
在這裡我們可以看到我們應該如何關聯驅動程序中的每個緩衝區關於描述方法和傳輸類型的Ioctl 代碼
由於驅動程序通常可以支持多個ioctl 代碼,因此對於每個不同的ioctl 代碼,它都有一個大的switch case,影響緩衝區在內存中的存儲位置。在下一節中,我們將看到如果我們不注意會發生什麼。