333 lines
14 KiB
Python
Executable File
333 lines
14 KiB
Python
Executable File
#!/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())
|