feat(fifa17): report verified client patch capability
autopatch side of the verified patched-client capability handshake: prove, at
runtime, that the empty-My-Packs resolver guard is active for a specific FIFA
process, and advertise it once on stdout for the launcher to relay.
- Add a per-pid guard verification state derived by a pure, testable
guard_state_after(cur_before, orig, patch, wrote_ok, cur_after) returning one
of VERIFIED / UNSUPPORTED_BUILD / WRITE_FAILED / VERIFY_FAILED (NOT_ATTEMPTED
is the pre-evaluation constant). VERIFIED means the live bytes at RVA 0x14858
are 7f 0f (JG) after enforcement (from an applied 75 0f->7f 0f, or already
patched). The existing fail-closed byte guard (guarded_action / STORE_PATCHES_
GUARDED) is unchanged — this only observes the outcome.
- Emit exactly once per FIFA pid: on VERIFIED,
[store-guard] verified capability fifa17.empty_mypacks_resolver=1 fifa_pid=<pid>
otherwise a non-advertising
[store-guard] guard status=<STATE> fifa_pid=<pid> (no capability advertised)
- Capability constants: EMPTY_MYPACKS_RESOLVER_VERSION=1, fully-qualified name
"fifa17.empty_mypacks_resolver".
- Tests: 5 guard-state cases + capability-constant assertions (standalone-runnable).
The capability = "the guard was verified in THIS FIFA process", never merely
"the code is present". Design: docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md.
This commit is contained in:
@@ -48,6 +48,21 @@ STORE_PATCHES_GUARDED = {
|
|||||||
0x180014858: (bytes.fromhex("750f"), bytes.fromhex("7f0f")), # JNZ 0x14869 -> JG 0x14869
|
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")
|
LOG=os.environ.get("OPENFUT_AUTOPATCH_LOG", f"/tmp/openfut-autopatch-{os.getuid()}.log")
|
||||||
|
|
||||||
def log(m):
|
def log(m):
|
||||||
@@ -89,8 +104,30 @@ def guarded_action(cur, orig, patch):
|
|||||||
return "patch"
|
return "patch"
|
||||||
return "skip"
|
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()
|
patched=set()
|
||||||
store_patched=set()
|
store_patched=set()
|
||||||
|
guard_reported=set()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
launcher_pid = None
|
launcher_pid = None
|
||||||
@@ -132,13 +169,30 @@ if __name__ == "__main__":
|
|||||||
live = cbase + (va - IMG_BASE)
|
live = cbase + (va - IMG_BASE)
|
||||||
cur = rd(pid, live, len(patch))
|
cur = rd(pid, live, len(patch))
|
||||||
action = guarded_action(cur, orig, patch)
|
action = guarded_action(cur, orig, patch)
|
||||||
if action == "noop":
|
wrote_ok = True
|
||||||
continue
|
cur_after = cur
|
||||||
if action == "patch":
|
if action == "patch":
|
||||||
wr(pid, live, patch)
|
try:
|
||||||
log(f"pid {pid}: ENFORCED guarded store patch @ {live:#x} (JNZ->JG, empty My Packs)")
|
wr(pid, live, patch)
|
||||||
else:
|
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)")
|
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:
|
if pid not in store_patched:
|
||||||
log(f"pid {pid}: PATCHED store gates in CardsDLL @ {cbase:#x}")
|
log(f"pid {pid}: PATCHED store gates in CardsDLL @ {cbase:#x}")
|
||||||
store_patched.add(pid)
|
store_patched.add(pid)
|
||||||
|
|||||||
@@ -32,7 +32,34 @@ def test_decision():
|
|||||||
assert autopatch.guarded_action(b"\x90", ORIG, PATCH) == "skip" # wrong length
|
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__":
|
if __name__ == "__main__":
|
||||||
test_table_exact()
|
test_table_exact()
|
||||||
test_decision()
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user