diff --git a/fifa17-recon/tools/autopatch.py b/fifa17-recon/tools/autopatch.py index 28dc036..ed649e0 100755 --- a/fifa17-recon/tools/autopatch.py +++ b/fifa17-recon/tools/autopatch.py @@ -48,6 +48,21 @@ 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//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): @@ -89,8 +104,30 @@ def guarded_action(cur, orig, patch): 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() if __name__ == "__main__": launcher_pid = None @@ -132,13 +169,30 @@ if __name__ == "__main__": live = cbase + (va - IMG_BASE) cur = rd(pid, live, len(patch)) action = guarded_action(cur, orig, patch) - if action == "noop": - continue + wrote_ok = True + cur_after = cur if action == "patch": - wr(pid, live, patch) - log(f"pid {pid}: ENFORCED guarded store patch @ {live:#x} (JNZ->JG, empty My Packs)") - else: + 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) diff --git a/fifa17-recon/tools/test_autopatch_guard.py b/fifa17-recon/tools/test_autopatch_guard.py index e6c1932..ccd52e1 100755 --- a/fifa17-recon/tools/test_autopatch_guard.py +++ b/fifa17-recon/tools/test_autopatch_guard.py @@ -32,7 +32,34 @@ def test_decision(): assert autopatch.guarded_action(b"\x90", ORIG, PATCH) == "skip" # wrong length +def test_guard_state_after(): + # already patched (7f0f) -> VERIFIED (guarded_action "noop"); write args irrelevant. + assert autopatch.guard_state_after(PATCH, ORIG, PATCH, True, PATCH) == autopatch.GUARD_VERIFIED + # original (750f) + write ok + reread 7f0f -> VERIFIED (guarded_action "patch"). + assert autopatch.guard_state_after(ORIG, ORIG, PATCH, True, PATCH) == autopatch.GUARD_VERIFIED + # original + write FAILS -> WRITE_FAILED. + assert autopatch.guard_state_after(ORIG, ORIG, PATCH, False, ORIG) == autopatch.GUARD_WRITE_FAILED + # original + write ok but reread != 7f0f -> VERIFY_FAILED. + assert autopatch.guard_state_after(ORIG, ORIG, PATCH, True, ORIG) == autopatch.GUARD_VERIFY_FAILED + assert autopatch.guard_state_after(ORIG, ORIG, PATCH, True, b"") == autopatch.GUARD_VERIFY_FAILED + # unknown bytes -> UNSUPPORTED_BUILD (guarded_action "skip"); write args irrelevant. + assert autopatch.guard_state_after(b"\x00\x00", ORIG, PATCH, True, PATCH) == autopatch.GUARD_UNSUPPORTED_BUILD + + +def test_capability_constants(): + assert autopatch.EMPTY_MYPACKS_RESOLVER_VERSION == 1 + assert autopatch.EMPTY_MYPACKS_RESOLVER_CAPABILITY == "fifa17.empty_mypacks_resolver" + # State constant values are the exact tokens carried in the emitted status line. + assert autopatch.GUARD_VERIFIED == "VERIFIED" + assert autopatch.GUARD_UNSUPPORTED_BUILD == "UNSUPPORTED_BUILD" + assert autopatch.GUARD_WRITE_FAILED == "WRITE_FAILED" + assert autopatch.GUARD_VERIFY_FAILED == "VERIFY_FAILED" + assert autopatch.GUARD_NOT_ATTEMPTED == "NOT_ATTEMPTED" + + if __name__ == "__main__": test_table_exact() test_decision() - print("OK: autopatch guard table + fail-closed decision (PATCH/NOOP/SKIP)") + test_guard_state_after() + test_capability_constants() + print("OK: autopatch guard table + fail-closed decision + guard-state function + capability constants")