feat(fifa17-hook): patch FIFA17 TLS gates in-process
Milestone B: move FIFA17 ProtoSSL certificate compatibility into version.dll so
the client-local contract is openfut.cfg + LSX + version.dll with no external
/proc-writing patcher. The proven external openfut-autopatch remains the oracle
and is NOT removed; this reaches behavioral parity for the fail-closed patches.
Patch set (ASLR-relocated at runtime; fail-closed byte-verified; one-shot):
- FIFA17.exe ProtoSSL cert gates (REQUIRED_FOR_TLS), preferred base 0x140000000:
GATE1 rva 0x6132548 0f85 76010000 (JNZ) -> 90*6 (NOP)
GATE2 rva 0x61361b0 48 89 5c (prologue) -> 31 c0 c3 (xor eax,eax; ret)
Applied as a pair only when BOTH read their known original, exactly like the
external patcher's cert_pass; polled until the STEAMPUNKS packer unpacks them.
- CardsDLL empty-My-Packs store crash-guard (REQUIRED_FOR_STORE_TLS, bug 6c),
preferred base 0x180000000: rva 0x14858 75 0f (JNZ) -> 7f 0f (JG). Applied once
CardsDLL maps (module-late).
Deliberately NOT ported: the external patcher's 8 unconditional STORE_PATCHES.
They carry no recovered original bytes (cannot be fail-closed) and are re-applied
every tick (would require the constant-rewrite loop this milestone forbids); the
external source records no rationale for them. Documented in the Vault ADR.
Architecture:
- patch_mem.rs: generic fail-closed primitive over a Mem trait — classify
(ORIGINAL/ALREADY_PATCHED/MISMATCH), apply_checked (read->classify->write only on
ORIGINAL->reread verify), VirtualQuery-guarded read + VirtualProtect/Flush write
(WinMem). Trait abstraction makes every outcome host-testable without FIFA.
- fifa17_tls.rs: FIFA17-specific patch table + bounded poll worker (250ms, 15min
cap, no busy-spin) started from fifa17::install() after the network redirect.
Never patches an absolute address; never blind-writes on mismatch; a write/verify
failure is reported, never pretended.
Phase 11: removed the season_trace CACHE_PACKNAMES_FAILED->SUCCESS force-success
bypass (a staging-only behavior-changer that was armed unconditionally in the
candidate); season_trace is now genuinely read-only passive tracing. sbc_dispatch
and store_entry remain the intended REPAIR_PROMOTED fixes.
Tests: 39 hook tests (26 baseline + 13 new: classify states, apply/idempotence,
no-blind-write on mismatch, unreadable-module wait, write-failure reporting, RVA/
live-addr relocation across bases, cert-gate pairing, patch-table integrity).
clippy --features fifa17 -D warnings clean; fmt clean; x86_64-pc-windows-gnu
cross-build. No network-config authority added (routing stays Milestone A).
Runtime validation (x64dbg site check + Windows/Linux retail) still outstanding.
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
//! flow. Targets are chosen so their copied prologues are position-independent
|
||||
//! (no rip-relative / rel32 in the copied bytes).
|
||||
|
||||
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache;
|
||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
|
||||
@@ -28,8 +28,6 @@ use crate::sbc_trace::{
|
||||
use crate::write_log;
|
||||
|
||||
static REPORTS: AtomicUsize = AtomicUsize::new(0);
|
||||
/// One-shot guard for the staging-only CACHE_PACKNAMES_FAILED -> SUCCESS bypass.
|
||||
static BYPASS_DONE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
unsafe fn rd_i32(addr: usize) -> Option<i32> {
|
||||
readable_range(addr, 4).then(|| core::ptr::read_volatile(addr as *const i32))
|
||||
@@ -390,18 +388,6 @@ unsafe extern "system" fn final_completion_wrapper(
|
||||
"SEASONS_LOAD_CALLBACK: final kind={kind} result={shown:?} flag={flag:?} ctx={ctx:#x} cbref={cbref:#x}\n"
|
||||
));
|
||||
}
|
||||
// Guarded one-shot bypass (staging diagnostic only): rewrite the pack-names
|
||||
// failure to SUCCESS so the offline-season load advances to
|
||||
// LoadCurrentOfflineSeason. Fires only for the exact CACHE_PACKNAMES failure,
|
||||
// once per process; verified by the error string before touching memory.
|
||||
if flag == Some(0)
|
||||
&& errstr.contains("CACHE_PACKNAMES")
|
||||
&& readable_range(result, 1)
|
||||
&& !BYPASS_DONE.swap(true, Ordering::AcqRel)
|
||||
{
|
||||
core::ptr::write_volatile(result as *mut u8, 1u8); // take the SUCCESS branch
|
||||
write_log("SEASONS_BYPASS: forced CACHE_PACKNAMES_FAILED -> SUCCESS (one-shot, staging)\n");
|
||||
}
|
||||
let t = FINAL_COMPLETION_TRAMP.load(Ordering::Acquire);
|
||||
if t == 0 {
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user