Skip to content

Instantly share code, notes, and snippets.

@wdormann
Created October 29, 2025 13:21
Show Gist options
  • Save wdormann/885e1ffd53c04509057b0dc74ded8999 to your computer and use it in GitHub Desktop.
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.
#!/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