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:
@@ -15,10 +15,89 @@
|
|||||||
//!
|
//!
|
||||||
//! Signature assumption: each probed fn takes ≤4 integer args (Win64: rcx/rdx/
|
//! 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.
|
//! 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::LibraryLoader::GetModuleHandleA;
|
||||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
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 {
|
struct Target {
|
||||||
/// DLL name (nul-terminated) or ignored when `main_exe` is true.
|
/// DLL name (nul-terminated) or ignored when `main_exe` is true.
|
||||||
module: &'static [u8],
|
module: &'static [u8],
|
||||||
@@ -100,6 +179,7 @@ pub fn install_probes_deferred() {
|
|||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
}
|
}
|
||||||
install_probes();
|
install_probes();
|
||||||
|
install_listener_probe();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user