//! FIFA 17 in-process TLS/certificate + store crash-guard compatibility. //! //! Ports the *proven* subset of the external `openfut-autopatch` patch set into //! `version.dll`, so the client-local contract no longer needs an external //! `/proc`-writing patcher. Two concerns, both fail-closed and one-shot: //! //! 1. ProtoSSL certificate gates in FIFA17.exe (REQUIRED_FOR_TLS) — let the //! TLS handshake against the OpenFUT bridge cert succeed. Present only after //! the STEAMPUNKS packer maps/decrypts the real code, so they are polled for. //! 2. The empty-"My Packs" store resolver crash-guard in CardsDLL //! (REQUIRED_FOR_STORE_TLS, bug 6c) — CardsDLL loads lazily on entering UT, //! so it is applied once the module appears. //! //! Deliberately NOT ported: the eight unconditional `STORE_PATCHES` from the //! external patcher. They carry no recovered original bytes (cannot be //! fail-closed) and are re-applied every tick (would require the very //! constant-rewrite loop this milestone forbids); the external patcher's own //! source records no rationale for them. See the Vault ADR. //! //! Every address is ASLR-relocated from its preferred image base at runtime //! (`live = module_base + (static_va - preferred_base)`); nothing patches an //! absolute address. Every write goes through [`crate::patch_mem`]'s fail-closed //! primitive: original → write+verify, already-patched → no-op, anything else → //! logged and skipped. use crate::patch_mem::{self, ApplyOutcome, Mem, PatchState, WinMem}; use crate::write_log; use std::time::{Duration, Instant}; /// FIFA17.exe preferred image base (confirmed: futmem reports the client mapped /// flat at this base; Wine honours it, native Windows ASLR may not — hence the /// runtime-base + RVA model below). const FIFA17_PREFERRED_BASE: u64 = 0x1_4000_0000; /// CardsDLL_Win64_retail.dll preferred image base. const CARDS_PREFERRED_BASE: u64 = 0x1_8000_0000; /// Which module a site lives in. #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Module { Fifa17Exe, CardsDll, } impl Module { const fn preferred_base(self) -> u64 { match self { Module::Fifa17Exe => FIFA17_PREFERRED_BASE, Module::CardsDll => CARDS_PREFERRED_BASE, } } /// Runtime base of the loaded module, or `None` if not mapped yet. FIFA17.exe /// is the main image (null name); CardsDLL is resolved by its retail name. unsafe fn runtime_base(self) -> Option { match self { Module::Fifa17Exe => patch_mem::module_base(core::ptr::null()), Module::CardsDll => { patch_mem::module_base(c"CardsDLL_Win64_retail.dll".as_ptr().cast()) .or_else(|| patch_mem::module_base(c"CardsDLL.dll".as_ptr().cast())) } } } } /// One fail-closed byte patch, expressed as a static VA in its module's preferred /// image so the derivation `RVA = VA - preferred_base` is auditable. struct Site { module: Module, static_va: u64, orig: &'static [u8], patch: &'static [u8], label: &'static str, } impl Site { const fn rva(&self) -> u64 { patch_mem::rva(self.static_va, self.module.preferred_base()) } fn live_addr(&self, base: usize) -> usize { patch_mem::live_addr(base, self.rva()) } } // ── ProtoSSL certificate gates (FIFA17.exe) — REQUIRED_FOR_TLS ────────────────── // GATE1: JNZ rel32 -> 6×NOP (fall through the cert-verify failure branch). // GATE2: function prologue -> `xor eax,eax; ret` (cert-verify returns 0/false). // Applied as a pair, exactly like the external patcher: written only when BOTH // read their known original, treated as done when BOTH already hold the patch. const GATE1: Site = Site { module: Module::Fifa17Exe, static_va: 0x1_4613_2548, orig: &[0x0f, 0x85, 0x76, 0x01, 0x00, 0x00], patch: &[0x90, 0x90, 0x90, 0x90, 0x90, 0x90], label: "GATE1", }; const GATE2: Site = Site { module: Module::Fifa17Exe, static_va: 0x1_4613_61b0, orig: &[0x48, 0x89, 0x5c], patch: &[0x31, 0xc0, 0xc3], label: "GATE2", }; // ── Empty "My Packs" store resolver crash-guard (CardsDLL) — REQUIRED_FOR_STORE_TLS // JNZ 0x14869 (75 0f) -> JG 0x14869 (7f 0f): routes zero/negative store category // ids through the Browse path instead of a NULL deref. Fail-closed one-shot. const STORE_GUARD: Site = Site { module: Module::CardsDll, static_va: 0x1_8001_4858, orig: &[0x75, 0x0f], patch: &[0x7f, 0x0f], label: "empty-mypacks-store-guard", }; /// Poll cadence while waiting for the packer to unpack / CardsDLL to load. Low /// frequency: the thread sleeps between ticks, so idle CPU is negligible. const POLL: Duration = Duration::from_millis(250); /// Upper bound on the whole worker's lifetime so it can never spin forever if the /// user never enters Ultimate Team (CardsDLL never loads). const MAX_WAIT: Duration = Duration::from_secs(15 * 60); /// Decision for the FIFA17.exe cert-gate pair. #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum CertAction { /// Not both readable yet, or a mixed/unrecognised state — keep polling. Wait, /// Both gates hold their known original — safe to apply the pair. Apply, /// Both gates already hold the patch — nothing to do. Done, } /// Pure pairing rule (unit-tested): only act when both gates agree. fn cert_action(g1: Option, g2: Option) -> CertAction { match (g1, g2) { (Some(PatchState::AlreadyPatched), Some(PatchState::AlreadyPatched)) => CertAction::Done, (Some(PatchState::Original), Some(PatchState::Original)) => CertAction::Apply, _ => CertAction::Wait, } } /// Arm the FIFA17 TLS/store compatibility patcher: spawns a bounded background /// worker so it never touches the loader lock and never blocks `install()`. pub fn install() { std::thread::spawn(|| unsafe { worker() }); } unsafe fn worker() { write_log("fifa17_tls: patch worker start\n"); let mut mem = WinMem; let start = Instant::now(); let mut cert_done = false; let mut guard_done = false; // Throttle the "still waiting" diagnostics to one line each. let mut logged_cert_wait = false; let mut logged_guard_wait = false; loop { if !cert_done { cert_done = try_cert_gates(&mut mem, &mut logged_cert_wait); } if !guard_done { match Module::CardsDll.runtime_base() { Some(cbase) => guard_done = try_store_guard(&mut mem, cbase), None => { if !logged_guard_wait { write_log("fifa17_tls: waiting for CardsDLL (enter Ultimate Team)\n"); logged_guard_wait = true; } } } } if cert_done && guard_done { write_log("fifa17_tls: TLS patch set complete\n"); return; } if start.elapsed() >= MAX_WAIT { write_log(&format!( "fifa17_tls: worker stop (timeout {MAX_WAIT:?}); cert_gates_done={cert_done} store_guard_done={guard_done}\n" )); return; } std::thread::sleep(POLL); } } /// Apply the FIFA17.exe cert-gate pair. Returns `true` once the pair is settled /// (applied or already patched); `false` while still unpacking / not both ready. unsafe fn try_cert_gates(mem: &mut WinMem, logged_wait: &mut bool) -> bool { let base = match Module::Fifa17Exe.runtime_base() { Some(b) => b, None => return false, }; let g1_addr = GATE1.live_addr(base); let g2_addr = GATE2.live_addr(base); let g1 = patch_mem::read_state(mem, g1_addr, GATE1.orig, GATE1.patch); let g2 = patch_mem::read_state(mem, g2_addr, GATE2.orig, GATE2.patch); match cert_action(g1, g2) { CertAction::Done => { write_log("fifa17_tls: cert gates already patched\n"); true } CertAction::Apply => { let o1 = patch_mem::apply_checked(mem, g1_addr, GATE1.orig, GATE1.patch); let o2 = patch_mem::apply_checked(mem, g2_addr, GATE2.orig, GATE2.patch); if o1.is_patched() && o2.is_patched() { write_log(&format!( "fifa17_tls: PATCHED cert gates ({} @ {g1_addr:#x} {o1:?}; {} @ {g2_addr:#x} {o2:?})\n", GATE1.label, GATE2.label )); true } else { write_log(&format!( "fifa17_tls: cert gate write FAILED ({} {o1:?}; {} {o2:?}) — TLS NOT installed\n", GATE1.label, GATE2.label )); // Terminal: a write/verify failure will not fix itself by retrying. true } } CertAction::Wait => { if !*logged_wait { write_log(&format!( "fifa17_tls: cert gates not ready (still unpacking?) {}={g1:?} {}={g2:?}\n", GATE1.label, GATE2.label )); *logged_wait = true; } false } } } /// Apply the CardsDLL store crash-guard once CardsDLL is mapped. Returns `true` /// once the site is settled (its bytes are final the moment CardsDLL is loaded, /// so any read outcome is a terminal decision — no further polling). unsafe fn try_store_guard(mem: &mut WinMem, cbase: usize) -> bool { let addr = STORE_GUARD.live_addr(cbase); let outcome = patch_mem::apply_checked(mem, addr, STORE_GUARD.orig, STORE_GUARD.patch); match outcome { ApplyOutcome::NotReadable => false, // CardsDLL mapped but this page not yet — retry ApplyOutcome::Applied | ApplyOutcome::AlreadyPatched => { write_log(&format!( "fifa17_tls: store guard {} @ {addr:#x} {outcome:?} (VERIFIED empty-My-Packs)\n", STORE_GUARD.label )); true } ApplyOutcome::Mismatch => { let mut cur = [0u8; patch_mem::MAX_PATCH_LEN]; let n = STORE_GUARD.patch.len(); let seen = if mem.read(addr, &mut cur[..n]) { patch_mem::hex(&cur[..n]) } else { "unreadable".into() }; write_log(&format!( "fifa17_tls: SKIP store guard @ {addr:#x}: unexpected {seen} (build mismatch)\n" )); true } ApplyOutcome::WriteFailed | ApplyOutcome::VerifyFailed => { write_log(&format!( "fifa17_tls: store guard @ {addr:#x} {outcome:?}\n" )); true } } } #[cfg(test)] mod tests { use super::*; #[test] fn every_site_is_well_formed() { for s in [&GATE1, &GATE2, &STORE_GUARD] { assert_eq!( s.orig.len(), s.patch.len(), "{}: orig/patch length", s.label ); assert!(!s.orig.is_empty(), "{}: empty", s.label); assert!( s.patch.len() <= patch_mem::MAX_PATCH_LEN, "{}: exceeds MAX_PATCH_LEN", s.label ); assert_ne!(s.orig, s.patch, "{}: orig == patch", s.label); } } #[test] fn rvas_match_the_recovered_derivation() { assert_eq!(GATE1.rva(), 0x613_2548); assert_eq!(GATE2.rva(), 0x613_61b0); assert_eq!(STORE_GUARD.rva(), 0x1_4858); } #[test] fn live_addresses_track_the_runtime_base() { // At the preferred base the live address is the recorded static VA. assert_eq!(GATE1.live_addr(0x1_4000_0000), 0x1_4613_2548); assert_eq!(STORE_GUARD.live_addr(0x1_8000_0000), 0x1_8001_4858); // Relocated bases shift every site by the same delta. assert_eq!(GATE1.live_addr(0x3_0000_0000), 0x3_0613_2548); } #[test] fn cert_pair_only_acts_when_both_gates_agree() { use PatchState::*; assert_eq!( cert_action(Some(Original), Some(Original)), CertAction::Apply ); assert_eq!( cert_action(Some(AlreadyPatched), Some(AlreadyPatched)), CertAction::Done ); // Not yet unpacked / partial / mismatched => never a blind half-write. assert_eq!(cert_action(None, None), CertAction::Wait); assert_eq!(cert_action(Some(Original), None), CertAction::Wait); assert_eq!( cert_action(Some(Original), Some(AlreadyPatched)), CertAction::Wait ); assert_eq!( cert_action(Some(Mismatch), Some(Mismatch)), CertAction::Wait ); } }