bfs.sys BfsInsertDirectoryEntry name length overrun
bfs.sys is the Boot File System driver, reached whenever the OS needs to
read or modify the blob-based boot configuration store — boot and servicing
tooling, image deployment, virtual-machine setup, recovery media preparation.
Anything that lets a user point the driver at a store they crafted is an
entry point, and every on-disk structure the driver parses is
attacker-controlled at that point.
A directory entry in bfs.sys lives in a fixed-size slot of 0x214
bytes inside a directory page; the per-page bitmap that
RtlFindClearBits walks to find a free slot manages thirty of them.
The slot’s variable-length name lives at slot->Name /* +0x1c */. The
vulnerable build reserved the slot, zeroed its body, and memcpy’d the
caller-supplied NameDesc->Length straight into slot->Name, with
no upper bound on the length:
// BfsInsertDirectoryEntry - vulnerable build
// The slot is reserved in the bitmap, but the name is taken straight
// from the caller's descriptor with no width check, so the bound is
// NameDesc->Length, not the destination width.
memset(&slot->Type, 0, 0x210); /* +0x0c - zero the entry body */
slot->Type = Type; /* +0x0c */
slot->Parent = Parent; /* +0x08 */
memcpy(slot->Name, NameDesc->Buffer, NameDesc->Length); /* +0x1c - caller length, NO BOUND */
slot->Flags = Flags; /* +0x10 */
NameDesc->Length is the length field out of the caller’s directory-entry
descriptor, which on the parse path is the on-disk store, so a name longer
than the slot’s name field overruns it, runs into the next directory entry in
the page, and ultimately into pool metadata. The destination width was never
the authority; the caller’s claimed length was. This is the classic
memcpy(dst, src, attacker_len) into a fixed-size destination, the kind of
pool overflow that still ships because the length looks constrained by an
outer layer that does not actually enforce it.
The patch
The patch gates the whole insert on the name actually fitting the slot’s name field:
// BfsInsertDirectoryEntry - patched
// The name must fit, or the insert is rejected before a single byte
// is written into the slot.
if (NameDesc->Length <= 0x1fe) {
memset(&slot->Type, 0, 0x210); /* +0x0c - zero the entry body */
slot->Type = Type; /* +0x0c */
slot->Parent = Parent; /* +0x08 */
memcpy(slot->Name, NameDesc->Buffer, NameDesc->Length); /* +0x1c - now bounded */
slot->Flags = Flags; /* +0x10 */
/* ...allocate the entry's data block and write it out... */
} else {
Status = 0xC0000106; /* name too long for the slot */
EtwWriteTransfer(...);
RtlClearBits(&BitMapHeader, SlotIndex, 1); /* release the reserved slot */
return NULL;
}
The destination size is the authority now, not the caller’s length. The check sits right after the slot is reserved in the bitmap and before any byte is written into it; on rejection the reservation is cleared and the function returns NULL.
Attack path
flowchart TD
A["crafted bfs store with a long-named directory entry"] --> B["BfsInsertDirectoryEntry"]
B --> C["no length gate, slot reserved, body zeroed"]
C --> D["memcpy NameDesc.Length bytes into slot.Name at +0x1c"]
D --> E["length over 0x1fe overruns the slot into the next entry and pool metadata"]
memcpy(dst, src, attacker_len) into a fixed-size destination needs an
explicit upper bound, and the destination size has to be the authority — the
caller’s claimed length is only acceptable after it has been checked against
that authority. The smell is the same everywhere this pattern appears: a
length field that appears to be constrained by an outer layer, used directly
as the copy bound without the destination-width check that has to back it.