chore(fifa17-client): preserve .105 client tooling WIP; gitignore local .screens
This commit is contained in:
+141
-38
@@ -24,6 +24,45 @@ STORE_PATCHES = {
|
||||
0x1800175aa: NOP2,
|
||||
}
|
||||
|
||||
# Store resolver crash-guard for the empty "My Packs" case (bug 6c; PROVEN R1 on the
|
||||
# tested FIFA 17 build -- see docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md PART IV and
|
||||
# docs/evidence/FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md).
|
||||
#
|
||||
# When no `mypacks` group exists, FIFA's Store resolver receives category id -1. CardsDLL
|
||||
# FUN_1800147f0 @ 0x180014858 is `JNZ 0x14869` (75 0f): the original treats every non-zero
|
||||
# category (including -1) as resolvable, calls FUN_180014420, gets NULL, and crashes at the
|
||||
# [NULL+0x48] deref in FUN_1800147f0 (0x180014882). Changing JNZ->JG (7f 0f) preserves
|
||||
# positive-category resolution (EDI>0 branch) while routing zero/negative categories through
|
||||
# the existing Browse/list-all path -> no NULL lookup, no crash, Store opens on Browse Packs.
|
||||
#
|
||||
# CAVEAT: this guards the category SIGN only. It does NOT protect a stale *positive* invalid
|
||||
# ordinal produced by changing the Store group topology (sentinel-present <-> sentinel-absent)
|
||||
# DURING one running FIFA process -- that reproduced the same crash in the confounded run F3.
|
||||
# The empty-My-Packs representation MUST stay stable for a FIFA session (see the SESSION-STABLE
|
||||
# invariant in the client-fix plan).
|
||||
#
|
||||
# Orig-verified / fail-closed: applied only when the live bytes are the known original (75 0f);
|
||||
# already-patched (7f 0f) is a no-op; anything else is logged and SKIPPED (never blindly
|
||||
# overwritten), so an unrecognised CardsDLL build is not patched.
|
||||
STORE_PATCHES_GUARDED = {
|
||||
0x180014858: (bytes.fromhex("750f"), bytes.fromhex("7f0f")), # JNZ 0x14869 -> JG 0x14869
|
||||
}
|
||||
|
||||
# Capability advertised to the launcher/backend once the resolver guard is VERIFIED
|
||||
# live in a specific FIFA process (docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md #3/#4).
|
||||
EMPTY_MYPACKS_RESOLVER_CAPABILITY = "fifa17.empty_mypacks_resolver"
|
||||
EMPTY_MYPACKS_RESOLVER_VERSION = 1
|
||||
|
||||
# The guarded site whose verified enforcement backs the capability above.
|
||||
RESOLVER_GUARD_VA = 0x180014858
|
||||
|
||||
# Per-FIFA-pid guard status (fail-closed; FIFA17_PATCHED_CLIENT_CAPABILITY.md #4).
|
||||
GUARD_NOT_ATTEMPTED = "NOT_ATTEMPTED" # CardsDLL not mapped / guard not yet evaluated
|
||||
GUARD_VERIFIED = "VERIFIED" # live bytes == patch after enforcement (patch or noop)
|
||||
GUARD_UNSUPPORTED_BUILD = "UNSUPPORTED_BUILD" # neither original nor patched (guarded_action -> skip)
|
||||
GUARD_WRITE_FAILED = "WRITE_FAILED" # /proc/<pid>/mem write raised
|
||||
GUARD_VERIFY_FAILED = "VERIFY_FAILED" # post-write re-read != patch
|
||||
|
||||
LOG=os.environ.get("OPENFUT_AUTOPATCH_LOG", f"/tmp/openfut-autopatch-{os.getuid()}.log")
|
||||
|
||||
def log(m):
|
||||
@@ -52,48 +91,112 @@ def wr(pid,va,b):
|
||||
with open(f'/proc/{pid}/mem','r+b') as f:
|
||||
f.seek(va); f.write(b)
|
||||
|
||||
def guarded_action(cur, orig, patch):
|
||||
"""Fail-closed decision for a guarded byte patch (see STORE_PATCHES_GUARDED).
|
||||
|
||||
Returns "noop" when the live bytes are already patched, "patch" when they are the
|
||||
known original (safe to apply), or "skip" for anything else -- an unrecognised
|
||||
CardsDLL build that must never be blindly overwritten.
|
||||
"""
|
||||
if cur == patch:
|
||||
return "noop"
|
||||
if cur == orig:
|
||||
return "patch"
|
||||
return "skip"
|
||||
|
||||
def guard_state_after(cur_before, orig, patch, wrote_ok, cur_after):
|
||||
"""Map a guarded-patch enforcement outcome to a per-pid guard STATE (pure).
|
||||
|
||||
Mirrors guarded_action's decision, extended with post-write verification so the
|
||||
caller advertises the capability only on VERIFIED. No /proc access -- unit-testable.
|
||||
|
||||
- cur_before == patch -> VERIFIED (already patched; guarded_action "noop")
|
||||
- cur_before == orig -> WRITE_FAILED if the write raised, else VERIFIED when the
|
||||
re-read is patch, else VERIFY_FAILED (guarded_action "patch")
|
||||
- otherwise -> UNSUPPORTED_BUILD (guarded_action "skip")
|
||||
"""
|
||||
if cur_before == patch:
|
||||
return GUARD_VERIFIED
|
||||
if cur_before == orig:
|
||||
if not wrote_ok:
|
||||
return GUARD_WRITE_FAILED
|
||||
if cur_after == patch:
|
||||
return GUARD_VERIFIED
|
||||
return GUARD_VERIFY_FAILED
|
||||
return GUARD_UNSUPPORTED_BUILD
|
||||
|
||||
patched=set()
|
||||
store_patched=set()
|
||||
guard_reported=set()
|
||||
|
||||
launcher_pid = None
|
||||
if "--launcher-pid" in sys.argv:
|
||||
try: launcher_pid = int(sys.argv[sys.argv.index("--launcher-pid") + 1])
|
||||
except (ValueError, IndexError): raise SystemExit("invalid --launcher-pid")
|
||||
if __name__ == "__main__":
|
||||
launcher_pid = None
|
||||
if "--launcher-pid" in sys.argv:
|
||||
try: launcher_pid = int(sys.argv[sys.argv.index("--launcher-pid") + 1])
|
||||
except (ValueError, IndexError): raise SystemExit("invalid --launcher-pid")
|
||||
|
||||
log("=== AUTOPATCH watching for FIFA17.exe ===")
|
||||
while True:
|
||||
if launcher_pid and not os.path.exists(f"/proc/{launcher_pid}"):
|
||||
log(f"launcher pid {launcher_pid} exited; stopping autopatch")
|
||||
break
|
||||
for pid in find_pids():
|
||||
if pid not in patched:
|
||||
try:
|
||||
g2=rd(pid,GATE2,3); g1=rd(pid,GATE1,6)
|
||||
except Exception:
|
||||
continue # code not mapped yet
|
||||
if g2==GATE2_PATCH and g1==GATE1_PATCH:
|
||||
log(f"pid {pid}: cert gates already patched"); patched.add(pid)
|
||||
elif g2==GATE2_ORIG and g1==GATE1_ORIG:
|
||||
log("=== AUTOPATCH watching for FIFA17.exe ===")
|
||||
while True:
|
||||
if launcher_pid and not os.path.exists(f"/proc/{launcher_pid}"):
|
||||
log(f"launcher pid {launcher_pid} exited; stopping autopatch")
|
||||
break
|
||||
for pid in find_pids():
|
||||
if pid not in patched:
|
||||
try:
|
||||
wr(pid,GATE2,GATE2_PATCH); wr(pid,GATE1,GATE1_PATCH)
|
||||
log(f"pid {pid}: PATCHED cert gates")
|
||||
patched.add(pid)
|
||||
g2=rd(pid,GATE2,3); g1=rd(pid,GATE1,6)
|
||||
except Exception:
|
||||
continue # code not mapped yet
|
||||
if g2==GATE2_PATCH and g1==GATE1_PATCH:
|
||||
log(f"pid {pid}: cert gates already patched"); patched.add(pid)
|
||||
elif g2==GATE2_ORIG and g1==GATE1_ORIG:
|
||||
try:
|
||||
wr(pid,GATE2,GATE2_PATCH); wr(pid,GATE1,GATE1_PATCH)
|
||||
log(f"pid {pid}: PATCHED cert gates")
|
||||
patched.add(pid)
|
||||
except Exception as e:
|
||||
log(f"pid {pid}: cert patch write failed: {e}")
|
||||
|
||||
# Continuously enforce store patches every tick
|
||||
cbase = cardsdll_base(pid)
|
||||
if cbase is not None:
|
||||
try:
|
||||
for va, data in STORE_PATCHES.items():
|
||||
live = cbase + (va - IMG_BASE)
|
||||
if rd(pid, live, len(data)) != data:
|
||||
wr(pid, live, data)
|
||||
log(f"pid {pid}: ENFORCED store patch @ {live:#x}")
|
||||
for va, (orig, patch) in STORE_PATCHES_GUARDED.items():
|
||||
live = cbase + (va - IMG_BASE)
|
||||
cur = rd(pid, live, len(patch))
|
||||
action = guarded_action(cur, orig, patch)
|
||||
wrote_ok = True
|
||||
cur_after = cur
|
||||
if action == "patch":
|
||||
try:
|
||||
wr(pid, live, patch)
|
||||
log(f"pid {pid}: ENFORCED guarded store patch @ {live:#x} (JNZ->JG, empty My Packs)")
|
||||
except Exception as e:
|
||||
wrote_ok = False
|
||||
log(f"pid {pid}: guarded patch write failed @ {live:#x}: {e}")
|
||||
if wrote_ok:
|
||||
try:
|
||||
cur_after = rd(pid, live, len(patch))
|
||||
except Exception:
|
||||
cur_after = b""
|
||||
elif action == "skip":
|
||||
log(f"pid {pid}: SKIP guarded patch @ {live:#x}: unexpected {cur.hex()} (build mismatch)")
|
||||
# action == "noop": already patched; nothing to write.
|
||||
if va == RESOLVER_GUARD_VA and pid not in guard_reported:
|
||||
state = guard_state_after(cur, orig, patch, wrote_ok, cur_after)
|
||||
if state == GUARD_VERIFIED:
|
||||
log(f"[store-guard] verified capability {EMPTY_MYPACKS_RESOLVER_CAPABILITY}={EMPTY_MYPACKS_RESOLVER_VERSION} fifa_pid={pid}")
|
||||
else:
|
||||
log(f"[store-guard] guard status={state} fifa_pid={pid} (no capability advertised)")
|
||||
guard_reported.add(pid)
|
||||
if pid not in store_patched:
|
||||
log(f"pid {pid}: PATCHED store gates in CardsDLL @ {cbase:#x}")
|
||||
store_patched.add(pid)
|
||||
except Exception as e:
|
||||
log(f"pid {pid}: cert patch write failed: {e}")
|
||||
log(f"pid {pid}: store patch write failed: {e}")
|
||||
|
||||
# Continuously enforce store patches every tick
|
||||
cbase = cardsdll_base(pid)
|
||||
if cbase is not None:
|
||||
try:
|
||||
for va, data in STORE_PATCHES.items():
|
||||
live = cbase + (va - IMG_BASE)
|
||||
if rd(pid, live, len(data)) != data:
|
||||
wr(pid, live, data)
|
||||
log(f"pid {pid}: ENFORCED store patch @ {live:#x}")
|
||||
if pid not in store_patched:
|
||||
log(f"pid {pid}: PATCHED store gates in CardsDLL @ {cbase:#x}")
|
||||
store_patched.add(pid)
|
||||
except Exception as e:
|
||||
log(f"pid {pid}: store patch write failed: {e}")
|
||||
|
||||
time.sleep(1)
|
||||
time.sleep(1)
|
||||
|
||||
@@ -55,6 +55,34 @@ verify_exports() {
|
||||
done
|
||||
}
|
||||
|
||||
# Refuse any DLL that is not a FIFA-17-profile build.
|
||||
#
|
||||
# openfut-hook builds TWO mutually exclusive injection paths from one crate: the
|
||||
# default (FIFA 23) path installs getaddrinfo/connect/ProtoSSL/origin hooks, while
|
||||
# `--features fifa17` installs ONLY the FIFA-17-safe logic (module map, FIFA 17
|
||||
# cert-verify, SBC dispatch, store tab bind). Deploying a default-feature build
|
||||
# into FIFA 17 hijacks the login transport and the client reports "Unable to
|
||||
# connect to the EA servers", with none of the FIFA 17 repairs present.
|
||||
#
|
||||
# That exact mistake happened on 2026-08-19 (artifact 1c71a17a, hand-built without
|
||||
# the feature): two failed launches, diagnosed only by comparing embedded strings.
|
||||
# `build` below passes the feature, but a hand-built DLL can reach `stage`/`deploy`
|
||||
# via OPENFUT_FIFA17_HOOK_DLL, so assert the profile on the bytes themselves.
|
||||
verify_fifa17_profile() {
|
||||
local dll=$1 marker
|
||||
# Markers that MUST be present: the FIFA 17 target module and its repairs.
|
||||
for marker in 'CardsDLL_Win64_retail.dll' 'SBC_DISPATCH'; do
|
||||
grep -qaF -- "$marker" "$dll" ||
|
||||
die "$dll is not a --features fifa17 build (missing $marker); refusing to stage/deploy"
|
||||
done
|
||||
# Markers that MUST be absent: the FIFA-23-only transport hooking.
|
||||
for marker in 'getaddrinfo IAT patched' 'connect: inline-hooked' 'origin_spy'; do
|
||||
if grep -qaF -- "$marker" "$dll"; then
|
||||
die "$dll contains FIFA-23-only hook '$marker'; build with --features fifa17"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
verify_inputs() {
|
||||
command -v sha256sum >/dev/null || die "sha256sum is required"
|
||||
command -v x86_64-w64-mingw32-objdump >/dev/null ||
|
||||
@@ -62,6 +90,7 @@ verify_inputs() {
|
||||
need_file "$hook_dll"
|
||||
need_file "$system_version"
|
||||
verify_pe64 "$hook_dll"
|
||||
verify_fifa17_profile "$hook_dll"
|
||||
}
|
||||
|
||||
inspect() {
|
||||
@@ -129,6 +158,7 @@ deploy() {
|
||||
need_file "$manifest"
|
||||
verify_pe64 "$staged"
|
||||
verify_exports "$staged"
|
||||
verify_fifa17_profile "$staged"
|
||||
local recorded actual
|
||||
recorded="$(awk -F= '$1=="artifact_sha256"{print $2}' "$manifest")"
|
||||
actual="$(sha256 "$staged")"
|
||||
@@ -158,7 +188,7 @@ launch() {
|
||||
local trace_enabled=0
|
||||
local request_trace_enabled=0
|
||||
local notifier_trace_enabled=0
|
||||
local commit_enabled=0
|
||||
local dispatch_enabled=0
|
||||
case "$mode" in
|
||||
baseline)
|
||||
[[ "${OPENFUT_FIFA17_LAUNCH:-}" == "I_ACCEPT_M1_BASELINE_LAUNCH" ]] ||
|
||||
@@ -177,14 +207,11 @@ launch() {
|
||||
request_trace_enabled=1
|
||||
notifier_trace_enabled=1
|
||||
;;
|
||||
commit)
|
||||
[[ "${OPENFUT_FIFA17_COMMIT:-}" == "I_ACCEPT_POST_PARSE_READY_BYTE" ]] ||
|
||||
die "launch-commit requires OPENFUT_FIFA17_COMMIT=I_ACCEPT_POST_PARSE_READY_BYTE"
|
||||
hook_enabled=1
|
||||
trace_enabled=1
|
||||
dispatch)
|
||||
[[ "${OPENFUT_FIFA17_DISPATCH:-}" == "I_ACCEPT_GUARDED_NATIVE_DISPATCH" ]] ||
|
||||
die "launch-dispatch requires OPENFUT_FIFA17_DISPATCH=I_ACCEPT_GUARDED_NATIVE_DISPATCH"
|
||||
request_trace_enabled=1
|
||||
notifier_trace_enabled=1
|
||||
commit_enabled=1
|
||||
dispatch_enabled=1
|
||||
;;
|
||||
*) die "unknown launch mode: $mode" ;;
|
||||
esac
|
||||
@@ -207,7 +234,7 @@ launch() {
|
||||
done
|
||||
mkdir -p "${wine_prefix}/dosdevices"
|
||||
ln -sfn /mnt "${wine_prefix}/dosdevices/w:"
|
||||
note "Launching $mode mode (SBC_HOOK=$hook_enabled; SBC_TRACE=$trace_enabled; SBC_REQUEST_TRACE=$request_trace_enabled; SBC_NOTIFIER_TRACE=$notifier_trace_enabled; SBC_COMMIT=$commit_enabled); log=/tmp/fifa17-hook-m1-launch.log"
|
||||
note "Launching $mode mode (SBC_HOOK=$hook_enabled; SBC_TRACE=$trace_enabled; SBC_REQUEST_TRACE=$request_trace_enabled; SBC_NOTIFIER_TRACE=$notifier_trace_enabled; SBC_DISPATCH=$dispatch_enabled); log=/tmp/fifa17-hook-m1-launch.log"
|
||||
cd "$game_dir"
|
||||
env \
|
||||
GAMEID=fifa17 \
|
||||
@@ -218,8 +245,8 @@ launch() {
|
||||
OPENFUT_SBC_TRACE="$trace_enabled" \
|
||||
OPENFUT_SBC_REQUEST_TRACE="$request_trace_enabled" \
|
||||
OPENFUT_SBC_NOTIFIER_TRACE="$notifier_trace_enabled" \
|
||||
OPENFUT_SBC_DISPATCH=0 \
|
||||
OPENFUT_SBC_COMMIT="$commit_enabled" \
|
||||
OPENFUT_SBC_DISPATCH="$dispatch_enabled" \
|
||||
OPENFUT_SBC_DISPATCH_TRACE=0 \
|
||||
OPENFUT_SBC_ARM_ONLY=0 \
|
||||
OPENFUT_SBC_POPULATE=0 \
|
||||
umu-run _fifa17.exe 2>&1 | tee /tmp/fifa17-hook-m1-launch.log
|
||||
@@ -227,7 +254,7 @@ launch() {
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: fifa17-hook-m1.sh [inspect|build|stage|deploy|launch|launch-resolve|launch-trace|launch-commit]
|
||||
Usage: fifa17-hook-m1.sh [inspect|build|stage|deploy|launch|launch-resolve|launch-trace|launch-dispatch]
|
||||
|
||||
inspect Read-only PE/hash/export preflight (default).
|
||||
build Cross-build the inert FIFA17 hook, then run inspect.
|
||||
@@ -240,11 +267,11 @@ Usage: fifa17-hook-m1.sh [inspect|build|stage|deploy|launch|launch-resolve|launc
|
||||
Start M2 resolve-only mode (guarded reads/logging, no detours/writes); requires:
|
||||
OPENFUT_FIFA17_RESOLVE=I_ACCEPT_M2_RESOLVE_LAUNCH
|
||||
launch-trace
|
||||
Start the single M3 passive factory/deserializer trace; requires:
|
||||
Start the M3-M6 passive parser/request/notifier trace; requires:
|
||||
OPENFUT_FIFA17_TRACE=I_ACCEPT_M3_PASSIVE_TRACE
|
||||
launch-commit
|
||||
Trace and arm the SBC cache only after a validated native parse; requires:
|
||||
OPENFUT_FIFA17_COMMIT=I_ACCEPT_POST_PARSE_READY_BYTE
|
||||
launch-dispatch
|
||||
Trace and repair only a fully validated native status-999 completion; requires:
|
||||
OPENFUT_FIFA17_DISPATCH=I_ACCEPT_GUARDED_NATIVE_DISPATCH
|
||||
|
||||
Optional path overrides:
|
||||
OPENFUT_FIFA17_HOOK_DLL, OPENFUT_FIFA17_GAME_DIR,
|
||||
@@ -260,7 +287,7 @@ case "${1:-inspect}" in
|
||||
launch) launch baseline ;;
|
||||
launch-resolve) launch resolve ;;
|
||||
launch-trace) launch trace ;;
|
||||
launch-commit) launch commit ;;
|
||||
launch-dispatch) launch dispatch ;;
|
||||
-h|--help|help) usage ;;
|
||||
*) usage >&2; die "unknown command: $1" ;;
|
||||
esac
|
||||
|
||||
@@ -162,6 +162,19 @@ def log(*a):
|
||||
print("[lsx]", *a, flush=True)
|
||||
|
||||
|
||||
def spawn_parent_watchdog():
|
||||
parent = os.getppid()
|
||||
|
||||
def _watch():
|
||||
while True:
|
||||
time.sleep(1)
|
||||
if os.getppid() != parent:
|
||||
log(f"launcher pid {parent} exited; stopping lsx")
|
||||
os._exit(0)
|
||||
|
||||
threading.Thread(target=_watch, daemon=True).start()
|
||||
|
||||
|
||||
_SECRET_ATTR_RE = re.compile(
|
||||
r'(?i)\b(AuthCode|AuthToken|SessionKey|Token|Sid)="[^"]*"')
|
||||
_AUTH_CODE_ATTR_RE = re.compile(r'(?i)\b(value|Code|Return)="[^"]*"')
|
||||
@@ -572,6 +585,7 @@ def serve(sock, addr):
|
||||
|
||||
|
||||
def main():
|
||||
spawn_parent_watchdog()
|
||||
s = socket.socket()
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind((os.environ.get("OPENFUT_BIND", "127.0.0.1"), 4216))
|
||||
|
||||
Reference in New Issue
Block a user