Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I
在過去的幾十年中 Windows Kernel 的漏洞層出不窮,熱門的攻擊面逐漸從 Win32k 慢慢轉移到 CLFS (Common Log File System) 上。微軟也持續且積極地修補這些漏洞,使得這些元件越來越安全。而下一個熱門的目標會是哪個元件呢?去年開始,MSKSSRV (Microsoft Kernel Streaming Service) 成為駭客喜愛的目標之一。這個驅動程式小到可以在幾天內完成分析。這是否意味著可能不太會有新的漏洞了?
在這篇研究將講述一個長期被忽視的攻擊面,讓我們在兩個月內就找出了超過 10 個漏洞。此外,也將深入探討了一種 Proxy-Based 的邏輯漏洞類型,使我們可以忽略掉大多數的檢查,最終成功在 Pwn2Own Vancouver 2024 中,攻下 Windows 11 的項目。
這份研究將分成數個部分來撰寫,分別講述不同的漏洞類型及漏洞型態,亦發表於 HITCON CMT 2024 中。
Start from MSKSSRV
對於一項漏洞研究來說,從歷史的漏洞看起,是不可或缺的。
起初,我們為了挑戰 Pwn2Own Vancouver 2024 中 Windows 11 的項目,開始從過去的 Pwn2Own 以及近期 in-the-wild 的漏洞中開始審視,尋找可能的攻擊面。沿著歷史軌跡可以得知,過去主要負責 GDI 相關操作的 Win32K 一直是個很熱門的目標,從 2018 年以來,CLFS (Common Log File System) 也漸漸成為了熱門目標之一。這兩個元件都非常複雜,並且直到現在仍然有不少新漏洞出現,但要熟悉這兩個元件需要花不少時間,同時也有許多研究員在看這兩個元件,所以最終我們沒有先選擇分析他們。
去年 Synacktiv 在 Pwn2Own 2023 中,使用 MSKSSRV 的漏洞成功攻下 Windows 11 後,便有不少人往這個元件開始看起,短時間內就又出現了第二個漏洞 CVE-2023-36802,這時 chompie 也發表了一篇非常詳細的文章,講述這個漏洞成因及其利用細節。由於這個元件非常的小,只看檔案大小約略只有 72 KB,可能認真看個幾天就可以全部看完,因此我們便挑了 MSKSSRV 來做歷史漏洞分析,看看是否有機會抓出其他漏洞。
接下來我們會提一下這兩個漏洞,但不會著墨過多。
CVE-2023-29360 - logical vulnerability
第一個是 Synacktiv 在 Pwn2Own 2023 中所使用的漏洞 :
這是一個邏輯上的漏洞。當 MSKSSRV 使用 MmProbeAndLockPages 鎖定使用者給的記憶體位置作為 FrameBuffer 時,並沒有設置正確的 AccessMode,導致沒有檢查使用者指定的位置是否屬於 User space。如果使用者給的是 Kernel space 中的位置,它就會把指定的 Kernel 位置映射到 User space 給使用者用,最終導致使用者可以對 Kernel 中的任意位置寫入,利用上簡單且非常穩定,成為了受歡迎的漏洞之一。
更多細節可以參考 Synacktiv 在 HITB 2023 HKT 的演講及 Nicolas Zilio(@Big5_sec) 的部落格文章
CVE-2023-36802 - type confusion
這個漏洞則是在 CVE-2023-29360 出來後沒多久被許多人發現,並且在微軟發佈更新時,就已經偵測到利用,是個非常容易被發現的漏洞。MSKSSRV 會先將內部使用的物件(FSContextReg、FSStreamReg)存放在 FILE_OBJECT 的 FsContext2 中,然而後續使用時並沒有對 FsContext2 的型態做檢查,導致 type confusion,詳細內容可參考 IBM X-Force 的部落格。
至此之後,就很少有關於 MSKSSRV 的相關漏洞了。
But is that the end of it ?
然而是否這樣就沒洞了呢?
而我要更準確地回答,No!
實際上整個 Kernel Streaming 就像下面這張圖這樣 :
MSKSSRV 只是冰山一角而已,實際上還有不少潛在的元件,上圖中所寫的都是屬於 Kernel Streaming 的一部分。實際往這方向挖掘之後,最終也在這個攻擊面上取得不少漏洞,就如同流水般的流出漏洞來。
順帶一提,我在寫這篇部落格時,chompie 也發表了有關於他在今年 Pwn2Own Vancouver 2024 中所使用的漏洞 CVE-2024-30089。這個漏洞也在 MSKSSRV 中,該漏洞發生在 Reference Count 的處理,其成因也很有趣,不過這邊就不多談,詳細內容可參考她發表的文章。
Brief overview of Kernel Streaming
那麼,什麼是 Kernel Streaming 呢? 事實上,我們正常使用電腦情況下就會用到 :
在 Windows 系統上,當我們打開鏡頭、開啟音效以及麥克風等音訊設備時,系統需要從這些設備讀取你的聲音、影像等相關資料到 RAM 中。為了更高效地完成這些資料的傳輸,微軟提供了一個名為 Kernel Streaming 的框架,用來處理這些資料。這個框架主要在 Kernel mode 下運行,具有低延遲、良好的擴充性和統一介面等特性,使你能更方便、更高效地處理串流(Stream)資料。
Kernel Streaming 中,提供了三種多媒體驅動模型:port class、AVStream 和 stream class。這裡將主要介紹 port class 和 AVStream,而 stream class 因為較為罕見且過時,不會多加討論。
Port Class
大多數用於 PCI 和 DMA 型音效裝置的硬體驅動程式,它處理與音訊相關的數據傳輸,例如音量控制、麥克風輸入等等,主要會使用到的元件函式庫會是 portcls.sys。
AVStream
AVStream 則是由微軟提供的多媒體類驅動程式,主要支援僅限影片的串流和整合音訊/影片串流,目前跟影像有關的處理多數都跟這類別有關,例如你的視訊鏡頭、擷取卡等等。
實際上 Kernel Streaming 的使用很複雜,因此這裡只會簡單的敘述一下,更多詳細內容可以參考微軟官方文件。
Interact with device
在我們想要與音訊設備或是視訊鏡頭等設備互動時該怎麼做呢?其實就跟一般設備互動一樣,可以透過 CreateFile 函數來開啟一個設備。那麼這類型的設備,名稱又會是甚麼呢?其實這邊不太會像是 \Devcie\NamedPipe
這類型的名稱,而是會像下面這樣的路徑 :
\\?\hdaudio#subfunc_01&ven_8086&dev_2812&nid_0001&subsys_00000000&rev_1000#6&2f1f346a&0&0002&0000001d#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\ehdmiouttopo
Enumerate device
每台電腦都可能不一樣,必須使用 SetupDiGetClassDevs 等 API 去列舉設備,一般來說 KS 系列的設備都會註冊在 KSCATEGORY*
底下,像是音訊設備就會註冊在 KSCATEGORY_AUDIO 中。
你也可以使用 KS 所提供的 KsOpenDefaultDevice 獲得該類別中第一個符合的 PnP 裝置的 Handle,實際上來說也只是 SetupDiGetClassDevs 和 CreateFile 的封裝而已。
hr = KsOpenDefaultDevice(KSCATEGORY_VIDEO_CAMERA,GENERIC_READ|GENERIC_WRITE, &g_hDevice)
Kernel Streaming object
我們在開啟這些設備之後,Kernel Streaming 會在 Kernel 中建立一些相關的 Instance,其中最為重要的就是 KS Filters 及 KS Pins。在 Kernel Streaming 的使用過程中,這些 Instance 會被頻繁使用,它們主要用來封裝設備的硬體功能,方便開發者透過統一的介面進行串流的處理。
這邊先以 Audio Filters 作為例子,其他多數大同小異,我們也只會簡單介紹,其他細節請自行參考微軟官方文件。
KS filters
每個 KS Filter 通常代表一個設備或設備的特定功能,在我們打開一個音訊設備後,大部分情況下會對應到一個 Kernel Filter,當我們從音訊設備讀取資料時,這些資料就會先通過這個 KS Filter 進行處理。
概念上如下圖所示,中間的大框表示一個代表音訊設備的 KS filter。而我們想要從音訊設備中讀取資料時,會從左邊讀入 Filter,經過幾個節點進行處理後,從右邊輸出。
(From: https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/audio-filters)
KS pins
上圖中,讀取及輸出資料的點稱為 Pin,Kernel 也有相對應的 KS Pin Object,用於描述這些 Pin 的行為,例如 Pin 是輸入端還是輸出端、支援的格式有哪些等。我們使用時必須在 Filters 上,開啟一個 Pin 來建立 Instance,才能從設備讀取或輸出資料。
KS Property
這些 KS Object 都會有自己的 Property,每個 Property 都會有相對應的功能,前面所提到的 Pin 中的資料格式、音量大小及設備的狀態等等,這些都是一個 Property,通常會對應到一組 GUID,我們可以透過 IOCTL_KS_PROPERTY 來讀取或設定這些 Property。
這大大簡化了多媒體驅動程式的開發,並且確保了不同設備之間的一致性和可擴展性。
Read streams from webcam
這邊就用個簡單的範例來介紹一下 Application 如何從視訊鏡頭讀取資料
其最簡單的流程大概如這張圖所示 :
- 開啟設備後獲得設備 Handle
- 使用這個 Handle 在這個 Filter 上建立 Pin 的 Instance 並獲得 Pin handle
- 使用 IOCTL_KS_PROPERTY 設置 Pin 的狀態到 RUN
- 最後就可以使用 IOCTL_KS_READ_STREAM 從這個 Pin 中讀資料進來
Kernel Streaming architecture
對漏洞研究而言,我們必須先了解其架構,思考有哪些可能的攻擊面
在初步了解 Kernel Streaming 有哪些功能和操作後,為了找尋漏洞必須先了解一下架構,了解 Windows 是怎麼實作這些功能、分別有哪些元件等等,才知道應該要分析哪些 sys,從哪邊下手會比較好。
經過我們分析後,整個架構約略會像這張圖所示 :
在 Kernel Stearming 元件中,最為核心的就是 ksthunk.sys 及 ks.sys,幾乎所有功能都會與它們有關。
ksthunk (Kernel Streaming WOW Thunk Service Driver)
Application 呼叫 DeviceIoControl 後,在 Kernel Streaming 中的入口點,但它功能很簡單,負責將 WoW64 process 中 32-bit 的 requests 轉換成 64-bit 的 requests,使得下層的 driver 就可以不必為 32 位元的結構另外處理。
ks (Kernel Connection and Streaming Architecture Library)
Kernel Streaming 的核心元件之一,它是 Kernel Streaming 的函示庫,負責及轉發 IOCTL_KS_PROPERTY 等 requests 到對應設備的 driver 中,同時也會負責處理 AVStream 的相關功能。
The work flow of IOCTL_KS_*
而在呼叫 DeviceIoControl 時,就會像下圖一樣,將使用者的 requests 依序給相對應的 driver 來處理
而到第 6 步時 ks.sys 就會根據你 requests 的 Property 來決定要交給哪個 driver 及 handler 來處理你的 request。
最終再轉發給相對應的 Driver,如上圖中最後轉發給 portcls 中的 handler 來操作音訊設備。
到這邊應該對 Kernel Streaming 的架構及流程有初步概念了,接下來就是找洞的時刻。依照現有的元素來看,哪些是值得一看的攻擊面呢?
From attacker’s view
在挖掘漏洞前,如果能仔細思考怎樣的情況下容易有洞,可以達到事半功倍的效果
從一個漏洞研究員的角度來說,大概會有下列這幾個點
-
每個設備中的 Property handler 每個設備中的 KS Object 都有各自的 Property,而且每個 Property 都有各自的實作,有些 Property 處理起來容易出問題。
-
ks 及 ksthunk ks 及 ksthunk 已經有很長一段時間沒有漏洞,但卻是個最容易接觸到的入口點,也許是一個好目標,上一次出現的漏洞是在 2020 年 @nghiadt1098 所找到的兩個漏洞 CVE-2020-16889 及 CVE-2020-17045
-
每個 driver 都各自處理一部分的內容 在 Kernel Streaming 的部分功能中,有些 driver 會各自先處理部分的內容,可能會造成一些不一致性的問題。
我們針對上面幾個角度去對整個 Kernel Streaming 做 Code Review 後,很快的就發現了幾個比較容易發現的漏洞
- portcls.sys
- CVE-2024-38055 (out-of-bounds read when set dataformat for Pin)
- CVE-2024-38056
- ksthunk
- CVE-2024-38054 (out-of-bounds write)
- CVE-2024-38057
不過我們這一篇不會一一講解這些漏洞,這幾個多數都是沒有檢查長度或是 index 之類的越界存取等等明顯的洞,也許會在後續的部分慢慢來講解,@Fr0st1706 也在前陣子寫出了 CVE-2024-38054 的利用,這邊就暫時留給讀者研究了。
這篇要提的是,我們在 Review 過程中發現了一些有趣的事情。
你覺得下面這段程式碼是否安全呢?
__int64 __fastcall CKSThunkDevice::CheckIrpForStackAdjustmentNative(__int64 a1, struct _IRP *irp, __int64 a3, int *a4)
{
if ( irp->RequestorMode )
{
v14 = 0xC0000010;
}
else
{
UserBuffer = (unsigned int *)irp->UserBuffer;
...
v14 = (*(__int64 (__fastcall **)(_QWORD, _QWORD, __int64 *)) (Type3InputBuffer + 0x38))(// call Type3InputBuffer+0x38
*UserBuffer,
0LL,
v19);
}
}
看到這段程式碼讓我想起了 CVE-2024-21338,該漏洞原先並沒有任何檢查,而在修補後則是新增了 ExGetPreviousMode,但這邊檢查則是使用了 IRP 中的 RequestorMode 來做檢查,不過一般情況下從使用者呼叫的 IOCTL 的 RequestorMode 都會是 UserMode(1) 是不會有問題的。
此時我又想起來 James Forshaw 的 Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager 這篇文章。
The overlooked bug class
這邊我們必須先來提一下幾個名詞跟概念,不過如果你對 PreviousMode 及 RequestorMode 很熟悉,可以跳至 A logical bug class
PreviousMode
第一個是 PreviousMode,在 Application 中如果使用者透過 Nt* 等 System Service Call 來對設備或檔案中操作時,進入 Kernel 後就會在 _ETHREAD 中的 PreviousMode 標註 UserMode(1) 表示這個 System Service Call 是來自 User mode 的 Application。如果你是從 Kernel mode 中,例如設備 driver 呼叫 Zw* System Service Call 的 API 就會標記成 KernelMode(0)。
RequestorMode
另外一個類似的則是 IRP 中的 RequestorMode 這邊就是記錄你原始的 requests 是來自 UserMode 還是 KernelMode,在 Kernel driver 中的程式碼是非常常用到的欄位,通常會來自 PreviousMode。
很常被用來決定是否要對來自使用者的 requests 做額外檢查,像是 Memory Access Check 或是 Security Access Check,例如下面這個例子中,如果 requests 來自 UserMode 就會檢查使用者提供的位置,如果是從 Kernel 來的,就不做額外檢查增加效率。
但實際上這也出現了一些問題…
A logical bug class
在 James Forshaw 的 Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager 中,就提到了一種 Bug Class
這邊可以先想想看,使用者呼叫 NtDeviceIoControlFile 之類的 System Service Call 之後,如果處理的 driver 又去用使用者可控的資料來作為 ZwOpenFile 的參數,會發生什麼事
在 driver 呼叫 ZwOpenFile 之後, PreviousMode 會轉換成為 KernelMode
,並且在 NtOpenFile 處理時,就會因為 PreviousMode 是 KernelMode
的關係少掉大部分的檢查,而後續的 Irp->RequestorMode
也會因此變成 KernelMode
,從而繞過 Security Access Check 及 Memory Access Check。不過這邊很看後續處理的 driver 怎麼去實作這些檢查,如果只依賴 RequestorMode 來決定要不要檢查,就可能會有問題。這邊省略了一些細節,實際上的狀況會稍微再複雜一點點,也會跟 CreateFile 的 flag 有關,細節可參考下列幾篇文章 :
- Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager
- Hunting for Bugs in Windows Mini-Filter Drivers
- Local privilege escalation via the Windows I/O Manager: a variant finding collaboration
這邊有這樣的概念就好,原先這些研究主要是在 Zw* 系列的 System Service Call 上面,大家可以思考一下,有沒有其他類似的情況,也可能造成這種邏輯漏洞呢?
The new bug pattern
事實上來說是有的,使用 IoBuildDeviceIoControlRequest 這個方法去創建一個 DeviceIoControl 的 IRP 時,萬一沒注意到也很容易有這樣的問題。這個 API 主要是 Kernel driver 用來呼叫 IOCTL 的其中一種方法,它會幫你建好 IRP,而後續在去呼叫 IofCallDriver,就可以在 Kernel driver 中呼叫 IOCTL。在 Microsoft Learn 中,有一段話特別值得注意 :
也就是預設情況下,如果你沒有特別去設置 RequestorMode 就會直接以 KernelMode 形式去呼叫 IOCTL。
按照這個思路,我們重新回頭審視一下我們的目標 Kernel Streaming,我們發現了一個吸引我們的地方。
在 Kernel Streaming 中使用這個 IoBuildDeviceIoControlRequest 地方是在 ks!KsSynchronousIoControlDevice
,而主要內容明顯就是在用剛剛提到的方法,在 Kernel 中呼叫 DeviceIoControl,不過這邊看似有好好的設置 Irp->RequestorMode
,且會根據 KsSynchronousIoControlDevice 參數不同而去設置不同的數值,對於開發者來說會是一個方便的函式庫。
然而…
ks!CKsPin::GetState
ks!SerializePropertySet
ks!UnserializePropertySet
我們發現到在 Kernel Streaming 中,全部有使用到 KsSynchronousIoControlDevice
的地方都是固定的使用 KernelMode(0),到這邊就可以仔細的檢查看看,有用到的地方是否有安全上的問題了。因此我們將 Kernel Streaming 中的 bug pattern 轉換成下列幾點:
- 有使用 KsSynchronousIoControlDevice
- 有可控的
- InputBuffer
- OutputBuffer
- 第二次處理 IOCTL 的地方有依賴 RequestorMode 做安全檢查,並且有可以作為提權利用的地方。
按照這個 Pattern 我們很快地就找到了第一個洞
The vulnerability & exploitation
CVE-2024-35250
這個漏洞也是我們今年在 Pwn2Own Vancouver 2024 中所使用的漏洞。在 Kernel Streaming 的 IOCTL_KS_PROPERTY 功能中,為了讓效率增加,提供了 KSPROPERTY_TYPE_SERIALIZESET
和 KSPROPERTY_TYPE_UNSERIALIZESET
功能允許使用者透過單一呼叫與多個 Property 進行操作。當我們用這功能時,這些 requests 將被 KsPropertyHandler 函數分解成多個呼叫,詳情可參考這篇。
該功能實作在 ks.sys 中
上圖中可以看到,在 ks 處理 Property 時,如果有給上述的 flag 就會由 UnserializePropertySet 來處理你的 request
我們這邊就先來看一下 UnserializePropertySet
unsigned __int64 __fastcall UnserializePropertySet(
PIRP irp,
KSIDENTIFIER* UserProvideProperty,
KSPROPERTY_SET* propertyset_)
{
...
New_KsProperty_req = ExAllocatePoolWithTag(NonPagedPoolNx, InSize, 0x7070534Bu);
...
memmove(New_KsProperty_req, CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer, InSize); //------[1]
...
status = KsSynchronousIoControlDevice(
CurrentStackLocation->FileObject,
0,
CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode,
New_KsProperty_req,
InSize,
OutBuffer,
OutSize,
&BytesReturned); //-----------[2]
...
}
可看到在處理過程中會先將原始的 request,複製到新分配出來的 Buffer 中 [1],而後續就會使用這個 Buffer 來使用 KsSynchronousIoControlDevice 呼叫新的 IOCTL [2]。其中 New_KsProperty_req
及 OutBuffer
都是使用者所傳入的內容。
而呼叫 UnserializePropertySet 時的流程,大概如下圖所示 :
這邊呼叫 IOCTL 時可以看到圖中第 2 步 I/O Manager 會將 Irp->RequestorMode
設成 UserMode(1),直到第 6 步時,ks 會去判斷使用者 requests 的 Property 是否存在於該 KS Object 中,如果該 KS Object 的 Property 存在,並且有設置 KSPROPERTY_TYPE_UNSERIALIZESET
就會用 UnserializePropertySet
來處理指定的 Property
而接下來第 7 步就會呼叫 KsSynchronousIoControlDevice 重新做一次 IOCTL,而此時新的 Irp->RequestorMode
就變成了 KernelMode(0) 了,而後續的處理就如一般的 IOCTL_KS_PROPERTY 相同,就不另外詳述了,總之我們到這裡已經有個可以任意做 IOCTL_KS_PROPERTY 的 primitive 了,接下來我們必須尋找看看是否有可以 EoP 的地方。
The EoP
最先看到的想必就是入口點 ksthunk,我們這邊可以直接來看 ksthunk!CKSThunkDevice::DispatchIoctl
__int64 __fastcall CKSThunkDevice::DispatchIoctl(CKernelFilterDevice *a1, IRP *irp, unsigned int a3, NTSTATUS *a4)
{
...
if ( IoIs32bitProcess(irp) && irp->RequestorMode ) //------[3]
{
//Convert 32-bit requests to 64-bit requests
}
else if ( CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode == IOCTL_KS_PROPERTY )
{
return CKSThunkDevice::CheckIrpForStackAdjustmentNative((__int64)a1, irp, v11, a4) //-----[4];
}
}
ksthunk 會先判斷是否是 WoW64 的 Process 的 request,如果是就會將原本 32-bit 的 requests 轉換成 64-bit 的 [3],如果原本就是 64-bit 則會呼叫 CKSThunkDevice::CheckIrpForStackAdjustmentNative
[4] 往下傳遞
__int64 __fastcall CKSThunkDevice::CheckIrpForStackAdjustmentNative(__int64 a1, struct _IRP *irp, __int64 a3, int *a4)
{
...
if ( *(_OWORD *)&Type3InputBuffer->Set == *(_OWORD *)&KSPROPSETID_DrmAudioStream
&& !type3inputbuf.Id
&& (type3inputbuf.Flags & 2) != 0 ) //-----[5]
{
if ( irp->RequestorMode ) //-------[6]
{
v14 = 0xC0000010;
}
else
{
UserBuffer = (unsigned int *)irp->UserBuffer;
...
v14 = (*(__int64 (__fastcall **)(_QWORD, _QWORD, __int64 *))(Type3InputBuffer + 0x38))(// call Type3InputBuffer+0x38
*UserBuffer,
0LL,
v19); //------------[7]
}
}
}
我們在 [5] 看到,如果我們給定的 Property Set 是 KSPROPSETID_DrmAudioStream ,就有特別的處理。而在 [6] 時,會先去判斷 Irp->RequestorMode 是否為 KernelMode(0),如果從 UserMode(1) 呼叫的 IOCTL 就會直接返回錯誤,但如果我們使用前面所說的 KSPROPERTY_TYPE_UNSERIALIZESET
來呼叫 IOCTL,並指定 KSPROPSETID_DrmAudioStream
這個 Property,那麼這裡 [6] 就會是 KerenlMode(0)。接下來就會在 [7] 直接使用使用者所傳入的內容做為 function 呼叫,甚至第一個參數是可控的,實際寫 PoC 後,驗證了我們的結果。
這邊可能會有人有疑惑,什麼設備或是情況下會有 KSPROPSETID_DrmAudioStream
?實際上來說音訊設備大多情況下都會有,主要是用來設置 DRM 相關內容用的。
Exploitation
在有了任意呼叫之後,要達成 EoP 就不是太大的問題,雖然會遇到 kCFG、kASLR、SMEP 等等保護,但在 Medium IL 下唯一比較需要處理的就只有 kCFG。
- kCFG
- kASLR
- NtQuerySystemInformation
- SMEP
- Reuse Kernel Code
- …
Bypass kCFG
那我們目標很簡單,就是從合法的 function 做出任意寫的 primitive,而之後就可以利用常見的方法用 System token 取代當前的 Process token 或是 Abuse token privilege 去做到 EoP。
直覺地會直接去找看看,kCFG 中合法的 function 名稱有 set 的 function,比較可能是可以寫入的。我們這裡是直接拿 ntoskrnl.exe 中 export fucntion 去尋找看看是否有合法的 function,這些大多情況下都是合法的。
而很快的我們就找到了 RtlSetAllBits
它是個非常好用的 gadget 而且是 kCFG 中合法的 function,另外也只要控制一個參數 _RTL_BITMAP
struct _RTL_BITMAP
{
ULONG SizeOfBitMap;
ULONG* Buffer;
};
我們可將 Buffer 指定到任意位置並指定大小,就可以將一段範圍的 bits 全部設置起來,到這邊就差不多結束了,只要 將 Token->Privilege
全部設置起來,就可以利用 Abuse Privilege 方法來做到 EoP 了。
然而…在 Pwn2Own 比賽前,我們在 Hyper-V 上安裝一個全新 Windows 11 23H2 VM 測試 Exploit,結果失敗了。 而且是在開啟設備階段就失敗。
經過調查後發現到 Hyper-V 在預設情況下並不會有音訊設備,造成 exploit 會失敗。
在 Hyper-V 中,預設情況下只會有 MSKSSRV,然而 MSKSSRV 也沒有 KSPROPSETID_DrmAudioStream 這個 Property,使得我們無法成功利用這個漏洞達成 EoP,因此我們必須找其他方式觸發或者找新的漏洞,此時我們決定重新 Review 一遍整個流程,看看是否還有其他可能利用的地方。
CVE-2024-30084
重新審視後,發現到 IOCTL_KS_PROPERTY 是使用 Neither I/O 來傳遞資料的,也就是說會直接拿使用者的 Input buffer 來做資料上的處理,一般來說不太建議使用這個方法,很常出現 Double Fetch 的問題。
我們可從上圖中 KspPropertyHandler 看到,在使用者呼叫 IOCTL 之後,會直接將 Type3InputBuffer 複製到新分配出來的 Buffer 中,其中會存有 KSPROPERTY 結構,接下來會用這結構中的 GUID 來查詢 Property 是否有在該設備所支援的 Property 中,若存在才會繼續往下呼叫 UnserializePropertySet
。
這邊我們再回頭看一眼 UnserializePropertySet
。
我們可以發現到,它又再次從 Type3InputBuffer 複製使用者所提供的資料做為新的 IOCTL 的輸入,很明顯的這邊就存在了一個 Double Fetch 的漏洞,因此我們將整個利用流程改成下圖的樣子
我們一開始發送 IOCTL_KS_PROPERTY 時,就會先以 MSKSSRV 既有的 Property KSPROPSETID_Service
來做後續操作,而在圖中第 6 步時,會先複製一份 Property 的 GUID 到 Kernel 中,而後再用這個 Property GUID 去查詢是否有在該 KS Object 的支援清單中,而這邊因為 MSKSSRV 有支援,就會往下呼叫 UnserializePropertySet
。
在呼叫 UnserializePropertySet 後,因為有 Double Fetch 的漏洞,讓我們可以在檢查後到使用之間,將 KSPROPSETID_Service
換成 KSPROPSETID_DrmAudioStream
,而接下來就可以讓 ks 使用 KSPROPSETID_DrmAudioStream
作為 requests 來發送 IOCTL,從而觸發前述了 CVE-2024-35250 邏輯漏洞,使這個漏洞不論在甚麼環境下都可以使用。
最終我們成功在 Pwn2Own Vancouver 2024 中,成功攻下 Micorsoft Windows 11。
在 Pwn2Own 結束後,經過我們調查,發現到這個漏洞從 Windows 7 就存在了,至少存在將近 20 年,而且利用上非常穩定,有著百分之百的成功率,強烈建議大家盡速更新至最新版本 。
To be continued
這篇主要著重在我們如何找到今年在 Pwn2Own 中所使用的漏洞及 Kernel Streaming 的攻擊面分析。在找到這個洞之後,我們後續也持續朝這個方向繼續研究,也發現了另外一個也是 Exploitable 的漏洞以及其他更多有趣的漏洞,我們預計在今年十月發表,敬請期待 Part II。
Reference
- Critically Close to Zero-Day: Exploiting Microsoft Kernel Streaming Service
- Windows Kernel Security - A Deep Dive into Two Exploits Demonstrated at Pwn2Own
- CVE-2023-29360 Analysis
- Racing Round and Round: The Little Bug That Could
- Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager
- Hunting for Bugs in Windows Mini-Filter Drivers
- Local Privilege Escalation via the Windows I/O Manager: A Variant Finding & Collaboration