DF-2361 / rsp.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 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | #!/usr/bin/env python3 # rsp.py -- minimal GDB remote-serial-protocol peer for talking to the # DragonFlyBSD kernel gdb stub (sys/cpu/x86_64/misc/x86_64-gdbstub.c) # over the QEMU guest's com0, which is wired to a unix-socket chardev. # # Evidence rig for DF-2361 / DF-2362 verification. import socket, sys, time, re def cks(data: bytes) -> str: return "%02x" % (sum(data) & 0xff) class StubDead(Exception): pass class SerialPeer: def __init__(self, sockpath, rawlog=None, verbose=True, name="peer"): self.name = name self.verbose = verbose self.s = socket.socket(socket.AF_UNIX) last = None for i in range(300): try: self.s.connect(sockpath) break except Exception as e: last = e time.sleep(0.1) else: raise SystemExit("cannot connect to %s: %s" % (sockpath, last)) self.s.settimeout(0.05) self.buf = bytearray() self.raw = open(rawlog, "ab", 0) if rawlog else None self.alive = True self.rx_count = 0 def close(self): try: self.s.close() except Exception: pass if self.raw: self.raw.close() self.alive = False def pump(self, quiet=False): try: d = self.s.recv(65536) except socket.timeout: return except Exception: self.alive = False return if not d: self.alive = False return self.buf.extend(d) self.rx_count += len(d) if self.raw: self.raw.write(b"<< " + d) if self.verbose and not quiet: sys.stdout.write("<< " + d.decode(errors="replace") .replace("\r", "\\r").replace("\n", "\\n\n")) sys.stdout.flush() def send(self, data: bytes): if not self.alive: raise StubDead("peer gone") self.s.sendall(data) if self.raw: self.raw.write(b">> " + data) if self.verbose: sys.stdout.write(">> " + data.decode(errors="replace") .replace("\r", "\\r").replace("\n", "\\n\n")) sys.stdout.flush() def wait(self, pattern, timeout): rx = re.compile(pattern.encode() if isinstance(pattern, str) else pattern, re.DOTALL) deadline = time.time() + timeout while time.time() < deadline: m = rx.search(bytes(self.buf)) if m: return m if not self.alive: return None self.pump() return None def pkt(self, data: str, timeout=20.0, expect_reply=True): """One RSP round trip. Returns reply payload str, or None on timeout.""" payload = data.encode() assert len(payload) <= 398, "packet too long for stub getpacket(): %d" % len(payload) assert payload[2:3] != b":", "3rd char ':' triggers sequence-id path" frame = b"$" + payload + b"#" + cks(payload).encode() for attempt in range(4): self.buf.clear() self.send(frame) m = self.wait(r"[+\-]", timeout) if m is None: if not self.alive: raise StubDead("peer gone waiting ack for %r" % data) continue # timeout: resend if m.group(0) == b"-": continue # nack: resend break # '+' else: return None if not expect_reply: return "" m = self.wait(r"\$[^#]*#[0-9a-fA-F]{2}", timeout) if m is None: return None reply = m.group(0)[1:-3].decode(errors="replace") self.send(b"+") # ack stub's reply return reply def read_mem(self, addr, length, chunk=192): """Read kernel memory via bounded m packets. Returns bytes or None.""" assert 0 < length and length <= chunk * 64 out = b"" while len(out) < length: n = min(chunk, length - len(out)) r = self.pkt("m%x,%x" % (addr + len(out), n)) if r is None or r.startswith("E"): return None out += bytes.fromhex(r) return out def write_mem(self, addr, data: bytes): """Write via M packets, 128 bytes max per packet (protocol limit).""" off = 0 while off < len(data): n = min(128, len(data) - off) r = self.pkt("M%x,%x:%s" % (addr + off, n, data[off:off+n].hex())) if r is None or not r.startswith("OK"): return False off += n return True def find_registers(stub, t05): """Locate ®isters on the stopped kernel stack. The T05 packet embeds reg16(rip)@off128, reg6(rbp)@off48, reg7(rsp)@off56 of `struct x86_64regs registers` (see gdbstub lines 507-529). We scan the stack below/around tf_rsp for a 152-byte window whose rip/rsp slots match the T05 values, then confirm with a second marker (rbp). Returns the VA of ®isters or None. """ rip = int(t05["10"], 16) rbp = int(t05["06"], 16) rsp = int(t05["07"], 16) # search a window of stack around the trap rsp base = (rsp - 0x800) & ~0xf blob = stub.read_mem(base, 0x900) if blob is None: return None, None for off in range(0, len(blob) - 152, 8): A = base + off try: 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: return A, blob[off:off+152] except Exception: pass return None, None def parse_t05(reply): """Parse the stub's T05 announcement: T05 10:<rip>;06:<rbp>;07:<rsp>;""" out = {} body = reply if body.startswith("T") or body.startswith("S"): body = body[3:] # drop "T05" sig prefix for field in body.split(";"): if not field or ":" not in field: continue k, v = field.split(":", 1) out[k] = v return out |