#!/usr/bin/env python3 """Find static GPS/map route candidates in a Cyberpunk 2077 executable.""" from __future__ import annotations import argparse import dataclasses import json import re 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: return bool(self.characteristics & 0x20) and bool(self.characteristics & 0x20000000) @property def is_readable_data(self) -> bool: return bool(self.characteristics & 0x40000000) and not self.is_code def contains_rva(self, rva: int) -> bool: return self.virtual_address <= rva < self.virtual_address + max(self.virtual_size, self.raw_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(" list[tuple[int, str, str]]: results: list[tuple[int, str, str]] = [] start = section.raw_offset end = min(len(data), section.raw_offset + section.raw_size) blob = data[start:end] ascii_re = re.compile(rb"[\x20-\x7e]{%d,}" % min_len) for match in ascii_re.finditer(blob): text = match.group(0).decode("ascii", "replace") results.append((section.virtual_address + match.start(), "ascii", text)) utf16_re = re.compile((rb"(?:[\x20-\x7e]\x00){%d,}" % min_len)) for match in utf16_re.finditer(blob): raw = match.group(0) text = raw.decode("utf-16le", "replace") results.append((section.virtual_address + match.start(), "utf16", text)) return results def find_xrefs(data: bytes, sections: list[Section], image_base: int, target_rvas: set[int]) -> dict[int, list[int]]: target_vas = {image_base + rva: rva for rva in target_rvas} hits = {rva: [] for rva in target_rvas} for section in sections: if not section.is_code: continue start = section.raw_offset end = min(len(data), section.raw_offset + section.raw_size) for offset in range(start, max(start, end - 4)): disp = struct.unpack_from(" list[tuple[int, str]]: payload = json.loads(path.read_text()) symbols: list[tuple[int, str]] = [] for item in payload.get("Addresses", []): symbol = item.get("symbol") offset = item.get("offset") if not symbol or not offset or not pattern.search(symbol): continue section, _, value = offset.partition(":") if section != "0001": continue symbols.append((int(value, 16), symbol)) return sorted(symbols) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("exe", type=Path) parser.add_argument("--addresses", type=Path) parser.add_argument( "--pattern", default=r"gps|route|path|mappin|track|quest|journal|navigation|map", help="case-insensitive regex for string and symbol filtering", ) parser.add_argument("--min-len", type=int, default=5) parser.add_argument("--max-strings", type=int, default=300) args = parser.parse_args() data = args.exe.read_bytes() image_base, sections = parse_pe(data) pattern = re.compile(args.pattern, re.IGNORECASE) matches: list[tuple[int, str, str]] = [] for section in sections: if section.is_readable_data: matches.extend(item for item in iter_strings(data, section, args.min_len) if pattern.search(item[2])) matches.sort(key=lambda item: (item[0], item[1], item[2])) target_rvas = {rva for rva, _, _ in matches} xrefs = find_xrefs(data, sections, image_base, target_rvas) print(f"image_base=0x{image_base:x}") print(f"string_matches={len(matches)} pattern={args.pattern!r}") for rva, encoding, text in matches[: args.max_strings]: hit_text = ", ".join(f"0x{hit:x}" for hit in xrefs.get(rva, [])[:12]) if len(xrefs.get(rva, [])) > 12: hit_text += f", ... +{len(xrefs[rva]) - 12}" print(f"string rva=0x{rva:x} enc={encoding} xrefs={len(xrefs.get(rva, []))} text={text!r}") if hit_text: print(f" xref_disp_rvas={hit_text}") if args.addresses: print() symbols = load_address_symbols(args.addresses, pattern) print(f"address_symbols={len(symbols)}") for rva, symbol in symbols[: args.max_strings]: print(f"symbol rva=0x{rva:x} name={symbol}") return 0 if __name__ == "__main__": raise SystemExit(main())