202 lines
8.5 KiB
Python
Executable File
202 lines
8.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Trace FIFA17 create-response readiness versus UI provider dispatch.
|
|
|
|
The probe correlates four concrete lifecycle boundaries:
|
|
|
|
* FutCreateMatchServerResponse data-source request;
|
|
* the POST /match network response callback;
|
|
* the response readiness/completion callback;
|
|
* FIFA's global UI dispatch of providers 0x7563 and 0x7565.
|
|
|
|
This distinguishes network completion from the separate DataManager readiness
|
|
lifecycle without assuming any screen or heap-object identity. The generated GDB
|
|
program uses hardware-assisted execution breakpoints only. It never writes
|
|
client memory and never drives game input.
|
|
|
|
match_provider_producer_trace.py [pid] [--output PATH]
|
|
match_provider_producer_trace.py --print-script [pid]
|
|
match_provider_producer_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
|
|
|
|
DATA_SOURCE_REQUEST_RVA = 0x120270
|
|
NETWORK_RESPONSE_RVA = transition.RESPONSE_CALLBACK_RVA
|
|
CREATE_COMPLETE_RVA = 0x120000
|
|
GLOBAL_UI_DISPATCH_RVA = advance.GLOBAL_UI_DISPATCH_RVA
|
|
CREATE_RESPONSE_OFFSET = 0xA0
|
|
CREATE_DATA_SOURCE_OFFSET = CREATE_RESPONSE_OFFSET + 0x50
|
|
CREATE_READY_OFFSET = CREATE_RESPONSE_OFFSET + 0x88
|
|
ACTIVE_CALLBACK_OFFSET = 0x47D0
|
|
|
|
|
|
def trace_addresses(cards_base: int, fifa_base: int) -> dict[str, int]:
|
|
return {
|
|
"data_source_request": cards_base + DATA_SOURCE_REQUEST_RVA,
|
|
"network_response": cards_base + NETWORK_RESPONSE_RVA,
|
|
"create_complete": cards_base + CREATE_COMPLETE_RVA,
|
|
"ui_dispatch": fifa_base + GLOBAL_UI_DISPATCH_RVA,
|
|
}
|
|
|
|
|
|
def build_gdb_script(
|
|
pid: int, cards_base: int, fifa_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, fifa_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 $last_central = 0
|
|
set $last_response = 0
|
|
set $last_data_source = 0
|
|
set $last_descriptor = 0
|
|
|
|
hbreak *0x{address['data_source_request']:x}
|
|
commands
|
|
silent
|
|
set $request_data_source = $rcx
|
|
set $request_response = $rcx - 0x50
|
|
python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d DATA_SOURCE_REQUEST" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
|
printf "thread=%d response=%p data_source=%p request=%p ready_before=%#x callback_adapter=%p callback_context=%p callback_target=%p\\n", $_thread, $request_response, $request_data_source, $rdx, *(unsigned char*)($request_data_source+0x38), *(void**)($request_response+0x90), *(void**)($request_response+0x98), *(void**)($request_response+0xa0)
|
|
bt 10
|
|
continue
|
|
end
|
|
|
|
hbreak *0x{address['network_response']:x}
|
|
commands
|
|
silent
|
|
set $last_central = $rcx
|
|
set $last_response = $rcx + 0x{CREATE_RESPONSE_OFFSET:x}
|
|
set $last_data_source = $rcx + 0x{CREATE_DATA_SOURCE_OFFSET:x}
|
|
set $last_descriptor = $rdx
|
|
python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d NETWORK_RESPONSE" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
|
if $rdx == 0
|
|
printf "thread=%d central=%p descriptor=(nil) status=UNKNOWN wire_payload=(nil) response=%p data_source=%p ready=%#x active_adapter=%p active_context=%p active_target=%p\\n", $_thread, $last_central, $last_response, $last_data_source, *(unsigned char*)($last_central+0x{CREATE_READY_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 8:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 16:x})
|
|
else
|
|
printf "thread=%d central=%p descriptor=%p status=%#x wire_payload=%p response=%p data_source=%p ready=%#x active_adapter=%p active_context=%p active_target=%p\\n", $_thread, $last_central, $rdx, *(unsigned int*)($rdx+0x1c), *(void**)($rdx+0x28), $last_response, $last_data_source, *(unsigned char*)($last_central+0x{CREATE_READY_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 8:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 16:x})
|
|
end
|
|
bt 10
|
|
continue
|
|
end
|
|
|
|
hbreak *0x{address['create_complete']:x}
|
|
commands
|
|
silent
|
|
python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d CREATE_COMPLETE" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
|
if $rdx == 0
|
|
printf "thread=%d response=%p data_source=%p ready_before=%#x descriptor=(nil) status=UNKNOWN last_response=%p same_response=%d\\n", $_thread, $rcx, $rcx+0x50, *(unsigned char*)($rcx+0x88), $last_response, $rcx==$last_response
|
|
else
|
|
printf "thread=%d response=%p data_source=%p ready_before=%#x descriptor=%p status=%#x last_response=%p same_response=%d\\n", $_thread, $rcx, $rcx+0x50, *(unsigned char*)($rcx+0x88), $rdx, *(unsigned int*)($rdx+0x1c), $last_response, $rcx==$last_response
|
|
end
|
|
bt 10
|
|
continue
|
|
end
|
|
|
|
hbreak *0x{address['ui_dispatch']:x}
|
|
condition 4 $r8d == 0x{transition.FUT_CREATE_MATCH_DP:x} || $r8d == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
|
|
commands
|
|
silent
|
|
python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d UI_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
|
if $last_response == 0
|
|
printf "thread=%d provider=%#x payload=%p ui_manager=%p last_response=(nil) ready=UNKNOWN\\n", $_thread, $r8d, $rdx, $rcx
|
|
else
|
|
printf "thread=%d provider=%#x payload=%p ui_manager=%p last_response=%p data_source=%p ready=%#x descriptor=%p\\n", $_thread, $r8d, $rdx, $rcx, $last_response, $last_data_source, *(unsigned char*)($last_response+0x88), $last_descriptor
|
|
end
|
|
bt 10
|
|
continue
|
|
end
|
|
|
|
printf "RESPTRACE ARMED pid={pid} data_source_request=0x{address['data_source_request']:x} network_response=0x{address['network_response']:x} create_complete=0x{address['create_complete']:x} ui_dispatch=0x{address['ui_dispatch']:x}\\n"
|
|
continue
|
|
"""
|
|
|
|
|
|
def selftest() -> None:
|
|
address = trace_addresses(0x180000000, 0x140000000)
|
|
assert address == {
|
|
"data_source_request": 0x180120270,
|
|
"network_response": 0x180114D90,
|
|
"create_complete": 0x180120000,
|
|
"ui_dispatch": 0x1480D1070,
|
|
}
|
|
script = build_gdb_script(
|
|
38872, 0x180000000, 0x140000000, "/tmp/response-lifecycle.log"
|
|
)
|
|
assert script.count("hbreak *") == 4
|
|
assert "DATA_SOURCE_REQUEST" in script
|
|
assert "NETWORK_RESPONSE" in script
|
|
assert "CREATE_COMPLETE" in script
|
|
assert "UI_DISPATCH" in script
|
|
assert f"$r8d == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script
|
|
assert f"$r8d == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" in script
|
|
assert "set *(" not in script
|
|
print("match_provider_producer_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-response-lifecycle-{pid}.log"
|
|
script = build_gdb_script(pid, cards_base, fifa_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-response-lifecycle-{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())
|