⬢ DragonFlyBSD Kernel Audit
DF-3017 / hunt.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""Hunt the racing cdev X at the DDB panic via QEMU gdbstub (port 1235).
Finds the panicking cpu (RIP inside panic/Debugger), recovers the
reference_dev/v_associate_rdev frame from RBP chain, extracts the cdev
pointer (v_associate_rdev's dev arg / devfs_allocv frame), then
classifies X: zone ownership, freelist state, content (PUTAWAY/image)."""
import socket, sys, time

_sock = None
def rsp(cmd):
    global _sock
    if _sock is None:
        _sock = socket.create_connection(("127.0.0.1", 1235), timeout=10)
        _sock.settimeout(3.0)
        _sock.sendall(b"$qSupported#37")
        t0 = time.time()
        while time.time() - t0 < 3:
            try:
                d = _sock.recv(65536)
                if b"#" in d:
                    break
            except socket.timeout:
                break
        _sock.sendall(b"+")
    pkt = b"$" + cmd + b"#%02x" % (sum(cmd) & 0xFF)
    _sock.sendall(pkt)
    buf = b""
    t0 = time.time()
    while time.time() - t0 < 4:
        try:
            d = _sock.recv(65536)
        except socket.timeout:
            break
        if not d:
            raise RuntimeError("no reply")
        buf += d
        if b"#" in buf[1:]:
            try: _sock.sendall(b"+")
            except Exception: pass
            i = buf.index(b"$")
            j = buf.index(b"#", i + 1)
            return buf[i+1:j]
    raise RuntimeError("bad reply %r" % buf[:64])

def rd(addr, n):
    body = rsp(b"m%x,%x" % (addr, n))
    return bytes.fromhex(body.decode())

def rq(a):
    return int.from_bytes(rd(a, 8), "little")

def regs(cpu):
    rsp(b"Hg%x" % (cpu + 1))
    body = rsp(b"g")
    names = ["rax","rbx","rcx","rdx","rsi","rdi","rbp","rsp","r8","r9",
             "r10","r11","r12","r13","r14","r15","rip"]
    out = {}
    for i, n in enumerate(names):
        off = i * 16
        if off + 16 <= len(body):
            out[n] = int.from_bytes(bytes.fromhex(body[off:off+16].decode()), "little")
    return out

ZI320 = 25
DEAD_OPS = 0xffffffff81187fe0

print("[*] scanning cpus for the panicking one")
pend = None
for c in range(6):
    try:
        r = regs(c)
    except Exception as e:
        print(" cpu%d regs err %s" % (c, e)); continue
    print(" cpu%d rip=%x rbp=%x rsp=%x rsi=%x" % (c, r["rip"], r["rbp"], r["rsp"], r["rsi"]))
    if r["rip"] != 0xffffffff80bd2022:  # not the common stopped-spin
        pend = (c, r)
if not pend:
    print("[-] no cpu in panic(); tryDebugger range")
    sys.exit(1)

c, r = pend
print("[+] panicking cpu=%d" % c)
# walk frames: rbp chain from rbp
fp = r["rbp"]
frames = []
for i in range(12):
    try:
        ret = rq(fp + 8)
        nxt = rq(fp)
    except Exception:
        break
    frames.append((fp, ret))
    if nxt == 0 or not (0xfffff80000000000 <= nxt < 0xfffff81000000000):
        break
    fp = nxt
for f, ret in frames:
    print("  frame %x ret=%x" % (f, ret))

# v_associate_rdev(dev) is called with dev in rsi; also devfs_allocv keeps
# node in a register.  Hunt cdevs near all frames (forensics2 recipe).
cands = set()
for f, ret in frames:
    try:
        stack = rd(f - 0x400, 0x800)
    except Exception:
        continue
    for off in range(0, len(stack), 8):
        v = int.from_bytes(stack[off:off+8], "little")
        if 0xfffff80000000000 <= v < 0xfffff81000000000:
            cands.add(v & ~7)
print("[*] %d base candidates" % len(cands))

def classify(q):
    zbase = q & ~0x1ffff  # 128KB zone alignment guess
    out = {}
    try:
        magic = int.from_bytes(rd(q + 0, 4), "little") # si_flags at +0? struct starts si_flags
        ops = rq(q + 168)
        name = rd(q + 88, 16)
        ref = rq(q + 232)
        out = dict(ops=ops, name=name, refcnt=ref & 0xffffffff,
                   flags=(ref >> 32) & 0xffffffff)
    except Exception as e:
        out = dict(err=str(e))
    return out

hits = []
for p in sorted(cands):
    for d in range(-0x140, 0x148, 8):
        q = p + d
        try:
            ops = rq(q + 168)
        except Exception:
            continue
        if ops in (DEAD_OPS, 0x40000000, 0x0000000040000000) or \
           (0xffffffff81000000 <= ops < 0xffffffff82000000):
            cl = classify(q)
            print("[!] cdev? %x ops=%x %s" % (q, ops, cl))
            hits.append((q, cl))
if not hits:
    print("[-] none; widen search")