//! In-process RE probes: passive logging detours on FIFA's online-flow functions. //! //! Purpose: FIFA reacts to our pushed LSX events (OnlineStatusEvent/Login) but //! never starts the GetAuthCode→Nucleus→Blaze chain, and that decision lives in //! FIFA's game-side / in-process EbisuSDK logic that is invisible from the LSX //! wire. These probes log when the key online-flow functions are called (and //! their return values), so we can SEE where FIFA stalls after our events. //! //! Mechanism: the same unhook/rehook detour `connect_hook` uses — on entry we //! restore the original bytes, log, call the real function, then re-install the //! jump. This needs no trampoline/relocation, so it works even on prologues with //! RIP-relative operands (e.g. GoOnline). Not thread-safe (a concurrent call //! during the unhook window runs un-logged) but never corrupts the target — fine //! for read-only RE. //! //! Signature assumption: each probed fn takes ≤4 integer args (Win64: rcx/rdx/ //! r8/r9) and returns in rax. All targets here are SDK methods with few args. use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA; use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE}; // ─── live game-side listener capture ──────────────────────────────────────────── // // OnlineStatusEventT::HandleMessage dispatches the parsed bool to the game's online // listener via a virtual call `call [rax+0x28]` at FIFA23.exe+0x274d4e2, where rax // is the vtable of the object at [rsi-0x38]. The concrete listener is only known at // runtime. We capture it with a behavior-preserving mid-function detour: patch the // 14 bytes at +0x274d4d7 (which are exactly `lea rcx,[rsi-0x38]; mov rax,[rcx]; lea // rdx,[rbp-0x49]; call [rax+0x28]`) to jump to a stub that replicates those four // instructions but logs the resolved listener address in between, then resumes at // +0x274d4e5. Non-volatile regs (rsi/rbp/…) are preserved by the ABI; volatiles // match the original dispatch's clobbers. /// Runtime absolute address to resume at after the replicated dispatch /// (main-exe base + 0x274d4e5). Read by the asm stub. #[no_mangle] static mut RESUME_ADDR: u64 = 0; static MAIN_BASE: AtomicUsize = AtomicUsize::new(0); static LISTENER_LOGGED: AtomicBool = AtomicBool::new(false); /// Called by the stub with the listener object's vtable and the resolved listener /// function pointer (vtable[0x28]). Logs once (RVAs for static RE). unsafe extern "C" fn listener_log(vtable: usize, func: usize) { if LISTENER_LOGGED.swap(true, Ordering::Relaxed) { return; } let base = MAIN_BASE.load(Ordering::Relaxed); crate::write_log(&format!( "PROBE OnlineStatus.listener: vtable={vtable:#x} (rva {:#x}) fn={func:#x} (rva {:#x})\n", vtable.wrapping_sub(base), func.wrapping_sub(base), )); } core::arch::global_asm!( ".intel_syntax noprefix", ".global openfut_listener_stub", "openfut_listener_stub:", "lea rcx, [rsi - 0x38]", "mov rax, [rcx]", // rax = listener vtable "mov rdx, [rax + 0x28]", // rdx = listener fn (arg2) "mov rcx, rax", // rcx = vtable (arg1) // Align the stack to 16 before the call, saving the original rsp so we can // restore it (0x28 misaligns; SSE code in the logger then faults). "mov r11, rsp", "and rsp, -16", "sub rsp, 0x30", // 0x20 shadow + 0x10 spare, stays 16-aligned "mov [rsp + 0x20], r11", // stash original rsp "call {log}", "mov rsp, [rsp + 0x20]", // restore original rsp "lea rcx, [rsi - 0x38]", // replicate the original dispatch "mov rax, [rcx]", "lea rdx, [rbp - 0x49]", "call qword ptr [rax + 0x28]", "mov r10, qword ptr [rip + {resume}]", "jmp r10", ".att_syntax prefix", log = sym listener_log, resume = sym RESUME_ADDR, ); extern "C" { fn openfut_listener_stub(); } /// Patch the OnlineStatusEvent dispatch site to route through the logging stub. pub unsafe fn install_listener_probe() { let base = GetModuleHandleA(core::ptr::null()); if base.is_null() { crate::write_log("PROBE listener: main exe not found\n"); return; } let base = base as usize; MAIN_BASE.store(base, Ordering::Relaxed); RESUME_ADDR = (base + 0x274d4e5) as u64; let target = (base + 0x274d4d7) as *mut u8; write_jmp(target, openfut_listener_stub as usize as u64); crate::write_log(&format!("PROBE listener: dispatch site patched @ {:#x}\n", target as usize)); } struct Target { /// DLL name (nul-terminated) or ignored when `main_exe` is true. module: &'static [u8], /// Offset from the module base (RVA). For main_exe, VA − 0x140000000. rva: usize, label: &'static str, /// True → resolve against the main executable (FIFA23.exe) base. main_exe: bool, } /// Probe targets. Addresses recovered by RE (see docs/connection-gate-findings.md): /// - anadius64.dll GetInternetConnectedState / GoOnline — FIFA's in-process online /// decision points (anadius emulates EbisuSDK in-process). /// - FIFA23.exe OnlineStatusEvent/Login deserializers — confirm our pushed events /// actually reach FIFA's parser, and with what timing. const TARGETS: &[Target] = &[ Target { module: b"anadius64.dll\0", rva: 0x27790, label: "GetInternetConnectedState", main_exe: false }, Target { module: b"anadius64.dll\0", rva: 0x2bb90, label: "GoOnline", main_exe: false }, Target { module: b"\0", rva: 0x278a4d0, label: "OnlineStatus.deser", main_exe: true }, Target { module: b"\0", rva: 0x2787a30, label: "Login.deser", main_exe: true }, ]; const N: usize = 4; // must equal TARGETS.len() static ADDRS: [AtomicUsize; N] = [const { AtomicUsize::new(0) }; N]; static mut ORIG: [[u8; 14]; N] = [[0u8; 14]; N]; type ProbeFn = unsafe extern "system" fn(usize, usize, usize, usize) -> usize; const PROBE_FNS: [ProbeFn; N] = [p0, p1, p2, p3]; unsafe fn write_jmp(addr: *mut u8, dest: u64) { let mut old: u32 = 0; VirtualProtect(addr as _, 14, PAGE_EXECUTE_READWRITE, &mut old); // FF 25 00 00 00 00 JMP [rip+0] ; then absolute dest addr.write(0xFF); addr.add(1).write(0x25); (addr.add(2) as *mut u32).write(0); (addr.add(6) as *mut u64).write(dest); VirtualProtect(addr as _, 14, old, &mut old); } unsafe fn restore(slot: usize) { let addr = ADDRS[slot].load(Ordering::Relaxed) as *mut u8; let mut old: u32 = 0; VirtualProtect(addr as _, 14, PAGE_EXECUTE_READWRITE, &mut old); core::ptr::copy_nonoverlapping((&raw const ORIG[slot]) as *const u8, addr, 14); VirtualProtect(addr as _, 14, old, &mut old); } unsafe fn generic(slot: usize, a: usize, b: usize, c: usize, d: usize) -> usize { let label = TARGETS[slot].label; let addr = ADDRS[slot].load(Ordering::Relaxed) as *mut u8; // Unhook, log entry, call the real function, re-hook, log return. restore(slot); crate::write_log(&format!( "PROBE {label} enter rcx={a:#x} rdx={b:#x} r8={c:#x} r9={d:#x}\n" )); let f: ProbeFn = core::mem::transmute(addr); let r = f(a, b, c, d); write_jmp(addr, PROBE_FNS[slot] as u64); crate::write_log(&format!("PROBE {label} ret={r:#x}\n")); r } unsafe extern "system" fn p0(a: usize, b: usize, c: usize, d: usize) -> usize { generic(0, a, b, c, d) } unsafe extern "system" fn p1(a: usize, b: usize, c: usize, d: usize) -> usize { generic(1, a, b, c, d) } unsafe extern "system" fn p2(a: usize, b: usize, c: usize, d: usize) -> usize { generic(2, a, b, c, d) } unsafe extern "system" fn p3(a: usize, b: usize, c: usize, d: usize) -> usize { generic(3, a, b, c, d) } /// Spawn a background thread that waits for anadius64.dll to load, then installs /// all probes. anadius may not be present when our DllMain runs, so we defer /// off the loader lock and poll for it (up to ~30s) before installing. pub fn install_probes_deferred() { std::thread::spawn(|| unsafe { for _ in 0..60 { if !GetModuleHandleA(b"anadius64.dll\0".as_ptr()).is_null() { break; } std::thread::sleep(std::time::Duration::from_millis(500)); } install_probes(); install_listener_probe(); }); } /// Install all probe detours. Modules must already be loaded (call late in /// DllMain, after anadius64.dll is present). pub unsafe fn install_probes() { for (i, t) in TARGETS.iter().enumerate() { let base = if t.main_exe { GetModuleHandleA(core::ptr::null()) } else { GetModuleHandleA(t.module.as_ptr()) }; if base.is_null() { crate::write_log(&format!("PROBE {}: module not loaded, skipped\n", t.label)); continue; } let addr = (base as usize + t.rva) as *mut u8; core::ptr::copy_nonoverlapping(addr, (&raw mut ORIG[i]) as *mut u8, 14); ADDRS[i].store(addr as usize, Ordering::Relaxed); write_jmp(addr, PROBE_FNS[i] as u64); crate::write_log(&format!("PROBE {} installed @ {:#x}\n", t.label, addr as usize)); } }