Quantcast
Channel: DEVCORE 戴夫寇爾
Viewing all articles
Browse latest Browse all 145

Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part II

$
0
0

English Version, 中文版本

這是一系列有關 Kernel Streaming 的相關的漏洞研究,建議先閱讀以下文章

在先前 Proxying to Kernel 的研究中,我們在 Kernel Stearming 中找到了多個漏洞以及一個被忽視的 Bug Class,並在今年 Pwn2Own Vancouver 2024 中利用漏洞 CVE-2024-35250CVE-2024-30084成功攻下 Windows 11。

而在這篇研究中,我們將繼續延續這個攻擊面和這個 Bug Class,也將揭露另外一個漏洞和利用手法,亦發表於 HEXACON 2024中。


在 Pwn2Own Vancouver 2024 之後,我們繼續針對 ks!KsSynchronousIoControlDevice這個 bug pattern 去看看有沒有其他安全性上的問題,然而找了一段時間後,針對 KS Object 的 Property 操作中,並沒有找到其他可以利用的點,因而我們將方向轉往另外一個功能 KS Event上。

KS Event

KS Event 與前一篇提到的 KS Property 類似。KS Object 中除了有自己的 Property Set 之外,也有提供設定 KS Event 的功能,比如說你可以設定設備狀態改變或是每個一段時間就觸發 Event,方便播放軟體等開發者定義後續的行為,而每個 KS Event 就如同 Property 一樣,要使用就必須該 KS Object 有支援。我們可以透過 IOCTL_KS_ENABLE_EVENTIOCTL_KS_DISABLE_EVENT來註冊或關閉這些 Event。

KSEVENTDATA

而在註冊 KS Event 時,你可以藉由提供 KSEVENTDATA來註冊你想要的事件,其中可以提供 EVENT_HANDLE 及 SEMAPHORE_HANDLE 等 handle 來註冊,當 KS 觸發這個事件時,就會藉由這個 handle 來通知你。

The work flow of IOCTL_KS_ENABLE_EVENT

其整個運作流程也與 IOCTL_KS_PROPERTY 雷同,在呼叫 DeviceIoControl 時,就會像下圖一樣,將使用者的 requests 依序給相對應的 driver 來處理

同樣會在第 3 步時做 32-bit 的 requests 轉換成 64-bit 的 requests。到第 6 步時 ks.sys 就會根據你 requests 的 Event 來決定要交給哪個 driver 及 addhandler來處理你的 request。

最終再轉發給相對應的 Driver。如上圖中最後轉發給 ks 中的 KsiDefaultClockAddMarkEvent 來設置 Timer

在了解了 KS Event 功能及流程後,根據之前的 bug pattern很快地又找到了一個可以利用的漏洞 CVE-2024-30090

Proxying to kernel again !

這次的問題點發生在 ksthunk 將 32-bit request 轉換成 64-bit request 的過程。

如下圖,當 ksthunk 接收到來自 WoW64 Process 的 IOCTL_KS_ENABLE_EVENT 時,會進行 32-bit 結構到 64-bit 結構的轉換

轉換過程會呼叫 ksthunk!CKSAutomationThunk::ThunkEnableEventIrp來處理

__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4)
{
  ...
  if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED )  
  {
    // Convert 32-bit requests and pass down directly
  }
  else if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) 
  {
    ...
    newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK');
    ...
    memcpy(newinputbuf,Type3InputBuffer,0x28);  //------------------------[1]
    ...
    v18 = KsSynchronousIoControlDevice( 
            v25->FileObject,
            0,
            IOCTL_KS_ENABLE_EVENT,
            newinputbuf,
            inputbuflen + 8,
            OutBuffer,
            outbuflen,
            &BytesReturned);  //-----------------[2]
    ...
  }
  ...
}

而在 CKSAutomationThunk::ThunkEnableEventIrp中,明顯可以看到類似的 bug pattern,從 [1] 中可以看到,它會複製使用者的輸入到新分配出來的 Buffer 中,接著在 [2] 處,就會利用該 Buffer 來使用 KsSynchronousIoControlDevice 呼叫新的 IOCTL,。其中 newinputbuf 及 OutBuffer 都是使用者所傳入的內容。

