win32kfull.sys xxxGetDummyDib hand-rolled DIB size overflow
xxxGetDummyDib is the clipboard’s CF_BITMAP-to-CF_DIB synthesiser. When a
consumer asks for CF_DIB (format 8) and only CF_BITMAP (format 2) is on
the clipboard, this helper realises the bitmap through xxxBMPtoDIB and
hands the resulting BITMAPINFOHEADER to _ConvertMemHandle so it can be
registered as a dummy DIB. The byte length passed to _ConvertMemHandle
drove the kernel allocation, and that length came from an expression the
helper rolled itself in the caller instead of asking the converter that had
already done the same arithmetic correctly:
// xxxGetDummyDib — vulnerable build, CF_BITMAP -> CF_DIB synthesis
HBITMAP hbmp = xxxGetClipboardData(pwinsta, CF_BITMAP /* 2 */, pgcbd);
if (hbmp != NULL) {
/* NULL third arg: "convert only, do not hand the validated size back to me" */
BITMAPINFOHEADER *pih = xxxBMPtoDIB(hbmp, hpal, NULL);
if (pih != NULL) {
ULONG colourTable = SizeOfDibColorTable(pih);
/* |biHeight|: top-down DIBs store the height negative */
LONG absHeight = -pih->biHeight; /* +0x8 */
if (absHeight < 0)
absHeight = pih->biHeight;
/* hand-rolled DIB byte length; every term attacker-controlled, all 32-bit: */
/* |biHeight| * (((biBitCount * biWidth + 31) >> 3) & ~3) + colourTable + biSize */
ULONG size = absHeight *
(((ULONG)pih->biBitCount * pih->biWidth + 31) >> 3 & ~3) /* +0xe * +0x4 */
+ colourTable + pih->biSize; /* +0x0 */
HANDLE hmem = _ConvertMemHandle(pih, size); /* size drives the kernel allocation */
Win32FreePool(pih);
if (hmem != NULL) {
tagCLIP *pc = FindClipFormat(pwinsta, CF_DIB /* 8 */, 1);
UT_FreeCBFormat(pc);
pc->hData = hmem; /* +0x8 */
pgcbd->Format = CF_DIB; /* +0x0 */
}
return hmem;
}
}
That single-line expression mixes multiplication, shift, mask and addition
in 32-bit signed arithmetic, and it overflows in several of the input
combinations a clipboard attacker controls. The absHeight * stride
product alone can collapse a large width × bpp × height triple down to a
small positive value; the trailing + colourTable + biSize puts the
result back in range as a plausible-but-tiny byte count. size then goes
straight into _ConvertMemHandle as the allocation length, the bitmap
copy that fills the new handle writes the real, unwrapped byte count, and
the result is the same session-pool overflow the sibling converter has
already been hit for — just reached through a different size expression.
The patch
The interesting part is what the fix does not do. It does not patch the
expression. It deletes it. xxxBMPtoDIB had been computing the safe size
all along — for its own internal allocation it walks the header, multiplies
width × bpp × height in 64-bit, validates that the result still fits in
32-bit, and stores it through a third argument that the vulnerable caller
passed as NULL. The fix simply stops passing NULL:
// xxxGetDummyDib — patched, CF_BITMAP -> CF_DIB synthesis
HBITMAP hbmp = xxxGetClipboardData(pwinsta, CF_BITMAP /* 2 */, pgcbd);
if (hbmp != NULL) {
ULONG dibSize = 0;
/* xxxBMPtoDIB now hands back the byte length it computed for its own
allocation (64-bit multiply, 32-bit fit check). The caller no longer
recomputes the size at all. */
BITMAPINFOHEADER *pih = xxxBMPtoDIB(hbmp, hpal, &dibSize);
if (pih == NULL)
return NULL;
/* new zero-length guard: refuse when the safe size collapses to 0,
which is exactly the signature of the old overflow wrapping to 0 */
if (dibSize == 0) {
Win32FreePool(pih);
return NULL;
}
HANDLE hmem = _ConvertMemHandle(pih, dibSize); /* straight from the converter */
Win32FreePool(pih);
if (hmem != NULL) {
tagCLIP *pc = FindClipFormat(pwinsta, CF_DIB /* 8 */, 1);
UT_FreeCBFormat(pc);
pc->hData = hmem; /* +0x8 */
pgcbd->Format = CF_DIB; /* +0x0 */
}
return hmem;
}
No bespoke DIB arithmetic remains in this helper. A legacy fallback that
calls a freshly-factored SizeOfDib helper sits behind the build’s
compat toggle so the build can still run with the new code disabled,
but on the patched path the caller simply defers to the function that
already knew the layout. The two additions are otherwise minimal: take the
out-param, and refuse on a zero result.
Attack path
flowchart TD
A["attacker puts a hostile CF_BITMAP on the clipboard: wide, tall, high bpp"] --> B["xxxGetDummyDib synthesises the missing CF_DIB via xxxBMPtoDIB"]
B --> C["caller-side size: abs(biHeight) times stride plus colourTable plus biSize overflows in 32-bit"]
C --> D["_ConvertMemHandle allocates a too-small kernel buffer for the DIB"]
D --> E["the bitmap copy writes the real, unwrapped byte count, overruns the allocation, session-pool overflow to EoP"]
The shape to remember is broader than this one helper. Across the
clipboard render path the patch replaced several different hand-rolled
size expressions with the converter that already knew the layout — this
function, xxxBMPtoDIB’s own internal arithmetic, and the sibling
xxxGetDummyPalette next door — because each bespoke expression was a
fresh overflow surface. A helper that recomputes a structure’s size by
hand from header fields, instead of asking the function that just
allocated it, is the grep target; the converter already had the answer.