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.