呼叫 CKSAutomationThunk::ThunkEnableEventIrp時的流程,大概如下圖所示 :

在 WoW64 的程式中呼叫 IOCTL 時,可以看到圖中第 2 步 I/O Manager 會將 Irp->RequestorMode設成 UserMode(1),而在第 3 步時,ksthunk 會將使用者的 request 從 32-bit 轉換成 64-bit,這邊就會用 CKSAutomationThunk::ThunkEnableEventIrp來處理。

之後第 5 步,就會透過 KsSynchronousIoControlDevice重新呼叫 IOCTL ,而此時新的 Irp->RequestorMode就變成了 KernelMode(0) 了,而後續的處理就如一般的 IOCTL_KS_ENABLE_EVENT 相同,就不另外詳述了,總之我們到這裡已經有個可以任意做 IOCTL_KS_ENABLE_EVENT 的 primitive 了,接下來我們必須尋找看看是否有可以 EoP 的地方。

The Exploitation

跟先前思路一樣,一開始還是會先分析入口點 ksthunk,然而我們找尋了一陣子之後,並沒有看到可以做為提權的地方,而且在 ksthunk 中,大多數只要看到 Irp->RequestMode是 KernelMode(0) 就會直接往下傳遞而不另外做處理。因此我們將我們的目標轉向位在下一層的 ks,看看它在處理 event 的過程中,是否有可以用來提權的地方。

很快的就找到一個吸引我們目光的地方:

在 KspEnableEvent 的 Handler 中,有一處會先判斷你所傳入的 KSEVENTDATA 中的 NotificationType 來決定要怎麼註冊及處理你的事件,在一般情況下通常是給一個 EVENT_HANDLE或是 SEMAPHORE_HANDLE,然而在 ks 中,如果是從 KernelMode 呼叫的就給以提供 Event Object甚至 DPC來註冊你的事件,讓整體的處理上更有效率。

也就是說我們可以藉由這個 KernelMode 的 DeviceIoControl 的 primitive 來提供任意 Kernel Object,讓它做後續處理,構造的好就有機會達成 EoP 但要看後續怎麼使用這個 Object 就是了。

但是我們在嘗試了一段時間後發現到……

