feat(hook): capture LSX/Winsock traffic and harden logging

- Hook send/recv and WSASend/WSARecv, filtered to LSX/XML content, to read
  the Ebisu-SDK <-> anadius conversation (challenge handshake, etc.).
- Hook getaddrinfo/GetAddrInfoW to log DNS lookups.
- Log connect() return value + WSA error; add accept/close diagnostics on
  the local listeners.
- Serialize log writes behind a mutex so concurrent threads don't corrupt
  each other's lines.

Used to establish that the online/offline decision is made via in-process
detoured calls, not socket traffic (no GetAuthCode/GoOnline on the wire).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-29 17:56:23 -07:00
parent bc6612697a
commit ce8a3b32ec
+140
View File
@@ -188,6 +188,7 @@ unsafe extern "system" fn init_thread(_: *mut c_void) -> u32 {
start_listener(LSX_PORT, "LSX"); start_listener(LSX_PORT, "LSX");
start_listener(LOCAL_PORT, "BLZ"); start_listener(LOCAL_PORT, "BLZ");
hook_dns(); hook_dns();
hook_winsock_data();
hook_connect(); hook_connect();
// Then the plaintext-capture detour on _ProtoSSLSendPacket. // Then the plaintext-capture detour on _ProtoSSLSendPacket.
@@ -405,6 +406,145 @@ unsafe fn hook_dns() {
install_detour(ws2, b"getaddrinfo\0", hooked_gai as *const (), &ORIG_GAI, "getaddrinfo"); install_detour(ws2, b"getaddrinfo\0", hooked_gai as *const (), &ORIG_GAI, "getaddrinfo");
} }
// --- LSX capture: read the Ebisu-SDK <-> anadius XML conversation ----------
static ORIG_SEND: AtomicUsize = AtomicUsize::new(0);
static ORIG_RECV: AtomicUsize = AtomicUsize::new(0);
type SendFn = unsafe extern "system" fn(usize, *const u8, i32, i32) -> i32;
type RecvFn = unsafe extern "system" fn(usize, *mut u8, i32, i32) -> i32;
/// Cheap test: does this buffer look like LSX/Ebisu XML (not TLS/binary)?
fn looks_like_lsx(buf: &[u8]) -> bool {
let n = buf.len().min(64);
let head = &buf[..n];
let has_lt = head.iter().any(|&b| b == b'<');
let has_gt = head.iter().any(|&b| b == b'>');
head.windows(3).any(|w| w == b"LSX")
|| head.windows(5).any(|w| w == b"Ebisu")
|| (has_lt && has_gt)
}
unsafe extern "system" fn hooked_send(s: usize, buf: *const u8, len: i32, flags: i32) -> i32 {
if len > 0 && !buf.is_null() {
let head = core::slice::from_raw_parts(buf, (len as usize).min(64));
if looks_like_lsx(head) {
let show = core::slice::from_raw_parts(buf, (len as usize).min(800));
log(&format!("LSX send sock={s} {len}B: {}", ascii_render(show)));
}
}
let orig: SendFn = core::mem::transmute(ORIG_SEND.load(Ordering::SeqCst));
orig(s, buf, len, flags)
}
unsafe extern "system" fn hooked_recv(s: usize, buf: *mut u8, len: i32, flags: i32) -> i32 {
let orig: RecvFn = core::mem::transmute(ORIG_RECV.load(Ordering::SeqCst));
let ret = orig(s, buf, len, flags);
if ret > 0 && !buf.is_null() {
let head = core::slice::from_raw_parts(buf, (ret as usize).min(64));
if looks_like_lsx(head) {
let show = core::slice::from_raw_parts(buf, (ret as usize).min(800));
log(&format!("LSX recv sock={s} {ret}B: {}", ascii_render(show)));
}
}
ret
}
// Async (overlapped/IOCP) variants. A WSABUF is { len, buf }.
#[repr(C)]
struct WsaBuf {
len: u32,
buf: *mut u8,
}
static ORIG_WSASEND: AtomicUsize = AtomicUsize::new(0);
static ORIG_WSARECV: AtomicUsize = AtomicUsize::new(0);
type WsaSendFn = unsafe extern "system" fn(
usize,
*const WsaBuf,
u32,
*mut u32,
u32,
*mut c_void,
*mut c_void,
) -> i32;
type WsaRecvFn = unsafe extern "system" fn(
usize,
*const WsaBuf,
u32,
*mut u32,
*mut u32,
*mut c_void,
*mut c_void,
) -> i32;
unsafe extern "system" fn hooked_wsasend(
s: usize,
bufs: *const WsaBuf,
count: u32,
sent: *mut u32,
flags: u32,
ovl: *mut c_void,
cr: *mut c_void,
) -> i32 {
// Outgoing data is readable before the call — capture the first buffer.
if !bufs.is_null() && count > 0 {
let b0 = &*bufs;
if b0.len > 0 && !b0.buf.is_null() {
let head = core::slice::from_raw_parts(b0.buf, (b0.len as usize).min(64));
if looks_like_lsx(head) {
let show = core::slice::from_raw_parts(b0.buf, (b0.len as usize).min(800));
log(&format!("LSX WSASend sock={s} {}B: {}", b0.len, ascii_render(show)));
}
}
}
let orig: WsaSendFn = core::mem::transmute(ORIG_WSASEND.load(Ordering::SeqCst));
orig(s, bufs, count, sent, flags, ovl, cr)
}
unsafe extern "system" fn hooked_wsarecv(
s: usize,
bufs: *const WsaBuf,
count: u32,
recvd: *mut u32,
flags: *mut u32,
ovl: *mut c_void,
cr: *mut c_void,
) -> i32 {
let orig: WsaRecvFn = core::mem::transmute(ORIG_WSARECV.load(Ordering::SeqCst));
let ret = orig(s, bufs, count, recvd, flags, ovl, cr);
// Only the synchronous case (no overlapped) has data ready on return.
if ret == 0 && ovl.is_null() && !recvd.is_null() && !bufs.is_null() && count > 0 {
let n = *recvd as usize;
let b0 = &*bufs;
if n > 0 && !b0.buf.is_null() {
let cap = n.min(b0.len as usize);
let head = core::slice::from_raw_parts(b0.buf, cap.min(64));
if looks_like_lsx(head) {
let show = core::slice::from_raw_parts(b0.buf, cap.min(800));
log(&format!("LSX WSARecv sock={s} {n}B: {}", ascii_render(show)));
}
}
}
ret
}
/// Detour ws2_32 send/recv (sync) and WSASend/WSARecv (async) to capture LSX XML.
unsafe fn hook_winsock_data() {
let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) {
Ok(m) => m,
Err(e) => {
log(&format!("ERROR: load ws2_32 for send/recv: {e:?}"));
return;
}
};
install_detour(ws2, b"send\0", hooked_send as *const (), &ORIG_SEND, "send");
install_detour(ws2, b"recv\0", hooked_recv as *const (), &ORIG_RECV, "recv");
install_detour(ws2, b"WSASend\0", hooked_wsasend as *const (), &ORIG_WSASEND, "WSASend");
install_detour(ws2, b"WSARecv\0", hooked_wsarecv as *const (), &ORIG_WSARECV, "WSARecv");
}
/// Resolve and detour ws2_32 `connect`. /// Resolve and detour ws2_32 `connect`.
unsafe fn hook_connect() { unsafe fn hook_connect() {
let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) { let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) {