probe: add behavior-preserving mid-function detour to capture the live OnlineStatusEvent listener

The OnlineStatusEventT::HandleMessage dispatch resolves its game-side
listener only at runtime (call [rax+0x28]). openfut_listener_stub patches
FIFA23.exe+0x274d4d7 to replicate the four dispatch instructions while
logging the resolved vtable/fn, then resumes. Alignment-safe (saves/rounds
rsp before the log call). Result: listener = FIFA23.exe+0x2751060 = ret 0,
a no-op default vtable slot -> the online->auth transition is state-polled,
not callback-driven.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-02 17:25:33 -07:00
parent feaff0443f
commit 3c3dc32fc6
+81 -1
View File
@@ -15,10 +15,89 @@
//!
//! 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::{AtomicUsize, Ordering};
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],
@@ -100,6 +179,7 @@ pub fn install_probes_deferred() {
std::thread::sleep(std::time::Duration::from_millis(500));
}
install_probes();
install_listener_probe();
});
}