⬢ DragonFlyBSD Kernel Audit
DF-2362 / rsp.py
← back to finding ↓ download raw
#!/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 &registers 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 &registers 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