"""GDB Python payload for read-only FIFA17 match-team writer tracing. Loaded by trace_match_team_writer.py. Uses hardware execute breakpoints and a 4-byte hardware WRITE watchpoint only; never inserts INT3 and never writes game memory. Breakpoints (CardsDLL image VAs): * FUN_1800fc500 entry -- derives output pair from RDX and arms *(int*)(rdx+4). * 0x1800fc595 -- pre-write opponent lookup into pair[1]. * 0x1800fc5b8 -- mirrored pre-write opponent lookup into pair[0]. The dynamic watchpoint catches the exact write establishing pair[1], whether it is the opponent lookup at 0x1800fc595 or the own-club store at 0x1800fc5a0. """ from __future__ import annotations import json import os import struct import time import traceback import gdb CARDS_IMAGE_BASE = 0x180000000 ENTRY_RVA = 0x0FC500 LOOKUP_TO_TEAM1_RVA = 0x0FC595 LOOKUP_TO_TEAM0_RVA = 0x0FC5B8 TEAM1_POST_PC_TO_WRITER = { 0x1800FC599: 0x1800FC595, # mov [r14+4],ecx 0x1800FC5A4: 0x1800FC5A0, # mov [r14+4],eax } _STATE = None def _reg(name: str) -> int: return int(gdb.parse_and_eval(f"${name}")) def _thread() -> dict: thread = gdb.selected_thread() if thread is None: return {} return {"name": thread.name, "ptid": list(thread.ptid), "global_num": thread.global_num} def _read(address: int, size: int) -> bytes | None: if not address or address < 0 or size < 0: return None try: return bytes(gdb.selected_inferior().read_memory(address, size)) except gdb.error: return None def _u8(address: int) -> int | None: data = _read(address, 1) return data[0] if data else None def _u32(address: int) -> int | None: data = _read(address, 4) return struct.unpack(" int | None: data = _read(address, 4) return struct.unpack(" int | None: data = _read(address, 8) return struct.unpack(" str | None: data = _read(address, maximum) if not data: return None return data.split(b"\0", 1)[0].decode("utf-8", "replace") def _rtti_name(vtable: int, cards_base: int) -> str | None: """MSVC x64 RTTI name from vtable[-1] CompleteObjectLocator. PE RVAs in the locator are module-relative. Failure is evidence-free and is logged as null; no pointer is named from an offset coincidence. """ locator = _u64(vtable - 8) if vtable else None if not locator: return None raw = _read(locator, 24) if not raw: return None _signature, _offset, _cd_offset, type_rva, _hier_rva, self_rva = struct.unpack( " 0x100000: return None return _cstring(image_base + type_rva + 16) def _object(address: int, cards_base: int) -> dict: vtable = _u64(address) if address else None return { "address": address, "vtable": vtable, "vtable_image_va": ( CARDS_IMAGE_BASE + (vtable - cards_base) if vtable and cards_base <= vtable < cards_base + 0x400000 else None ), "rtti": _rtti_name(vtable, cards_base) if vtable else None, } def _registers() -> dict: names = ( "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15", "rip", ) return {name: _reg(name) for name in names} def _provenance(state, destination: int | None = None) -> dict: """Recover the candidate's live input chain without naming the objects.""" regs = _registers() context = regs["rbx"] output_pair = regs["r14"] obj = regs["rbp"] nested = _u64(obj + 0xB0) if obj else None field_2e8 = nested + 0x2E8 if nested else None source_base = _u64(field_2e8) if field_2e8 else None participant_holder = regs["r12"] participant = _u64(participant_holder) if participant_holder else None index_70 = _u8(participant + 0x70) if participant else None source_address = ( source_base + index_70 * 16 if source_base is not None and index_70 is not None else None ) source_bytes = _read(source_address, 16) if source_address else None decoded = None if source_bytes and len(source_bytes) == 16: team_id, byte4, byte5, pad, word8, wordc = struct.unpack("