Created
October 29, 2025 13:21
-
-
Save wdormann/885e1ffd53c04509057b0dc74ded8999 to your computer and use it in GitHub Desktop.
ROP tools don't do great on finding gadgets that end in a retpoline. This finds such gadgets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| find_retpoline_gadgets_auto.py | |
| Find "retpoline-style" gadgets in an uncompressed vmlinux by looking for | |
| call/jmp rel32 instructions that target retpoline thunks (e.g. | |
| __x86_indirect_thunk_rax, __x86_return_thunk). The script will attempt to | |
| auto-compute the KASLR slide using /proc/kallsyms and the symbols inside the | |
| vmlinux ELF. You can also pass an explicit --slide to override. | |
| Requirements: | |
| pip install capstone pyelftools | |
| Usage examples: | |
| python3 find_retpoline_gadgets_auto.py ./vmlinux --names __x86_indirect_thunk_rax __x86_return_thunk --window 24 --verbose | |
| python3 find_retpoline_gadgets_auto.py ./vmlinux --names __x86_indirect_thunk_rax --slide 0x55f000000 | |
| """ | |
| import sys | |
| import struct | |
| import argparse | |
| import subprocess | |
| import os | |
| from capstone import Cs, CS_ARCH_X86, CS_MODE_64 | |
| from elftools.elf.elffile import ELFFile | |
| def parse_args(): | |
| p = argparse.ArgumentParser(description="Find retpoline-style gadgets: call/jmp -> thunk (auto-slide)") | |
| p.add_argument("elf", help="uncompressed vmlinux ELF file") | |
| p.add_argument("--names", nargs='+', required=True, | |
| help="retpoline symbol names to match (e.g. __x86_indirect_thunk_rax)") | |
| p.add_argument("--window", type=int, default=20, help="bytes before terminator to show") | |
| p.add_argument("--slide", help="explicit slide (hex). If provided, overrides auto-detection.") | |
| p.add_argument("--verbose", action='store_true') | |
| return p.parse_args() | |
| def read_elf_symbols(path, names): | |
| syms = {} | |
| with open(path,'rb') as f: | |
| elf = ELFFile(f) | |
| for sec in elf.iter_sections(): | |
| if not hasattr(sec,'iter_symbols'): | |
| continue | |
| for s in sec.iter_symbols(): | |
| if not s.name: | |
| continue | |
| if s.name in names: | |
| syms[s.name] = s['st_value'] | |
| return syms | |
| def get_live_addrs_from_kallsyms(names): | |
| live = {} | |
| # try with sudo (more likely to succeed if kernel.kptr_restrict > 0) | |
| try: | |
| out = subprocess.check_output(['sudo','cat','/proc/kallsyms'], stderr=subprocess.DEVNULL).decode() | |
| except Exception: | |
| try: | |
| with open('/proc/kallsyms','r') as fh: | |
| out = fh.read() | |
| except Exception: | |
| return live | |
| for line in out.splitlines(): | |
| parts = line.split() | |
| if len(parts) >= 3: | |
| addr_s, t, name = parts[0], parts[1], parts[2] | |
| if name in names: | |
| try: | |
| live[name] = int(addr_s, 16) | |
| except Exception: | |
| pass | |
| return live | |
| def build_pt_loads(path): | |
| segs = [] | |
| with open(path,'rb') as f: | |
| elf = ELFFile(f) | |
| for seg in elf.iter_segments(): | |
| if seg['p_type'] == 'PT_LOAD': | |
| segs.append((seg['p_vaddr'], seg['p_offset'], seg.data())) | |
| return segs | |
| def read_bytes(segs, va, n): | |
| for vaddr, off, data in segs: | |
| if va >= vaddr and va < vaddr + len(data): | |
| idx = va - vaddr | |
| return data[idx:idx+n] | |
| return None | |
| def compute_slide(sym_file_map, live_map): | |
| counts = {} | |
| for name, file_va in sym_file_map.items(): | |
| if name in live_map: | |
| slide = live_map[name] - file_va | |
| counts[slide] = counts.get(slide, 0) + 1 | |
| if not counts: | |
| return None | |
| best = max(counts.items(), key=lambda kv: kv[1])[0] | |
| return best | |
| def scan_for_terminators(segs, thunks_live_set, slide, window, verbose): | |
| md = Cs(CS_ARCH_X86, CS_MODE_64) | |
| found = [] | |
| for vaddr, off, data in segs: | |
| L = len(data) | |
| i = 0 | |
| while i < L - 4: | |
| b = data[i] | |
| matched = False | |
| target_file = None | |
| instr_va_file = vaddr + i | |
| # Rel32 call/jmp | |
| if b in (0xE8, 0xE9) and i+4 < L: | |
| rel = struct.unpack_from('<i', data, i+1)[0] | |
| target_file = instr_va_file + 5 + rel | |
| matched = True | |
| # (Could extend to handle other encodings) | |
| if matched: | |
| target_live = (target_file + slide) if slide is not None else target_file | |
| # Compare both the live-set and file-side (in case user passed file addresses) | |
| if (target_live in thunks_live_set) or (target_file in thunks_live_set): | |
| start = max(vaddr, instr_va_file - window) | |
| nb = (instr_va_file - start) + 8 | |
| chunk = read_bytes(segs, start, nb) | |
| if chunk is None: | |
| i += 1 | |
| continue | |
| gadget_lines = [] | |
| for ins in md.disasm(chunk, start): | |
| gadget_lines.append((ins.address, ins.mnemonic, ins.op_str)) | |
| if ins.address == instr_va_file: | |
| break | |
| found.append((instr_va_file, target_file, target_live, gadget_lines)) | |
| i += 1 | |
| return found | |
| def main(): | |
| args = parse_args() | |
| if not os.path.exists(args.elf): | |
| print("ELF not found:", args.elf) | |
| sys.exit(2) | |
| names = args.names | |
| if args.verbose: | |
| print("[*] looking for thunk names:", names) | |
| # file-side syms | |
| sym_file = read_elf_symbols(args.elf, names) | |
| if args.verbose: | |
| print("[*] file-side thunk symbols found:", sym_file) | |
| # live syms from /proc/kallsyms (if available) | |
| live = get_live_addrs_from_kallsyms(names) | |
| if args.verbose: | |
| print("[*] live thunk symbols from /proc/kallsyms:", live) | |
| # slide handling | |
| slide = None | |
| if args.slide: | |
| try: | |
| slide = int(args.slide, 16) | |
| if args.verbose: | |
| print("[*] using explicit slide 0x%x" % slide) | |
| except Exception: | |
| print("Bad --slide value"); sys.exit(2) | |
| else: | |
| slide = compute_slide(sym_file, live) | |
| if args.verbose: | |
| print("[*] computed slide:", None if slide is None else hex(slide)) | |
| # prepare final thunk live set: | |
| thunks_live_set = set() | |
| if live: | |
| thunks_live_set.update(live.values()) | |
| # if slide present and we have file symbols, add file->live mapping | |
| if sym_file and slide is not None: | |
| for v in sym_file.values(): | |
| thunks_live_set.add(v + slide) | |
| # also accept if user passed literal hex strings in --names (unlikely but possible) | |
| for n in names: | |
| if n.startswith("0x"): | |
| try: | |
| thunks_live_set.add(int(n,16)) | |
| except: | |
| pass | |
| if args.verbose: | |
| print("[*] final thunk live-set has %d entries" % len(thunks_live_set)) | |
| # build PT_LOADs and scan | |
| segs = build_pt_loads(args.elf) | |
| found = scan_for_terminators(segs, thunks_live_set, slide, args.window, args.verbose) | |
| if not found: | |
| print("No retpoline-style terminators found. Try: (A) run with --slide computed from readelf/kallsyms, (B) use a distro 'vmlinux' with symbols, or (C) disassemble live with gdb /proc/kcore on the live VA.") | |
| return | |
| for instr_va_file, target_file, target_live, gadget_lines in found: | |
| print("== gadget-ending instr file-VA 0x{0:x} live-target 0x{1:x} ==".format(instr_va_file, target_live if target_live else 0)) | |
| for a,mn,op in gadget_lines: | |
| print("0x{0:x}:\t{1}\t{2}".format(a,mn,op)) | |
| print() | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment