wip: checkpoint FIFA 17 hook diagnostics
This commit is contained in:
@@ -5,27 +5,27 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
const AF_INET: u16 = 2;
|
const AF_INET: u16 = 2;
|
||||||
const PORT_HTTPS_NBO: u16 = 0xBB01; // 443 big-endian
|
const PORT_HTTPS_NBO: u16 = 0xBB01; // 443 big-endian
|
||||||
const PORT_BRIDGE_NBO: u16 = 0xFB20; // 8443 big-endian
|
const PORT_BRIDGE_NBO: u16 = 0xFB20; // 8443 big-endian
|
||||||
const PORT_BLAZE_REDIRECTOR_NBO: u16 = 0x3927; // 10041 big-endian
|
const PORT_BLAZE_REDIRECTOR_NBO: u16 = 0x3927; // 10041 big-endian
|
||||||
const PORT_BLAZE_MAIN_NBO: u16 = 0x8FA4; // 42127 big-endian
|
const PORT_BLAZE_MAIN_NBO: u16 = 0x8FA4; // 42127 big-endian
|
||||||
// EA App LSX. anadius handles :3216 in-process before it reaches the host TCP
|
// EA App LSX. anadius handles :3216 in-process before it reaches the host TCP
|
||||||
// stack (keyed on port 3216 specifically), so redirecting FIFA's LSX connect to a
|
// stack (keyed on port 3216 specifically), so redirecting FIFA's LSX connect to a
|
||||||
// *different* host port (:3217) slips past that interception and lands on the
|
// *different* host port (:3217) slips past that interception and lands on the
|
||||||
// native openfut-bridge LSX server. This is the load-bearing redirect that routes
|
// native openfut-bridge LSX server. This is the load-bearing redirect that routes
|
||||||
// LSX to our bridge; without it FIFA uses anadius's in-process emu instead.
|
// LSX to our bridge; without it FIFA uses anadius's in-process emu instead.
|
||||||
#[allow(dead_code)] // unused when built with the `capture_baseline` feature
|
#[allow(dead_code)] // unused when built with the `capture_baseline` feature
|
||||||
const PORT_LSX_NBO: u16 = 0x900C; // 3216 big-endian (EA App LSX)
|
const PORT_LSX_NBO: u16 = 0x900C; // 3216 big-endian (EA App LSX)
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const PORT_LSX_TARGET_NBO: u16 = 0x910C; // 3217 big-endian (bridge LSX target)
|
const PORT_LSX_TARGET_NBO: u16 = 0x910C; // 3217 big-endian (bridge LSX target)
|
||||||
const ADDR_LOOPBACK_NBO: u32 = 0x0100_007F; // 127.0.0.1 big-endian
|
const ADDR_LOOPBACK_NBO: u32 = 0x0100_007F; // 127.0.0.1 big-endian
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct SockaddrIn {
|
struct SockaddrIn {
|
||||||
sin_family: u16,
|
sin_family: u16,
|
||||||
sin_port: u16,
|
sin_port: u16,
|
||||||
sin_addr: u32,
|
sin_addr: u32,
|
||||||
sin_zero: [u8; 8],
|
sin_zero: [u8; 8],
|
||||||
}
|
}
|
||||||
|
|
||||||
const AF_INET6: u16 = 23; // Windows AF_INET6 value (we run under the Win ABI in Wine)
|
const AF_INET6: u16 = 23; // Windows AF_INET6 value (we run under the Win ABI in Wine)
|
||||||
@@ -34,10 +34,10 @@ const AF_INET6: u16 = 23; // Windows AF_INET6 value (we run under the Win ABI in
|
|||||||
/// address bytes in network order. 28 bytes total.
|
/// address bytes in network order. 28 bytes total.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct SockaddrIn6 {
|
struct SockaddrIn6 {
|
||||||
sin6_family: u16,
|
sin6_family: u16,
|
||||||
sin6_port: u16,
|
sin6_port: u16,
|
||||||
sin6_flowinfo: u32,
|
sin6_flowinfo: u32,
|
||||||
sin6_addr: [u8; 16],
|
sin6_addr: [u8; 16],
|
||||||
sin6_scope_id: u32,
|
sin6_scope_id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +46,7 @@ struct SockaddrIn6 {
|
|||||||
/// existing IPv4 listener on :8443 — no separate IPv6 listener needed. The game's own
|
/// existing IPv4 listener on :8443 — no separate IPv6 listener needed. The game's own
|
||||||
/// EA dials already use v4-mapped addresses (`::ffff:x.x.x.x`), so its sockets are not
|
/// EA dials already use v4-mapped addresses (`::ffff:x.x.x.x`), so its sockets are not
|
||||||
/// `IPV6_V6ONLY` and will accept this target.
|
/// `IPV6_V6ONLY` and will accept this target.
|
||||||
const V4MAPPED_LOOPBACK: [u8; 16] =
|
const V4MAPPED_LOOPBACK: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1];
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1];
|
|
||||||
|
|
||||||
// Address of ws2_32!connect (set at hook installation)
|
// Address of ws2_32!connect (set at hook installation)
|
||||||
static CONNECT_ADDR: AtomicUsize = AtomicUsize::new(0);
|
static CONNECT_ADDR: AtomicUsize = AtomicUsize::new(0);
|
||||||
@@ -57,18 +56,26 @@ static mut ORIGINAL_BYTES: [u8; 14] = [0u8; 14];
|
|||||||
|
|
||||||
// For WSAConnect IAT fallback
|
// For WSAConnect IAT fallback
|
||||||
type WsaConnectFn = unsafe extern "system" fn(
|
type WsaConnectFn = unsafe extern "system" fn(
|
||||||
s: usize, name: *const u8, namelen: i32,
|
s: usize,
|
||||||
caller: *const (), callee: *const (),
|
name: *const u8,
|
||||||
sqos: *const (), gqos: *const ()) -> i32;
|
namelen: i32,
|
||||||
|
caller: *const (),
|
||||||
|
callee: *const (),
|
||||||
|
sqos: *const (),
|
||||||
|
gqos: *const (),
|
||||||
|
) -> i32;
|
||||||
static REAL_WSA: OnceLock<WsaConnectFn> = OnceLock::new();
|
static REAL_WSA: OnceLock<WsaConnectFn> = OnceLock::new();
|
||||||
pub fn set_real_wsa_connect(f: WsaConnectFn) { let _ = REAL_WSA.set(f); }
|
pub fn set_real_wsa_connect(f: WsaConnectFn) {
|
||||||
|
let _ = REAL_WSA.set(f);
|
||||||
|
}
|
||||||
|
|
||||||
unsafe fn write_hook(target: *mut u8, dest: u64) {
|
unsafe fn write_hook(target: *mut u8, dest: u64) {
|
||||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||||
let mut old: u32 = 0;
|
let mut old: u32 = 0;
|
||||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||||
// FF 25 00 00 00 00 JMP [rip+0]
|
// FF 25 00 00 00 00 JMP [rip+0]
|
||||||
target.write(0xFF); target.add(1).write(0x25);
|
target.write(0xFF);
|
||||||
|
target.add(1).write(0x25);
|
||||||
(target.add(2) as *mut u32).write(0u32);
|
(target.add(2) as *mut u32).write(0u32);
|
||||||
(target.add(6) as *mut u64).write(dest);
|
(target.add(6) as *mut u64).write(dest);
|
||||||
VirtualProtect(target as _, 14, old, &mut old);
|
VirtualProtect(target as _, 14, old, &mut old);
|
||||||
@@ -78,7 +85,7 @@ unsafe fn restore_original(target: *mut u8) {
|
|||||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||||
let mut old: u32 = 0;
|
let mut old: u32 = 0;
|
||||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||||
core::ptr::copy_nonoverlapping(ORIGINAL_BYTES.as_ptr(), target, 14);
|
core::ptr::copy_nonoverlapping(core::ptr::addr_of!(ORIGINAL_BYTES) as *const u8, target, 14);
|
||||||
VirtualProtect(target as _, 14, old, &mut old);
|
VirtualProtect(target as _, 14, old, &mut old);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,26 +110,30 @@ pub(crate) unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u
|
|||||||
// SAFE: family is AF_INET and namelen >= 8 == the sockaddr_in fields we read.
|
// SAFE: family is AF_INET and namelen >= 8 == the sockaddr_in fields we read.
|
||||||
let sa = &*(name as *const SockaddrIn);
|
let sa = &*(name as *const SockaddrIn);
|
||||||
let new_port_nbo = match sa.sin_port {
|
let new_port_nbo = match sa.sin_port {
|
||||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
||||||
#[cfg(not(feature = "capture_baseline"))]
|
#[cfg(not(feature = "capture_baseline"))]
|
||||||
PORT_LSX_NBO => PORT_LSX_TARGET_NBO,
|
PORT_LSX_NBO => PORT_LSX_TARGET_NBO,
|
||||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
||||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
// sin_addr is network order; to_le_bytes gives memory order = the dotted
|
// sin_addr is network order; to_le_bytes gives memory order = the dotted
|
||||||
// quad, so b[0].b[1].b[2].b[3] is correct (the old code printed it reversed).
|
// quad, so b[0].b[1].b[2].b[3] is correct (the old code printed it reversed).
|
||||||
let o = sa.sin_addr.to_le_bytes();
|
let o = sa.sin_addr.to_le_bytes();
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"connect_hook: v4 {}.{}.{}.{}:{} → 127.0.0.1:{}\n",
|
"connect_hook: v4 {}.{}.{}.{}:{} → 127.0.0.1:{}\n",
|
||||||
o[0], o[1], o[2], o[3], u16::from_be(sa.sin_port),
|
o[0],
|
||||||
|
o[1],
|
||||||
|
o[2],
|
||||||
|
o[3],
|
||||||
|
u16::from_be(sa.sin_port),
|
||||||
u16::from_be(new_port_nbo)
|
u16::from_be(new_port_nbo)
|
||||||
));
|
));
|
||||||
// SAFE: buf is 28 bytes, larger than the 16-byte sockaddr_in we write.
|
// SAFE: buf is 28 bytes, larger than the 16-byte sockaddr_in we write.
|
||||||
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn);
|
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn);
|
||||||
out.sin_family = AF_INET;
|
out.sin_family = AF_INET;
|
||||||
out.sin_port = new_port_nbo;
|
out.sin_port = new_port_nbo;
|
||||||
out.sin_addr = ADDR_LOOPBACK_NBO;
|
out.sin_addr = ADDR_LOOPBACK_NBO;
|
||||||
Some((buf, 16))
|
Some((buf, 16))
|
||||||
}
|
}
|
||||||
AF_INET6 => {
|
AF_INET6 => {
|
||||||
@@ -133,23 +144,27 @@ pub(crate) unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u
|
|||||||
let sa6 = &*(name as *const SockaddrIn6);
|
let sa6 = &*(name as *const SockaddrIn6);
|
||||||
// LSX is IPv4-only (anadius keys on it), so it is intentionally omitted here.
|
// LSX is IPv4-only (anadius keys on it), so it is intentionally omitted here.
|
||||||
let new_port_nbo = match sa6.sin6_port {
|
let new_port_nbo = match sa6.sin6_port {
|
||||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
||||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
||||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let a = sa6.sin6_addr;
|
let a = sa6.sin6_addr;
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"connect_hook: v6 [{:02x}{:02x}:..:{:02x}{:02x}]:{} → ::ffff:127.0.0.1:{}\n",
|
"connect_hook: v6 [{:02x}{:02x}:..:{:02x}{:02x}]:{} → ::ffff:127.0.0.1:{}\n",
|
||||||
a[0], a[1], a[14], a[15], u16::from_be(sa6.sin6_port),
|
a[0],
|
||||||
|
a[1],
|
||||||
|
a[14],
|
||||||
|
a[15],
|
||||||
|
u16::from_be(sa6.sin6_port),
|
||||||
u16::from_be(new_port_nbo)
|
u16::from_be(new_port_nbo)
|
||||||
));
|
));
|
||||||
// SAFE: buf is exactly 28 bytes == sizeof(sockaddr_in6).
|
// SAFE: buf is exactly 28 bytes == sizeof(sockaddr_in6).
|
||||||
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn6);
|
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn6);
|
||||||
out.sin6_family = AF_INET6;
|
out.sin6_family = AF_INET6;
|
||||||
out.sin6_port = new_port_nbo;
|
out.sin6_port = new_port_nbo;
|
||||||
out.sin6_flowinfo = 0;
|
out.sin6_flowinfo = 0;
|
||||||
out.sin6_addr = V4MAPPED_LOOPBACK;
|
out.sin6_addr = V4MAPPED_LOOPBACK;
|
||||||
out.sin6_scope_id = 0;
|
out.sin6_scope_id = 0;
|
||||||
Some((buf, 28))
|
Some((buf, 28))
|
||||||
}
|
}
|
||||||
@@ -174,7 +189,13 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen:
|
|||||||
use windows_sys::Win32::Networking::WinSock::{getsockopt, SOL_SOCKET, SO_TYPE};
|
use windows_sys::Win32::Networking::WinSock::{getsockopt, SOL_SOCKET, SO_TYPE};
|
||||||
let mut ty: i32 = -1;
|
let mut ty: i32 = -1;
|
||||||
let mut len: i32 = 4;
|
let mut len: i32 = 4;
|
||||||
getsockopt(s, SOL_SOCKET as i32, SO_TYPE, &mut ty as *mut i32 as *mut u8, &mut len);
|
getsockopt(
|
||||||
|
s,
|
||||||
|
SOL_SOCKET as i32,
|
||||||
|
SO_TYPE,
|
||||||
|
&mut ty as *mut i32 as *mut u8,
|
||||||
|
&mut len,
|
||||||
|
);
|
||||||
ty
|
ty
|
||||||
};
|
};
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
@@ -190,11 +211,11 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen:
|
|||||||
let (call_name, call_len) = if let Some((buf, len)) = redirect_if_ea(name, namelen) {
|
let (call_name, call_len) = if let Some((buf, len)) = redirect_if_ea(name, namelen) {
|
||||||
restore_original(addr);
|
restore_original(addr);
|
||||||
let r = {
|
let r = {
|
||||||
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32
|
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32 =
|
||||||
= core::mem::transmute(addr);
|
core::mem::transmute(addr);
|
||||||
f(s, buf.as_ptr(), len)
|
f(s, buf.as_ptr(), len)
|
||||||
};
|
};
|
||||||
write_hook(addr, hooked_connect as u64);
|
write_hook(addr, hooked_connect as *const () as u64);
|
||||||
return r;
|
return r;
|
||||||
} else {
|
} else {
|
||||||
(name, namelen)
|
(name, namelen)
|
||||||
@@ -202,18 +223,19 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen:
|
|||||||
|
|
||||||
restore_original(addr);
|
restore_original(addr);
|
||||||
let r = {
|
let r = {
|
||||||
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32
|
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32 = core::mem::transmute(addr);
|
||||||
= core::mem::transmute(addr);
|
|
||||||
f(s, call_name, call_len)
|
f(s, call_name, call_len)
|
||||||
};
|
};
|
||||||
write_hook(addr, hooked_connect as u64);
|
write_hook(addr, hooked_connect as *const () as u64);
|
||||||
if namelen >= 8 {
|
if namelen >= 8 {
|
||||||
let sa = &*(call_name as *const SockaddrIn);
|
let sa = &*(call_name as *const SockaddrIn);
|
||||||
if sa.sin_family == AF_INET {
|
if sa.sin_family == AF_INET {
|
||||||
let err = if r != 0 {
|
let err = if r != 0 {
|
||||||
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
|
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
|
||||||
WSAGetLastError()
|
WSAGetLastError()
|
||||||
} else { 0 };
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
crate::write_log(&format!("connect_hook: result={r} wsa_err={err}\n"));
|
crate::write_log(&format!("connect_hook: result={r} wsa_err={err}\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,9 +243,13 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen:
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe extern "system" fn hooked_wsa_connect(
|
pub unsafe extern "system" fn hooked_wsa_connect(
|
||||||
s: usize, name: *const u8, namelen: i32,
|
s: usize,
|
||||||
caller: *const (), callee: *const (),
|
name: *const u8,
|
||||||
sqos: *const (), gqos: *const (),
|
namelen: i32,
|
||||||
|
caller: *const (),
|
||||||
|
callee: *const (),
|
||||||
|
sqos: *const (),
|
||||||
|
gqos: *const (),
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
// Milestone-0 transport watch (self-gates on OPENFUT_TRANSPORT_WATCH).
|
// Milestone-0 transport watch (self-gates on OPENFUT_TRANSPORT_WATCH).
|
||||||
crate::transport_watch::note_connect("WSAConnect", name, namelen, s);
|
crate::transport_watch::note_connect("WSAConnect", name, namelen, s);
|
||||||
@@ -240,17 +266,23 @@ pub unsafe fn install_inline_connect_hook() -> bool {
|
|||||||
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||||
|
|
||||||
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
||||||
if ws2.is_null() { return false; }
|
if ws2.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let connect_fn = match GetProcAddress(ws2, b"connect\0".as_ptr()) {
|
let connect_fn = match GetProcAddress(ws2, b"connect\0".as_ptr()) {
|
||||||
Some(f) => f as *mut u8,
|
Some(f) => f as *mut u8,
|
||||||
None => return false,
|
None => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save original 14 bytes
|
// Save original 14 bytes
|
||||||
core::ptr::copy_nonoverlapping(connect_fn, ORIGINAL_BYTES.as_mut_ptr(), 14);
|
core::ptr::copy_nonoverlapping(
|
||||||
|
connect_fn,
|
||||||
|
core::ptr::addr_of_mut!(ORIGINAL_BYTES) as *mut u8,
|
||||||
|
14,
|
||||||
|
);
|
||||||
CONNECT_ADDR.store(connect_fn as usize, Ordering::Relaxed);
|
CONNECT_ADDR.store(connect_fn as usize, Ordering::Relaxed);
|
||||||
|
|
||||||
// Overwrite first 14 bytes with absolute indirect JMP to our hook
|
// Overwrite first 14 bytes with absolute indirect JMP to our hook
|
||||||
write_hook(connect_fn, hooked_connect as u64);
|
write_hook(connect_fn, hooked_connect as *const () as u64);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
use core::ffi::c_void;
|
||||||
/// Intercepts ConnectEx (EA/DirtySDK's preferred async connect API).
|
/// Intercepts ConnectEx (EA/DirtySDK's preferred async connect API).
|
||||||
///
|
///
|
||||||
/// DirtySDK calls WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, WSAID_CONNECTEX) once at
|
/// DirtySDK calls WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, WSAID_CONNECTEX) once at
|
||||||
/// startup to get a ConnectEx function pointer, bypassing all IAT hooks. We hook WSAIoctl
|
/// startup to get a ConnectEx function pointer, bypassing all IAT hooks. We hook WSAIoctl
|
||||||
/// inline so that when it returns a ConnectEx pointer we swap it for our own wrapper.
|
/// inline so that when it returns a ConnectEx pointer we swap it for our own wrapper.
|
||||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use core::ffi::c_void;
|
|
||||||
|
|
||||||
// Address rewriting (v4 + v6) is shared from connect_hook::redirect_if_ea, so the port
|
// Address rewriting (v4 + v6) is shared from connect_hook::redirect_if_ea, so the port
|
||||||
// constants and sockaddr structs no longer live here.
|
// constants and sockaddr structs no longer live here.
|
||||||
@@ -14,9 +14,7 @@ const SIO_GET_EXT_FN: u32 = 0xC8000006;
|
|||||||
|
|
||||||
// WSAID_CONNECTEX = {25A207B9-DDF3-4660-8EE9-76E58C74063E}
|
// WSAID_CONNECTEX = {25A207B9-DDF3-4660-8EE9-76E58C74063E}
|
||||||
const CONNECTEX_GUID: [u8; 16] = [
|
const CONNECTEX_GUID: [u8; 16] = [
|
||||||
0xB9, 0x07, 0xA2, 0x25,
|
0xB9, 0x07, 0xA2, 0x25, 0xF3, 0xDD, 0x60, 0x46, 0x8E, 0xE9, 0x76, 0xE5, 0x8C, 0x74, 0x06, 0x3E,
|
||||||
0xF3, 0xDD, 0x60, 0x46,
|
|
||||||
0x8E, 0xE9, 0x76, 0xE5, 0x8C, 0x74, 0x06, 0x3E,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// The real ConnectEx pointer, saved after WSAIoctl returns it
|
// The real ConnectEx pointer, saved after WSAIoctl returns it
|
||||||
@@ -53,7 +51,8 @@ unsafe fn write_hook(target: *mut u8, dest: u64) {
|
|||||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||||
let mut old: u32 = 0;
|
let mut old: u32 = 0;
|
||||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||||
target.write(0xFF); target.add(1).write(0x25);
|
target.write(0xFF);
|
||||||
|
target.add(1).write(0x25);
|
||||||
(target.add(2) as *mut u32).write(0u32);
|
(target.add(2) as *mut u32).write(0u32);
|
||||||
(target.add(6) as *mut u64).write(dest);
|
(target.add(6) as *mut u64).write(dest);
|
||||||
VirtualProtect(target as _, 14, old, &mut old);
|
VirtualProtect(target as _, 14, old, &mut old);
|
||||||
@@ -63,7 +62,7 @@ unsafe fn restore_wsaioctl(target: *mut u8) {
|
|||||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||||
let mut old: u32 = 0;
|
let mut old: u32 = 0;
|
||||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||||
core::ptr::copy_nonoverlapping(WSAIOCTL_ORIG.as_ptr(), target, 14);
|
core::ptr::copy_nonoverlapping(core::ptr::addr_of!(WSAIOCTL_ORIG) as *const u8, target, 14);
|
||||||
VirtualProtect(target as _, 14, old, &mut old);
|
VirtualProtect(target as _, 14, old, &mut old);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,9 +84,25 @@ unsafe extern "system" fn hooked_connectex(
|
|||||||
// Share the one redirect implementation (v4 + v6) with connect_hook, so ConnectEx
|
// Share the one redirect implementation (v4 + v6) with connect_hook, so ConnectEx
|
||||||
// dials get the same IPv6 handling as plain connect().
|
// dials get the same IPv6 handling as plain connect().
|
||||||
if let Some((buf, len)) = crate::connect_hook::redirect_if_ea(name, namelen) {
|
if let Some((buf, len)) = crate::connect_hook::redirect_if_ea(name, namelen) {
|
||||||
return real_fn(s, buf.as_ptr(), len, send_buf, send_data_len, bytes_sent, overlapped);
|
return real_fn(
|
||||||
|
s,
|
||||||
|
buf.as_ptr(),
|
||||||
|
len,
|
||||||
|
send_buf,
|
||||||
|
send_data_len,
|
||||||
|
bytes_sent,
|
||||||
|
overlapped,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
real_fn(s, name, namelen, send_buf, send_data_len, bytes_sent, overlapped)
|
real_fn(
|
||||||
|
s,
|
||||||
|
name,
|
||||||
|
namelen,
|
||||||
|
send_buf,
|
||||||
|
send_data_len,
|
||||||
|
bytes_sent,
|
||||||
|
overlapped,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Our WSAIoctl hook: when ConnectEx is requested, save the real pointer and return ours
|
/// Our WSAIoctl hook: when ConnectEx is requested, save the real pointer and return ours
|
||||||
@@ -108,28 +123,28 @@ pub unsafe extern "system" fn hooked_wsaioctl(
|
|||||||
restore_wsaioctl(addr);
|
restore_wsaioctl(addr);
|
||||||
let result = {
|
let result = {
|
||||||
let f: WsaIoctlFn = core::mem::transmute(addr);
|
let f: WsaIoctlFn = core::mem::transmute(addr);
|
||||||
f(s, code, in_buf, in_len, out_buf, out_len, bytes_ret, overlapped, completion)
|
f(
|
||||||
|
s, code, in_buf, in_len, out_buf, out_len, bytes_ret, overlapped, completion,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
write_hook(addr, hooked_wsaioctl as u64);
|
write_hook(addr, hooked_wsaioctl as *const () as u64);
|
||||||
|
|
||||||
// If this was a ConnectEx request that succeeded, swap the pointer
|
// If this was a ConnectEx request that succeeded, swap the pointer
|
||||||
if result == 0
|
if result == 0 && code == SIO_GET_EXT_FN && in_len == 16 && !in_buf.is_null() {
|
||||||
&& code == SIO_GET_EXT_FN
|
|
||||||
&& in_len == 16
|
|
||||||
&& !in_buf.is_null()
|
|
||||||
{
|
|
||||||
let guid = core::slice::from_raw_parts(in_buf as *const u8, 16);
|
let guid = core::slice::from_raw_parts(in_buf as *const u8, 16);
|
||||||
if guid == CONNECTEX_GUID
|
if guid == CONNECTEX_GUID && out_len >= 8 && !out_buf.is_null() {
|
||||||
&& out_len >= 8
|
|
||||||
&& !out_buf.is_null()
|
|
||||||
{
|
|
||||||
let out_ptr = out_buf as *mut usize;
|
let out_ptr = out_buf as *mut usize;
|
||||||
let real_addr = *out_ptr;
|
let real_addr = *out_ptr;
|
||||||
if REAL_CONNECTEX.compare_exchange(0, real_addr, Ordering::Relaxed, Ordering::Relaxed).is_ok() {
|
if REAL_CONNECTEX
|
||||||
crate::write_log(&format!("connectex_hook: intercepted ConnectEx @ {real_addr:#x}\n"));
|
.compare_exchange(0, real_addr, Ordering::Relaxed, Ordering::Relaxed)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
crate::write_log(&format!(
|
||||||
|
"connectex_hook: intercepted ConnectEx @ {real_addr:#x}\n"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// Return our hook instead
|
// Return our hook instead
|
||||||
*out_ptr = hooked_connectex as usize;
|
*out_ptr = hooked_connectex as *const () as usize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
@@ -138,13 +153,19 @@ pub unsafe extern "system" fn hooked_wsaioctl(
|
|||||||
pub unsafe fn install_wsaioctl_hook() -> bool {
|
pub unsafe fn install_wsaioctl_hook() -> bool {
|
||||||
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||||
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
||||||
if ws2.is_null() { return false; }
|
if ws2.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let fn_ptr = match GetProcAddress(ws2, b"WSAIoctl\0".as_ptr()) {
|
let fn_ptr = match GetProcAddress(ws2, b"WSAIoctl\0".as_ptr()) {
|
||||||
Some(f) => f as *mut u8,
|
Some(f) => f as *mut u8,
|
||||||
None => return false,
|
None => return false,
|
||||||
};
|
};
|
||||||
core::ptr::copy_nonoverlapping(fn_ptr, WSAIOCTL_ORIG.as_mut_ptr(), 14);
|
core::ptr::copy_nonoverlapping(
|
||||||
|
fn_ptr,
|
||||||
|
core::ptr::addr_of_mut!(WSAIOCTL_ORIG) as *mut u8,
|
||||||
|
14,
|
||||||
|
);
|
||||||
WSAIOCTL_ADDR.store(fn_ptr as usize, Ordering::Relaxed);
|
WSAIOCTL_ADDR.store(fn_ptr as usize, Ordering::Relaxed);
|
||||||
write_hook(fn_ptr, hooked_wsaioctl as u64);
|
write_hook(fn_ptr, hooked_wsaioctl as *const () as u64);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -1,19 +1,15 @@
|
|||||||
use std::{
|
use std::{
|
||||||
ffi::CStr,
|
ffi::CStr,
|
||||||
sync::{
|
sync::{
|
||||||
OnceLock,
|
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
|
OnceLock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use windows_sys::Win32::Networking::WinSock::{ADDRINFOA, getaddrinfo as sys_getaddrinfo};
|
use windows_sys::Win32::Networking::WinSock::{getaddrinfo as sys_getaddrinfo, ADDRINFOA};
|
||||||
|
|
||||||
type GetaddrinfoFn = unsafe extern "system" fn(
|
type GetaddrinfoFn =
|
||||||
*const u8,
|
unsafe extern "system" fn(*const u8, *const u8, *const ADDRINFOA, *mut *mut ADDRINFOA) -> i32;
|
||||||
*const u8,
|
|
||||||
*const ADDRINFOA,
|
|
||||||
*mut *mut ADDRINFOA,
|
|
||||||
) -> i32;
|
|
||||||
|
|
||||||
static REAL: OnceLock<GetaddrinfoFn> = OnceLock::new();
|
static REAL: OnceLock<GetaddrinfoFn> = OnceLock::new();
|
||||||
static REDIRECT_IP: OnceLock<Vec<u8>> = OnceLock::new();
|
static REDIRECT_IP: OnceLock<Vec<u8>> = OnceLock::new();
|
||||||
@@ -62,9 +58,13 @@ pub unsafe extern "system" fn hooked_getaddrinfo(
|
|||||||
if !CERT_PATCHED.load(Ordering::Relaxed) {
|
if !CERT_PATCHED.load(Ordering::Relaxed) {
|
||||||
if crate::ssl_patch::patch_eawebkit_cert_verify() {
|
if crate::ssl_patch::patch_eawebkit_cert_verify() {
|
||||||
CERT_PATCHED.store(true, Ordering::Relaxed);
|
CERT_PATCHED.store(true, Ordering::Relaxed);
|
||||||
crate::write_log("openfut_hook: ProtoSSL cert-verify patched (lazy, from getaddrinfo)\n");
|
crate::write_log(
|
||||||
|
"openfut_hook: ProtoSSL cert-verify patched (lazy, from getaddrinfo)\n",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
crate::write_log("openfut_hook: ProtoSSL cert-verify patch FAILED in getaddrinfo\n");
|
crate::write_log(
|
||||||
|
"openfut_hook: ProtoSSL cert-verify patch FAILED in getaddrinfo\n",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-7
@@ -72,7 +72,11 @@ pub unsafe fn patch_iat(original_fn: *const (), hook_fn: *const ()) -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Patch the IAT of a specific already-loaded DLL (e.g. b"EAWebKit.dll\0").
|
/// Patch the IAT of a specific already-loaded DLL (e.g. b"EAWebKit.dll\0").
|
||||||
pub unsafe fn patch_iat_in(module_name: &[u8], original_fn: *const (), hook_fn: *const ()) -> usize {
|
pub unsafe fn patch_iat_in(
|
||||||
|
module_name: &[u8],
|
||||||
|
original_fn: *const (),
|
||||||
|
hook_fn: *const (),
|
||||||
|
) -> usize {
|
||||||
let module = GetModuleHandleA(module_name.as_ptr());
|
let module = GetModuleHandleA(module_name.as_ptr());
|
||||||
if module.is_null() {
|
if module.is_null() {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -80,11 +84,7 @@ pub unsafe fn patch_iat_in(module_name: &[u8], original_fn: *const (), hook_fn:
|
|||||||
patch_module(module, original_fn, hook_fn)
|
patch_module(module, original_fn, hook_fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn patch_module(
|
unsafe fn patch_module(module: HMODULE, original_fn: *const (), hook_fn: *const ()) -> usize {
|
||||||
module: HMODULE,
|
|
||||||
original_fn: *const (),
|
|
||||||
hook_fn: *const (),
|
|
||||||
) -> usize {
|
|
||||||
if module.is_null() {
|
if module.is_null() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,12 @@ unsafe fn patch_module(
|
|||||||
if val == original_fn as usize {
|
if val == original_fn as usize {
|
||||||
let target = iat_slot.add(i) as *const std::ffi::c_void;
|
let target = iat_slot.add(i) as *const std::ffi::c_void;
|
||||||
let mut old: u32 = 0;
|
let mut old: u32 = 0;
|
||||||
VirtualProtect(target, std::mem::size_of::<usize>(), PAGE_EXECUTE_READWRITE, &mut old);
|
VirtualProtect(
|
||||||
|
target,
|
||||||
|
std::mem::size_of::<usize>(),
|
||||||
|
PAGE_EXECUTE_READWRITE,
|
||||||
|
&mut old,
|
||||||
|
);
|
||||||
*iat_slot.add(i) = hook_fn as usize;
|
*iat_slot.add(i) = hook_fn as usize;
|
||||||
VirtualProtect(target, std::mem::size_of::<usize>(), old, &mut old);
|
VirtualProtect(target, std::mem::size_of::<usize>(), old, &mut old);
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|||||||
@@ -27,28 +27,49 @@ static REAL_REG_W: OnceLock<RegQueryValueExWFn> = OnceLock::new();
|
|||||||
static REAL_MUTEX_A: OnceLock<OpenMutexAFn> = OnceLock::new();
|
static REAL_MUTEX_A: OnceLock<OpenMutexAFn> = OnceLock::new();
|
||||||
static REAL_MUTEX_W: OnceLock<OpenMutexWFn> = OnceLock::new();
|
static REAL_MUTEX_W: OnceLock<OpenMutexWFn> = OnceLock::new();
|
||||||
|
|
||||||
pub fn set_real_reg_a(f: RegQueryValueExAFn) { let _ = REAL_REG_A.set(f); }
|
pub fn set_real_reg_a(f: RegQueryValueExAFn) {
|
||||||
pub fn set_real_reg_w(f: RegQueryValueExWFn) { let _ = REAL_REG_W.set(f); }
|
let _ = REAL_REG_A.set(f);
|
||||||
pub fn set_real_mutex_a(f: OpenMutexAFn) { let _ = REAL_MUTEX_A.set(f); }
|
}
|
||||||
pub fn set_real_mutex_w(f: OpenMutexWFn) { let _ = REAL_MUTEX_W.set(f); }
|
pub fn set_real_reg_w(f: RegQueryValueExWFn) {
|
||||||
|
let _ = REAL_REG_W.set(f);
|
||||||
|
}
|
||||||
|
pub fn set_real_mutex_a(f: OpenMutexAFn) {
|
||||||
|
let _ = REAL_MUTEX_A.set(f);
|
||||||
|
}
|
||||||
|
pub fn set_real_mutex_w(f: OpenMutexWFn) {
|
||||||
|
let _ = REAL_MUTEX_W.set(f);
|
||||||
|
}
|
||||||
|
|
||||||
fn narrow_to_string(p: *const u8) -> String {
|
fn narrow_to_string(p: *const u8) -> String {
|
||||||
if p.is_null() { return "(null)".into(); }
|
if p.is_null() {
|
||||||
|
return "(null)".into();
|
||||||
|
}
|
||||||
let bytes = unsafe { std::ffi::CStr::from_ptr(p as *const i8) };
|
let bytes = unsafe { std::ffi::CStr::from_ptr(p as *const i8) };
|
||||||
bytes.to_string_lossy().into_owned()
|
bytes.to_string_lossy().into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wide_to_string(p: *const u16) -> String {
|
fn wide_to_string(p: *const u16) -> String {
|
||||||
if p.is_null() { return "(null)".into(); }
|
if p.is_null() {
|
||||||
|
return "(null)".into();
|
||||||
|
}
|
||||||
let mut len = 0usize;
|
let mut len = 0usize;
|
||||||
unsafe { while *p.add(len) != 0 { len += 1; } }
|
unsafe {
|
||||||
|
while *p.add(len) != 0 {
|
||||||
|
len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(p, len) })
|
String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(p, len) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_interesting(name: &str) -> bool {
|
fn is_interesting(name: &str) -> bool {
|
||||||
name.contains("LSX") || name.contains("Origin") || name.contains("EAL") ||
|
name.contains("LSX")
|
||||||
name.contains("Client") || name.contains("lsx") || name.contains("Port") ||
|
|| name.contains("Origin")
|
||||||
name.contains("EA") || name.contains("Connection")
|
|| name.contains("EAL")
|
||||||
|
|| name.contains("Client")
|
||||||
|
|| name.contains("lsx")
|
||||||
|
|| name.contains("Port")
|
||||||
|
|| name.contains("EA")
|
||||||
|
|| name.contains("Connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe extern "system" fn hooked_reg_query_a(
|
pub unsafe extern "system" fn hooked_reg_query_a(
|
||||||
@@ -93,8 +114,10 @@ pub unsafe extern "system" fn hooked_open_mutex_a(
|
|||||||
let name = narrow_to_string(lpmutexname);
|
let name = narrow_to_string(lpmutexname);
|
||||||
let real = REAL_MUTEX_A.get().copied().unwrap();
|
let real = REAL_MUTEX_A.get().copied().unwrap();
|
||||||
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
||||||
crate::write_log(&format!("origin_spy: OpenMutexA({name}) → {}\n",
|
crate::write_log(&format!(
|
||||||
if handle == 0 { "NOT_FOUND" } else { "FOUND" }));
|
"origin_spy: OpenMutexA({name}) → {}\n",
|
||||||
|
if handle == 0 { "NOT_FOUND" } else { "FOUND" }
|
||||||
|
));
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +129,9 @@ pub unsafe extern "system" fn hooked_open_mutex_w(
|
|||||||
let name = wide_to_string(lpmutexname);
|
let name = wide_to_string(lpmutexname);
|
||||||
let real = REAL_MUTEX_W.get().copied().unwrap();
|
let real = REAL_MUTEX_W.get().copied().unwrap();
|
||||||
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
||||||
crate::write_log(&format!("origin_spy: OpenMutexW({name}) → {}\n",
|
crate::write_log(&format!(
|
||||||
if handle == 0 { "NOT_FOUND" } else { "FOUND" }));
|
"origin_spy: OpenMutexW({name}) → {}\n",
|
||||||
|
if handle == 0 { "NOT_FOUND" } else { "FOUND" }
|
||||||
|
));
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|||||||
+247
-82
@@ -17,11 +17,11 @@
|
|||||||
//! 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::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
|
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
|
||||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
|
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
|
||||||
use windows_sys::Win32::System::Threading::GetCurrentThreadId;
|
|
||||||
use windows_sys::Win32::System::Memory::{
|
use windows_sys::Win32::System::Memory::{
|
||||||
VirtualProtect, VirtualQuery, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_PRIVATE,
|
VirtualProtect, VirtualQuery, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_PRIVATE,
|
||||||
PAGE_EXECUTE_READWRITE, PAGE_GUARD, PAGE_NOACCESS,
|
PAGE_EXECUTE_READWRITE, PAGE_GUARD, PAGE_NOACCESS,
|
||||||
};
|
};
|
||||||
|
use windows_sys::Win32::System::Threading::GetCurrentThreadId;
|
||||||
|
|
||||||
/// Fault-safe pointer read: returns None unless `ptr` lands in a committed, readable
|
/// Fault-safe pointer read: returns None unless `ptr` lands in a committed, readable
|
||||||
/// page (checked via VirtualQuery). Avoids crashing FIFA when we sample pointers that
|
/// page (checked via VirtualQuery). Avoids crashing FIFA when we sample pointers that
|
||||||
@@ -31,7 +31,11 @@ unsafe fn read_ptr(ptr: usize) -> Option<usize> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
||||||
let n = VirtualQuery(ptr as _, &mut mbi, core::mem::size_of::<MEMORY_BASIC_INFORMATION>());
|
let n = VirtualQuery(
|
||||||
|
ptr as _,
|
||||||
|
&mut mbi,
|
||||||
|
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
||||||
|
);
|
||||||
if n == 0 || mbi.State != MEM_COMMIT {
|
if n == 0 || mbi.State != MEM_COMMIT {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -165,14 +169,18 @@ pub unsafe fn install_listener_probe() {
|
|||||||
// Arm the dial trigger from the env var, ONCE, at install (DLL-load) time. Default
|
// Arm the dial trigger from the env var, ONCE, at install (DLL-load) time. Default
|
||||||
// disarmed: OPENFUT_DIAL_TRIGGER must be explicitly "1". Orthogonal to the pump/ctx
|
// disarmed: OPENFUT_DIAL_TRIGGER must be explicitly "1". Orthogonal to the pump/ctx
|
||||||
// env vars.
|
// env vars.
|
||||||
let armed = std::env::var("OPENFUT_DIAL_TRIGGER").map(|v| v == "1").unwrap_or(false);
|
let armed = std::env::var("OPENFUT_DIAL_TRIGGER")
|
||||||
|
.map(|v| v == "1")
|
||||||
|
.unwrap_or(false);
|
||||||
DIAL_ARMED.store(armed, Ordering::Relaxed);
|
DIAL_ARMED.store(armed, Ordering::Relaxed);
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"DIAL_TRIGGER: {} (env OPENFUT_DIAL_TRIGGER)\n",
|
"DIAL_TRIGGER: {} (env OPENFUT_DIAL_TRIGGER)\n",
|
||||||
if armed { "ARMED" } else { "disarmed" }
|
if armed { "ARMED" } else { "disarmed" }
|
||||||
));
|
));
|
||||||
// Arm the (independent) connMgr enumeration from its own env var, once, at load.
|
// Arm the (independent) connMgr enumeration from its own env var, once, at load.
|
||||||
let enum_armed = std::env::var("OPENFUT_CONNMGR_ENUM").map(|v| v == "1").unwrap_or(false);
|
let enum_armed = std::env::var("OPENFUT_CONNMGR_ENUM")
|
||||||
|
.map(|v| v == "1")
|
||||||
|
.unwrap_or(false);
|
||||||
CONNMGR_ENUM_ARMED.store(enum_armed, Ordering::Relaxed);
|
CONNMGR_ENUM_ARMED.store(enum_armed, Ordering::Relaxed);
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"CONNMGR_ENUM: {} (env OPENFUT_CONNMGR_ENUM)\n",
|
"CONNMGR_ENUM: {} (env OPENFUT_CONNMGR_ENUM)\n",
|
||||||
@@ -180,16 +188,25 @@ pub unsafe fn install_listener_probe() {
|
|||||||
));
|
));
|
||||||
// Arm the (independent) [element+0x40] container-writer watchpoint from its own
|
// Arm the (independent) [element+0x40] container-writer watchpoint from its own
|
||||||
// env var, once, at load. Orthogonal to DIAL_TRIGGER / CONNMGR_ENUM.
|
// env var, once, at load. Orthogonal to DIAL_TRIGGER / CONNMGR_ENUM.
|
||||||
let elem_watch_armed = std::env::var("OPENFUT_ELEM_WATCH").map(|v| v == "1").unwrap_or(false);
|
let elem_watch_armed = std::env::var("OPENFUT_ELEM_WATCH")
|
||||||
|
.map(|v| v == "1")
|
||||||
|
.unwrap_or(false);
|
||||||
ELEM_WATCH_ARMED.store(elem_watch_armed, Ordering::Relaxed);
|
ELEM_WATCH_ARMED.store(elem_watch_armed, Ordering::Relaxed);
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"ELEM_WATCH: {} (env OPENFUT_ELEM_WATCH)\n",
|
"ELEM_WATCH: {} (env OPENFUT_ELEM_WATCH)\n",
|
||||||
if elem_watch_armed { "ARMED" } else { "disarmed" }
|
if elem_watch_armed {
|
||||||
|
"ARMED"
|
||||||
|
} else {
|
||||||
|
"disarmed"
|
||||||
|
}
|
||||||
));
|
));
|
||||||
RESUME_ADDR = (base + 0x274d4e5) as u64;
|
RESUME_ADDR = (base + 0x274d4e5) as u64;
|
||||||
let target = (base + 0x274d4d7) as *mut u8;
|
let target = (base + 0x274d4d7) as *mut u8;
|
||||||
write_jmp(target, openfut_listener_stub as usize as u64);
|
write_jmp(target, openfut_listener_stub as usize as u64);
|
||||||
crate::write_log(&format!("PROBE listener: dispatch site patched @ {:#x}\n", target as usize));
|
crate::write_log(&format!(
|
||||||
|
"PROBE listener: dispatch site patched @ {:#x}\n",
|
||||||
|
target as usize
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── dial trigger (sub-phase B) ──────────────────────────────────────────────────
|
// ─── dial trigger (sub-phase B) ──────────────────────────────────────────────────
|
||||||
@@ -262,7 +279,9 @@ fn observe_completion() {
|
|||||||
let c = crate::dial_notification::completion_stub_call_count();
|
let c = crate::dial_notification::completion_stub_call_count();
|
||||||
let last = LAST_COMPLETION_COUNT.swap(c, Ordering::Relaxed);
|
let last = LAST_COMPLETION_COUNT.swap(c, Ordering::Relaxed);
|
||||||
if c != last {
|
if c != last {
|
||||||
crate::write_log(&format!("DIAL_TRIGGER: completion stub count changed {last} → {c}\n"));
|
crate::write_log(&format!(
|
||||||
|
"DIAL_TRIGGER: completion stub count changed {last} → {c}\n"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,11 +348,17 @@ unsafe fn dial_trigger_tick() {
|
|||||||
|
|
||||||
// Step 5 — resolve connMgr (reuse the ctx-dump scan + tiebreaker).
|
// Step 5 — resolve connMgr (reuse the ctx-dump scan + tiebreaker).
|
||||||
let Some(g) = read_ptr(base + 0xacd02c0).filter(|&x| x != 0) else {
|
let Some(g) = read_ptr(base + 0xacd02c0).filter(|&x| x != 0) else {
|
||||||
log_skip(3, "DIAL_TRIGGER: connMgr resolution failed (G null) — skipping\n");
|
log_skip(
|
||||||
|
3,
|
||||||
|
"DIAL_TRIGGER: connMgr resolution failed (G null) — skipping\n",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(m) = read_ptr(g + 0x360).filter(|&x| x != 0) else {
|
let Some(m) = read_ptr(g + 0x360).filter(|&x| x != 0) else {
|
||||||
log_skip(3, "DIAL_TRIGGER: connMgr resolution failed (M null) — skipping\n");
|
log_skip(
|
||||||
|
3,
|
||||||
|
"DIAL_TRIGGER: connMgr resolution failed (M null) — skipping\n",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let expected_vtable = base + 0x80200b8;
|
let expected_vtable = base + 0x80200b8;
|
||||||
@@ -367,7 +392,9 @@ unsafe fn dial_trigger_tick() {
|
|||||||
if ctx == 0 || !(0x140000000..0x161000000).contains(&ctx_vt) {
|
if ctx == 0 || !(0x140000000..0x161000000).contains(&ctx_vt) {
|
||||||
log_skip(
|
log_skip(
|
||||||
4,
|
4,
|
||||||
&format!("DIAL_TRIGGER: ctx pointer implausible (ctx={ctx:#x} vt={ctx_vt:#x}) — skipping\n"),
|
&format!(
|
||||||
|
"DIAL_TRIGGER: ctx pointer implausible (ctx={ctx:#x} vt={ctx_vt:#x}) — skipping\n"
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -557,7 +584,11 @@ unsafe fn connmgr_enum_tick() {
|
|||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"CONNMGR_ENUM: [{i}] P={p:#x} vt={vtable:#x} vt[0]={vtable0:#x} ({}) \
|
"CONNMGR_ENUM: [{i}] P={p:#x} vt={vtable:#x} vt[0]={vtable0:#x} ({}) \
|
||||||
[+0x18]={} [+0x20]={} [+0x30]={} [+0xc38]={}\n",
|
[+0x18]={} [+0x20]={} [+0x30]={} [+0xc38]={}\n",
|
||||||
if info.vtable0_in_text { "in .text" } else { "NOT .text" },
|
if info.vtable0_in_text {
|
||||||
|
"in .text"
|
||||||
|
} else {
|
||||||
|
"NOT .text"
|
||||||
|
},
|
||||||
h(info.field_18),
|
h(info.field_18),
|
||||||
h(info.field_20),
|
h(info.field_20),
|
||||||
h(info.field_30),
|
h(info.field_30),
|
||||||
@@ -658,7 +689,13 @@ unsafe fn read_u32(addr: usize) -> Option<u32> {
|
|||||||
fn fourcc4(v: u32) -> String {
|
fn fourcc4(v: u32) -> String {
|
||||||
let b = v.to_le_bytes();
|
let b = v.to_le_bytes();
|
||||||
b.iter()
|
b.iter()
|
||||||
.map(|&c| if (0x20..0x7f).contains(&c) { c as char } else { '.' })
|
.map(|&c| {
|
||||||
|
if (0x20..0x7f).contains(&c) {
|
||||||
|
c as char
|
||||||
|
} else {
|
||||||
|
'.'
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,7 +705,10 @@ fn fourcc4(v: u32) -> String {
|
|||||||
/// (Refactoring `hex_dump` to take a prefix would touch the stable ctx-dump/enum probes
|
/// (Refactoring `hex_dump` to take a prefix would touch the stable ctx-dump/enum probes
|
||||||
/// for no real gain; a ~10-line duplicate is the lower-risk choice.)
|
/// for no real gain; a ~10-line duplicate is the lower-risk choice.)
|
||||||
fn elem_hex_dump(label: &str, start_va: usize, data: &[u8]) {
|
fn elem_hex_dump(label: &str, start_va: usize, data: &[u8]) {
|
||||||
let mut out = format!("ELEM_WATCH {label} @{start_va:#x} ({} bytes):\n", data.len());
|
let mut out = format!(
|
||||||
|
"ELEM_WATCH {label} @{start_va:#x} ({} bytes):\n",
|
||||||
|
data.len()
|
||||||
|
);
|
||||||
for (row, chunk) in data.chunks(16).enumerate() {
|
for (row, chunk) in data.chunks(16).enumerate() {
|
||||||
let mut hex = String::new();
|
let mut hex = String::new();
|
||||||
let mut ascii = String::new();
|
let mut ascii = String::new();
|
||||||
@@ -677,7 +717,11 @@ fn elem_hex_dump(label: &str, start_va: usize, data: &[u8]) {
|
|||||||
if i == 7 {
|
if i == 7 {
|
||||||
hex.push(' ');
|
hex.push(' ');
|
||||||
}
|
}
|
||||||
ascii.push(if (0x20..0x7f).contains(&b) { b as char } else { '.' });
|
ascii.push(if (0x20..0x7f).contains(&b) {
|
||||||
|
b as char
|
||||||
|
} else {
|
||||||
|
'.'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
out.push_str(&format!(" +{:#05x} {:<50}|{ascii}|\n", row * 16, hex));
|
out.push_str(&format!(" +{:#05x} {:<50}|{ascii}|\n", row * 16, hex));
|
||||||
}
|
}
|
||||||
@@ -752,7 +796,11 @@ unsafe fn elem_watch_tick() {
|
|||||||
// Step 6 — walk connMgr -> M -> ctx. (M here is re-read from [connMgr+8]; it should
|
// Step 6 — walk connMgr -> M -> ctx. (M here is re-read from [connMgr+8]; it should
|
||||||
// equal the global M we scanned with.)
|
// equal the global M we scanned with.)
|
||||||
let cm_m = read_ptr(conn_mgr + 8).unwrap_or(0);
|
let cm_m = read_ptr(conn_mgr + 8).unwrap_or(0);
|
||||||
let ctx = if cm_m != 0 { read_ptr(cm_m + 0x778).unwrap_or(0) } else { 0 };
|
let ctx = if cm_m != 0 {
|
||||||
|
read_ptr(cm_m + 0x778).unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
if cm_m == 0 || ctx == 0 {
|
if cm_m == 0 || ctx == 0 {
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"ELEM_WATCH: chain broke (connMgr={conn_mgr:#x} M={cm_m:#x} ctx={ctx:#x}) — watchpoint not armed\n"
|
"ELEM_WATCH: chain broke (connMgr={conn_mgr:#x} M={cm_m:#x} ctx={ctx:#x}) — watchpoint not armed\n"
|
||||||
@@ -768,11 +816,22 @@ unsafe fn elem_watch_tick() {
|
|||||||
// target_index = [[M+0x7b0]+0x650] (u32; the dial read this with `mov edx,...`)
|
// target_index = [[M+0x7b0]+0x650] (u32; the dial read this with `mov edx,...`)
|
||||||
let array_base = read_ptr(ctx + 0x1a8).unwrap_or(0);
|
let array_base = read_ptr(ctx + 0x1a8).unwrap_or(0);
|
||||||
let sub_object = read_ptr(ctx + 0x20).unwrap_or(0);
|
let sub_object = read_ptr(ctx + 0x20).unwrap_or(0);
|
||||||
let count = if sub_object != 0 { read_u32(sub_object + 0x51c) } else { None };
|
let count = if sub_object != 0 {
|
||||||
|
read_u32(sub_object + 0x51c)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let m7b0 = read_ptr(cm_m + 0x7b0).unwrap_or(0);
|
let m7b0 = read_ptr(cm_m + 0x7b0).unwrap_or(0);
|
||||||
let target_index = if m7b0 != 0 { read_u32(m7b0 + 0x650) } else { None };
|
let target_index = if m7b0 != 0 {
|
||||||
|
read_u32(m7b0 + 0x650)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let fmt_u = |o: Option<u32>| o.map(|v| v.to_string()).unwrap_or_else(|| "<unreadable>".to_string());
|
let fmt_u = |o: Option<u32>| {
|
||||||
|
o.map(|v| v.to_string())
|
||||||
|
.unwrap_or_else(|| "<unreadable>".to_string())
|
||||||
|
};
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"ELEM_WATCH: SNAPSHOT ctx={ctx:#x} array_base={array_base:#x} sub_object={sub_object:#x} \
|
"ELEM_WATCH: SNAPSHOT ctx={ctx:#x} array_base={array_base:#x} sub_object={sub_object:#x} \
|
||||||
count={} [M+0x7b0]={m7b0:#x} target_index={}\n",
|
count={} [M+0x7b0]={m7b0:#x} target_index={}\n",
|
||||||
@@ -786,7 +845,9 @@ unsafe fn elem_watch_tick() {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if array_base == 0 {
|
if array_base == 0 {
|
||||||
crate::write_log("ELEM_WATCH: array_base null — element array not allocated; watchpoint not armed\n");
|
crate::write_log(
|
||||||
|
"ELEM_WATCH: array_base null — element array not allocated; watchpoint not armed\n",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if idx >= count {
|
if idx >= count {
|
||||||
@@ -811,13 +872,19 @@ unsafe fn elem_watch_tick() {
|
|||||||
let interp = match begin {
|
let interp = match begin {
|
||||||
None => "unreadable",
|
None => "unreadable",
|
||||||
Some(0) => "container null-init (default-constructed empty vector — begin==end==0)",
|
Some(0) => "container null-init (default-constructed empty vector — begin==end==0)",
|
||||||
Some(v) if v < 0x10000 => "container UNINITIALIZED (small non-pointer sentinel — this is the crash shape)",
|
Some(v) if v < 0x10000 => {
|
||||||
Some(v) if read_bytes(v, 8).is_some() => "container appears INITIALIZED (begin is a readable heap pointer)",
|
"container UNINITIALIZED (small non-pointer sentinel — this is the crash shape)"
|
||||||
|
}
|
||||||
|
Some(v) if read_bytes(v, 8).is_some() => {
|
||||||
|
"container appears INITIALIZED (begin is a readable heap pointer)"
|
||||||
|
}
|
||||||
Some(_) => "container has a non-null but UNREADABLE begin (dangling / mid-construction?)",
|
Some(_) => "container has a non-null but UNREADABLE begin (dangling / mid-construction?)",
|
||||||
};
|
};
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"ELEM_WATCH: [elem+0x40]={} [elem+0x48]={end:#x} => {interp}\n",
|
"ELEM_WATCH: [elem+0x40]={} [elem+0x48]={end:#x} => {interp}\n",
|
||||||
begin.map(|v| format!("{v:#x}")).unwrap_or_else(|| "<unreadable>".to_string()),
|
begin
|
||||||
|
.map(|v| format!("{v:#x}"))
|
||||||
|
.unwrap_or_else(|| "<unreadable>".to_string()),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Step 10 — arm Phase 2: seed the baseline and spawn the poller. We watch regardless
|
// Step 10 — arm Phase 2: seed the baseline and spawn the poller. We watch regardless
|
||||||
@@ -872,7 +939,9 @@ fn spawn_elem_watcher(base: usize, elem: usize) {
|
|||||||
elem_hex_dump("element (after change)", elem, &bytes);
|
elem_hex_dump("element (after change)", elem, &bytes);
|
||||||
}
|
}
|
||||||
} else if changes == 6 {
|
} else if changes == 6 {
|
||||||
crate::write_log("ELEM_WATCH: (further changes suppressed; still tracking baseline)\n");
|
crate::write_log(
|
||||||
|
"ELEM_WATCH: (further changes suppressed; still tracking baseline)\n",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Beyond 6, keep updating the baseline silently so distinct future changes
|
// Beyond 6, keep updating the baseline silently so distinct future changes
|
||||||
// are still detected — we just stop spamming the log.
|
// are still detected — we just stop spamming the log.
|
||||||
@@ -890,11 +959,12 @@ fn spawn_elem_watcher(base: usize, elem: usize) {
|
|||||||
pub fn install_force_connect() {
|
pub fn install_force_connect() {
|
||||||
std::thread::spawn(|| unsafe {
|
std::thread::spawn(|| unsafe {
|
||||||
let base = GetModuleHandleA(core::ptr::null());
|
let base = GetModuleHandleA(core::ptr::null());
|
||||||
if base.is_null() { return; }
|
if base.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let base = base as usize;
|
let base = base as usize;
|
||||||
let x_slot = base + 0xacd02c0;
|
let x_slot = base + 0xacd02c0;
|
||||||
let rest: extern "system" fn() -> usize =
|
let rest: extern "system" fn() -> usize = core::mem::transmute(base + 0x2861910);
|
||||||
core::mem::transmute(base + 0x2861910);
|
|
||||||
// Wait for the ctx chain to be valid (FIFA past bootstrap / online), up to ~5 min.
|
// Wait for the ctx chain to be valid (FIFA past bootstrap / online), up to ~5 min.
|
||||||
let mut fired = 0;
|
let mut fired = 0;
|
||||||
for i in 0..600u32 {
|
for i in 0..600u32 {
|
||||||
@@ -905,17 +975,23 @@ pub fn install_force_connect() {
|
|||||||
.filter(|&m| m != 0)
|
.filter(|&m| m != 0)
|
||||||
.and_then(|m| read_ptr(m + 0x778))
|
.and_then(|m| read_ptr(m + 0x778))
|
||||||
.filter(|&c| c != 0);
|
.filter(|&c| c != 0);
|
||||||
let Some(ctx) = ctx else { continue; };
|
let Some(ctx) = ctx else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
// Give the game ~15s settled (ctx valid) before poking, then re-fire a few
|
// Give the game ~15s settled (ctx valid) before poking, then re-fire a few
|
||||||
// times spaced out (the FUT-tick pump needs a moment to reach state 2).
|
// times spaced out (the FUT-tick pump needs a moment to reach state 2).
|
||||||
if i < 30 { continue; }
|
if i < 30 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"FORCE: calling nucleusConnectREST() (ctx={ctx:#x}) attempt {fired}\n"
|
"FORCE: calling nucleusConnectREST() (ctx={ctx:#x}) attempt {fired}\n"
|
||||||
));
|
));
|
||||||
let r = rest();
|
let r = rest();
|
||||||
crate::write_log(&format!("FORCE: nucleusConnectREST returned {r:#x}\n"));
|
crate::write_log(&format!("FORCE: nucleusConnectREST returned {r:#x}\n"));
|
||||||
fired += 1;
|
fired += 1;
|
||||||
if fired >= 6 { break; }
|
if fired >= 6 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_millis(5000));
|
std::thread::sleep(std::time::Duration::from_millis(5000));
|
||||||
}
|
}
|
||||||
crate::write_log("FORCE: done\n");
|
crate::write_log("FORCE: done\n");
|
||||||
@@ -965,23 +1041,33 @@ pub fn install_force_netconn_pump() {
|
|||||||
// (an env var is fixed for the process lifetime). Unset or "0" => short-circuit:
|
// (an env var is fixed for the process lifetime). Unset or "0" => short-circuit:
|
||||||
// log and return, so the pump is wired in but completely inert — a safe default
|
// log and return, so the pump is wired in but completely inert — a safe default
|
||||||
// that can be flipped without a rebuild.
|
// that can be flipped without a rebuild.
|
||||||
let enabled = std::env::var("OPENFUT_NETCONN_PUMP").map(|v| v == "1").unwrap_or(false);
|
let enabled = std::env::var("OPENFUT_NETCONN_PUMP")
|
||||||
|
.map(|v| v == "1")
|
||||||
|
.unwrap_or(false);
|
||||||
if !enabled {
|
if !enabled {
|
||||||
crate::write_log("NETCONN_PUMP: disabled (set OPENFUT_NETCONN_PUMP=1 to enable)\n");
|
crate::write_log("NETCONN_PUMP: disabled (set OPENFUT_NETCONN_PUMP=1 to enable)\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let base = GetModuleHandleA(core::ptr::null());
|
let base = GetModuleHandleA(core::ptr::null());
|
||||||
if base.is_null() { return; }
|
if base.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let base = base as usize;
|
let base = base as usize;
|
||||||
let netconn_slot = base + 0x9fe5e50; // VA 0x149fe5e50 -> NetConn global (X)
|
let netconn_slot = base + 0x9fe5e50; // VA 0x149fe5e50 -> NetConn global (X)
|
||||||
// NetConnIdle core pump; takes no args (reads globals, sets its own rcx). Win64.
|
// NetConnIdle core pump; takes no args (reads globals, sets its own rcx). Win64.
|
||||||
let pump: extern "system" fn() = core::mem::transmute(base + 0xf16a50);
|
let pump: extern "system" fn() = core::mem::transmute(base + 0xf16a50);
|
||||||
|
|
||||||
// Render a 4-char status code the way DirtySDK stores it (e.g. 0x2b6f6e6c="+onl").
|
// Render a 4-char status code the way DirtySDK stores it (e.g. 0x2b6f6e6c="+onl").
|
||||||
let fourcc = |v: u32| -> String {
|
let fourcc = |v: u32| -> String {
|
||||||
[(v >> 24) as u8, (v >> 16) as u8, (v >> 8) as u8, v as u8]
|
[(v >> 24) as u8, (v >> 16) as u8, (v >> 8) as u8, v as u8]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&b| if (0x20..0x7f).contains(&b) { b as char } else { '.' })
|
.map(|&b| {
|
||||||
|
if (0x20..0x7f).contains(&b) {
|
||||||
|
b as char
|
||||||
|
} else {
|
||||||
|
'.'
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -998,7 +1084,9 @@ pub fn install_force_netconn_pump() {
|
|||||||
let mut samples = 0u32;
|
let mut samples = 0u32;
|
||||||
for _ in 0..18000u32 {
|
for _ in 0..18000u32 {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
let Some(nc) = read_ptr(netconn_slot).filter(|&x| x != 0) else { continue; };
|
let Some(nc) = read_ptr(netconn_slot).filter(|&x| x != 0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
// Read the conn status dword at [nc+0x48] (8-aligned; read_ptr is guarded).
|
// Read the conn status dword at [nc+0x48] (8-aligned; read_ptr is guarded).
|
||||||
let status = read_ptr(nc + 0x48).map(|w| w as u32).unwrap_or(0);
|
let status = read_ptr(nc + 0x48).map(|w| w as u32).unwrap_or(0);
|
||||||
// Drive the idle loop. Self-guards on 'open'; a no-op if not yet open.
|
// Drive the idle loop. Self-guards on 'open'; a no-op if not yet open.
|
||||||
@@ -1067,7 +1155,9 @@ pub fn install_force_netconn_pump() {
|
|||||||
pub fn install_force_fut_tick() {
|
pub fn install_force_fut_tick() {
|
||||||
std::thread::spawn(|| unsafe {
|
std::thread::spawn(|| unsafe {
|
||||||
let base = GetModuleHandleA(core::ptr::null());
|
let base = GetModuleHandleA(core::ptr::null());
|
||||||
if base.is_null() { return; }
|
if base.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let base = base as usize;
|
let base = base as usize;
|
||||||
let mgr_slot = base + 0xa199608; // VA 0x14a199608 -> FUT online manager ptr
|
let mgr_slot = base + 0xa199608; // VA 0x14a199608 -> FUT online manager ptr
|
||||||
let tick: extern "system" fn(usize, usize) -> usize =
|
let tick: extern "system" fn(usize, usize) -> usize =
|
||||||
@@ -1077,9 +1167,13 @@ pub fn install_force_fut_tick() {
|
|||||||
// ~15 min at 250ms. The tick is heavy (locks + sub-updates); don't spin at 100ms.
|
// ~15 min at 250ms. The tick is heavy (locks + sub-updates); don't spin at 100ms.
|
||||||
for _ in 0..3600u32 {
|
for _ in 0..3600u32 {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||||
let Some(mgr) = read_ptr(mgr_slot).filter(|&m| m != 0) else { continue; };
|
let Some(mgr) = read_ptr(mgr_slot).filter(|&m| m != 0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
// state @+0x1bb8 (low32) + latch byte @+0x1bbc share one 8-aligned qword.
|
// state @+0x1bb8 (low32) + latch byte @+0x1bbc share one 8-aligned qword.
|
||||||
let Some(w) = read_ptr(mgr + 0x1bb8) else { continue; };
|
let Some(w) = read_ptr(mgr + 0x1bb8) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let state = w as u32;
|
let state = w as u32;
|
||||||
let latch = ((w >> 32) & 0xff) as u8;
|
let latch = ((w >> 32) & 0xff) as u8;
|
||||||
// Post the go-online request ONLY at state 0 (mimics event-0 delivery) to
|
// Post the go-online request ONLY at state 0 (mimics event-0 delivery) to
|
||||||
@@ -1146,7 +1240,11 @@ unsafe fn read_bytes(addr: usize, len: usize) -> Option<Vec<u8>> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
||||||
let n = VirtualQuery(addr as _, &mut mbi, core::mem::size_of::<MEMORY_BASIC_INFORMATION>());
|
let n = VirtualQuery(
|
||||||
|
addr as _,
|
||||||
|
&mut mbi,
|
||||||
|
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
||||||
|
);
|
||||||
if n == 0 || mbi.State != MEM_COMMIT {
|
if n == 0 || mbi.State != MEM_COMMIT {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -1180,7 +1278,11 @@ fn hex_dump(label: &str, start_va: usize, data: &[u8]) {
|
|||||||
}
|
}
|
||||||
// Printable ASCII stays; everything else shows as '.' so pointer bytes
|
// Printable ASCII stays; everything else shows as '.' so pointer bytes
|
||||||
// don't corrupt the log line.
|
// don't corrupt the log line.
|
||||||
ascii.push(if (0x20..0x7f).contains(&b) { b as char } else { '.' });
|
ascii.push(if (0x20..0x7f).contains(&b) {
|
||||||
|
b as char
|
||||||
|
} else {
|
||||||
|
'.'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
out.push_str(&format!(" +{:#05x} {:<50}|{ascii}|\n", row * 16, hex));
|
out.push_str(&format!(" +{:#05x} {:<50}|{ascii}|\n", row * 16, hex));
|
||||||
}
|
}
|
||||||
@@ -1203,16 +1305,16 @@ fn hex_dump(label: &str, start_va: usize, data: &[u8]) {
|
|||||||
///
|
///
|
||||||
/// Returns every matching `P`. Read-only throughout. Also fills `stats` with
|
/// Returns every matching `P`. Read-only throughout. Also fills `stats` with
|
||||||
/// (regions_scanned, bytes_scanned) so we can report the cost.
|
/// (regions_scanned, bytes_scanned) so we can report the cost.
|
||||||
unsafe fn scan_conn_mgr(
|
unsafe fn scan_conn_mgr(m: usize, expected_vtable: usize, stats: &mut (u64, u64)) -> Vec<usize> {
|
||||||
m: usize,
|
|
||||||
expected_vtable: usize,
|
|
||||||
stats: &mut (u64, u64),
|
|
||||||
) -> Vec<usize> {
|
|
||||||
let mut hits = Vec::new();
|
let mut hits = Vec::new();
|
||||||
let mut addr: usize = 0x10000; // user space starts here; skip the null-guard page
|
let mut addr: usize = 0x10000; // user space starts here; skip the null-guard page
|
||||||
loop {
|
loop {
|
||||||
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
||||||
let n = VirtualQuery(addr as _, &mut mbi, core::mem::size_of::<MEMORY_BASIC_INFORMATION>());
|
let n = VirtualQuery(
|
||||||
|
addr as _,
|
||||||
|
&mut mbi,
|
||||||
|
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
||||||
|
);
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
break; // past the top of the user address space
|
break; // past the top of the user address space
|
||||||
}
|
}
|
||||||
@@ -1262,7 +1364,9 @@ unsafe fn scan_conn_mgr(
|
|||||||
/// env `OPENFUT_CTX_DUMP` — armed only when it equals "1" (unset/"0" = disabled).
|
/// env `OPENFUT_CTX_DUMP` — armed only when it equals "1" (unset/"0" = disabled).
|
||||||
/// Read once at install time; if disarmed we don't even spawn the thread.
|
/// Read once at install time; if disarmed we don't even spawn the thread.
|
||||||
pub fn install_ctx_dump() {
|
pub fn install_ctx_dump() {
|
||||||
let armed = std::env::var("OPENFUT_CTX_DUMP").map(|v| v == "1").unwrap_or(false);
|
let armed = std::env::var("OPENFUT_CTX_DUMP")
|
||||||
|
.map(|v| v == "1")
|
||||||
|
.unwrap_or(false);
|
||||||
if !armed {
|
if !armed {
|
||||||
crate::write_log("CTXDUMP: disabled (set OPENFUT_CTX_DUMP=1 to arm)\n");
|
crate::write_log("CTXDUMP: disabled (set OPENFUT_CTX_DUMP=1 to arm)\n");
|
||||||
return;
|
return;
|
||||||
@@ -1436,14 +1540,54 @@ const TARGETS: &[Target] = &[
|
|||||||
// Run 4: settle "connect state entered-but-stalled" vs "never entered". If the ctor
|
// Run 4: settle "connect state entered-but-stalled" vs "never entered". If the ctor
|
||||||
// fires but nothing else, the connect states are created at init but never used; if
|
// fires but nothing else, the connect states are created at init but never used; if
|
||||||
// GetByIdx / any vtable step fires, the online subsystem is iterating them.
|
// GetByIdx / any vtable step fires, the online subsystem is iterating them.
|
||||||
Target { module: b"\0", rva: 0x5078d20, label: "connectState.ctor", main_exe: true },
|
Target {
|
||||||
Target { module: b"\0", rva: 0x4f46570, label: "ctrl.GetConnState", main_exe: true },
|
module: b"\0",
|
||||||
Target { module: b"\0", rva: 0x507cd60, label: "connState.m_a8", main_exe: true },
|
rva: 0x5078d20,
|
||||||
Target { module: b"\0", rva: 0x507cf90, label: "connState.m_b0", main_exe: true },
|
label: "connectState.ctor",
|
||||||
Target { module: b"\0", rva: 0x507d660, label: "connState.tick_b8", main_exe: true },
|
main_exe: true,
|
||||||
Target { module: b"\0", rva: 0x507d760, label: "connState.m_c0", main_exe: true },
|
},
|
||||||
Target { module: b"\0", rva: 0x2861910, label: "nucleusConnectREST", main_exe: true },
|
Target {
|
||||||
Target { module: b"\0", rva: 0x278a4d0, label: "OnlineStatus.deser", main_exe: true },
|
module: b"\0",
|
||||||
|
rva: 0x4f46570,
|
||||||
|
label: "ctrl.GetConnState",
|
||||||
|
main_exe: true,
|
||||||
|
},
|
||||||
|
Target {
|
||||||
|
module: b"\0",
|
||||||
|
rva: 0x507cd60,
|
||||||
|
label: "connState.m_a8",
|
||||||
|
main_exe: true,
|
||||||
|
},
|
||||||
|
Target {
|
||||||
|
module: b"\0",
|
||||||
|
rva: 0x507cf90,
|
||||||
|
label: "connState.m_b0",
|
||||||
|
main_exe: true,
|
||||||
|
},
|
||||||
|
Target {
|
||||||
|
module: b"\0",
|
||||||
|
rva: 0x507d660,
|
||||||
|
label: "connState.tick_b8",
|
||||||
|
main_exe: true,
|
||||||
|
},
|
||||||
|
Target {
|
||||||
|
module: b"\0",
|
||||||
|
rva: 0x507d760,
|
||||||
|
label: "connState.m_c0",
|
||||||
|
main_exe: true,
|
||||||
|
},
|
||||||
|
Target {
|
||||||
|
module: b"\0",
|
||||||
|
rva: 0x2861910,
|
||||||
|
label: "nucleusConnectREST",
|
||||||
|
main_exe: true,
|
||||||
|
},
|
||||||
|
Target {
|
||||||
|
module: b"\0",
|
||||||
|
rva: 0x278a4d0,
|
||||||
|
label: "OnlineStatus.deser",
|
||||||
|
main_exe: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const N: usize = 8; // must equal TARGETS.len()
|
const N: usize = 8; // must equal TARGETS.len()
|
||||||
@@ -1498,19 +1642,37 @@ unsafe fn generic(slot: usize, a: usize, b: usize, c: usize, d: usize) -> usize
|
|||||||
if log {
|
if log {
|
||||||
crate::write_log(&format!("PROBE {label} #{n} ret={r:#x}\n"));
|
crate::write_log(&format!("PROBE {label} #{n} ret={r:#x}\n"));
|
||||||
} else if n == LOG_CAP {
|
} else if n == LOG_CAP {
|
||||||
crate::write_log(&format!("PROBE {label} (capped; still firing past {LOG_CAP})\n"));
|
crate::write_log(&format!(
|
||||||
|
"PROBE {label} (capped; still firing past {LOG_CAP})\n"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
r
|
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 p0(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||||
unsafe extern "system" fn p1(a: usize, b: usize, c: usize, d: usize) -> usize { generic(1, a, b, c, d) }
|
generic(0, 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) }
|
unsafe extern "system" fn p1(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||||
unsafe extern "system" fn p4(a: usize, b: usize, c: usize, d: usize) -> usize { generic(4, a, b, c, d) }
|
generic(1, a, b, c, d)
|
||||||
unsafe extern "system" fn p5(a: usize, b: usize, c: usize, d: usize) -> usize { generic(5, a, b, c, d) }
|
}
|
||||||
unsafe extern "system" fn p6(a: usize, b: usize, c: usize, d: usize) -> usize { generic(6, a, b, c, d) }
|
unsafe extern "system" fn p2(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||||
unsafe extern "system" fn p7(a: usize, b: usize, c: usize, d: usize) -> usize { generic(7, a, b, c, d) }
|
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)
|
||||||
|
}
|
||||||
|
unsafe extern "system" fn p4(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||||
|
generic(4, a, b, c, d)
|
||||||
|
}
|
||||||
|
unsafe extern "system" fn p5(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||||
|
generic(5, a, b, c, d)
|
||||||
|
}
|
||||||
|
unsafe extern "system" fn p6(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||||
|
generic(6, a, b, c, d)
|
||||||
|
}
|
||||||
|
unsafe extern "system" fn p7(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||||
|
generic(7, a, b, c, d)
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn a background thread that waits for anadius64.dll to load, then installs
|
/// 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
|
/// all probes. anadius may not be present when our DllMain runs, so we defer
|
||||||
@@ -1526,23 +1688,23 @@ pub fn install_probes_deferred() {
|
|||||||
install_probes();
|
install_probes();
|
||||||
install_listener_probe();
|
install_listener_probe();
|
||||||
install_state_sampler();
|
install_state_sampler();
|
||||||
install_ctx_dump(); // ENABLED 2026-07-03: READ-ONLY menu-time ctx dump (env
|
install_ctx_dump(); // ENABLED 2026-07-03: READ-ONLY menu-time ctx dump (env
|
||||||
// OPENFUT_CTX_DUMP=1). Resolves connMgr and hex-dumps M + ctx. No game calls.
|
// OPENFUT_CTX_DUMP=1). Resolves connMgr and hex-dumps M + ctx. No game calls.
|
||||||
// install_force_connect(); // DISABLED 2026-07-03: re-enabling it CRASHED FIFA at
|
// install_force_connect(); // DISABLED 2026-07-03: re-enabling it CRASHED FIFA at
|
||||||
// ~15s (EXCEPTION_ACCESS_VIOLATION, RIP 0x15d5e8dd7 in FIFA's packed/anti-tamper
|
// ~15s (EXCEPTION_ACCESS_VIOLATION, RIP 0x15d5e8dd7 in FIFA's packed/anti-tamper
|
||||||
// region, all registers garbage). Once state 2 makes the Nucleus ctx live,
|
// region, all registers garbage). Once state 2 makes the Nucleus ctx live,
|
||||||
// nucleusConnectREST's `ctx->vtable[0x40]` send path runs into protected code that
|
// nucleusConnectREST's `ctx->vtable[0x40]` send path runs into protected code that
|
||||||
// does not tolerate being called from our background thread. GetAuthCode must be
|
// does not tolerate being called from our background thread. GetAuthCode must be
|
||||||
// triggered on the GAME thread (via a detour), not a bg-thread forcing call.
|
// triggered on the GAME thread (via a detour), not a bg-thread forcing call.
|
||||||
install_force_netconn_pump(); // RE-ENABLED 2026-07-03 (sub-phase B prereq): pump
|
install_force_netconn_pump(); // RE-ENABLED 2026-07-03 (sub-phase B prereq): pump
|
||||||
// NetConn toward '+onl' and capture the pump thread id. Gated by env
|
// NetConn toward '+onl' and capture the pump thread id. Gated by env
|
||||||
// OPENFUT_NETCONN_PUMP=1 — completely inert unless set. This is the known-good
|
// OPENFUT_NETCONN_PUMP=1 — completely inert unless set. This is the known-good
|
||||||
// pump path (never crashed); the FUT-tick pump below stays OFF (it crashes the VM).
|
// pump path (never crashed); the FUT-tick pump below stays OFF (it crashes the VM).
|
||||||
// install_force_fut_tick(); // DISABLED 2026-07-03 for the ctx-dump build: it
|
// install_force_fut_tick(); // DISABLED 2026-07-03 for the ctx-dump build: it
|
||||||
// WRITES the go-online latch and drives FifaOnline toward state 2, which
|
// WRITES the go-online latch and drives FifaOnline toward state 2, which
|
||||||
// deterministically CRASHES the anti-tamper VM before the menu — so leaving it on
|
// deterministically CRASHES the anti-tamper VM before the menu — so leaving it on
|
||||||
// would prevent this menu-time probe from ever observing. Re-enable only if we
|
// would prevent this menu-time probe from ever observing. Re-enable only if we
|
||||||
// deliberately want the (crash-prone) state-2 path.
|
// deliberately want the (crash-prone) state-2 path.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1563,7 +1725,10 @@ pub unsafe fn install_probes() {
|
|||||||
core::ptr::copy_nonoverlapping(addr, (&raw mut ORIG[i]) as *mut u8, 14);
|
core::ptr::copy_nonoverlapping(addr, (&raw mut ORIG[i]) as *mut u8, 14);
|
||||||
ADDRS[i].store(addr as usize, Ordering::Relaxed);
|
ADDRS[i].store(addr as usize, Ordering::Relaxed);
|
||||||
write_jmp(addr, PROBE_FNS[i] as u64);
|
write_jmp(addr, PROBE_FNS[i] as u64);
|
||||||
crate::write_log(&format!("PROBE {} installed @ {:#x}\n", t.label, addr as usize));
|
crate::write_log(&format!(
|
||||||
|
"PROBE {} installed @ {:#x}\n",
|
||||||
|
t.label, addr as usize
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+116
-32
@@ -12,7 +12,8 @@ unsafe fn write_jmp(target: *mut u8, dest: u64) {
|
|||||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||||
let mut old: u32 = 0;
|
let mut old: u32 = 0;
|
||||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||||
target.write(0xFF); target.add(1).write(0x25);
|
target.write(0xFF);
|
||||||
|
target.add(1).write(0x25);
|
||||||
(target.add(2) as *mut u32).write(0);
|
(target.add(2) as *mut u32).write(0);
|
||||||
(target.add(6) as *mut u64).write(dest);
|
(target.add(6) as *mut u64).write(dest);
|
||||||
VirtualProtect(target as _, 14, old, &mut old);
|
VirtualProtect(target as _, 14, old, &mut old);
|
||||||
@@ -43,10 +44,15 @@ unsafe fn make_trampoline(orig: *mut u8, name: &str) -> Option<usize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mem = VirtualAlloc(
|
let mem = VirtualAlloc(
|
||||||
core::ptr::null_mut(), 64,
|
core::ptr::null_mut(),
|
||||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE,
|
64,
|
||||||
|
MEM_COMMIT | MEM_RESERVE,
|
||||||
|
PAGE_EXECUTE_READWRITE,
|
||||||
);
|
);
|
||||||
if mem.is_null() { crate::write_log("recv_hook: VirtualAlloc failed\n"); return None; }
|
if mem.is_null() {
|
||||||
|
crate::write_log("recv_hook: VirtualAlloc failed\n");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let t = mem as *mut u8;
|
let t = mem as *mut u8;
|
||||||
core::ptr::copy_nonoverlapping(orig, t, copy_len);
|
core::ptr::copy_nonoverlapping(orig, t, copy_len);
|
||||||
@@ -56,7 +62,9 @@ unsafe fn make_trampoline(orig: *mut u8, name: &str) -> Option<usize> {
|
|||||||
t.add(copy_len + 1).write(0x25);
|
t.add(copy_len + 1).write(0x25);
|
||||||
(t.add(copy_len + 2) as *mut u32).write(0);
|
(t.add(copy_len + 2) as *mut u32).write(0);
|
||||||
(t.add(copy_len + 6) as *mut u64).write(cont);
|
(t.add(copy_len + 6) as *mut u64).write(cont);
|
||||||
crate::write_log(&format!("recv_hook: {name} trampoline copy_len={copy_len}\n"));
|
crate::write_log(&format!(
|
||||||
|
"recv_hook: {name} trampoline copy_len={copy_len}\n"
|
||||||
|
));
|
||||||
Some(t as usize)
|
Some(t as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +75,12 @@ fn has_rip_relative_branch(bytes: &[u8]) -> bool {
|
|||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
while pos < bytes.len() {
|
while pos < bytes.len() {
|
||||||
let (len, branch) = decode_instr_len(&bytes[pos..]);
|
let (len, branch) = decode_instr_len(&bytes[pos..]);
|
||||||
if branch { return true; }
|
if branch {
|
||||||
if len == 0 { break; } // unknown/truncated — stop safely
|
return true;
|
||||||
|
}
|
||||||
|
if len == 0 {
|
||||||
|
break;
|
||||||
|
} // unknown/truncated — stop safely
|
||||||
pos += len;
|
pos += len;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -78,9 +90,29 @@ fn modrm_extra(modrm: u8) -> usize {
|
|||||||
let md = (modrm >> 6) & 3;
|
let md = (modrm >> 6) & 3;
|
||||||
let rm = modrm & 7;
|
let rm = modrm & 7;
|
||||||
match md {
|
match md {
|
||||||
0 => if rm == 5 { 4 } else if rm == 4 { 1 } else { 0 },
|
0 => {
|
||||||
1 => if rm == 4 { 2 } else { 1 },
|
if rm == 5 {
|
||||||
2 => if rm == 4 { 5 } else { 4 },
|
4
|
||||||
|
} else if rm == 4 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
if rm == 4 {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
if rm == 4 {
|
||||||
|
5
|
||||||
|
} else {
|
||||||
|
4
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,16 +120,31 @@ fn modrm_extra(modrm: u8) -> usize {
|
|||||||
/// Returns (instruction_length_in_bytes, is_rip_relative_branch).
|
/// Returns (instruction_length_in_bytes, is_rip_relative_branch).
|
||||||
/// Returns (0, false) for unknown/truncated.
|
/// Returns (0, false) for unknown/truncated.
|
||||||
fn decode_instr_len(b: &[u8]) -> (usize, bool) {
|
fn decode_instr_len(b: &[u8]) -> (usize, bool) {
|
||||||
if b.is_empty() { return (0, false); }
|
if b.is_empty() {
|
||||||
|
return (0, false);
|
||||||
|
}
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
// Legacy prefixes
|
// Legacy prefixes
|
||||||
while let Some(&p) = b.get(i) {
|
while let Some(&p) = b.get(i) {
|
||||||
if matches!(p, 0x66 | 0x67 | 0xF0 | 0xF2 | 0xF3) { i += 1; } else { break; }
|
if matches!(p, 0x66 | 0x67 | 0xF0 | 0xF2 | 0xF3) {
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// REX prefix (40–4F)
|
// REX prefix (40–4F)
|
||||||
if b.get(i).copied().map(|x| (0x40..=0x4F).contains(&x)).unwrap_or(false) { i += 1; }
|
if b.get(i)
|
||||||
|
.copied()
|
||||||
|
.map(|x| (0x40..=0x4F).contains(&x))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
let op = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
let op = match b.get(i) {
|
||||||
|
Some(&x) => x,
|
||||||
|
None => return (0, false),
|
||||||
|
};
|
||||||
i += 1;
|
i += 1;
|
||||||
|
|
||||||
match op {
|
match op {
|
||||||
@@ -112,28 +159,45 @@ fn decode_instr_len(b: &[u8]) -> (usize, bool) {
|
|||||||
0xE9 | 0xE8 => (i + 4, true),
|
0xE9 | 0xE8 => (i + 4, true),
|
||||||
// 0F prefix
|
// 0F prefix
|
||||||
0x0F => {
|
0x0F => {
|
||||||
let op2 = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
let op2 = match b.get(i) {
|
||||||
|
Some(&x) => x,
|
||||||
|
None => return (0, false),
|
||||||
|
};
|
||||||
i += 1;
|
i += 1;
|
||||||
if (0x80..=0x8F).contains(&op2) { return (i + 4, true); } // Jcc rel32
|
if (0x80..=0x8F).contains(&op2) {
|
||||||
// Most 0F XX: ModRM
|
return (i + 4, true);
|
||||||
let modrm = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
} // Jcc rel32
|
||||||
|
// Most 0F XX: ModRM
|
||||||
|
let modrm = match b.get(i) {
|
||||||
|
Some(&x) => x,
|
||||||
|
None => return (0, false),
|
||||||
|
};
|
||||||
(i + 1 + modrm_extra(modrm), false)
|
(i + 1 + modrm_extra(modrm), false)
|
||||||
}
|
}
|
||||||
// Instructions with ModRM only (no immediate)
|
// Instructions with ModRM only (no immediate)
|
||||||
0x85 | 0x87 | 0x88 | 0x89 | 0x8A | 0x8B | 0x8C | 0x8D | 0x8E | 0x8F |
|
0x85 | 0x87 | 0x88 | 0x89 | 0x8A | 0x8B | 0x8C | 0x8D | 0x8E | 0x8F | 0x01 | 0x03
|
||||||
0x01 | 0x03 | 0x09 | 0x0B | 0x11 | 0x13 | 0x21 | 0x23 | 0x29 | 0x2B |
|
| 0x09 | 0x0B | 0x11 | 0x13 | 0x21 | 0x23 | 0x29 | 0x2B | 0x31 | 0x33 | 0x39 | 0x3B
|
||||||
0x31 | 0x33 | 0x39 | 0x3B | 0xD3 | 0xFF | 0xF7 => {
|
| 0xD3 | 0xFF | 0xF7 => {
|
||||||
let modrm = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
let modrm = match b.get(i) {
|
||||||
|
Some(&x) => x,
|
||||||
|
None => return (0, false),
|
||||||
|
};
|
||||||
(i + 1 + modrm_extra(modrm), false)
|
(i + 1 + modrm_extra(modrm), false)
|
||||||
}
|
}
|
||||||
// ModRM + imm8
|
// ModRM + imm8
|
||||||
0x6B | 0x80 | 0x83 | 0xC0 | 0xC1 | 0xC6 => {
|
0x6B | 0x80 | 0x83 | 0xC0 | 0xC1 | 0xC6 => {
|
||||||
let modrm = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
let modrm = match b.get(i) {
|
||||||
|
Some(&x) => x,
|
||||||
|
None => return (0, false),
|
||||||
|
};
|
||||||
(i + 1 + modrm_extra(modrm) + 1, false)
|
(i + 1 + modrm_extra(modrm) + 1, false)
|
||||||
}
|
}
|
||||||
// ModRM + imm32
|
// ModRM + imm32
|
||||||
0x69 | 0x81 | 0xC7 => {
|
0x69 | 0x81 | 0xC7 => {
|
||||||
let modrm = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
let modrm = match b.get(i) {
|
||||||
|
Some(&x) => x,
|
||||||
|
None => return (0, false),
|
||||||
|
};
|
||||||
(i + 1 + modrm_extra(modrm) + 4, false)
|
(i + 1 + modrm_extra(modrm) + 4, false)
|
||||||
}
|
}
|
||||||
// MOV reg, imm8/imm32
|
// MOV reg, imm8/imm32
|
||||||
@@ -152,7 +216,9 @@ fn decode_instr_len(b: &[u8]) -> (usize, bool) {
|
|||||||
unsafe fn get_fn(dll: &[u8], sym: &[u8]) -> Option<*mut u8> {
|
unsafe fn get_fn(dll: &[u8], sym: &[u8]) -> Option<*mut u8> {
|
||||||
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||||
let h = GetModuleHandleA(dll.as_ptr());
|
let h = GetModuleHandleA(dll.as_ptr());
|
||||||
if h.is_null() { return None; }
|
if h.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
GetProcAddress(h, sym.as_ptr()).map(|f| f as *mut u8)
|
GetProcAddress(h, sym.as_ptr()).map(|f| f as *mut u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +232,9 @@ unsafe fn peer_is_lsx(s: usize) -> bool {
|
|||||||
use windows_sys::Win32::Networking::WinSock::getpeername;
|
use windows_sys::Win32::Networking::WinSock::getpeername;
|
||||||
let mut sa = [0u8; 16];
|
let mut sa = [0u8; 16];
|
||||||
let mut sl: i32 = 16;
|
let mut sl: i32 = 16;
|
||||||
if getpeername(s, sa.as_mut_ptr() as *mut _, &mut sl) != 0 { return false; }
|
if getpeername(s, sa.as_mut_ptr() as *mut _, &mut sl) != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// sockaddr_in: sa_family (2 bytes) then sin_port (2 bytes, network order).
|
// sockaddr_in: sa_family (2 bytes) then sin_port (2 bytes, network order).
|
||||||
u16::from_be_bytes([sa[2], sa[3]]) == 3216
|
u16::from_be_bytes([sa[2], sa[3]]) == 3216
|
||||||
}
|
}
|
||||||
@@ -190,7 +258,10 @@ pub fn set_real_send(f: unsafe extern "system" fn(usize, *const u8, i32, i32) ->
|
|||||||
/// hook calls) and overwrite the entry with a JMP to `hooked_recv`. Inline hooks
|
/// hook calls) and overwrite the entry with a JMP to `hooked_recv`. Inline hooks
|
||||||
/// catch calls from every module and dynamically-resolved calls, unlike IAT.
|
/// catch calls from every module and dynamically-resolved calls, unlike IAT.
|
||||||
pub unsafe fn install_recv_hook() -> bool {
|
pub unsafe fn install_recv_hook() -> bool {
|
||||||
let ptr = match get_fn(b"ws2_32.dll\0", b"recv\0") { Some(p) => p, None => return false };
|
let ptr = match get_fn(b"ws2_32.dll\0", b"recv\0") {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
match make_trampoline(ptr, "recv") {
|
match make_trampoline(ptr, "recv") {
|
||||||
Some(t) => REAL_RECV.store(t, Ordering::Relaxed),
|
Some(t) => REAL_RECV.store(t, Ordering::Relaxed),
|
||||||
None => return false,
|
None => return false,
|
||||||
@@ -200,7 +271,10 @@ pub unsafe fn install_recv_hook() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe fn install_send_hook() -> bool {
|
pub unsafe fn install_send_hook() -> bool {
|
||||||
let ptr = match get_fn(b"ws2_32.dll\0", b"send\0") { Some(p) => p, None => return false };
|
let ptr = match get_fn(b"ws2_32.dll\0", b"send\0") {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
match make_trampoline(ptr, "send") {
|
match make_trampoline(ptr, "send") {
|
||||||
Some(t) => REAL_SEND.store(t, Ordering::Relaxed),
|
Some(t) => REAL_SEND.store(t, Ordering::Relaxed),
|
||||||
None => return false,
|
None => return false,
|
||||||
@@ -211,7 +285,9 @@ pub unsafe fn install_send_hook() -> bool {
|
|||||||
|
|
||||||
pub unsafe extern "system" fn hooked_recv(s: usize, buf: *mut u8, len: i32, flags: i32) -> i32 {
|
pub unsafe extern "system" fn hooked_recv(s: usize, buf: *mut u8, len: i32, flags: i32) -> i32 {
|
||||||
let t = REAL_RECV.load(Ordering::Relaxed);
|
let t = REAL_RECV.load(Ordering::Relaxed);
|
||||||
if t == 0 { return -1; }
|
if t == 0 {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
let f: unsafe extern "system" fn(usize, *mut u8, i32, i32) -> i32 = core::mem::transmute(t);
|
let f: unsafe extern "system" fn(usize, *mut u8, i32, i32) -> i32 = core::mem::transmute(t);
|
||||||
// Pass through to anadius's real socket, then log what it sent back
|
// Pass through to anadius's real socket, then log what it sent back
|
||||||
// (anadius's LSX response — the ground truth we want to diff against).
|
// (anadius's LSX response — the ground truth we want to diff against).
|
||||||
@@ -219,7 +295,10 @@ pub unsafe extern "system" fn hooked_recv(s: usize, buf: *mut u8, len: i32, flag
|
|||||||
if n > 0 && peer_is_lsx(s) {
|
if n > 0 && peer_is_lsx(s) {
|
||||||
let data = core::slice::from_raw_parts(buf, n as usize);
|
let data = core::slice::from_raw_parts(buf, n as usize);
|
||||||
let text = core::str::from_utf8(data).unwrap_or("(binary)");
|
let text = core::str::from_utf8(data).unwrap_or("(binary)");
|
||||||
crate::write_log(&format!("CAP recv<-anadius s={s} n={n}: {}\n", &text[..text.len().min(2400)]));
|
crate::write_log(&format!(
|
||||||
|
"CAP recv<-anadius s={s} n={n}: {}\n",
|
||||||
|
&text[..text.len().min(2400)]
|
||||||
|
));
|
||||||
}
|
}
|
||||||
n
|
n
|
||||||
}
|
}
|
||||||
@@ -228,10 +307,15 @@ pub unsafe extern "system" fn hooked_send(s: usize, buf: *const u8, len: i32, fl
|
|||||||
if len > 0 && peer_is_lsx(s) {
|
if len > 0 && peer_is_lsx(s) {
|
||||||
let data = core::slice::from_raw_parts(buf, len as usize);
|
let data = core::slice::from_raw_parts(buf, len as usize);
|
||||||
let text = core::str::from_utf8(data).unwrap_or("(binary)");
|
let text = core::str::from_utf8(data).unwrap_or("(binary)");
|
||||||
crate::write_log(&format!("CAP send->anadius s={s} len={len}: {}\n", &text[..text.len().min(2400)]));
|
crate::write_log(&format!(
|
||||||
|
"CAP send->anadius s={s} len={len}: {}\n",
|
||||||
|
&text[..text.len().min(2400)]
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let t = REAL_SEND.load(Ordering::Relaxed);
|
let t = REAL_SEND.load(Ordering::Relaxed);
|
||||||
if t == 0 { return -1; }
|
if t == 0 {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
let f: unsafe extern "system" fn(usize, *const u8, i32, i32) -> i32 = core::mem::transmute(t);
|
let f: unsafe extern "system" fn(usize, *const u8, i32, i32) -> i32 = core::mem::transmute(t);
|
||||||
f(s, buf, len, flags)
|
f(s, buf, len, flags)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,9 @@
|
|||||||
// server's certificate chain. Always returning 1 is equivalent to trusting all certs,
|
// server's certificate chain. Always returning 1 is equivalent to trusting all certs,
|
||||||
// which is the behaviour we want for the local self-signed bridge certificate.
|
// which is the behaviour we want for the local self-signed bridge certificate.
|
||||||
|
|
||||||
use windows_sys::Win32::{
|
use windows_sys::Win32::System::{
|
||||||
System::{
|
LibraryLoader::GetModuleHandleA,
|
||||||
LibraryLoader::GetModuleHandleA,
|
Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE},
|
||||||
Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Unique 22-byte prologue of ProtoSSL's cert-verify function.
|
// Unique 22-byte prologue of ProtoSSL's cert-verify function.
|
||||||
@@ -21,24 +19,26 @@ use windows_sys::Win32::{
|
|||||||
const PROLOGUE: &[u8] = &[
|
const PROLOGUE: &[u8] = &[
|
||||||
0x44, 0x89, 0x44, 0x24, 0x18, // mov [rsp+0x18], r8d
|
0x44, 0x89, 0x44, 0x24, 0x18, // mov [rsp+0x18], r8d
|
||||||
0x48, 0x89, 0x54, 0x24, 0x10, // mov [rsp+0x10], rdx
|
0x48, 0x89, 0x54, 0x24, 0x10, // mov [rsp+0x10], rdx
|
||||||
0x56, // push rsi
|
0x56, // push rsi
|
||||||
0x57, // push rdi
|
0x57, // push rdi
|
||||||
0x41, 0x55, // push r13
|
0x41, 0x55, // push r13
|
||||||
0x41, 0x56, // push r14
|
0x41, 0x56, // push r14
|
||||||
0x41, 0x57, // push r15
|
0x41, 0x57, // push r15
|
||||||
0x48, 0x83, 0xec, 0x30, // sub rsp, 0x30
|
0x48, 0x83, 0xec, 0x30, // sub rsp, 0x30
|
||||||
];
|
];
|
||||||
|
|
||||||
// Return 0 (PROTOSSL_ERROR_NONE = success). ProtoSSL convention: 0 = ok, negative = error.
|
// Return 0 (PROTOSSL_ERROR_NONE = success). ProtoSSL convention: 0 = ok, negative = error.
|
||||||
// The function sets r15d = 0xFFFFFFFF (-1) for its own error returns, confirming 0 = success.
|
// The function sets r15d = 0xFFFFFFFF (-1) for its own error returns, confirming 0 = success.
|
||||||
const PATCH: &[u8] = &[
|
const PATCH: &[u8] = &[
|
||||||
0x31, 0xc0, // xor eax, eax (eax = 0 = PROTOSSL_ERROR_NONE)
|
0x31, 0xc0, // xor eax, eax (eax = 0 = PROTOSSL_ERROR_NONE)
|
||||||
0xc3, // ret
|
0xc3, // ret
|
||||||
0x90, 0x90, 0x90, // nop padding
|
0x90, 0x90, 0x90, // nop padding
|
||||||
];
|
];
|
||||||
|
|
||||||
fn patch_module(module: isize, scan_bytes: usize) -> bool {
|
fn patch_module(module: isize, scan_bytes: usize) -> bool {
|
||||||
if module == 0 { return false; }
|
if module == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let base = module as usize;
|
let base = module as usize;
|
||||||
let image: &[u8] = unsafe { core::slice::from_raw_parts(base as *const u8, scan_bytes) };
|
let image: &[u8] = unsafe { core::slice::from_raw_parts(base as *const u8, scan_bytes) };
|
||||||
let offset = match image.windows(PROLOGUE.len()).position(|w| w == PROLOGUE) {
|
let offset = match image.windows(PROLOGUE.len()).position(|w| w == PROLOGUE) {
|
||||||
@@ -48,9 +48,19 @@ fn patch_module(module: isize, scan_bytes: usize) -> bool {
|
|||||||
let target = (base + offset) as *mut u8;
|
let target = (base + offset) as *mut u8;
|
||||||
let mut old_prot: u32 = 0;
|
let mut old_prot: u32 = 0;
|
||||||
unsafe {
|
unsafe {
|
||||||
VirtualProtect(target as *const core::ffi::c_void, PATCH.len(), PAGE_EXECUTE_READWRITE, &mut old_prot);
|
VirtualProtect(
|
||||||
|
target as *const core::ffi::c_void,
|
||||||
|
PATCH.len(),
|
||||||
|
PAGE_EXECUTE_READWRITE,
|
||||||
|
&mut old_prot,
|
||||||
|
);
|
||||||
core::ptr::copy_nonoverlapping(PATCH.as_ptr(), target, PATCH.len());
|
core::ptr::copy_nonoverlapping(PATCH.as_ptr(), target, PATCH.len());
|
||||||
VirtualProtect(target as *const core::ffi::c_void, PATCH.len(), old_prot, &mut old_prot);
|
VirtualProtect(
|
||||||
|
target as *const core::ffi::c_void,
|
||||||
|
PATCH.len(),
|
||||||
|
old_prot,
|
||||||
|
&mut old_prot,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ use windows_sys::Win32::Foundation::BOOL;
|
|||||||
// CERT_CHAIN_POLICY_STATUS.dwError offset 0 = u32 error code; 0 = success.
|
// CERT_CHAIN_POLICY_STATUS.dwError offset 0 = u32 error code; 0 = success.
|
||||||
// We use raw pointers to avoid pulling in the full Cryptography struct tree.
|
// We use raw pointers to avoid pulling in the full Cryptography struct tree.
|
||||||
type CertVerifyChainPolicyFn = unsafe extern "system" fn(
|
type CertVerifyChainPolicyFn = unsafe extern "system" fn(
|
||||||
*const u8, // pszPolicyOID
|
*const u8, // pszPolicyOID
|
||||||
*const (), // pChainContext
|
*const (), // pChainContext
|
||||||
*const (), // pPolicyPara
|
*const (), // pPolicyPara
|
||||||
*mut u32, // &mut pPolicyStatus.dwError (first field)
|
*mut u32, // &mut pPolicyStatus.dwError (first field)
|
||||||
) -> BOOL;
|
) -> BOOL;
|
||||||
|
|
||||||
static REAL: OnceLock<CertVerifyChainPolicyFn> = OnceLock::new();
|
static REAL: OnceLock<CertVerifyChainPolicyFn> = OnceLock::new();
|
||||||
@@ -25,7 +25,12 @@ pub unsafe extern "system" fn hooked_cert_verify_chain_policy(
|
|||||||
p_policy_status: *mut u32,
|
p_policy_status: *mut u32,
|
||||||
) -> BOOL {
|
) -> BOOL {
|
||||||
if let Some(real) = REAL.get().copied() {
|
if let Some(real) = REAL.get().copied() {
|
||||||
real(psz_policy_oid, p_chain_context, p_policy_para, p_policy_status);
|
real(
|
||||||
|
psz_policy_oid,
|
||||||
|
p_chain_context,
|
||||||
|
p_policy_para,
|
||||||
|
p_policy_status,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Clear the error field of CERT_CHAIN_POLICY_STATUS regardless
|
// Clear the error field of CERT_CHAIN_POLICY_STATUS regardless
|
||||||
if !p_policy_status.is_null() {
|
if !p_policy_status.is_null() {
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ pub fn note_getaddrinfo(host: &str) {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
crate::write_log(&format!("TRANSPORT_WATCH: getaddrinfo host=\"{host}\"{tag}\n"));
|
crate::write_log(&format!(
|
||||||
|
"TRANSPORT_WATCH: getaddrinfo host=\"{host}\"{tag}\n"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
const AF_INET: u16 = 2; // IPv4
|
const AF_INET: u16 = 2; // IPv4
|
||||||
|
|||||||
+1
-2
@@ -66,9 +66,8 @@ impl ServiceHandle {
|
|||||||
}
|
}
|
||||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||||
|
|
||||||
let mut child = cmd.spawn().map_err(|e| {
|
let mut child = cmd.spawn().inspect_err(|e| {
|
||||||
*self.status.lock().unwrap() = ServiceStatus::Failed(e.to_string());
|
*self.status.lock().unwrap() = ServiceStatus::Failed(e.to_string());
|
||||||
e
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Drain stdout
|
// Drain stdout
|
||||||
|
|||||||
Reference in New Issue
Block a user