Files
openfut-launcher/openfut-hook/src/fifa17.rs
T
funman300 9aecc658ad hook(fifa17): guarded store-entry category clamp (promoted)
The FUT store flashes a Browse-Packs overview on first open: the store
screen ctor leaves screen+0x290 (CATEGORY_ID) at 0, and the resolver
FUN_1800147f0 treats 0 as list-all, so the first render draws the group
overview before the movie posts a tab ordinal.

Detour the store render FUN_18007dab0 (RVA 0x7dab0): when the incoming
category is 0, substitute the first present group ordinal (1) so the
first frame lands on a real tab. Provably crash-safe: it writes 1 only
after FUN_180014420(_, 1) (the resolver's own ordinal->group lookup,
whose first arg is dead) returns non-NULL, which is exactly the
resolver's non-crash precondition; the positive-invalid NULL deref at
0x14882 is thus unreachable. No group yet -> category left 0 -> Browse,
still safe.

Promoted like the SBC dispatch: build-armed (CLAMP_PROMOTED), no env.
Signature-gated on both the detoured render and the called lookup,
image-validated, installed under thread suspension, fail-closed. Only
the overview flash is addressed; the empty-My-Packs entry dialog is
movie-side (packed .apt) and out of CardsDLL reach (see Vault
Store Resolver Guard 2026-08-19). fmt/clippy -D warnings clean both
feature sets, 26 hook tests pass, x86_64-pc-windows-gnu release builds.
2026-08-19 15:20:47 +00:00

110 lines
4.3 KiB
Rust

//! FIFA 17 injection path (feature = "fifa17").
//!
//! This is a *separate, minimal* entry point from the FIFA-23 `install_hooks`.
//! FIFA 17 is a different game with different in-memory structures, so we run NONE
//! of the FIFA-23 connect/LSX/origin_spy/dial logic here — that would at best
//! no-op and at worst crash. For now this proves the version.dll hijack actually
//! loads us into FIFA17.exe and dumps the module map, which we need to locate
//! DirtySDK/ProtoSSL's cert-verify function (the next milestone: patch it so the
//! secure Blaze redirector's TLS handshake succeeds against our bridge cert).
//!
//! Everything here is read-only except the (not-yet-enabled) cert-verify patch.
use crate::write_log;
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Module32FirstW, Module32NextW, MODULEENTRY32W, TH32CS_SNAPMODULE,
TH32CS_SNAPMODULE32,
};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
/// Read the SizeOfImage from a module's in-memory PE headers.
unsafe fn size_of_image(base: usize) -> u32 {
if base == 0 {
return 0;
}
// DOS header -> e_lfanew (i32 @ 0x3c) -> PE header. SizeOfImage is in the
// optional header at offset 0x50 from the PE signature (same for PE32/PE32+).
let e_lfanew = *((base + 0x3c) as *const i32);
let pe = base + e_lfanew as usize;
// sanity: 'PE\0\0'
if *(pe as *const u32) != 0x0000_4550 {
return 0;
}
*((pe + 24 + 0x38) as *const u32) // opt header +0x38 = SizeOfImage
}
fn wide_to_string(w: &[u16]) -> String {
let end = w.iter().position(|&c| c == 0).unwrap_or(w.len());
String::from_utf16_lossy(&w[..end])
}
/// Enumerate loaded modules (name, base, size) via ToolHelp and log them.
unsafe fn dump_modules() {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, 0);
if snap == INVALID_HANDLE_VALUE {
write_log("fifa17: module snapshot FAILED\n");
return;
}
let mut me: MODULEENTRY32W = core::mem::zeroed();
me.dwSize = core::mem::size_of::<MODULEENTRY32W>() as u32;
if Module32FirstW(snap, &mut me) != 0 {
loop {
let name = wide_to_string(&me.szModule);
let base = me.modBaseAddr as usize;
let size = me.modBaseSize;
write_log(&format!(
"fifa17: module {name:<28} base={base:#018x} size={size:#x}\n"
));
me.dwSize = core::mem::size_of::<MODULEENTRY32W>() as u32;
if Module32NextW(snap, &mut me) == 0 {
break;
}
}
} else {
write_log("fifa17: Module32FirstW FAILED\n");
}
CloseHandle(snap);
}
/// Worker that runs AFTER DllMain returns (loader lock released). ToolHelp and
/// other loader-touching calls are unsafe under the loader lock, so we defer them
/// to this thread. This is what fixed the "game exits right after DllMain" issue.
unsafe extern "system" fn worker(_: *mut core::ffi::c_void) -> u32 {
write_log("=== fifa17 hook: worker thread start ===\n");
let main_base = GetModuleHandleA(core::ptr::null()) as usize;
let img = size_of_image(main_base);
write_log(&format!(
"fifa17: main exe base={main_base:#018x} SizeOfImage={img:#x}\n"
));
dump_modules();
write_log("fifa17: worker complete (injection healthy)\n");
// The promoted SBC dispatch repair (and the evidence traces it decides on) arms
// itself from the build; its safety is the runtime signature/evidence gate. The
// remaining legacy experiment modules stay inert unless their env gate is `1`.
crate::sbc_hook::install();
crate::sbc_trace::install();
crate::sbc_dispatch::install();
crate::sbc_request_trace::install();
crate::store_entry::install();
0
}
/// Minimal FIFA-17 install. Keep DllMain itself trivial: only spawn a worker
/// thread and return immediately, so we never touch the loader lock from here.
pub unsafe fn install() {
use windows_sys::Win32::System::Threading::CreateThread;
write_log("=== fifa17 hook: DllMain ATTACH (spawning worker) ===\n");
let h = CreateThread(
core::ptr::null(),
0,
Some(worker),
core::ptr::null(),
0,
core::ptr::null_mut(),
);
if h == 0 as _ {
write_log("fifa17: CreateThread FAILED\n");
}
}