trace FIFA17 provider lifecycle
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Trace FIFA17 ACTION_ADVANCE dispatch after MATCH_CREATED delivery.
|
||||
"""Trace FIFA17 provider dispatch and ACTION_ADVANCE delivery boundaries.
|
||||
|
||||
Stage one (`match_transition_trace.py`) proves the HTTP response, deserializer,
|
||||
and FUT_CREATE_MATCH_DP delivery. This stage uses that provider callback to
|
||||
capture the screen key, then records every nested global UI dispatch for that
|
||||
same screen until the positive-control FUT_GET_MATCH_KITS_DP arrives. It also
|
||||
captures the low-level create event and final native-to-UI provider bridge.
|
||||
This probe correlates the global UI dispatch of FUT_CREATE_MATCH_DP and
|
||||
FUT_GET_MATCH_KITS_DP, the subscribed CardsDLL provider, the internal 0x7546
|
||||
create-response callback that can replay FUT_CREATE_MATCH_DP, and the final
|
||||
native-to-UI bridge. At global dispatch, r8d is the provider ID and rdx is the
|
||||
payload; neither register is a screen key.
|
||||
|
||||
The generated GDB program uses hardware-assisted execution breakpoints only.
|
||||
It never writes client memory and never drives game input.
|
||||
@@ -84,28 +84,22 @@ handle SIGUSR1 nostop noprint pass
|
||||
handle SIGUSR2 nostop noprint pass
|
||||
|
||||
attach {pid}
|
||||
set $screen_key = 0
|
||||
set $target_seen = 0
|
||||
|
||||
hbreak *0x{address['provider']:x}
|
||||
condition 1 $edx == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edx == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
|
||||
commands
|
||||
silent
|
||||
if $edx == 0x{transition.FUT_CREATE_MATCH_DP:x}
|
||||
set $screen_key = $r8
|
||||
set $target_seen = 1
|
||||
end
|
||||
python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d PROVIDER" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d provider=%#x screen_key=%p controller=%p caller=%p\\n", $_thread, $edx, $r8, $rcx, *(void**)$rsp
|
||||
printf "thread=%d provider=%#x payload=%p controller=%p caller=%p\\n", $_thread, $edx, $r8, $rcx, *(void**)$rsp
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['global_dispatch']:x}
|
||||
condition 2 $target_seen != 0 && $r8 == $screen_key
|
||||
condition 2 $r8d == 0x{transition.FUT_CREATE_MATCH_DP:x} || $r8d == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
|
||||
commands
|
||||
silent
|
||||
python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d GLOBAL_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d id=%#x screen_key=%p registry=%p caller=%p\\n", $_thread, $rdx, $r8, $rcx, *(void**)$rsp
|
||||
printf "thread=%d provider=%#x payload=%p manager=%p caller=%p\\n", $_thread, $r8d, $rdx, $rcx, *(void**)$rsp
|
||||
continue
|
||||
end
|
||||
|
||||
@@ -144,7 +138,8 @@ def selftest() -> None:
|
||||
assert script.count("hbreak *") == 4
|
||||
assert f"$edx == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script
|
||||
assert f"$edx == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" in script
|
||||
assert "$r8 == $screen_key" 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 "CREATE_MATCH_CONTROLLER" in script
|
||||
assert "PROVIDER_BRIDGE" in script
|
||||
assert "set *(" not in script
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Trace the FIFA17 ACTION_CREATE_MATCH-to-provider lifecycle.
|
||||
|
||||
The probe correlates:
|
||||
|
||||
* the select-team action handler for UIF action IDs 0x7574..0x757b;
|
||||
* DataManager's request dispatch for FutCreateMatchServerResponse (0x7546);
|
||||
* the concrete FutCreateMatchServerResponse data-source request method;
|
||||
* FIFA's global UI dispatch of providers 0x7563 and 0x7565.
|
||||
|
||||
Static decoding identifies action 0x7577 as the branch that constructs the
|
||||
create-match request and calls DataManager for source 0x7546. The trace proves
|
||||
whether that authentic trigger executes in the failing flow. It uses four
|
||||
hardware-assisted execution breakpoints, never writes client memory, and never
|
||||
drives game input.
|
||||
|
||||
match_create_action_trace.py [pid] [--output PATH]
|
||||
match_create_action_trace.py --print-script [pid]
|
||||
match_create_action_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
|
||||
|
||||
SELECT_TEAM_ACTION_HANDLER_RVA = 0x0BFCC0
|
||||
DATA_MANAGER_REQUEST_RVA = 0x80D2340
|
||||
DATA_SOURCE_REQUEST_RVA = 0x120270
|
||||
GLOBAL_UI_DISPATCH_RVA = advance.GLOBAL_UI_DISPATCH_RVA
|
||||
FIRST_SELECT_TEAM_ACTION = 0x7574
|
||||
LAST_SELECT_TEAM_ACTION = 0x757B
|
||||
ACTION_CREATE_MATCH = 0x7577
|
||||
CREATE_DATA_SOURCE = 0x7546
|
||||
|
||||
|
||||
def trace_addresses(cards_base: int, fifa_base: int) -> dict[str, int]:
|
||||
return {
|
||||
"action_handler": cards_base + SELECT_TEAM_ACTION_HANDLER_RVA,
|
||||
"manager_request": fifa_base + DATA_MANAGER_REQUEST_RVA,
|
||||
"data_source_request": cards_base + DATA_SOURCE_REQUEST_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 $create_action_seen = 0
|
||||
set $manager_request_seen = 0
|
||||
set $data_source_request_seen = 0
|
||||
|
||||
hbreak *0x{address['action_handler']:x}
|
||||
condition 1 $edx >= 0x{FIRST_SELECT_TEAM_ACTION:x} && $edx <= 0x{LAST_SELECT_TEAM_ACTION:x}
|
||||
commands
|
||||
silent
|
||||
if $edx == 0x{ACTION_CREATE_MATCH:x}
|
||||
set $create_action_seen = 1
|
||||
end
|
||||
python import time; print("ACTIONTRACE epoch_ns=%d mono_ns=%d SELECT_TEAM_ACTION" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d action=%#x is_create=%d controller=%p payload=%p create_seen=%d\\n", $_thread, $edx, $edx==0x{ACTION_CREATE_MATCH:x}, $rcx, $r8, $create_action_seen
|
||||
bt 10
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['manager_request']:x}
|
||||
condition 2 $edx == 0x{CREATE_DATA_SOURCE:x}
|
||||
commands
|
||||
silent
|
||||
set $manager_request_seen = 1
|
||||
set $tree_sentinel = $rcx + 0x10
|
||||
set $tree_cursor = *(void**)($rcx+0x20)
|
||||
set $data_node = $tree_sentinel
|
||||
while $tree_cursor != 0 && $tree_cursor != $tree_sentinel
|
||||
if *(unsigned int*)($tree_cursor+0x20) >= 0x{CREATE_DATA_SOURCE:x}
|
||||
set $data_node = $tree_cursor
|
||||
set $tree_cursor = *(void**)($tree_cursor+0x08)
|
||||
else
|
||||
set $tree_cursor = *(void**)$tree_cursor
|
||||
end
|
||||
end
|
||||
set $data_source = 0
|
||||
set $request_method = 0
|
||||
if $data_node != $tree_sentinel && *(unsigned int*)($data_node+0x20) == 0x{CREATE_DATA_SOURCE:x}
|
||||
set $data_source = *(void**)($data_node+0x28)
|
||||
if $data_source != 0
|
||||
set $request_method = *(void**)(*(void**)$data_source+0x18)
|
||||
end
|
||||
end
|
||||
python import time; print("ACTIONTRACE epoch_ns=%d mono_ns=%d MANAGER_REQUEST" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d source=%#x manager=%p request=%p node=%p data_source=%p request_method=%p create_seen=%d\\n", $_thread, $edx, $rcx, $r8, $data_node, $data_source, $request_method, $create_action_seen
|
||||
bt 10
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['data_source_request']:x}
|
||||
commands
|
||||
silent
|
||||
set $data_source_request_seen = 1
|
||||
python import time; print("ACTIONTRACE 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 create_seen=%d manager_seen=%d\\n", $_thread, $rcx-0x50, $rcx, $rdx, *(unsigned char*)($rcx+0x38), $create_action_seen, $manager_request_seen
|
||||
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("ACTIONTRACE epoch_ns=%d mono_ns=%d UI_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d provider=%#x payload=%p ui_manager=%p create_seen=%d manager_seen=%d data_source_seen=%d\\n", $_thread, $r8d, $rdx, $rcx, $create_action_seen, $manager_request_seen, $data_source_request_seen
|
||||
bt 10
|
||||
continue
|
||||
end
|
||||
|
||||
printf "ACTIONTRACE ARMED pid={pid} action_handler=0x{address['action_handler']:x} manager_request=0x{address['manager_request']:x} data_source_request=0x{address['data_source_request']:x} ui_dispatch=0x{address['ui_dispatch']:x}\\n"
|
||||
continue
|
||||
"""
|
||||
|
||||
|
||||
def selftest() -> None:
|
||||
address = trace_addresses(0x180000000, 0x140000000)
|
||||
assert address == {
|
||||
"action_handler": 0x1800BFCC0,
|
||||
"manager_request": 0x1480D2340,
|
||||
"data_source_request": 0x180120270,
|
||||
"ui_dispatch": 0x1480D1070,
|
||||
}
|
||||
script = build_gdb_script(
|
||||
45949, 0x180000000, 0x140000000, "/tmp/create-action.log"
|
||||
)
|
||||
assert script.count("hbreak *") == 4
|
||||
assert f"$edx == 0x{ACTION_CREATE_MATCH:x}" in script
|
||||
assert f"$edx == 0x{CREATE_DATA_SOURCE:x}" 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 "request_method" in script
|
||||
assert "set *(" not in script
|
||||
print("match_create_action_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-create-action-{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-create-action-{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())
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
#!/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())
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user