Palette / Bitmap Objects
For nearly a decade, from roughly Windows 7 through Windows 10 RS3 (1709), GDI bitmap and palette objects were the dominant kernel read/write primitive on Windows. The technique exploited a fundamental property of the Windows graphics subsystem: GDI objects like bitmaps and palettes were allocated in a user-accessible paged pool (the session pool), and their internal data pointers could be directly dereferenced by kernel-mode GDI functions to read and write pixel or palette data. Corrupting the pvScan0 pointer of a SURFOBJ (the kernel structure backing a GDI bitmap) turned every subsequent GetBitmapBits/SetBitmapBits call into an arbitrary kernel read or write.
The technique was elegant in its simplicity. The attacker created two GDI bitmaps. Through a pool overflow, UAF, or write-what-where, they corrupted the pvScan0 pointer of the first bitmap (the "manager") to point at the SURFOBJ header of the second bitmap (the "worker"). Now, SetBitmapBits on the manager bitmap could overwrite the worker's pvScan0 pointer to any arbitrary address. Then GetBitmapBits/SetBitmapBits on the worker bitmap would read from or write to that arbitrary address. Two bitmaps, two corruptions, unlimited kernel R/W.
Why this technique is historical
Microsoft incrementally mitigated GDI object exploitation across several Windows 10 releases, culminating in effective neutralization by RS4 (1803).
The first blow came in RS1 (Anniversary Update, build 14393), when Microsoft moved GDI object structures out of the user-accessible GdiSharedHandleTable. Previously, user-mode code could look up the kernel address of any GDI object through a table mapped into every process, making the information leak step trivial. After RS1, finding a GDI object's kernel address required a separate information leak.
RS2 (Creators Update, build 15063) introduced additional randomization to the session pool layout and began isolating certain GDI allocations. RS3 (Fall Creators Update, build 16299) moved GDI objects to kernel-only pool regions, removing user-mode accessibility to the SURFOBJ and palette structures entirely. This was the decisive mitigation: without the ability to read the corrupted pvScan0 pointer or access the GDI object's kernel data from user mode, the manager/worker bitmap technique stopped working.
Relevance today
On modern Windows (RS4 and later), palette/bitmap exploitation is not viable. The technique is documented here for three reasons.
First, historical context. Understanding how GDI exploitation worked illuminates why newer techniques like WNF state data, pipe attributes, and I/O Ring exist. They were developed specifically to replace the capabilities that GDI object exploitation provided. The evolution from bitmaps to WNF to I/O Ring traces the cat-and-mouse between Microsoft's mitigations and exploit developers' adaptation.
Second, legacy targets. IoT devices, embedded systems, and some industrial control systems still run Windows versions where GDI exploitation works. Windows Embedded 8.1, Windows 10 LTSB 2016, and similar long-lifecycle deployments remain in service and are vulnerable to this technique.
Third, the manager/worker pattern itself. The concept of using two corrupted objects, one to control the other's data pointer, producing stable arbitrary R/W, is a fundamental exploitation pattern that recurs in different forms across many platforms. Understanding it through GDI bitmaps provides intuition for recognizing similar patterns in other kernel subsystems.
The manager/worker pattern
The pattern works as follows. The attacker creates two bitmaps of known size, causing two SURFOBJ allocations in the session pool. Through pool spray, they position these bitmaps adjacent to a vulnerable allocation. After triggering a corruption that modifies the first bitmap's pvScan0 pointer, the attacker sets it to point at the SURFOBJ header of the second bitmap.
Calling SetBitmapBits on the first bitmap writes data to the address stored in its pvScan0, which now points at the second bitmap's SURFOBJ. The written data overwrites the second bitmap's pvScan0 with an arbitrary kernel address. Calling GetBitmapBits on the second bitmap reads from its (now-controlled) pvScan0, returning data from the target kernel address. Calling SetBitmapBits on the second bitmap writes to the target address.
This two-level indirection provides unlimited R/W: the attacker repeatedly updates the worker's pvScan0 through the manager, then reads or writes through the worker, targeting any kernel address in sequence. The same pattern has been reinvented for I/O Ring exploitation, where the "manager" is the initial corruption and the "worker" is the registered buffer table entry.
See Also
- I/O Ring -- the modern replacement for GDI object exploitation on Windows 11
- WNF State Data -- the intermediate replacement that filled the gap between GDI mitigation and I/O Ring
- Pool Spray / Heap Feng Shui -- the pool grooming needed to position GDI objects for corruption
- Primitive Matrix -- tracks the availability window for palette/bitmap exploitation across Windows versions