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.