189 lines
7.5 KiB
Python
Executable File
189 lines
7.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Trace FIFA17 provider delivery lookup without heap-address assumptions.
|
|
|
|
The probe anchors the real CardsDLL call sequence in FUN_1801a4cd0 and the
|
|
provider-specific FUT_CREATE_MATCH_DP readiness check in FUN_1800be500:
|
|
|
|
vslot +0x38 call -> create gate return -> returned target -> UI bridge
|
|
|
|
For FUT_CREATE_MATCH_DP and FUT_GET_MATCH_KITS_DP it records the live controller
|
|
vtable, concrete lookup function, event service, readiness-gate implementation,
|
|
every register input, returned target, and whether the native-to-UI bridge
|
|
executes. No post-event object identity is used.
|
|
|
|
The generated GDB program uses hardware-assisted execution breakpoints only.
|
|
It never writes client memory and never drives game input.
|
|
|
|
match_delivery_lifecycle_trace.py [pid] [--output PATH]
|
|
match_delivery_lifecycle_trace.py --print-script [pid]
|
|
match_delivery_lifecycle_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
|
|
|
|
LOOKUP_CALL_RVA = transition.PROVIDER_DISPATCH_RVA + 0x2C
|
|
LOOKUP_RETURN_RVA = transition.PROVIDER_DISPATCH_RVA + 0x2F
|
|
BRIDGE_CALL_RVA = transition.PROVIDER_DISPATCH_RVA + 0x71
|
|
CREATE_GATE_RETURN_RVA = 0x0BE647
|
|
|
|
|
|
def trace_addresses(cards_base: int) -> dict[str, int]:
|
|
return {
|
|
"lookup_call": cards_base + LOOKUP_CALL_RVA,
|
|
"gate_return": cards_base + CREATE_GATE_RETURN_RVA,
|
|
"lookup_return": cards_base + LOOKUP_RETURN_RVA,
|
|
"bridge": cards_base + BRIDGE_CALL_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 $current_provider = 0
|
|
set $current_payload = 0
|
|
set $current_controller = 0
|
|
set $current_vtable = 0
|
|
set $current_lookup = 0
|
|
set $current_service = 0
|
|
set $current_service_vtable = 0
|
|
set $current_gate = 0
|
|
|
|
hbreak *0x{address['lookup_call']:x}
|
|
condition 1 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
|
|
commands
|
|
silent
|
|
set $current_provider = $edi
|
|
set $current_payload = $rbp
|
|
set $current_controller = $rcx
|
|
set $current_vtable = *(void**)$rcx
|
|
set $current_lookup = *(void**)(*(void**)$rcx+0x38)
|
|
set $current_service = *(void**)($rcx+0x18)
|
|
set $current_service_vtable = *(void**)$current_service
|
|
set $current_gate = *(void**)($current_service_vtable+0x58)
|
|
python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d LOOKUP_CALL" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
|
printf "thread=%d provider=%#x this=%p outer_controller=%p payload=%p vtable=%p lookup_fn=%p service=%p service_vtable=%p gate_fn=%p controller_mode=%#x controller_flag=%#x rdx=%p r8=%p r9=%p state_rbx=%p state_rbp=%p\\n", $_thread, $edi, $rcx, $rbx, $rbp, $current_vtable, $current_lookup, $current_service, $current_service_vtable, $current_gate, *(unsigned int*)($rcx+0x140), *(unsigned char*)($rcx+0x152), $rdx, $r8, $r9, $rbx, $rbp
|
|
continue
|
|
end
|
|
|
|
hbreak *0x{address['gate_return']:x}
|
|
condition 2 $current_provider == 0x{transition.FUT_CREATE_MATCH_DP:x} && $rbx == $current_controller
|
|
commands
|
|
silent
|
|
python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d CREATE_GATE_RETURN" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
|
printf "thread=%d provider=%#x controller=%p service=%p service_vtable=%p gate_fn=%p selector=0x7546 result_al=%#x\\n", $_thread, $current_provider, $current_controller, $current_service, $current_service_vtable, $current_gate, $al
|
|
continue
|
|
end
|
|
|
|
hbreak *0x{address['lookup_return']:x}
|
|
condition 3 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
|
|
commands
|
|
silent
|
|
python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d LOOKUP_RETURN" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
|
printf "thread=%d provider=%#x controller=%p payload=%p vtable=%p lookup_fn=%p result=%p\\n", $_thread, $edi, $rbx, $rbp, $current_vtable, $current_lookup, $rax
|
|
continue
|
|
end
|
|
|
|
hbreak *0x{address['bridge']:x}
|
|
condition 4 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
|
|
commands
|
|
silent
|
|
python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d BRIDGE" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
|
printf "thread=%d provider=%#x controller=%p payload=%p target=%p bridge=%p callback=%p\\n", $_thread, $edi, $current_controller, $current_payload, $rsi, $rbx, *(void**)(*(void**)$rbx+0x48)
|
|
continue
|
|
end
|
|
|
|
printf "LOOKUPTRACE ARMED pid={pid} lookup_call=0x{address['lookup_call']:x} gate_return=0x{address['gate_return']:x} lookup_return=0x{address['lookup_return']:x} bridge=0x{address['bridge']:x}\\n"
|
|
continue
|
|
"""
|
|
|
|
|
|
def selftest() -> None:
|
|
address = trace_addresses(0x180000000)
|
|
assert address == {
|
|
"lookup_call": 0x1801A4CFC,
|
|
"gate_return": 0x1800BE647,
|
|
"lookup_return": 0x1801A4CFF,
|
|
"bridge": 0x1801A4D41,
|
|
}
|
|
script = build_gdb_script(35632, 0x180000000, "/tmp/lookup.log")
|
|
assert script.count("hbreak *") == 4
|
|
assert f"$edi == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script
|
|
assert f"$edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" in script
|
|
assert "LOOKUP_CALL" in script
|
|
assert "CREATE_GATE_RETURN" in script
|
|
assert "LOOKUP_RETURN" in script
|
|
assert "gate_fn" in script
|
|
assert "BRIDGE" in script
|
|
assert "set *(" not in script
|
|
print("match_delivery_lifecycle_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-provider-lookup-{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-provider-lookup-{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())
|