#!/usr/bin/env python3 """Find likely x64 RIP-relative references to known string RVAs in a PE file.""" from __future__ import annotations import argparse import dataclasses import struct from pathlib import Path @dataclasses.dataclass(frozen=True) class Section: name: str virtual_address: int virtual_size: int raw_offset: int raw_size: int characteristics: int @property def is_code(self) -> bool: image_scn_cnt_code = 0x00000020 image_scn_mem_execute = 0x20000000 return bool(self.characteristics & image_scn_cnt_code) and bool(self.characteristics & image_scn_mem_execute) def contains_rva(self, rva: int) -> bool: size = max(self.virtual_size, self.raw_size) return self.virtual_address <= rva < self.virtual_address + size def rva_to_offset(self, rva: int) -> int: return self.raw_offset + (rva - self.virtual_address) def parse_pe(data: bytes) -> tuple[int, list[Section]]: if data[:2] != b"MZ": raise ValueError("not an MZ executable") pe_offset = struct.unpack_from(" Section | None: for section in sections: if section.contains_rva(rva): return section return None def iter_rip_xrefs(data: bytes, section: Section, image_base: int, target_vas: set[int]) -> dict[int, list[int]]: start = section.raw_offset end = min(len(data), section.raw_offset + section.raw_size) hits: dict[int, list[int]] = {target_va: [] for target_va in target_vas} # On x64, most string references are encoded as a signed 32-bit displacement # relative to the address immediately after the displacement. for offset in range(start, max(start, end - 4)): disp = struct.unpack_from(" int: parser = argparse.ArgumentParser() parser.add_argument("pe", type=Path) parser.add_argument("targets", nargs="+", help="name=rva_hex pairs") args = parser.parse_args() data = args.pe.read_bytes() image_base, sections = parse_pe(data) code_sections = [section for section in sections if section.is_code] targets: list[tuple[str, int, int]] = [] target_vas: set[int] = set() for item in args.targets: name, _, rva_text = item.partition("=") if not name or not rva_text: raise ValueError(f"target must be name=rva_hex: {item}") target_rva = int(rva_text, 16) target_va = image_base + target_rva targets.append((name, target_rva, target_va)) target_vas.add(target_va) xrefs: dict[int, list[int]] = {target_va: [] for target_va in target_vas} for section in code_sections: section_hits = iter_rip_xrefs(data, section, image_base, target_vas) for target_va, hits in section_hits.items(): xrefs[target_va].extend(hits) print(f"image_base=0x{image_base:x}") for name, target_rva, target_va in targets: target_section = section_for_rva(sections, target_rva) section_name = target_section.name if target_section else "" print(f"{name}: target_rva=0x{target_rva:x} target_section={section_name}") all_hits = xrefs[target_va] if not all_hits: print(" no code xrefs found") continue for hit in all_hits[:32]: print(f" code_xref_disp_rva=0x{hit:x} probable_instr_rva=0x{max(0, hit - 7):x}..0x{hit:x}") if len(all_hits) > 32: print(f" ... {len(all_hits) - 32} more") return 0 if __name__ == "__main__": raise SystemExit(main())