feat(hook): add SBC request tracing instrumentation

- sbc_hook.rs: trace SBC submission/response flow with request IDs
- sbc_request_trace.rs: capture request/response bodies for analysis
- sbc_trace.rs: runtime trace buffer with structured logging

Work in progress - needs validation against live FIFA 17 client
This commit is contained in:
funman300
2026-08-08 17:49:00 -07:00
parent 09ed26ba16
commit 958ff24546
3 changed files with 441 additions and 4 deletions
+148 -1
View File
@@ -32,6 +32,12 @@ const ABS_JUMP_LEN: usize = 14;
const TRAMPOLINE_LEN: usize = COPY_LEN + ABS_JUMP_LEN;
const NOTIFIER_RVA: usize = 0x17aa80;
const NOTIFIER_COPY_LEN: usize = 15;
const CONTROLLER_REGISTER_RVA: usize = 0x1a4a70;
const CONTROLLER_REGISTER_COPY_LEN: usize = 15;
const FUT_SBS_CATEGORIES_EVENT: u32 = 0x756c;
const CONTROLLER_REGISTER_SIGNATURE: [u8; CONTROLLER_REGISTER_COPY_LEN] = [
0x89, 0x54, 0x24, 0x10, 0x48, 0x83, 0xec, 0x28, 0x4c, 0x8d, 0x81, 0xc0, 0x00, 0x00, 0x00,
];
const NOTIFIER_SIGNATURE: [u8; 32] = [
0x48, 0x89, 0x5c, 0x24, 0x08, 0x48, 0x89, 0x74, 0x24, 0x10, 0x57, 0x48, 0x83, 0xec, 0x20, 0x48,
0x8b, 0x59, 0x58, 0x48, 0x8b, 0x71, 0x60, 0x33, 0xff, 0x48, 0x2b, 0xf3, 0xc6, 0x81, 0x88, 0x00,
@@ -75,6 +81,7 @@ static DESERIALIZER_EXIT_M: AtomicUsize = AtomicUsize::new(0);
static DESERIALIZER_EXIT_COUNT: AtomicUsize = AtomicUsize::new(0);
static DESERIALIZER_EXIT_B_READY: AtomicUsize = AtomicUsize::new(usize::MAX);
static NOTIFIER_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
static CONTROLLER_REGISTER_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
static NOTIFIER_ENTRIES: AtomicU64 = AtomicU64::new(0);
static NOTIFIER_EXITS: AtomicU64 = AtomicU64::new(0);
static NOTIFIER_CTX: AtomicUsize = AtomicUsize::new(0);
@@ -521,6 +528,77 @@ unsafe fn write_notifier_entry(
}
}
unsafe fn restore_controller_register_entry(target: usize) -> bool {
let mut old = 0u32;
if VirtualProtect(
target as _,
CONTROLLER_REGISTER_COPY_LEN,
PAGE_EXECUTE_READWRITE,
&mut old,
) == 0
{
return false;
}
core::ptr::copy_nonoverlapping(
CONTROLLER_REGISTER_SIGNATURE.as_ptr(),
target as *mut u8,
CONTROLLER_REGISTER_COPY_LEN,
);
let flushed = FlushInstructionCache(
GetCurrentProcess(),
target as _,
CONTROLLER_REGISTER_COPY_LEN,
) != 0;
let mut ignored = 0u32;
flushed && VirtualProtect(target as _, CONTROLLER_REGISTER_COPY_LEN, old, &mut ignored) != 0
}
unsafe fn write_controller_register_entry(
target: usize,
destination: usize,
published: &mut bool,
) -> Result<(), bool> {
let mut patch = [0x90u8; CONTROLLER_REGISTER_COPY_LEN];
patch[..ABS_JUMP_LEN].copy_from_slice(&absolute_jump(destination));
let mut old = 0u32;
if VirtualProtect(
target as _,
CONTROLLER_REGISTER_COPY_LEN,
PAGE_EXECUTE_READWRITE,
&mut old,
) == 0
{
return Err(true);
}
*published = true;
core::ptr::copy_nonoverlapping(
patch.as_ptr(),
target as *mut u8,
CONTROLLER_REGISTER_COPY_LEN,
);
let flushed = FlushInstructionCache(
GetCurrentProcess(),
target as _,
CONTROLLER_REGISTER_COPY_LEN,
) != 0;
let mut ignored = 0u32;
if flushed && VirtualProtect(target as _, CONTROLLER_REGISTER_COPY_LEN, old, &mut ignored) != 0
{
Ok(())
} else {
Err(restore_controller_register_entry(target))
}
}
unsafe extern "system" fn controller_register_wrapper(controller: *mut c_void, event: u32) {
let original: unsafe extern "system" fn(*mut c_void, u32) =
core::mem::transmute(CONTROLLER_REGISTER_TRAMPOLINE.load(Ordering::Acquire));
original(controller, event);
if event == FUT_SBS_CATEGORIES_EVENT {
crate::sbc_hook::note_sbc_controller(controller as usize);
}
}
unsafe extern "system" fn notifier_wrapper(ctx: *mut c_void) {
NOTIFIER_ENTRIES.fetch_add(1, Ordering::Relaxed);
let address = ctx as usize;
@@ -552,6 +630,7 @@ unsafe extern "system" fn notifier_wrapper(ctx: *mut c_void) {
let original: unsafe extern "system" fn(*mut c_void) =
core::mem::transmute(NOTIFIER_TRAMPOLINE.load(Ordering::Acquire));
original(ctx);
crate::sbc_hook::commit_after_native_parse();
NOTIFIER_BYTE_AFTER.store(
address
.checked_add(0x88)
@@ -946,10 +1025,78 @@ unsafe fn notifier_worker() {
crate::write_log("SBC_NOTIFIER_TRACE: report cap reached; hook remains passive\n");
}
unsafe fn controller_register_worker() {
let _pending = CodeInstallerPending;
let mut base = 0usize;
for _ in 0..600u32 {
base = GetModuleHandleA(b"CardsDLL_Win64_retail.dll\0".as_ptr()) as usize;
if base != 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
let Some(target) = target_va(base, CONTROLLER_REGISTER_RVA) else {
crate::write_log("SBC_CONTROLLER_TRACE: target resolution failed; inactive\n");
return;
};
if base == 0
|| !valid_cards_image(base)
|| !executable_range_in_image(base, target, CONTROLLER_REGISTER_SIGNATURE.len())
|| core::slice::from_raw_parts(target as *const u8, CONTROLLER_REGISTER_SIGNATURE.len())
!= CONTROLLER_REGISTER_SIGNATURE
{
crate::write_log("SBC_CONTROLLER_TRACE: PE/signature validation failed; inactive\n");
return;
}
let Some(trampoline) = allocate_trampoline(target, CONTROLLER_REGISTER_COPY_LEN) else {
crate::write_log("SBC_CONTROLLER_TRACE: trampoline allocation failed; inactive\n");
return;
};
CONTROLLER_REGISTER_TRAMPOLINE.store(trampoline, Ordering::Release);
let Some(_installer_gate) = acquire_patch_installer_gate() else {
VirtualFree(trampoline as _, 0, MEM_RELEASE);
CONTROLLER_REGISTER_TRAMPOLINE.store(0, Ordering::Release);
crate::write_log("SBC_CONTROLLER_TRACE: installer gate timeout; inactive\n");
return;
};
let mut peers = match suspend_peers(target, target) {
Ok(peers) => peers,
Err(_) => {
crate::write_log(
"SBC_CONTROLLER_TRACE: quiescence failed; terminate game if unresponsive\n",
);
return;
}
};
let mut published = false;
let installed = write_controller_register_entry(
target,
controller_register_wrapper as *const () as usize,
&mut published,
)
.is_ok();
let resumed = peers.resume_all();
drop(_installer_gate);
drop(_pending);
if !installed || !resumed {
if !published && resumed {
VirtualFree(trampoline as _, 0, MEM_RELEASE);
CONTROLLER_REGISTER_TRAMPOLINE.store(0, Ordering::Release);
crate::write_log("SBC_CONTROLLER_TRACE: clean install failure; inactive\n");
} else {
crate::write_log("SBC_CONTROLLER_TRACE: DEGRADED state; terminate game now\n");
}
return;
}
crate::write_log("SBC_CONTROLLER_TRACE: category controller registration hook installed\n");
}
fn install_notifier(enabled: bool) {
if enabled {
crate::write_log("SBC_NOTIFIER_TRACE: requested; deferred install starting\n");
std::thread::spawn(|| unsafe { notifier_worker() });
crate::write_log("SBC_CONTROLLER_TRACE: requested; deferred install starting\n");
std::thread::spawn(|| unsafe { controller_register_worker() });
} else {
crate::write_log("SBC_NOTIFIER_TRACE: disabled\n");
}
@@ -959,7 +1106,7 @@ pub(crate) fn install() {
let enabled = env_enabled(std::env::var("OPENFUT_SBC_TRACE").ok().as_deref());
let notifier_enabled = env_enabled(std::env::var("OPENFUT_SBC_NOTIFIER_TRACE").ok().as_deref());
CODE_PATCH_PENDING.store(
enabled as usize + notifier_enabled as usize,
enabled as usize + (notifier_enabled as usize * 2),
Ordering::Release,
);
install_notifier(notifier_enabled);