win32kfull.sys xxxGetDummyPalette trusted palette count OOB
xxxGetDummyPalette builds the dummy palette the clipboard render path
uses alongside the dummy DIB. The colour-table layout of a
BITMAPINFOHEADER bitmap is biClrUsed entries of RGBQUAD following
the header proper. The descriptor returned for the DIB handle inlines
the BITMAPINFOHEADER from +0x14, so biClrUsed lives at descriptor
+0x34 — and the helper reads it from there and hands it straight to
CreateDIBPalette:
/* xxxGetDummyPalette — vulnerable build. CF_DIBV5 (0x11) is tried first; if its
clip entry is present with hData already 2, the function falls through to
CF_DIB (0x8). */
ULONG fmt = CF_DIBV5; /* 0x11, tried first */
tagCLIP *clip = FindClipFormat(pwinsta, fmt, 1);
if (clip != NULL && clip->hData /* +0x8 */ == (HANDLE)2) { /* DIBV5 slot busy — try DIB */
fmt = CF_DIB; /* 0x8, fallback */
clip = FindClipFormat(pwinsta, fmt, 1);
if (clip == NULL || clip->hData /* +0x8 */ == (HANDLE)2)
return NULL; /* neither slot usable */
}
HANDLE hmem = xxxGetClipboardData(pwinsta, fmt, pgcbd);
if (hmem == NULL)
return NULL;
/* HMValidateHandleWithDescriptor returns the kernel clipboard descriptor
(tagDIBDATA) that wraps the DIB payload: cbData at +0x10 is the recorded
payload byte count, bmiHeader at +0x14 inlines the BITMAPINFOHEADER (so
biClrUsed sits at +0x34). */
tagDIBDATA *desc = HMValidateHandleWithDescriptor(hmem, TYPE_DIB /* 6 */);
if (desc == NULL)
return NULL;
tagCLIP *palclip = FindClipFormat(pwinsta, CF_PALETTE /* 9 */, 1);
if (palclip == NULL)
return NULL;
BITMAPINFOHEADER *bih = &desc->bmiHeader; /* +0x14 */
ULONG count = bih->biClrUsed; /* +0x34 — taken on trust */
HPALETTE hpal = CreateDIBPalette(bih, count); /* walks `count` RGBQUADs past bih */
if (hpal != NULL) {
UT_FreeCBFormat(palclip);
palclip->hData /* +0x8 */ = (PVOID)hpal;
GreSetPaletteOwner(hpal, 0); /* hand the palette to the kernel */
return hpal;
}
return NULL;
Nothing compares count against the descriptor’s recorded payload
size, so a forged biClrUsed that overshoots the colour-table region
sends CreateDIBPalette’s read past the descriptor allocation into
neighbouring session pool. One detail of the consumer matters for
forging the value: CreateDIBPalette takes the entry count as a
16-bit word — it reads its second argument with a word-width load
and then re-reads biClrUsed from the header the same way — so a
declared 0x40000000 truncates to zero and the walk never runs; the
forged count has to live in the low sixteen bits (0xFFFF being the
effective maximum). The leaked bytes come back as palette entries,
which the renderer can observe — the same session-pool info-leak shape
as the rest of the clipboard render path, reached by trusting a header
count instead of bounding it.
Reaching the helper has one precondition worth naming because it
decides whether the path exists at all on a given machine. The dispatch
into the dummy-render family happens when the requested format’s clip
entry carries the synthetic marker hData == 2, and a CF_PALETTE
slot with that marker is only created by the clipboard’s companion
synthesis at close time when the session runs in palette display mode —
a session-state bit that every depth-32 desktop leaves clear. A
SetClipboardData(CF_PALETTE, NULL) does not substitute: it registers
a delayed-render slot (hData == 0), which the getter routes to the
owner-render path, not to the dummy family. On a palette-mode session,
placing a forged CF_DIB or CF_DIBV5 on the clipboard and asking for
CF_PALETTE walks straight in.
The patch
The fix bounds every term against the descriptor’s recorded payload
size before CreateDIBPalette is reached. The payload must be at least
four bytes (else biSize is not even safe to read); a 0x0c
BITMAPCOREHEADER is treated as declaring zero colours, since the core
header ends before a biClrUsed field exists; any other biSize must
be a standard-or-larger header (>= 0x28) that the payload actually
contains. Only then is biClrUsed read, multiplied by
sizeof(RGBQUAD) wide, and the sum checked for both wrap and
buffer-fit:
/* xxxGetDummyPalette — patched build. Same format selection and descriptor
lookup as above; the new work is the bounds check before CreateDIBPalette. */
ULONG cbData = desc->cbData; /* +0x10 — descriptor's payload byte count */
if (cbData < 4)
return NULL; /* too small to read biSize safely */
BITMAPINFOHEADER *bih = &desc->bmiHeader; /* +0x14 */
ULONG biSize = bih->biSize; /* +0x14 — first DWORD of the header */
ULONG count;
if (biSize == 0x0c) { /* BITMAPCOREHEADER — no biClrUsed field */
count = 0;
} else if (biSize >= 0x28 && /* standard or larger header */
cbData >= 0x28 && cbData >= biSize) { /* payload actually holds the header */
count = bih->biClrUsed; /* +0x34 */
UINT64 table_bytes = (UINT64)count << 2; /* count * sizeof(RGBQUAD), done wide */
if (table_bytes > 0xffffffff)
return NULL; /* colour-table size overflows 32 bits */
UINT64 need = (UINT64)biSize + (UINT32)table_bytes; /* header plus colour table */
if (need < biSize || cbData < need)
return NULL; /* addition wrapped, or buffer too small */
} else {
return NULL; /* biSize in the unsupported 0x0d..0x27 gap */
}
HPALETTE hpal = CreateDIBPalette(bih, count); /* count is now provably in-buffer */
if (hpal != NULL) {
UT_FreeCBFormat(palclip);
palclip->hData /* +0x8 */ = (PVOID)hpal;
GreSetPaletteOwner(hpal, 0);
return hpal;
}
return NULL;
The 0x0c short-circuit exists because a BITMAPCOREHEADER ends at
byte twelve — there is no biClrUsed field at offset +0x20 to read,
so the count has to default to zero rather than be trusted. The
count << 2 is the multiplication the old code never did safely; the
need < biSize test catches the addition wrapping; the
cbData < need test is the actual freshness check that the buffer
carries what the header claims. Together they enforce the rule that a
count declared in a header is a claim about variable trailing data,
and the claim has to be bounded by the buffer that carries it before
it can be used as a loop or allocation bound.
Attack path
flowchart TD
A["attacker places a forged CF_DIB or CF_DIBV5 on the clipboard with biClrUsed oversized past the colour table"] --> B["xxxGetDummyPalette reads biClrUsed, no bound against descriptor payload size"]
B --> C["CreateDIBPalette iterates that many RGBQUADs"]
C --> D["read walks off the descriptor allocation into neighbouring session pool"]
D --> E["leaked bytes returned as palette entries, observable by the renderer, session-space info-leak"]
The wider pattern in this month’s clipboard render patch is that every
helper that took a size or count from a header field — biSize in
xxxDIBtoBMP, the hand-rolled width × bpp × height in xxxBMPtoDIB
and xxxGetDummyDib, the palette count here — had the same shape of bug
and got the same shape of fix. A sibling on the UMPD bitmap path,
UMPDOBJ::bThunkLargeBitmap, was fixed in the same patch for the
complementary mistake: it wrote the caller’s out-pointers (*arg3,
*arg4, *arg5) at the top of the function and only then ran the size
checks that could make it return 0, leaving the caller with references
into a surface that was never prepared. The fix there is the standard
“cache locally in tmp3/tmp4/tmp5, commit only on success” — out-
parameters are commitments, and the right order is to run every check
before touching them. The through-line for the whole corner is that a
header field that names a size or count is a claim about variable
trailing data; the render helper has to bound the claim against the
buffer before it acts on it, and call the central validator instead of
redoing the arithmetic.