win32kfull.sys xxxBMPtoDIB width bpp integer overflow
A process that puts a bitmap on the clipboard can hand it over as CF_DIB
or CF_DIBV5, raw bytes led by a BITMAPINFOHEADER. Paste, a clipboard
viewer, a format converter, or the UMPD dummy-bitmap path all ask win32k
to realise those bytes: validate the header, allocate a kernel buffer
sized for the pixel data plus colour table, copy. xxxBMPtoDIB is the
helper that does that size arithmetic. Every term in the size — biWidth,
biHeight, biBitCount, the colour-table entry count — comes from the
attacker-supplied header, so the helper’s job is to multiply and add those
fields without letting the result wrap.
The vulnerable build tries, but the guard only catches a full 32-bit wrap:
// xxxBMPtoDIB — vulnerable build
BITMAP bm; // GreExtGetObjectW fills this (sizeof = 0x20)
GreExtGetObjectW(hbmp, 0x20, &bm);
ULONG pp = (ULONG)bm.bmBitsPixel * bm.bmPlanes; // BITMAP.bmBitsPixel /* +0x12 */ × bmPlanes /* +0x10 */
ULONG bpp; // bucket pp down to a supported target
if (pp <= 1) bpp = 1;
else if (pp <= 4) bpp = 4;
else if (pp <= 8) bpp = 8;
else if (pp <= 0x10) bpp = 0x10; // a per-thread compat flag forces 0x18 for any pp > 8
else if (pp <= 0x18) bpp = 0x18;
else bpp = 0x20;
LONG width = bm.bmWidth; // BITMAP.bmWidth /* +0x04 */
LONG height = bm.bmHeight; // BITMAP.bmHeight /* +0x08 */
UINT64 prod = (UINT64)width * bpp; // 64-bit product
if (prod > 0xffffffff) // guard: width*bpp must fit in 32 bits
return NULL; // a product just under 2^32 PASSES
UINT32 row = ((UINT32)prod + 0x1f) >> 3 & 0x1ffffffc; // BMP row stride; 32-bit add on TRUNCATED prod
UINT64 bytes = (UINT64)row * height; // small once the +0x1f has carried
if (bytes > 0xffffffff) return NULL;
UINT32 colourTable;
if (bpp <= 8) colourTable = (1u << bpp) << 2; // 2^bpp RGBQUADs
else if (bpp == 0x10 || bpp == 0x20) colourTable = 0xc; // 3 DWORD bitfield masks, biCompression = BI_BITFIELDS
else colourTable = 0; // 24-bpp: no table
UINT32 total = colourTable + 0x28 + (UINT32)bytes; // header (0x28) + colourTable + pixels, 32-bit add
if (total < colourTable + 0x28) return NULL; // (32-bit overflow on total — never trips here)
BITMAPINFO *bi = Win32AllocPoolZInit(total, 'Usbc'); // undersized; copy uses the unwrapped dimensions
A product that sits in the 0xffffffe1..0xffffffff band passes the full-wrap
guard but carries out of bit 31 on the very next operation — the + 0x1f
inside the row-stride shift. The truncated row comes out near zero, the
row × height multiply and the colour-table addition each sail under their
own overflow checks, and bytes ends up far smaller than the pixel data the
copy will write. That is a textbook clipboard-render pool overflow: the
attacker picks header dimensions that the bespoke arithmetic turns into an
undersized allocation, and the copy that follows runs off the end into
neighbouring session pool.
The patch
The fix leaves the 64-bit product alone and instead tightens the upper bound
on its low 32 bits, anticipating the + 0x1f that the row-stride shift is
about to perform:
// xxxBMPtoDIB — patched
UINT64 wb = (UINT64)width * bpp;
if (wb <= 0xffffffff && (UINT32)wb <= 0xffffffe0) // hard-reject the +0x1f wrap zone
goto body; // body arithmetic unchanged
return NULL;
The cap is pulled down from the absolute 32-bit ceiling to 0xffffffe0 so
the subsequent + 0x1f cannot carry out of bit 31, which in turn keeps the
row × height multiply and the colour-table addition inside the range the
remaining guards already check. The principle is that an overflow guard has
to bound the value as it will be used, not only at the moment it is
computed — a check that lets through everything below 0xffffffff is a
check on the wrong operation.
Attack path
flowchart TD
A["attacker puts forged CF_DIB on the clipboard, width and bpp chosen so width times bpp lands in 0xffffffe1..0xffffffff"] --> B["consumer triggers render, xxxBMPtoDIB"]
B --> C["prod passes the full-wrap guard, the row-stride +0x1f carries out of bit 31"]
C --> D["row and bytes come out near zero, kernel allocation smaller than the copy needs"]
D --> E["copy runs off the allocation into neighbouring session pool, overflow to EoP"]
The clipboard render path is reachable from any standard user session — the attacker only needs a window that consumes the format. The overrun target is session pool, where the attacker also has allocation influence through other GDI surfaces, so the same process that plants the forged DIB can shape what neighbouring object the overrun hits. Pool overflow with attacker-shaped neighbours is the standard GDI/win32k LPE primitive, and the size helper that only catches a full 32-bit wrap is the smell to grep for across every clipboard and IPC render path: the guard has to bound the value at the operation that actually overflows, not at the multiplication that produces it.