Add post-kit gameplay transition tracers

This commit is contained in:
funman300
2026-08-26 01:40:41 +00:00
parent 43c460741b
commit f608dbc438
2 changed files with 416 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Trace FIFA17's post-kit boundary without changing client behavior.
The probe anchors both ACTION_SAVE_MATCH_KIT (0x7576) actions, their concrete
native save call, the action-handler return, and select-team provider teardown.
The second 0x7576 action is the temporal boundary for later drill/game-loader
instrumentation.
The generated GDB program uses four hardware-assisted execution breakpoints. It
never writes client memory, calls client functions, drives input, emits actions,
or alters timing deliberately.
match_post_kit_trace.py [pid] [--output PATH]
match_post_kit_trace.py --print-script [pid]
match_post_kit_trace.py --selftest
"""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import shutil
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
import match_advance_trace as advance
import match_transition_trace as transition
SAVE_KIT_ACTION = 0x7576
ACTION_HANDLER_RVA = 0x0BFCC0
SAVE_CALL_RVA = 0x0BFF25
ACTION_RETURN_RVA = 0x0C00AE
SELECT_TEAM_DESTRUCTOR_RVA = 0x0BDEC0
def trace_addresses(cards_base: int) -> dict[str, int]:
return {
"action": cards_base + ACTION_HANDLER_RVA,
"save_call": cards_base + SAVE_CALL_RVA,
"action_return": cards_base + ACTION_RETURN_RVA,
"destructor": cards_base + SELECT_TEAM_DESTRUCTOR_RVA,
}
def build_gdb_script(pid: int, cards_base: int, output: str) -> str:
if any(character in output for character in "\n\r"):
raise ValueError("output path cannot contain a newline")
address = trace_addresses(cards_base)
return f"""set pagination off
set confirm off
set print thread-events off
set breakpoint always-inserted on
set logging file {output}
set logging overwrite on
set logging redirect off
set logging enabled on
handle SIGSEGV nostop noprint pass
handle SIGILL nostop noprint pass
handle SIGFPE nostop noprint pass
handle SIGPIPE nostop noprint pass
handle SIGALRM nostop noprint pass
handle SIGUSR1 nostop noprint pass
handle SIGUSR2 nostop noprint pass
attach {pid}
set $save_count = 0
set $current_action = 0
set $current_controller = 0
set $current_payload = 0
set $second_save_epoch = 0
hbreak *0x{address['action']:x}
condition 1 $edx == 0x{SAVE_KIT_ACTION:x}
commands
silent
set $save_count = $save_count + 1
set $current_action = $edx
set $current_controller = $rcx
set $current_payload = $r8
python import time, gdb; now = time.time_ns(); gdb.set_convenience_variable("event_epoch", now); print("POSTKIT epoch_ns=%d mono_ns=%d SAVE_ACTION" % (now, time.monotonic_ns()), end=" ")
if $save_count == 2
set $second_save_epoch = $event_epoch
end
printf "thread=%d ordinal=%d action=%#x controller=%p payload=%p payload_vtable=%p mode=%#x flags_150=%#x flags_151=%#x flags_152=%#x flags_155=%#x second_boundary=%d\\n", $_thread, $save_count, $edx, $rcx, $r8, *(void**)$r8, *(unsigned int*)($rcx+0x140), *(unsigned char*)($rcx+0x150), *(unsigned char*)($rcx+0x151), *(unsigned char*)($rcx+0x152), *(unsigned char*)($rcx+0x155), $save_count==2
bt 10
continue
end
hbreak *0x{address['save_call']:x}
condition 2 $current_action == 0x{SAVE_KIT_ACTION:x}
commands
silent
set $save_target = *(void**)(*(void**)$rcx+0x1d0)
python import time; print("POSTKIT epoch_ns=%d mono_ns=%d NATIVE_SAVE_CALL" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d ordinal=%d central=%p central_vtable=%p target=%p side=%#x request=%p payload=%p\\n", $_thread, $save_count, $rcx, *(void**)$rcx, $save_target, $r8d, $rdx, $current_payload
x/12gx $rdx
bt 10
continue
end
hbreak *0x{address['action_return']:x}
condition 3 $current_action == 0x{SAVE_KIT_ACTION:x} && $rsi == $current_controller
commands
silent
python import time; print("POSTKIT epoch_ns=%d mono_ns=%d ACTION_RETURN" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d ordinal=%d controller=%p handled=%#x mode=%#x flags_150=%#x flags_151=%#x flags_152=%#x flags_155=%#x\\n", $_thread, $save_count, $rsi, $al, *(unsigned int*)($rsi+0x140), *(unsigned char*)($rsi+0x150), *(unsigned char*)($rsi+0x151), *(unsigned char*)($rsi+0x152), *(unsigned char*)($rsi+0x155)
set $current_action = 0
bt 10
continue
end
hbreak *0x{address['destructor']:x}
condition 4 $save_count >= 2 && $rcx == $current_controller
commands
silent
python import time; print("POSTKIT epoch_ns=%d mono_ns=%d SELECT_TEAM_DESTRUCTOR" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d controller=%p save_count=%d second_save_epoch=%lld vtable=%p mode=%#x\\n", $_thread, $rcx, $save_count, $second_save_epoch, *(void**)$rcx, *(unsigned int*)($rcx+0x140)
bt 12
continue
end
printf "POSTKIT ARMED pid={pid} action=0x{address['action']:x} save_call=0x{address['save_call']:x} action_return=0x{address['action_return']:x} destructor=0x{address['destructor']:x}\\n"
continue
"""
def selftest() -> None:
address = trace_addresses(0x180000000)
assert address == {
"action": 0x1800BFCC0,
"save_call": 0x1800BFF25,
"action_return": 0x1800C00AE,
"destructor": 0x1800BDEC0,
}
script = build_gdb_script(47872, 0x180000000, "/tmp/post-kit.log")
assert script.count("hbreak *") == 4
assert f"$edx == 0x{SAVE_KIT_ACTION:x}" in script
assert "second_boundary" in script
assert "NATIVE_SAVE_CALL" in script
assert "SELECT_TEAM_DESTRUCTOR" in script
assert "set *(" not in script
print("match_post_kit_trace selftest: PASS")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("pid", nargs="?", type=int)
parser.add_argument("--output")
parser.add_argument("--print-script", action="store_true")
parser.add_argument("--selftest", action="store_true")
args = parser.parse_args()
if args.selftest:
selftest()
return 0
pid = args.pid or transition.find_pid()
if not pid:
print("FIFA17.exe not found", file=sys.stderr)
return 2
try:
cards_base, cards_path = transition.cards_mapping(pid)
transition.validate_cards(cards_path)
_fifa_base, fifa_path = advance.module_mapping(pid, advance.FIFA_MODULE)
advance.validate_file(fifa_path, advance.PINNED_FIFA_SHA256, advance.FIFA_MODULE)
output = args.output or f"/tmp/fifa17-match-post-kit-{pid}.log"
script = build_gdb_script(pid, cards_base, output)
except (OSError, RuntimeError, ValueError) as error:
print(error, file=sys.stderr)
return 2
if args.print_script:
print(script, end="")
return 0
if not shutil.which("gdb"):
print("gdb not found", file=sys.stderr)
return 2
script_path = f"/tmp/fifa17-match-post-kit-{pid}.gdb"
with open(script_path, "w", encoding="utf-8") as handle:
handle.write(script)
os.execvp("gdb", ["gdb", "-q", "-nx", "-batch", "-x", script_path])
return 127
if __name__ == "__main__":
raise SystemExit(main())