diff --git a/Cargo.lock b/Cargo.lock index a8106e9f..ee1d5ad9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,6 +1089,19 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.2", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "der" version = "0.7.8" @@ -1792,6 +1805,18 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "futures-scopes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcf32827e803f1a3cd04c4319feb99156cb5968a3b393f8541efefa1e3b24c" +dependencies = [ + "crossbeam-channel", + "dashmap", + "futures", + "pin-project", +] + [[package]] name = "futures-sink" version = "0.3.29" @@ -2114,7 +2139,7 @@ name = "hook" version = "0.1.0" dependencies = [ "anyhow", - "patternsleuth_scanner", + "patternsleuth", "retour", "windows 0.52.0", ] @@ -2214,6 +2239,15 @@ dependencies = [ "cc", ] +[[package]] +name = "iced-x86" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd366a53278429c028367e0ba22a46cab6d565a57afb959f06e92c7a69e7828" +dependencies = [ + "lazy_static", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2332,6 +2366,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -2946,7 +2989,9 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ + "flate2", "memchr", + "ruzstd", ] [[package]] @@ -3089,10 +3134,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" +[[package]] +name = "patternsleuth" +version = "0.1.0" +source = "git+https://github.com/trumank/patternsleuth#4b99b771e2a55ecafa6a0c3837679a69f8ce128c" +dependencies = [ + "anyhow", + "futures", + "futures-scopes", + "iced-x86", + "inventory", + "itertools 0.11.0", + "libc", + "memchr", + "object", + "patternsleuth_scanner", + "rayon", + "strum 0.25.0", + "windows 0.52.0", +] + [[package]] name = "patternsleuth_scanner" version = "0.1.0" -source = "git+https://github.com/trumank/patternsleuth#ac917839951733cd4863fbeebe2503c6453c9185" +source = "git+https://github.com/trumank/patternsleuth#4b99b771e2a55ecafa6a0c3837679a69f8ce128c" dependencies = [ "anyhow", "memchr", @@ -3161,6 +3226,26 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -3261,7 +3346,7 @@ checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" dependencies = [ "difflib", "float-cmp", - "itertools", + "itertools 0.10.5", "normalize-line-endings", "predicates-core", "regex", @@ -3512,7 +3597,7 @@ dependencies = [ "libloading 0.7.4", "once_cell", "sha1", - "strum", + "strum 0.24.1", "thiserror", "ureq", "zstd", @@ -3749,6 +3834,17 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "ruzstd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3ffab8f9715a0d455df4bbb9d21e91135aab3cd3ca187af0cd0c3c3f868fdc" +dependencies = [ + "byteorder", + "thiserror-core", + "twox-hash", +] + [[package]] name = "ryu" version = "1.0.15" @@ -4159,7 +4255,16 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.3", ] [[package]] @@ -4175,6 +4280,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.39", +] + [[package]] name = "subtle" version = "2.5.0" @@ -4286,6 +4404,26 @@ dependencies = [ "thiserror-impl", ] +[[package]] +name = "thiserror-core" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d97345f6437bb2004cd58819d8a9ef8e36cdd7661c2abc4bbde0a7c40d9f497" +dependencies = [ + "thiserror-core-impl", +] + +[[package]] +name = "thiserror-core-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ac1c5050e43014d16b2f94d0d2ce79e65ffdd8b38d8048f9c8f6a8a6da62ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "thiserror-impl" version = "1.0.50" @@ -4575,6 +4713,16 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + [[package]] name = "typenum" version = "1.17.0" diff --git a/hook/Cargo.toml b/hook/Cargo.toml index 580d41ef..8e715ba2 100644 --- a/hook/Cargo.toml +++ b/hook/Cargo.toml @@ -9,15 +9,12 @@ crate-type = ["cdylib"] [dependencies] anyhow = "1.0.75" -patternsleuth_scanner = { git = "https://github.com/trumank/patternsleuth", version = "0.1.0" } +patternsleuth = { git = "https://github.com/trumank/patternsleuth", features = ["process-internal"] } retour = { version = "0.3.1", features = ["static-detour"] } windows = { version = "0.52.0", features = [ "Win32_Foundation", "Win32_System_SystemServices", - "Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Threading", - "Win32_Security", - "Win32_System_ProcessStatus", ] } diff --git a/hook/src/lib.rs b/hook/src/lib.rs index 3aedfdd5..aeb149d9 100644 --- a/hook/src/lib.rs +++ b/hook/src/lib.rs @@ -4,16 +4,12 @@ use std::{ }; use anyhow::{bail, Context, Result}; -use patternsleuth_scanner::Pattern; use retour::static_detour; use windows::Win32::{ Foundation::HMODULE, System::{ - LibraryLoader::GetModuleHandleA, - Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE, PAGE_PROTECTION_FLAGS}, - ProcessStatus::{GetModuleInformation, MODULEINFO}, + Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE}, SystemServices::*, - Threading::GetCurrentProcess, Threading::{GetCurrentThread, QueueUserAPC}, }, }; @@ -73,6 +69,95 @@ impl DRGInstallationType { } } +mod resolvers { + use patternsleuth::resolvers::futures::future::join_all; + use patternsleuth::resolvers::unreal::*; + use patternsleuth::resolvers::*; + use patternsleuth::scanner::Pattern; + + #[derive(Debug)] + pub struct GetServerName(pub usize); + impl_resolver_singleton!(GetServerName, |ctx| async { + let patterns = [ + "48 89 5C 24 10 48 89 6C 24 18 48 89 74 24 20 57 41 56 41 57 48 83 EC 30 45 33 FF 4C 8B F2 48 8B D9 44 89 7C 24 50 41 8B FF" + ]; + + let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await; + + // matches two but the first is the one we need + // could potentially get number of xrefs if this becomes problematic + Ok(Self( + res.into_iter() + .flatten() + .next() + .context("expected at least one")?, + )) + }); + + #[derive(Debug)] + pub struct Disable(pub usize); + impl_resolver_singleton!(Disable, |ctx| async { + let patterns = ["4C 8B B4 24 48 01 00 00 0F 84"]; + + let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await; + + Ok(Self(ensure_one(res.into_iter().flatten())?)) + }); + + #[derive(Debug)] + pub struct Resize16(pub usize); + impl_resolver_singleton!(Resize16, |ctx| async { + let patterns = [ + "48 89 5C 24 ?? 57 48 83 EC 20 48 63 DA 48 8B F9 85 D2 74 ?? 48 8B CB 33 D2 48 03 C9", + ]; + + let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await; + + Ok(Self(ensure_one(res.into_iter().flatten())?)) + }); + + #[derive(Debug)] + pub struct FMemoryFree(pub usize); + impl_resolver_singleton!(FMemoryFree, |ctx| async { + let patterns = ["48 85 C9 74 ?? 53 48 83 EC 20 48 8B D9 48 8B 0D"]; + + let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await; + + Ok(Self(ensure_one(res.into_iter().flatten())?)) + }); + + impl_try_collector! { + #[derive(Debug)] + pub struct ServerNameResolution { + pub fmemory_free: FMemoryFree, + pub resize16: Resize16, + pub get_server_name: GetServerName, + } + } + + impl_try_collector! { + #[derive(Debug)] + pub struct SaveGameResolution { + pub fmemory_free: FMemoryFree, + pub save_game_to_memory: UGameplayStaticsSaveGameToMemory, + pub save_game_to_slot: UGameplayStaticsSaveGameToSlot, + pub load_game_from_memory: UGameplayStaticsLoadGameFromMemory, + pub load_game_from_slot: UGameplayStaticsLoadGameFromSlot, + pub does_save_game_exist: UGameplayStaticsDoesSaveGameExist, + } + } + + impl_collector! { + #[derive(Debug)] + pub struct HookResolution { + pub fmemory_free: FMemoryFree, + pub disable: Disable, + pub server_name: ServerNameResolution, + pub save_game: SaveGameResolution, + } + } +} + unsafe extern "system" fn init(_: usize) { patch().ok(); } @@ -92,148 +177,97 @@ unsafe fn patch() -> Result<()> { let installation_type = DRGInstallationType::from_exe_path()?; - let module = GetModuleHandleA(None).context("could not find main module")?; - let process = GetCurrentProcess(); + let image = patternsleuth::process::internal::read_image()?; + let resolution = image.resolve(resolvers::HookResolution::resolver())?; + println!("{:#x?}", resolution); - let mut mod_info = MODULEINFO::default(); - GetModuleInformation( - process, - module, - &mut mod_info as *mut _, - std::mem::size_of::() as u32, - )?; - - let module_addr = mod_info.lpBaseOfDll; - - let memory = std::slice::from_raw_parts_mut( - mod_info.lpBaseOfDll as *mut u8, - mod_info.SizeOfImage as usize, - ); - - #[derive(Debug, Clone, Copy, Eq, PartialEq)] - enum Sig { - GetServerName, - Disable, - SaveGameToSlot, - LoadGameFromMemory, - LoadGameFromSlot, - DoesSaveGameExist, + if let Ok(fmemory_free) = resolution.fmemory_free { + Free = Some(std::mem::transmute(fmemory_free.0)); } - let patterns = [ - (Sig::GetServerName, Pattern::new("48 89 5C 24 10 48 89 6C 24 18 48 89 74 24 20 57 41 56 41 57 48 83 EC 30 45 33 FF 4C 8B F2 48 8B D9 44 89 7C 24 50 41 8B FF").unwrap()), - (Sig::Disable, Pattern::new("4C 8B B4 24 48 01 00 00 0F 84").unwrap()), - (Sig::SaveGameToSlot, Pattern::new("48 89 5c 24 08 48 89 74 24 10 57 48 83 ec 40 48 8b da 33 f6 48 8d 54 24 30 48 89 74 24 30 48 89 74 24 38 41 8b f8").unwrap()), - (Sig::LoadGameFromMemory, Pattern::new("40 55 48 8d ac 24 00 ff ff ff 48 81 ec 00 02 00 00 83 79 08 00").unwrap()), - (Sig::LoadGameFromSlot, Pattern::new("48 8b c4 55 57 48 8d a8 d8 fe ff ff 48 81 ec 18 02 00 00").unwrap()), - (Sig::DoesSaveGameExist, Pattern::new("48 89 5C 24 08 57 48 83 EC 20 8B FA 48 8B D9 E8 ?? ?? ?? ?? 48 8B C8 4C 8B 00 41 FF 50 40 48 8B C8 48 85 C0 74 38 83 7B 08 00 74 17 48 8B 00 44 8B C7 48 8B 13 48 8B 5C 24 30 48 83 C4 20 5F 48 FF 60 08 48 8B 00 48 8D ?? ?? ?? ?? ?? 44 8B C7 48 8B 5C 24 30 48 83 C4 20 5F 48 FF 60 08 48 8B 5C 24 30 48 83 C4 20 5F C3").unwrap()), - ]; - let pattern_refs = patterns.iter().map(|(_, p)| p).collect::>(); - - let results = patterns - .iter() - .map(|(s, _)| s) - .zip(patternsleuth_scanner::scan_pattern( - &pattern_refs, - 0, - memory, - )) - .collect::>(); + if let Ok(server_name) = resolution.server_name { + Resize16 = Some(std::mem::transmute(server_name.resize16.0)); - let get_sig = |sig: Sig| { - results - .iter() - .find_map(|(s, results)| (**s == sig).then(|| results.first().cloned())) - .flatten() - }; - - if let Some(rva) = get_sig(Sig::GetServerName) { - let address = module_addr.add(rva); - - Resize16 = Some(std::mem::transmute(address.add(53 + 4).offset( - i32::from_le_bytes(memory[rva + 53..rva + 53 + 4].try_into().unwrap()) as isize, - ))); - - let target: FnGetServerName = std::mem::transmute(address); GetServerName - .initialize(target, get_server_name_detour)? + .initialize( + std::mem::transmute(server_name.get_server_name.0), + get_server_name_detour, + )? .enable()?; } - if matches!(installation_type, DRGInstallationType::Steam) { - if let Some(rva) = get_sig(Sig::Disable) { - let patch = [0xB8, 0x01, 0x00, 0x00, 0x00]; - - let rva = rva + 29; - let patch_mem = &mut memory[rva..rva + 5]; - - let mut old: PAGE_PROTECTION_FLAGS = Default::default(); - VirtualProtect( - patch_mem.as_ptr() as *const c_void, - patch_mem.len(), - PAGE_EXECUTE_READWRITE, - &mut old, - )?; - - patch_mem.copy_from_slice(&patch); - - VirtualProtect( - patch_mem.as_ptr() as *const c_void, - patch_mem.len(), - old, - &mut old, - )?; - } - } - if matches!(installation_type, DRGInstallationType::Xbox) { - SAVES_DIR = Some( - std::env::current_exe() - .ok() - .as_deref() - .and_then(Path::parent) - .and_then(Path::parent) - .and_then(Path::parent) - .context("could not determine save location")? - .join("Saved") - .join("SaveGames"), - ); - - if let Some(rva) = get_sig(Sig::SaveGameToSlot) { - let address = module_addr.add(rva); - - SaveGameToMemory = Some(std::mem::transmute(address.add(39 + 4).offset( - i32::from_le_bytes(memory[rva + 39..rva + 39 + 4].try_into().unwrap()) as isize, - ))); - - let target: FnSaveGameToSlot = std::mem::transmute(address); - SaveGameToSlot - .initialize(target, save_game_to_slot_detour)? - .enable()?; + match installation_type { + DRGInstallationType::Steam => { + if let Ok(address) = resolution.disable { + patch_mem( + (address.0 as *mut u8).add(29), + [0xB8, 0x01, 0x00, 0x00, 0x00], + )?; + } } - - if let Some(rva) = get_sig(Sig::LoadGameFromMemory) { - let address = module_addr.add(rva); - LoadGameFromMemory = Some(std::mem::transmute(address)); - - if let Some(rva) = get_sig(Sig::LoadGameFromSlot) { - let address = module_addr.add(rva); - - let target: FnLoadGameFromSlot = std::mem::transmute(address); + DRGInstallationType::Xbox => { + SAVES_DIR = Some( + std::env::current_exe() + .ok() + .as_deref() + .and_then(Path::parent) + .and_then(Path::parent) + .and_then(Path::parent) + .context("could not determine save location")? + .join("Saved") + .join("SaveGames"), + ); + + if let Ok(save_game) = resolution.save_game { + SaveGameToMemory = Some(std::mem::transmute(save_game.save_game_to_memory.0)); + LoadGameFromMemory = Some(std::mem::transmute(save_game.load_game_from_memory.0)); + + SaveGameToSlot + .initialize( + std::mem::transmute(save_game.save_game_to_slot.0), + save_game_to_slot_detour, + )? + .enable()?; LoadGameFromSlot - .initialize(target, load_game_from_slot_detour)? + .initialize( + std::mem::transmute(save_game.load_game_from_slot.0), + load_game_from_slot_detour, + )? + .enable()?; + + DoesSaveGameExist + .initialize( + std::mem::transmute(save_game.does_save_game_exist.0), + does_save_game_exist_detour, + )? .enable()?; } } + } + Ok(()) +} - if let Some(rva) = get_sig(Sig::DoesSaveGameExist) { - let address = module_addr.add(rva); +unsafe fn patch_mem(address: *mut u8, patch: impl AsRef<[u8]>) -> Result<()> { + let patch = patch.as_ref(); + let patch_mem = std::slice::from_raw_parts_mut(address, patch.len()); + + let mut old = Default::default(); + VirtualProtect( + patch_mem.as_ptr() as *const c_void, + patch_mem.len(), + PAGE_EXECUTE_READWRITE, + &mut old, + )?; + + patch_mem.copy_from_slice(patch); + + VirtualProtect( + patch_mem.as_ptr() as *const c_void, + patch_mem.len(), + old, + &mut old, + )?; - let target: FnDoesSaveGameExist = std::mem::transmute(address); - DoesSaveGameExist - .initialize(target, does_save_game_exist_detour)? - .enable()?; - } - } Ok(()) } @@ -284,13 +318,10 @@ impl FString { } } +type FnFree = unsafe extern "system" fn(*const c_void); type FnResize16 = unsafe extern "system" fn(*const c_void, new_max: i32); -type FnGetServerName = unsafe extern "system" fn(*const c_void, *const c_void) -> *const FString; -type FnSaveGameToSlot = unsafe extern "system" fn(*const USaveGame, *const FString, i32) -> bool; type FnSaveGameToMemory = unsafe extern "system" fn(*const USaveGame, *mut TArray) -> bool; -type FnLoadGameFromSlot = unsafe extern "system" fn(*const FString, i32) -> *const USaveGame; type FnLoadGameFromMemory = unsafe extern "system" fn(*const TArray) -> *const USaveGame; -type FnDoesSaveGameExist = unsafe extern "system" fn(*const FString, i32) -> bool; static_detour! { static GetServerName: unsafe extern "system" fn(*const c_void, *const c_void) -> *const FString; @@ -299,6 +330,8 @@ static_detour! { static DoesSaveGameExist: unsafe extern "system" fn(*const FString, i32) -> bool; } +#[allow(non_upper_case_globals)] +static mut Free: Option = None; #[allow(non_upper_case_globals)] static mut Resize16: Option = None; #[allow(non_upper_case_globals)] @@ -352,7 +385,9 @@ fn save_game_to_slot_detour( std::fs::create_dir_all(parent).ok(); } - std::fs::write(path, data.as_slice()).is_ok() + let res = std::fs::write(path, data.as_slice()).is_ok(); + Free.unwrap()(data.data as *const c_void); + res } } } @@ -365,7 +400,6 @@ fn load_game_from_slot_detour(slot_name: *const FString, user_index: i32) -> *co } else if let Some(data) = get_path_for_slot(slot_name).and_then(|path| std::fs::read(path).ok()) { - // TODO this currently leaks the buffer but to free it we need to find the allocator LoadGameFromMemory.unwrap()(&TArray::from_slice(data.as_slice())) } else { std::ptr::null()