M1: gate is upstream + two-gate (state+token) finding; add callers mode
Read-only M1 investigation results appended to connection-gate-findings.md: - Confirmed ProtoSSLConnect is never reached on the "connecting" abort (gate is upstream/in-process) via the existing connect/DNS hooks. - Mapped the surface: redirector host table (gosredirector.* per env), the redirector request config (X-BLAZE-ERRORCODE, Authorization:, <errorCode>), and the enum-name serialization tables (ONLINE_ACCESS, ONLINE_STATUS_EVENT). - Key answer: the online-connect path requires a Nucleus auth token (the redirector request carries an Authorization header) => TWO gates (state + token), not a single boolean flip. - The exact connection-state function is behind C++ vtable indirection; pinning it needs Ghidra. protossl-scan stays the live-process bridge. protossl-scan: add `callers` mode (find call/jmp sites to a function) and restrict xref/callers scans to the app modules (FIFA23.exe/anadius64.dll) for speed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -100,11 +100,28 @@ fn main() {
|
||||
let process = open_for_read(pid);
|
||||
let modules = enumerate_modules(pid);
|
||||
|
||||
let addrs = find_string_addresses(process, text.as_bytes());
|
||||
if addrs.is_empty() {
|
||||
let all = find_string_addresses(process, text.as_bytes());
|
||||
// Only xref hits inside the app modules (FIFA23.exe / anadius64.dll).
|
||||
// Hits in system DLLs are noise and each one would trigger a slow
|
||||
// full-memory scan, so we skip them.
|
||||
let addrs: Vec<usize> = all
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|a| in_app_module(*a, &modules))
|
||||
.collect();
|
||||
if all.is_empty() {
|
||||
println!("String \"{text}\" not found in PID {pid}. Did you reach the menu?");
|
||||
} else if addrs.is_empty() {
|
||||
println!(
|
||||
"Found \"{text}\" at {} location(s), but none in FIFA23.exe/anadius64.dll.",
|
||||
all.len()
|
||||
);
|
||||
} else {
|
||||
println!("Found \"{text}\" at {} location(s):", addrs.len());
|
||||
println!(
|
||||
"Found \"{text}\" at {} location(s) ({} in app modules):",
|
||||
all.len(),
|
||||
addrs.len()
|
||||
);
|
||||
for a in addrs {
|
||||
println!();
|
||||
// Show the surrounding bytes and find the TRUE start of the
|
||||
@@ -119,6 +136,33 @@ fn main() {
|
||||
let _ = CloseHandle(process);
|
||||
}
|
||||
}
|
||||
// callers <hex-addr | module+0xoffset> [pid|name]
|
||||
// Find direct call/jmp sites that target an address — walks up the call
|
||||
// graph (e.g. from a connect helper to the code that gates it).
|
||||
Some("callers") => {
|
||||
let arg = match args.get(1) {
|
||||
Some(a) => a.clone(),
|
||||
None => {
|
||||
eprintln!("Usage: protossl-scan callers <hex-addr | module+0xoffset> [pid|name]");
|
||||
eprintln!("Example: protossl-scan callers anadius64.dll+0x2BB90");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let pid = resolve_pid(args.get(2).map(|s| s.as_str()));
|
||||
let process = open_for_read(pid);
|
||||
let modules = enumerate_modules(pid);
|
||||
let target = match resolve_target(&arg, &modules) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
eprintln!("Could not resolve '{arg}' (unknown module or bad address)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
run_callers(process, &modules, target);
|
||||
unsafe {
|
||||
let _ = CloseHandle(process);
|
||||
}
|
||||
}
|
||||
// disasm <hex-addr> [pid|name]
|
||||
// Disassemble the function enclosing an address, annotating string
|
||||
// loads and call targets. Use this on a code anchor (e.g. the lea that
|
||||
@@ -194,7 +238,7 @@ fn find_string_addresses(process: HANDLE, text: &[u8]) -> Vec<usize> {
|
||||
let mut hits: HashSet<usize> = HashSet::new();
|
||||
let overlap = text.len().saturating_sub(1);
|
||||
let finder = memmem::Finder::new(text);
|
||||
walk_regions(process, false, overlap, |chunk_base, bytes| {
|
||||
walk_regions(process, false, overlap, None, |chunk_base, bytes| {
|
||||
for off in finder.find_iter(bytes) {
|
||||
hits.insert(chunk_base + off);
|
||||
}
|
||||
@@ -220,7 +264,7 @@ fn run_marker_scan(process: HANDLE, modules: &[ModuleInfo]) {
|
||||
// Build one SIMD finder per marker, reused across every chunk.
|
||||
let finders: Vec<memmem::Finder> = MARKERS.iter().map(|m| memmem::Finder::new(m)).collect();
|
||||
|
||||
walk_regions(process, false, overlap, |chunk_base, bytes| {
|
||||
walk_regions(process, false, overlap, None, |chunk_base, bytes| {
|
||||
for (i, finder) in finders.iter().enumerate() {
|
||||
for off in finder.find_iter(bytes) {
|
||||
hits[i].insert(chunk_base + off);
|
||||
@@ -273,13 +317,18 @@ fn run_xref(process: HANDLE, modules: &[ModuleInfo], target: usize) {
|
||||
println!("== protossl-scan : xref of 0x{target:X} ==");
|
||||
println!("({})", describe(target, modules));
|
||||
|
||||
// Restrict the (expensive) scans to the app modules; the code/tables that
|
||||
// reference our target live in FIFA23.exe or anadius64.dll, not system DLLs.
|
||||
let app = app_module_ranges(modules);
|
||||
let allow = Some(app.as_slice());
|
||||
|
||||
// (a) Absolute 8-byte pointers to `target`. These usually live in a
|
||||
// read-only data table. If the table pairs names with functions, a
|
||||
// neighbouring slot will hold the function pointer we actually want.
|
||||
let needle = (target as u64).to_le_bytes();
|
||||
let ptr_finder = memmem::Finder::new(&needle);
|
||||
let mut ptr_hits: HashSet<usize> = HashSet::new();
|
||||
walk_regions(process, false, needle.len() - 1, |chunk_base, bytes| {
|
||||
walk_regions(process, false, needle.len() - 1, allow, |chunk_base, bytes| {
|
||||
for off in ptr_finder.find_iter(bytes) {
|
||||
ptr_hits.insert(chunk_base + off);
|
||||
}
|
||||
@@ -310,7 +359,7 @@ fn run_xref(process: HANDLE, modules: &[ModuleInfo], target: usize) {
|
||||
// at address P are `disp`, the referenced target is `P + 4 + disp`.
|
||||
// We scan executable pages for any P where that equals our target.
|
||||
let mut code_hits: HashSet<usize> = HashSet::new();
|
||||
walk_regions(process, true, 3, |chunk_base, bytes| {
|
||||
walk_regions(process, true, 3, allow, |chunk_base, bytes| {
|
||||
if bytes.len() < 4 {
|
||||
return;
|
||||
}
|
||||
@@ -371,6 +420,58 @@ fn dump_neighbours(process: HANDLE, at: usize, modules: &[ModuleInfo]) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mode 2b: find callers (who calls/jmps to a function)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Scan app-module executable memory for near `call`/`jmp` (E8/E9 + rel32)
|
||||
/// instructions whose target is `target`. This walks UP the call graph — e.g.
|
||||
/// from a connect helper to the code that decides whether to call it.
|
||||
fn run_callers(process: HANDLE, modules: &[ModuleInfo], target: usize) {
|
||||
println!("== protossl-scan : callers of 0x{target:X} ==");
|
||||
println!("({})\n", describe(target, modules));
|
||||
|
||||
let app = app_module_ranges(modules);
|
||||
let allow = Some(app.as_slice());
|
||||
let mut hits: Vec<(usize, u8)> = Vec::new();
|
||||
|
||||
walk_regions(process, true, 4, allow, |chunk_base, bytes| {
|
||||
if bytes.len() < 5 {
|
||||
return;
|
||||
}
|
||||
for i in 0..=bytes.len() - 5 {
|
||||
let op = bytes[i];
|
||||
if op != 0xE8 && op != 0xE9 {
|
||||
continue;
|
||||
}
|
||||
let rel = i32::from_le_bytes([bytes[i + 1], bytes[i + 2], bytes[i + 3], bytes[i + 4]]);
|
||||
let after = chunk_base + i + 5; // address just past the rel32
|
||||
let tgt = after.wrapping_add(rel as i64 as usize);
|
||||
if tgt == target {
|
||||
hits.push((chunk_base + i, op));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
hits.sort_unstable();
|
||||
hits.dedup();
|
||||
if hits.is_empty() {
|
||||
println!(" no direct call/jmp sites found (may be called indirectly via a pointer)");
|
||||
} else {
|
||||
println!("-- {} call/jmp site(s) --", hits.len());
|
||||
for (at, op) in hits.iter().take(40) {
|
||||
let kind = if *op == 0xE8 { "call" } else { "jmp " };
|
||||
println!(" {kind} from {}", describe(*at, modules));
|
||||
}
|
||||
if hits.len() > 40 {
|
||||
println!(" ... and {} more", hits.len() - 40);
|
||||
}
|
||||
println!("\n-- Next --");
|
||||
println!("`disasm <one of the call sites>` to read the calling function and find");
|
||||
println!("the branch/condition that gates the call.");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mode 3: disassemble the enclosing function
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -539,6 +640,31 @@ fn parse_hex(s: &str) -> Option<usize> {
|
||||
usize::from_str_radix(trimmed, 16).ok()
|
||||
}
|
||||
|
||||
/// Is this address inside one of the app modules we care about
|
||||
/// (FIFA23.exe or anadius64.dll)? Used to skip noisy system-DLL hits.
|
||||
fn in_app_module(addr: usize, modules: &[ModuleInfo]) -> bool {
|
||||
for m in modules {
|
||||
if addr >= m.base && addr < m.base + m.size {
|
||||
let n = m.name.to_ascii_lowercase();
|
||||
return n.starts_with("fifa23") || n.starts_with("anadius64");
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `[base, base+size)` ranges for the app modules (FIFA23.exe / anadius64.dll),
|
||||
/// used to restrict expensive scans to the code we care about.
|
||||
fn app_module_ranges(modules: &[ModuleInfo]) -> Vec<(usize, usize)> {
|
||||
modules
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
let n = m.name.to_ascii_lowercase();
|
||||
n.starts_with("fifa23") || n.starts_with("anadius64")
|
||||
})
|
||||
.map(|m| (m.base, m.base + m.size))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolve a target that is either a raw hex address or a `module+0xoffset`
|
||||
/// form (e.g. `anadius64.dll+0x2BB90`). The module form is ASLR-robust: it adds
|
||||
/// the offset to the module's CURRENT base in the running process.
|
||||
@@ -669,6 +795,7 @@ fn walk_regions<F: FnMut(usize, &[u8])>(
|
||||
process: HANDLE,
|
||||
exec_only: bool,
|
||||
overlap: usize,
|
||||
allow: Option<&[(usize, usize)]>,
|
||||
mut f: F,
|
||||
) {
|
||||
let mut buf = vec![0u8; CHUNK];
|
||||
@@ -696,7 +823,15 @@ fn walk_regions<F: FnMut(usize, &[u8])>(
|
||||
} else {
|
||||
is_readable(mbi.Protect.0)
|
||||
};
|
||||
if mbi.State == MEM_COMMIT && wanted {
|
||||
// If an allow-list of ranges is given, only scan regions that overlap
|
||||
// one of them (e.g. restrict to the FIFA23.exe / anadius64.dll images).
|
||||
let in_allow = match allow {
|
||||
None => true,
|
||||
Some(ranges) => ranges
|
||||
.iter()
|
||||
.any(|&(b, e)| region_base < e && region_base + region_size > b),
|
||||
};
|
||||
if mbi.State == MEM_COMMIT && wanted && in_allow {
|
||||
// Read this region in overlapping chunks.
|
||||
let end = region_base.saturating_add(region_size);
|
||||
let mut pos = region_base;
|
||||
|
||||
Reference in New Issue
Block a user