Trace FIFA17 command 0x128 lifecycle
This commit is contained in:
Executable
+332
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Trace FIFA17 Screen event 0x30 through command 0x128 and ScenarioModeStart.
|
||||
|
||||
The generated GDB program uses hardware breakpoints, only reads registers and
|
||||
client memory, logs, and continues. Seven breakpoints are rotated so no more
|
||||
than four are enabled. It never calls client functions, writes client memory,
|
||||
emits events, or drives input.
|
||||
|
||||
command_128_trace.py [pid] [--output PATH]
|
||||
command_128_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
|
||||
|
||||
MANAGER_SELECT_ACTION_SOURCE_RVA = 0x0705A620
|
||||
MANAGER_SELECT_ACTION_RESULT_RVA = 0x07CDC4A6
|
||||
SCREEN_EVENT_CHANNEL_ROUTER_RVA = 0x080CE230
|
||||
SCREEN_EVENT_DISPATCH_RVA = 0x080CF790
|
||||
SKILL_INSTRUCTIONS_SCREEN_RVA = 0x07DCA400
|
||||
GAMEPLAY_COMMAND_DISPATCH_RVA = 0x07A8F6C0
|
||||
FREE_ROAM_COMMAND_128_RVA = 0x07A92B0F
|
||||
SCENARIO_SCHEDULER_RVA = 0x07AC3A40
|
||||
SCENARIO_MANAGER_START_RVA = 0x07B1C2B0
|
||||
MODE_ZERO_SCENARIO_START_RVA = 0x07B1C190
|
||||
GAMEPLAY_GLOBAL_RVA = 0x04BFB910
|
||||
SCREEN_VTABLE_RVA = 0x03B3ECC0
|
||||
FREE_ROAM_VTABLE_RVA = 0x03AEDF58
|
||||
MODE_ZERO_CHILD_VTABLE_RVA = 0x03AE9C00
|
||||
|
||||
|
||||
def addresses(base: int) -> dict[str, int]:
|
||||
return {
|
||||
"manager_select_source": base + MANAGER_SELECT_ACTION_SOURCE_RVA,
|
||||
"manager_select_action": base + MANAGER_SELECT_ACTION_RESULT_RVA,
|
||||
"screen_event_router": base + SCREEN_EVENT_CHANNEL_ROUTER_RVA,
|
||||
"screen_event_dispatch": base + SCREEN_EVENT_DISPATCH_RVA,
|
||||
"instructions_screen": base + SKILL_INSTRUCTIONS_SCREEN_RVA,
|
||||
"command_dispatch": base + GAMEPLAY_COMMAND_DISPATCH_RVA,
|
||||
"free_roam_case": base + FREE_ROAM_COMMAND_128_RVA,
|
||||
"scheduler": base + SCENARIO_SCHEDULER_RVA,
|
||||
"manager_start": base + SCENARIO_MANAGER_START_RVA,
|
||||
"scenario_start": base + MODE_ZERO_SCENARIO_START_RVA,
|
||||
"gameplay_global": base + GAMEPLAY_GLOBAL_RVA,
|
||||
"screen_vtable": base + SCREEN_VTABLE_RVA,
|
||||
"free_roam_vtable": base + FREE_ROAM_VTABLE_RVA,
|
||||
"mode_zero_child_vtable": base + MODE_ZERO_CHILD_VTABLE_RVA,
|
||||
}
|
||||
|
||||
|
||||
def gdb_prelude(pid: int, output: str) -> str:
|
||||
if any(character in output for character in "\n\r"):
|
||||
raise ValueError("output path cannot contain a newline")
|
||||
return f"""set pagination off
|
||||
set confirm off
|
||||
set print thread-events off
|
||||
set breakpoint always-inserted off
|
||||
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}
|
||||
"""
|
||||
|
||||
|
||||
def build_script(pid: int, fifa_base: int, output: str) -> str:
|
||||
address = addresses(fifa_base)
|
||||
return (
|
||||
gdb_prelude(pid, output)
|
||||
+ f"""define snapshot_gameplay
|
||||
set $snap_gameplay_global = *(void**)0x{address['gameplay_global']:x}
|
||||
set $snap_listener_manager = 0
|
||||
set $snap_listener_table = 0
|
||||
set $snap_listener_index = -1
|
||||
set $snap_free_roam = 0
|
||||
set $snap_free_state = -1
|
||||
set $snap_free_111 = -1
|
||||
set $snap_free_112 = -1
|
||||
set $snap_free_124 = -1
|
||||
set $snap_selected = 0
|
||||
set $snap_selected_vtable = 0
|
||||
set $snap_selected_mode = -1
|
||||
if $snap_gameplay_global != 0
|
||||
set $snap_listener_manager = *(void**)($snap_gameplay_global+0x58)
|
||||
end
|
||||
if $snap_listener_manager != 0
|
||||
set $snap_listener_table = *(void**)$snap_listener_manager
|
||||
end
|
||||
if $snap_listener_table != 0
|
||||
set $snap_free_roam = *(void**)$snap_listener_table
|
||||
set $snap_listener_index = *(int*)($snap_listener_table+0x20)
|
||||
if $snap_listener_index >= 0 && $snap_listener_index < 3
|
||||
set $snap_selected = *(void**)($snap_listener_table+$snap_listener_index*8)
|
||||
end
|
||||
end
|
||||
if $snap_free_roam != 0
|
||||
set $snap_free_state = *(int*)($snap_free_roam+0x30)
|
||||
set $snap_free_111 = *(unsigned char*)($snap_free_roam+0x111)
|
||||
set $snap_free_112 = *(unsigned char*)($snap_free_roam+0x112)
|
||||
set $snap_free_124 = *(int*)($snap_free_roam+0x124)
|
||||
end
|
||||
if $snap_selected != 0
|
||||
set $snap_selected_vtable = *(void**)$snap_selected
|
||||
set $snap_selected_mode = *(int*)($snap_selected+0x18)
|
||||
end
|
||||
end
|
||||
|
||||
set $action_count = 0
|
||||
hbreak *0x{address['manager_select_action']:x}
|
||||
commands
|
||||
silent
|
||||
set $action_count = $action_count+1
|
||||
set $provider = $rbx
|
||||
snapshot_gameplay
|
||||
if $action_count <= 128
|
||||
python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d MANAGER_SELECT_ACTION" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d ordinal=%d instruction=%p caller_return=%p provider=%p provider_vtable=%p action_id=%#x free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $action_count, $pc, *(void**)($rsp+0x58), $provider, *(void**)$provider, $eax, $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode
|
||||
end
|
||||
if $eax == 0x30
|
||||
bt 16
|
||||
end
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['instructions_screen']:x}
|
||||
condition 2 $edx == 0x30 && *(void**)$rcx == 0x{address['screen_vtable']:x}
|
||||
commands
|
||||
silent
|
||||
set $screen = $rcx
|
||||
set $screen_owner = *(void**)($screen+0x140)
|
||||
set $screen_owner_vtable = 0
|
||||
if $screen_owner != 0
|
||||
set $screen_owner_vtable = *(void**)$screen_owner
|
||||
end
|
||||
snapshot_gameplay
|
||||
python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d INSTRUCTIONS_SCREEN_EVENT_30" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d handler=%p caller_return=%p screen=%p screen_vtable=%p event=%#x payload=%p allow_advance138=%d owner140=%p owner_vtable=%p free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $screen, *(void**)$screen, $edx, $r8, *(int*)($screen+0x138), $screen_owner, $screen_owner_vtable, $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode
|
||||
bt 16
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['command_dispatch']:x}
|
||||
condition 3 $edx == 0x128
|
||||
commands
|
||||
silent
|
||||
set $command_dispatcher = $rcx
|
||||
set $command_table = *(void**)$command_dispatcher
|
||||
snapshot_gameplay
|
||||
python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d GAMEPLAY_COMMAND_128" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d function=%p caller_return=%p dispatcher=%p command=%#x payload=%p arg_r9=%p table=%p free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $command_dispatcher, $edx, $r8, $r9, $command_table, $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode
|
||||
disable 1
|
||||
disable 2
|
||||
disable 3
|
||||
enable 5
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['free_roam_case']:x}
|
||||
commands
|
||||
silent
|
||||
set $owner = $rbx
|
||||
snapshot_gameplay
|
||||
python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d FREE_ROAM_COMMAND_128" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d callsite=%p caller_return=%p owner=%p owner_vtable=%p command=%#x payload=%p state=%d previous=%d free111=%d free112=%d free124=%d manager=%p selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $owner, *(void**)$owner, $esi, $rdi, *(int*)($owner+0x30), *(int*)($owner+0x34), *(unsigned char*)($owner+0x111), *(unsigned char*)($owner+0x112), *(int*)($owner+0x124), *(void**)($owner+0x168), $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['scheduler']:x}
|
||||
disable 5
|
||||
commands
|
||||
silent
|
||||
set $owner = $rcx
|
||||
set $manager = *(void**)($owner+0x168)
|
||||
set $manager_vtable = 0
|
||||
set $manager_mode = -1
|
||||
set $child = 0
|
||||
set $child_vtable = 0
|
||||
if $manager != 0
|
||||
set $manager_vtable = *(void**)$manager
|
||||
set $manager_mode = *(int*)($manager+0x50)
|
||||
set $child = *(void**)($manager+0x8)
|
||||
end
|
||||
if $child != 0
|
||||
set $child_vtable = *(void**)$child
|
||||
end
|
||||
python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d SCENARIO_SCHEDULER" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d function=%p caller_return=%p owner=%p owner_vtable=%p free124=%d command=%#x payload=%p manager=%p manager_vtable=%p manager_mode=%d child=%p child_vtable=%p\\n", $_thread, $pc, *(void**)$rsp, $owner, *(void**)$owner, *(int*)($owner+0x124), $edx, $r8, $manager, $manager_vtable, $manager_mode, $child, $child_vtable
|
||||
disable 4
|
||||
disable 5
|
||||
enable 6
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['manager_start']:x}
|
||||
disable 6
|
||||
commands
|
||||
silent
|
||||
set $manager = $rcx
|
||||
set $child = *(void**)($manager+0x8)
|
||||
python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d SCENARIO_MANAGER_START" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d function=%p caller_return=%p manager=%p manager_vtable=%p requested_countdown=%d mode=%d child=%p child_vtable=%p\\n", $_thread, $pc, *(void**)$rsp, $manager, *(void**)$manager, $rdx & 0xff, *(int*)($manager+0x50), $child, $child ? *(void**)$child : 0
|
||||
disable 6
|
||||
enable 7
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['scenario_start']:x}
|
||||
disable 7
|
||||
commands
|
||||
silent
|
||||
set $ctx = $rcx
|
||||
snapshot_gameplay
|
||||
python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d MODE_ZERO_SCENARIO_START" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d function=%p caller_return=%p ctx=%p ctx_vtable=%p descriptor=%p scenario_index=%d requested_countdown=%d flag40_before=%d callback_owner78=%p callback_vtable48=%p dispatcher_vtable80=%p free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $ctx, *(void**)$ctx, $rdx, $r8d, $r9 & 0xff, *(unsigned char*)($ctx+0x40), *(void**)($ctx+0x78), *(void**)($ctx+0x48), *(void**)($ctx+0x80), $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode
|
||||
disable 7
|
||||
continue
|
||||
end
|
||||
|
||||
printf "COMMAND128 ARMED pid={pid} action_id=0x{address['manager_select_action']:x} screen_handler=0x{address['instructions_screen']:x} command_dispatch=0x{address['command_dispatch']:x} free_roam=0x{address['free_roam_case']:x} scheduler=0x{address['scheduler']:x} manager=0x{address['manager_start']:x} scenario=0x{address['scenario_start']:x}\\n"
|
||||
continue
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def effective_environment(pid: int) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
for item in Path(f"/proc/{pid}/environ").read_bytes().split(b"\0"):
|
||||
if not item.startswith(b"OPENFUT_FIFA17_"):
|
||||
continue
|
||||
key, _, value = item.decode("utf-8", errors="replace").partition("=")
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
|
||||
def selftest() -> None:
|
||||
address = addresses(0x140000000)
|
||||
script = build_script(1234, 0x140000000, "/tmp/command-128.log")
|
||||
assert address["manager_select_source"] == 0x14705A620
|
||||
assert address["manager_select_action"] == 0x147CDC4A6
|
||||
assert address["instructions_screen"] == 0x147DCA400
|
||||
assert address["command_dispatch"] == 0x147A8F6C0
|
||||
assert address["free_roam_case"] == 0x147A92B0F
|
||||
assert address["scheduler"] == 0x147AC3A40
|
||||
assert address["manager_start"] == 0x147B1C2B0
|
||||
assert address["scenario_start"] == 0x147B1C190
|
||||
assert script.count("hbreak *") == 7
|
||||
assert "set $action_count = 0" in script
|
||||
assert "MANAGER_SELECT_ACTION" in script
|
||||
assert "condition 2 $edx == 0x30" in script
|
||||
assert "condition 3 $edx == 0x128" in script
|
||||
assert "disable 4" in script
|
||||
assert "disable 5" in script and "enable 5" in script
|
||||
assert "disable 6" in script and "enable 6" in script
|
||||
assert "disable 7" in script and "enable 7" in script
|
||||
assert "set *(" not in script
|
||||
print("command_128_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:
|
||||
fifa_base, fifa_path = advance.module_mapping(pid, advance.FIFA_MODULE)
|
||||
advance.validate_file(
|
||||
fifa_path,
|
||||
advance.PINNED_FIFA_SHA256,
|
||||
advance.FIFA_MODULE,
|
||||
)
|
||||
cards_base = 0
|
||||
cards_path = "<not-loaded>"
|
||||
try:
|
||||
cards_base, cards_path = transition.cards_mapping(pid)
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
transition.validate_cards(cards_path)
|
||||
output = args.output or f"/tmp/fifa17-command-128-{pid}.log"
|
||||
script = build_script(pid, fifa_base, output)
|
||||
environment = effective_environment(pid)
|
||||
print(
|
||||
"COMMAND128 PREPARED "
|
||||
f"pid={pid} fifa_base={fifa_base:#x} cards_base={cards_base:#x} "
|
||||
f"cards_path={cards_path} "
|
||||
f"team_compat={environment.get('OPENFUT_FIFA17_SEASON_TEAM_COMPAT', '<absent>')} "
|
||||
f"pma_fix={environment.get('OPENFUT_FIFA17_OFFLINE_SEASONS_PMA_FIX', '<absent>')}"
|
||||
)
|
||||
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-command-128-{pid}.gdb"
|
||||
Path(script_path).write_text(script, encoding="utf-8")
|
||||
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