DF-3017 / hunt.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 | #!/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") |