__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4)
{
  ...
  if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT
    || (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED )  //-------[3]
  {
    // Convert 32-bit requests and pass down directly
  }
  else if ( (v25->Parameters.DeviceIoControl.Type3InputBuffer->Flags & 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) //-------[4]
  {
    ...
    newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK');
    ...
    memcpy(newinputbuf,Type3InputBuffer,0x28); //------[5] 
    ...
    v18 = KsSynchronousIoControlDevice( 
            v25->FileObject,
            0,
            IOCTL_KS_ENABLE_EVENT,
            newinputbuf,
            inputbuflen + 8,
            OutBuffer,
            outbuflen,
            &BytesReturned);  
    ...
  }
  ...
}

如果要提供任意 Kernel Object 去註冊事件,那麼 IOCTL 中所給定的 KSEVENT的 flag,必須要是 KSEVENT_TYPE_ENABLE,也就是上面程式碼 [3] 的部分,然而在程式碼片段 [4] 處是觸發漏洞的地方卻是要是 KSEVENT_TYPE_QUERYBUFFER,並沒辦法如我們所想的一樣直接給一個 Kernel Object。

然而幸運的是整個 IOCTL_KS_ENABLE_EVENT 也是使用 Neither I/O直接拿使用者的 Input buffer 來做資料上的處理,再次出現了 Double Fetch 的問題。

如上圖中所示,我們可以在呼叫 IOCTL 前把 flag 設置成 KSEVENT_TYPE_QUERYBUFFER,檢查時就會以 KSEVENT_TYPE_QUERYBUFFER的功能處理,而在第二次呼叫 IOCTL 前,就把 flag 換成 KSEVENT_TYPE_ENABLE,這樣就可以成功觸發漏洞並構造特定的 Kernel Object 來註冊事件了。

Trigger the event

至於甚麼時候會用到你所構造的 KS Object 呢? 當事件觸發時, ks 會透過 DPC 呼叫 ks!ksGenerateEvent,此時就會依照你所給定的 NotificationType 來決定要怎麼處理你的事件。

我們就來看一下 KsGenerateEvent

NTSTATUS __stdcall KsGenerateEvent(PKSEVENT_ENTRY EventEntry)
{

  switch ( EventEntry->NotificationType )
  {
    case KSEVENTF_DPC:
      ...
      if ( !KeInsertQueueDpc(EventEntry->EventData->Dpc.Dpc, EventEntry->EventData, 0LL) )
        _InterlockedAdd(&EventEntry->EventData->EventObject.Increment, 0xFFFFFFFF); //--------[6]
      ...
    case KSEVENTF_KSWORKITEM:
      ...
      KsIncrementCountedWorker(eventdata->KsWorkItem.KsWorkerObject); //-----------[7]

  }
}

其實到這邊就有多種利用方式可以利用,最直接的莫過於直接構造 DPC 結構註冊 DPC 來達成任意 Kernel 程式碼執行,也就是上面程式碼片段 [6] 的地方,但在呼叫 KsGenerateEvent 時的 IRQL 是 DISPATCH_LEVEL很難在 User space 下構造 DPC object 利用過程也會遇到許多問題

所以我們改用另外一條 KSEVENTF_KSWORKITEM,也就是程式碼片段 [7] 的部分,藉由傳入 Kernel 位置,讓他誤認為是 KSWORKITEM 的指標。

其中就會對該指標指向位置加上 0x5c 的地方加一,也就是可以達到任意 Kerenl Address 加一的寫入,其整個過程就如下圖:

在呼叫 IOCTL_KS_ENABLE_EVENT 時,構造 KSEVENTDATA 指向 Kernel 記憶體位置後,ks 處理時就會將它作為 Kernel Object 來操作,並註冊指定的事件

而到觸發時,ks 就會將我們給的記憶體位置內容 +1,因此我們這邊就有了一個 kernel 任意 +1 的 primitive 了。

Arbitrary increment primitive to EoP

從任意記憶體位置 +1 到提權有許多方法可以利用,其中最知名的莫過於 Abuse token privilege以及 IoRing,原本以為到這邊就差不多結束了…

然而上述兩種方法在這個情境中都有一定的侷限:

Abuse token Privilege

如果是以 Abuse token privilege 方法來做提權,其關鍵在於覆寫 Privileges.Enable 及 Privileges.Present,而我們漏洞一次只能 +1 ,如果要拿到 SeDebugPrivilege 就必須兩個欄位都要寫到,這兩格欄位的預設數值為 0x6028800000x800000必須要變成,0x602980000 及 0x900000,也就是說分別都要各寫 0x10 次,總共要 0x20 次的寫入,每次的寫入都要 race,需要花上不少時間,穩定度也大幅下降。

IoRing

透過 IoRing 來達到任意寫入,也許會是個更簡單的方法,只需覆寫 IoRing->RegBuffersCount and IoRing->RegBuffers就可達到任意寫入,然而有個問題就發生了…

在觸發任意記憶體位置 +1 這個 primitive 時,如果原先的數值是 0 時,就會進到 KsQueueWorkItem 中,其中會有一些相對應複雜的處理,就會導致 BSoD, IoRing 的利用方式剛好就會遇到這狀況…

是不是真的沒辦法穩定利用了呢?

Let’s find a new way !

當傳統的利用方法遇到瓶頸時,深入探討技術的核心機制可能會是值得的。你或許會在此過程中意外發現新的方法。

經過幾天沉思之後,我們決定找尋新方法,但從頭找新的方法可能會花不少時間也可能找不到,於是我們決定從舊有的兩個方法中找尋新的靈感,首先來看的是 Abuse token privilege,其中最關鍵的就是利用漏洞拿到 SeDebugPrivilege 使得我們可以 Open 像是 winlogon 等高權限的 Process。

問題就來了,為什麼只要有 SeDebugPrivilege 就可以開啟高權限的 Process 呢?

這邊就要來看一下 PsOpenProcess,以下是 PsOpenProcess 的程式碼片段:

由此可見,當我們在 Open Process 時, kernel 會優先使用 SeSinglePrivilegeCheck 檢查你是否有 SeDebugPrivilege,如果你具有 SeDebugPrivilege 那就會給你 PROCESS_ALL_ACCESS的權限,不會有其他 ACL 的檢查,讓你可以對任意 Process 去做任何事情,顧名思義就是讓你 Debug 用的,然而有一點值得注意的地方是 SeDebugPrivilege 是在 ntoskrnl.exe上的全域變數。

它會是個 LUID結構,會在系統啟動時初始化,實際數值為 0x14 ,表示在 Privileges.Enable 及 Privileges.Present 欄位中哪個 bit 是代表 SeDebugPrivilege。所以當我們在用 NtOpenProcess 時,系統去查看這個全域變數中的數值。

獲得要檢查的數值後,就會依照這個數值去檢查 Token 中的 Privileges 欄位是否有 Enable 及 Present 這個欄位,以 SeDebugPrivilege 來說就會檢查第 0x14 bit

然而有一件有趣的事情是…

nt!SeDebugPrivilege這個全域變數是位於可寫的區段中!

因此一個新的想法就誕生了。

Make abusing token privilege great again !

預設情況下,一般權限的使用者會像這張圖一樣,僅有少數的 Privileges

不過我們可以注意到的是,大部分情況下都會有 SeChangeNotifyPrivilege 且是 Enable 的。這時我們就可以來看看初始化的地方,就可發現 SeChangeNotifyPrivilege 所代表是數值為 0x17。

那如果我們利用漏洞把 SeDebugPrivilege 從 0x14 換成 0x17 會發生甚麼事情呢?

如上圖,在原先 OpenProcess 的流程中,依舊會先去看 nt!SeDebugPrivilege中的數值,而這時獲得的數值為 0x17(SeChangeNotifyPrivilege)

接下來的檢查就會以 0x17 對當前 Process token 做驗證,看看有沒有這個 Privilege,然而一般使用者都會有這個 Privilege,因此即使你沒有 SeDebugPrivilege 也會直接通過檢查,拿到 PROCESS_ALL_ACCESS也就是說任何擁有 SeChangeNotifyPrivilege 都可以 open 除了 PPL 之外的高權限的 Process

此外利用我們上述的漏洞來將 nt!SeDebugPrivilege從 0x14 改成 0x17,因為原本的數值不是 0 是不會受到 KsQueueWorkItem 影響的,因此非常適合我們。

在可以 open 高權限的 Process 後,提權方式就與一般的 Abuse token privilege 方法相同就不再這邊多提了,最終我們又在一次利用 Proxying to kernel 成功在 Windows 11 23H2 上達成 EoP。

Remark

實際上來說,這個方法也適用於其他高權限的 Privilege 中

  • SeTcbPrivilege = 0x7
  • SeTakeOwnershipPrivilege = 0x9
  • SeLoadDriverPrivilege = 0xa

The Next & Summary

這兩篇文章中,主要著重於我們怎麼從過往的漏洞分析到發現新漏洞的過程,如何從過去的研究之中獲得新的想法、新的利用方式,新的漏洞以及新的攻擊面。關於這種 Proxy 類型的 Bug class 可能還存在很多,也可能不只侷限於 Kernel Streaming 和 IoBuildDeviceIoControlRequest,我認為算是 Windows 設計上的一個小缺陷,如果認真找可能還會找到一些漏洞,這類型的漏洞你需要關注的地方就是 Irp->RequestorMode設置的時間點,如果設置 KernelMode 之後還有拿使用者的輸入做事情,就有機會出問題,而且這類型的漏洞往往都很好用。

在 Kernel Streaming 中,我認為應該不少潛在的安全性漏洞,他也還有很多元件像是 Hdaudio.sys或是 Usbvideo.sys可能也是個可以看的方向,也是個適合 fuzzing 的地方。如果你是個 Kernel driver 開發者最好不要只有檢查 Irp->Requestormode,Windows 架構下很有可能還是有問題。最後再次強烈建議大家盡速更新 Windows 到最新版本中。

Is that the end of it ?

實際上來說除了 Proxy 類型的漏洞之外,我們還有找到其他更多的 Bug class 使得我們在 Kernel Streaming 上找到超過 20 個漏洞,有些漏洞非常特別,敬請期待 Part III。

Reference


Viewing all articles
Browse latest Browse all 145

Trending Articles