From a493e86b86e4df1beb6d18ed4a372aaa3b2b7c43 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Thu, 12 Oct 2023 00:04:24 +0200 Subject: [PATCH 01/14] Added the ability to iterate over exported symbols in a given pe module Some processes do export useful symbols that can be used to recover memory addresses or other useful values. The newly added `symbols()` function allows to iterate over them in order to recover, if needed, the addresses of interest. For example, Duckstation (a PS1 emulator) exports the address of the emulated RAM as a symbol, so it can be easily recovered by simply querying: ```rust let pointer: Address = pe::symbols::<5>(&process, main_module).find(|symbol| symbol.name.matches(b"RAM")); ``` --- src/file_format/pe.rs | 78 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/src/file_format/pe.rs b/src/file_format/pe.rs index 53765dc..4697b6d 100644 --- a/src/file_format/pe.rs +++ b/src/file_format/pe.rs @@ -4,7 +4,7 @@ use core::{fmt, mem}; use bytemuck::{Pod, Zeroable}; -use crate::{Address, FromEndian, Process}; +use crate::{string::ArrayCString, Address, FromEndian, Process}; // Reference: // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format @@ -95,6 +95,16 @@ struct OptionalCOFFHeader { // There's more but those vary depending on whether it's PE or PE+. } +#[derive(Debug, Copy, Clone, Zeroable, Pod, Default)] +#[repr(C)] +struct ExportedSymbolsTableDef { + _unk: [u8; 0x14], + number_of_functions: u32, + _unk_1: u32, + function_address_array_index: u32, + function_name_array_index: u32, +} + /// The machine type (architecture) of a module in a process. An image file can /// be run only on the specified machine or on a system that emulates the /// specified machine. @@ -264,3 +274,69 @@ fn read_coff_header(process: &Process, module_address: Address) -> Option<(COFFH Some((coff_header, coff_header_address)) } + +/// A symbol exported into the current module. +pub struct Symbol { + /// The name of the current symbol + pub name: ArrayCString, + /// The address associated with the current symbol + pub address: Address, +} + +/// Recovers and iterates over the exported symbols for a given module. +/// Returns an empty iterator if no symbols are exported into the current module. +pub fn symbols( + process: &Process, + module_address: impl Into
, +) -> impl DoubleEndedIterator> + '_ { + let address: Address = module_address.into(); + let dos_header = process.read::(address); + + let is_64_bit = match dos_header { + Ok(_) => matches!( + MachineType::read(process, address), + Some(MachineType::X86_64) + ), + _ => false, + }; + + let export_directory = match dos_header { + Ok(x) => process + .read::(address + x.e_lfanew + if is_64_bit { 0x88 } else { 0x78 }) + .ok(), + _ => None, + }; + + let symbols_def = match dos_header { + Ok(_) => match export_directory { + Some(0) => None, + Some(export_dir) => process + .read::(address + export_dir) + .ok(), + _ => None, + }, + _ => None, + } + .unwrap_or_default(); + + (0..symbols_def.number_of_functions).filter_map(move |i| { + let name_index = process + .read::( + address + symbols_def.function_name_array_index + i.wrapping_mul(4), + ) + .ok()?; + + let name = process + .read::>(address + name_index) + .ok()?; + + let address: Address = address + + process + .read::( + address + symbols_def.function_address_array_index + i.wrapping_mul(4), + ) + .ok()?; + + Some(Symbol { name, address }) + }) +} From 749650d2074bcb9b28034682592cdbbfbd0fa8b7 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Thu, 12 Oct 2023 11:47:53 +0200 Subject: [PATCH 02/14] adding `impl Symbols` --- src/file_format/pe.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/file_format/pe.rs b/src/file_format/pe.rs index 4697b6d..f3561ee 100644 --- a/src/file_format/pe.rs +++ b/src/file_format/pe.rs @@ -276,11 +276,18 @@ fn read_coff_header(process: &Process, module_address: Address) -> Option<(COFFH } /// A symbol exported into the current module. -pub struct Symbol { - /// The name of the current symbol - pub name: ArrayCString, +pub struct Symbol { /// The address associated with the current symbol pub address: Address, + /// The address storing the name of the current symbol + name_addr: Address, +} + +impl Symbol { + /// Tries to retrieve the name of the current symbol + pub fn get_name(&self, process: &Process) -> Result, Error> { + process.read(self.name_addr) + } } /// Recovers and iterates over the exported symbols for a given module. @@ -320,15 +327,12 @@ pub fn symbols( .unwrap_or_default(); (0..symbols_def.number_of_functions).filter_map(move |i| { - let name_index = process - .read::( - address + symbols_def.function_name_array_index + i.wrapping_mul(4), - ) - .ok()?; - - let name = process - .read::>(address + name_index) - .ok()?; + let name_addr = address + + process + .read::( + address + symbols_def.function_name_array_index + i.wrapping_mul(4), + ) + .ok()?; let address: Address = address + process @@ -337,6 +341,6 @@ pub fn symbols( ) .ok()?; - Some(Symbol { name, address }) + Some(Symbol { address, name_addr }) }) } From 8a12397be92036ec4edafda15696a145b5edc306 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Thu, 12 Oct 2023 11:49:33 +0200 Subject: [PATCH 03/14] Fixing dumb issues --- src/file_format/pe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file_format/pe.rs b/src/file_format/pe.rs index f3561ee..a2ef88a 100644 --- a/src/file_format/pe.rs +++ b/src/file_format/pe.rs @@ -292,7 +292,7 @@ impl Symbol { /// Recovers and iterates over the exported symbols for a given module. /// Returns an empty iterator if no symbols are exported into the current module. -pub fn symbols( +pub fn symbols( process: &Process, module_address: impl Into
, ) -> impl DoubleEndedIterator> + '_ { From 886dd41bcbe97f356032bef03113205ead88e8c9 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Thu, 12 Oct 2023 11:50:01 +0200 Subject: [PATCH 04/14] Fixing dumb issues n.2 --- src/file_format/pe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file_format/pe.rs b/src/file_format/pe.rs index a2ef88a..e3bd412 100644 --- a/src/file_format/pe.rs +++ b/src/file_format/pe.rs @@ -295,7 +295,7 @@ impl Symbol { pub fn symbols( process: &Process, module_address: impl Into
, -) -> impl DoubleEndedIterator> + '_ { +) -> impl DoubleEndedIterator + '_ { let address: Address = module_address.into(); let dos_header = process.read::(address); From eadf75f6632947e88a8b4e276468ec2011c5940a Mon Sep 17 00:00:00 2001 From: Jujstme Date: Thu, 12 Oct 2023 11:51:24 +0200 Subject: [PATCH 05/14] Fixing dumb stuff n.3 --- src/file_format/pe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file_format/pe.rs b/src/file_format/pe.rs index e3bd412..9a4540d 100644 --- a/src/file_format/pe.rs +++ b/src/file_format/pe.rs @@ -4,7 +4,7 @@ use core::{fmt, mem}; use bytemuck::{Pod, Zeroable}; -use crate::{string::ArrayCString, Address, FromEndian, Process}; +use crate::{string::ArrayCString, Address, FromEndian, Process, Error}; // Reference: // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format From 7556ffa151815af0ee9bff92b0d9172bd336fe73 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Thu, 12 Oct 2023 16:01:46 +0200 Subject: [PATCH 06/14] Forgot formatting, like an idiot --- src/file_format/pe.rs | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/file_format/pe.rs b/src/file_format/pe.rs index 9a4540d..9b0781c 100644 --- a/src/file_format/pe.rs +++ b/src/file_format/pe.rs @@ -4,7 +4,7 @@ use core::{fmt, mem}; use bytemuck::{Pod, Zeroable}; -use crate::{string::ArrayCString, Address, FromEndian, Process, Error}; +use crate::{string::ArrayCString, Address, Error, FromEndian, Process}; // Reference: // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format @@ -285,7 +285,10 @@ pub struct Symbol { impl Symbol { /// Tries to retrieve the name of the current symbol - pub fn get_name(&self, process: &Process) -> Result, Error> { + pub fn get_name( + &self, + process: &Process, + ) -> Result, Error> { process.read(self.name_addr) } } @@ -308,8 +311,8 @@ pub fn symbols( }; let export_directory = match dos_header { - Ok(x) => process - .read::(address + x.e_lfanew + if is_64_bit { 0x88 } else { 0x78 }) + Ok(header) => process + .read::(address + header.e_lfanew + if is_64_bit { 0x88 } else { 0x78 }) .ok(), _ => None, }; @@ -327,20 +330,19 @@ pub fn symbols( .unwrap_or_default(); (0..symbols_def.number_of_functions).filter_map(move |i| { - let name_addr = address - + process - .read::( - address + symbols_def.function_name_array_index + i.wrapping_mul(4), - ) - .ok()?; - - let address: Address = address - + process - .read::( - address + symbols_def.function_address_array_index + i.wrapping_mul(4), - ) - .ok()?; - - Some(Symbol { address, name_addr }) + Some(Symbol { + address: address + + process + .read::( + address + symbols_def.function_address_array_index + i.wrapping_mul(4), + ) + .ok()?, + name_addr: address + + process + .read::( + address + symbols_def.function_name_array_index + i.wrapping_mul(4), + ) + .ok()?, + }) }) } From 0999595f7e7f87598e385ce4fa4244d5789a0efa Mon Sep 17 00:00:00 2001 From: Jujstme Date: Sat, 14 Oct 2023 21:40:52 +0200 Subject: [PATCH 07/14] Code optimization (WIP) --- src/game_engine/unity/mono.rs | 397 +++++++++++++++------------------- 1 file changed, 178 insertions(+), 219 deletions(-) diff --git a/src/game_engine/unity/mono.rs b/src/game_engine/unity/mono.rs index 993bc7f..6aca45d 100644 --- a/src/game_engine/unity/mono.rs +++ b/src/game_engine/unity/mono.rs @@ -10,6 +10,8 @@ use core::iter; #[cfg(feature = "derive")] pub use asr_derive::MonoClass as Class; +const CSTR: usize = 128; + /// Represents access to a Unity game that is using the standard Mono backend. pub struct Module { is_64_bit: bool, @@ -38,53 +40,17 @@ impl Module { .find_map(|&name| process.get_module_address(name).ok())?; let is_64_bit = pe::MachineType::read(process, module)? == pe::MachineType::X86_64; - let pe_offsets = PEOffsets::new(is_64_bit); let offsets = Offsets::new(version, is_64_bit); - // Get root domain address: code essentially taken from UnitySpy - - // See https://github.com/hackf5/unityspy/blob/master/src/HackF5.UnitySpy/AssemblyImageFactory.cs#L123 - let start_index = process.read::(module + pe_offsets.signature).ok()?; - - let export_directory = process - .read::(module + start_index + pe_offsets.export_directory_index_pe) - .ok()?; - - let number_of_functions = process - .read::(module + export_directory + pe_offsets.number_of_functions) - .ok()?; - let function_address_array_index = process - .read::(module + export_directory + pe_offsets.function_address_array_index) - .ok()?; - let function_name_array_index = process - .read::(module + export_directory + pe_offsets.function_name_array_index) - .ok()?; - - let mut root_domain_function_address = Address::NULL; - - for val in 0..number_of_functions { - let function_name_index = process - .read::(module + function_name_array_index + (val as u64).wrapping_mul(4)) - .ok()?; - - if process - .read::<[u8; 22]>(module + function_name_index) - .is_ok_and(|function_name| &function_name == b"mono_assembly_foreach\0") - { - root_domain_function_address = module - + process - .read::( - module + function_address_array_index + (val as u64).wrapping_mul(4), - ) - .ok()?; - break; - } - } - - if root_domain_function_address.is_null() { - return None; - } + let root_domain_function_address = pe::symbols(process, module) + .find(|symbol| { + symbol + .get_name::<25>(process) + .is_ok_and(|name| name.matches("mono_assembly_foreach")) + })? + .address; - let assemblies: Address = match is_64_bit { + let assemblies_pointer: Address = match is_64_bit { true => { const SIG_MONO_64: Signature<3> = Signature::new("48 8B 0D"); let scan_address: Address = SIG_MONO_64 @@ -96,65 +62,81 @@ impl Module { const SIG_32_1: Signature<2> = Signature::new("FF 35"); const SIG_32_2: Signature<2> = Signature::new("8B 0D"); - if let Some(addr) = - SIG_32_1.scan_process_range(process, (root_domain_function_address, 0x100)) - { - process.read::(addr + 2).ok()?.into() - } else if let Some(addr) = - SIG_32_2.scan_process_range(process, (root_domain_function_address, 0x100)) - { - process.read::(addr + 2).ok()?.into() - } else { - return None; - } + let ptr = [SIG_32_1, SIG_32_2].iter().find_map(|sig| { + sig.scan_process_range(process, (root_domain_function_address, 0x100)) + })? + 2; + + process.read::(ptr + 2).ok()?.into() } }; - Some(Self { - is_64_bit, - version, - offsets, - assemblies, + let assemblies: Address = match is_64_bit { + true => process.read::(assemblies_pointer).ok()?.into(), + false => process.read::(assemblies_pointer).ok()?.into(), + }; + + if assemblies.is_null() { + None + } else { + Some(Self { + is_64_bit, + version, + offsets, + assemblies, + }) + } + } + + fn assemblies<'a>(&'a self, process: &'a Process) -> impl Iterator + 'a { + let mut assembly = self.assemblies; + let mut iter_break = assembly.is_null(); + iter::from_fn(move || { + if iter_break { + None + } else { + let [data, next_assembly]: [Address; 2] = match self.is_64_bit { + true => process + .read::<[Address64; 2]>(assembly) + .ok()? + .map(|item| item.into()), + false => process + .read::<[Address32; 2]>(assembly) + .ok()? + .map(|item| item.into()), + }; + + if next_assembly.is_null() { + iter_break = true; + } else { + assembly = next_assembly; + } + + Some(data) + } }) } /// Looks for the specified binary [image](Image) inside the target process. - /// An [image](Image), also called an assembly, is a .NET DLL that is loaded + /// An [image](Image) is a .NET DLL that is loaded /// by the game. The `Assembly-CSharp` [image](Image) is the main game /// assembly, and contains all the game logic. The /// [`get_default_image`](Self::get_default_image) function is a shorthand /// for this function that accesses the `Assembly-CSharp` [image](Image). pub fn get_image(&self, process: &Process, assembly_name: &str) -> Option { - let mut assemblies = self.read_pointer(process, self.assemblies).ok()?; - - let image = loop { - let data = self.read_pointer(process, assemblies).ok()?; - - if data.is_null() { - return None; - } - - let name_addr = self - .read_pointer( - process, - data + self.offsets.monoassembly_aname + self.offsets.monoassemblyname_name, - ) - .ok()?; - - let name = process.read::>(name_addr).ok()?; - - if name.matches(assembly_name) { - break self - .read_pointer(process, data + self.offsets.monoassembly_image) - .ok()?; - } - - assemblies = self - .read_pointer(process, assemblies + self.offsets.glist_next) - .ok()?; - }; + let assembly = self.assemblies(process).find(|&assembly| { + self.read_pointer(process, assembly + self.offsets.monoassembly_aname) + .is_ok_and(|name_ptr| { + process + .read::>(name_ptr) + .is_ok_and(|name| name.matches(assembly_name)) + }) + })?; - Some(Image { image }) + Some(Image { + image: self + .read_pointer(process, assembly + self.offsets.monoassembly_image) + .ok()?, + }) } /// Looks for the `Assembly-CSharp` binary [image](Image) inside the target @@ -220,18 +202,16 @@ impl Module { #[inline] const fn size_of_ptr(&self) -> u64 { - if self.is_64_bit { - 8 - } else { - 4 + match self.is_64_bit { + true => 8, + false => 4, } } fn read_pointer(&self, process: &Process, address: Address) -> Result { - Ok(if self.is_64_bit { - process.read::(address)?.into() - } else { - process.read::(address)?.into() + Ok(match self.is_64_bit { + true => process.read::(address)?.into(), + false => process.read::(address)?.into(), }) } } @@ -248,71 +228,57 @@ impl Image { &self, process: &'a Process, module: &'a Module, - ) -> Result + 'a, Error> { - let Ok(class_cache_size) = process.read::( - self.image - + module.offsets.monoimage_class_cache - + module.offsets.monointernalhashtable_size, - ) else { - return Err(Error {}); - }; - - let table_addr = module.read_pointer( - process, - self.image - + module.offsets.monoimage_class_cache - + module.offsets.monointernalhashtable_table, - )?; - - Ok((0..class_cache_size).flat_map(move |i| { - let mut table = module + ) -> impl Iterator + 'a { + let class_cache_size = process + .read::( + self.image + + module.offsets.monoimage_class_cache + + module.offsets.monointernalhashtable_size, + ); + + let table_addr = match class_cache_size { + Ok(_) => module .read_pointer( process, - table_addr + (i as u64).wrapping_mul(module.size_of_ptr()), - ) - .unwrap_or_default(); + self.image + + module.offsets.monoimage_class_cache + + module.offsets.monointernalhashtable_table, + ), + _ => Err(Error{}), + }; + + (0..class_cache_size.unwrap_or_default()).flat_map(move |i| { + let mut table = if let Ok(table_addr) = table_addr { + module + .read_pointer( + process, + table_addr + (i as u64).wrapping_mul(module.size_of_ptr()), + ) + .ok() + } else { + None + }; iter::from_fn(move || { - if !table.is_null() { - let class = module.read_pointer(process, table).ok()?; - table = module - .read_pointer( - process, - table + module.offsets.monoclassdef_next_class_cache, - ) - .unwrap_or_default(); - Some(Class { class }) - } else { - None - } + let class = module.read_pointer(process, table?).ok()?; + + table = module + .read_pointer( + process, + table? + module.offsets.monoclassdef_next_class_cache, + ) + .ok(); + + Some(Class { class }) }) - })) + }) } /// Tries to find the specified [.NET class](struct@Class) in the image. pub fn get_class(&self, process: &Process, module: &Module, class_name: &str) -> Option { - let mut classes = self.classes(process, module).ok()?; - classes.find(|c| { - let Ok(name_addr) = module.read_pointer( - process, - c.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_name, - ) else { - return false; - }; - - let Ok(name) = process.read::>(name_addr) else { - return false; - }; - if !name.matches(class_name) { - return false; - } - - module - .read_pointer( - process, - c.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_fields, - ) - .is_ok_and(|fields| !fields.is_null()) + self.classes(process, module).find(|class| { + class.get_name::(process, module) + .is_some_and(|name| name.matches(class_name)) }) } @@ -335,20 +301,44 @@ pub struct Class { } impl Class { - fn fields(&self, process: &Process, module: &Module) -> impl Iterator { - let field_count = process - .read::(self.class + module.offsets.monoclassdef_field_count) - .unwrap_or_default(); - - let fields = module + fn get_name( + &self, + process: &Process, + module: &Module, + ) -> Option> { + let name_addr = module .read_pointer( process, - self.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_fields, + self.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_name, ) - .unwrap_or_default(); + .ok()?; + process.read(name_addr).ok() + } + + fn fields( + &self, + process: &Process, + module: &Module, + ) -> impl DoubleEndedIterator { + let field_count = process + .read::(self.class + module.offsets.monoclassdef_field_count) + .ok(); + + let fields = match field_count { + Some(_) => module + .read_pointer( + process, + self.class + + module.offsets.monoclassdef_klass + + module.offsets.monoclass_fields, + ) + .ok(), + _ => None, + }; let monoclassfieldalignment = module.offsets.monoclassfieldalignment as u64; - (0..field_count).map(move |i| fields + (i as u64).wrapping_mul(monoclassfieldalignment)) + (0..field_count.unwrap_or_default()) + .filter_map(move |i| Some(fields? + (i as u64).wrapping_mul(monoclassfieldalignment))) } /// Tries to find a field with the specified name in the class. This returns @@ -357,17 +347,13 @@ impl Class { /// table. pub fn get_field(&self, process: &Process, module: &Module, field_name: &str) -> Option { let field = self.fields(process, module).find(|&field| { - let Ok(name_addr) = - module.read_pointer(process, field + module.offsets.monoclassfield_name) - else { - return false; - }; - - let Ok(name) = process.read::>(name_addr) else { - return false; - }; - - name.matches(field_name) + module + .read_pointer(process, field + module.offsets.monoclassfield_name) + .is_ok_and(|name_addr| { + process + .read::>(name_addr) + .is_ok_and(|name| name.matches(field_name)) + }) })?; process @@ -483,8 +469,6 @@ impl Class { struct Offsets { monoassembly_aname: u8, monoassembly_image: u8, - monoassemblyname_name: u8, - glist_next: u8, monoimage_class_cache: u16, monointernalhashtable_table: u8, monointernalhashtable_size: u8, @@ -510,8 +494,6 @@ impl Offsets { Version::V1 => &Self { monoassembly_aname: 0x10, monoassembly_image: 0x58, - monoassemblyname_name: 0x0, - glist_next: 0x8, monoimage_class_cache: 0x3D0, monointernalhashtable_table: 0x20, monointernalhashtable_size: 0x18, @@ -532,8 +514,6 @@ impl Offsets { Version::V2 => &Self { monoassembly_aname: 0x10, monoassembly_image: 0x60, - monoassemblyname_name: 0x0, - glist_next: 0x8, monoimage_class_cache: 0x4C0, monointernalhashtable_table: 0x20, monointernalhashtable_size: 0x18, @@ -554,8 +534,6 @@ impl Offsets { Version::V3 => &Self { monoassembly_aname: 0x10, monoassembly_image: 0x60, - monoassemblyname_name: 0x0, - glist_next: 0x8, monoimage_class_cache: 0x4D0, monointernalhashtable_table: 0x20, monointernalhashtable_size: 0x18, @@ -578,8 +556,6 @@ impl Offsets { Version::V1 => &Self { monoassembly_aname: 0x8, monoassembly_image: 0x40, - monoassemblyname_name: 0x0, - glist_next: 0x4, monoimage_class_cache: 0x2A0, monointernalhashtable_table: 0x14, monointernalhashtable_size: 0xC, @@ -600,8 +576,6 @@ impl Offsets { Version::V2 => &Self { monoassembly_aname: 0x8, monoassembly_image: 0x44, - monoassemblyname_name: 0x0, - glist_next: 0x4, monoimage_class_cache: 0x354, monointernalhashtable_table: 0x14, monointernalhashtable_size: 0xC, @@ -622,8 +596,6 @@ impl Offsets { Version::V3 => &Self { monoassembly_aname: 0x8, monoassembly_image: 0x48, - monoassemblyname_name: 0x0, - glist_next: 0x4, monoimage_class_cache: 0x35C, monointernalhashtable_table: 0x14, monointernalhashtable_size: 0xC, @@ -646,28 +618,6 @@ impl Offsets { } } -struct PEOffsets { - signature: u8, - export_directory_index_pe: u8, - number_of_functions: u8, - function_address_array_index: u8, - function_name_array_index: u8, - //function_entry_size: u32, -} - -impl PEOffsets { - const fn new(is_64_bit: bool) -> Self { - PEOffsets { - signature: 0x3C, - export_directory_index_pe: if is_64_bit { 0x88 } else { 0x78 }, - number_of_functions: 0x14, - function_address_array_index: 0x1C, - function_name_array_index: 0x20, - //function_entry_size: 0x4, - } - } -} - /// The version of Mono that was used for the game. These don't correlate to the /// Mono version numbers. #[derive(Copy, Clone, PartialEq, Hash, Debug)] @@ -685,18 +635,27 @@ fn detect_version(process: &Process) -> Option { return Some(Version::V1); } - const SIG: Signature<25> = Signature::new( - "55 00 6E 00 69 00 74 00 79 00 20 00 56 00 65 00 72 00 73 00 69 00 6F 00 6E", - ); - const ZERO: u16 = b'0' as u16; - const NINE: u16 = b'9' as u16; + let unity_module = process.get_module_address("UnityPlayer.dll").ok()?; + let mut unity_module_size = pe::read_size_of_image(process, unity_module)? as u64; + + const SIGG: Signature<8> = Signature::new("00 32 ?? ?? ?? 2E ?? 2E"); + const ZERO: u8 = b'0'; + const NINE: u8 = b'9'; + + let mut start_address = unity_module; + let mut scan_all = iter::from_fn(move || { + let found: Address = SIGG.scan_process_range(process, (start_address, unity_module_size))? + 1; + + unity_module_size = found.value() - start_address.value() - 1; + start_address = found + 1; - let unity_module = process.get_module_range("UnityPlayer.dll").ok()?; + Some(found) + }); + let found = scan_all.find(|addr| addr.value() & 3 == 0)?; - let addr = SIG.scan_process_range(process, unity_module)? + 0x1E; - let version_string = process.read::<[u16; 6]>(addr).ok()?; + let version_string = process.read::<[u8; 6]>(found).ok()?; let (before, after) = - version_string.split_at(version_string.iter().position(|&x| x == b'.' as u16)?); + version_string.split_at(version_string.iter().position(|&x| x == b'.')?); let mut unity: u32 = 0; for &val in before { From 5691fa575927c409d450d5fabf6333e76db34562 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Sun, 15 Oct 2023 15:53:17 +0200 Subject: [PATCH 08/14] [Unity] Various improvements Several improvements were made to the Mono and IL2CPP code that should allow for easier mantainance and less spaghetti code: - Mono was already using the `mono_assembly_foreach` symbol to find the game assemblies. This code is now part of the crate itself so the relative call has been changed - PE module sizes are now queried using `pe::read_size_of_image` in order to avoid issues with Proton/Wine under linux - The function to automatically detect the IL2CPP stuct version to use was broken. It has been fixed now, although there is no guarantee this will work fine on every game, so help by experienced Unity hackers might be appreciated anyway - A `CSTR` const is now used instead of manually defining a capacity of 128 every time an `ArrayCString` is read from memory. This way, in case of need, the capacity of the `ArrayCString`s can be changed as needed - A (private) `Assembly` struct has been added, as a `MonoImage` is technically loaded from the assembly. There is no need to make this struct public, but it allows to streamline some code. Also, if more functions are needed that rely on an `Assembly` instead of a `Image`, this should allow for easier future implementations - A `Field` struct has been added in order to use some functions specific for `MonoField` (`get_name()` and `get_offset()`). `Class::get_field` has been renamed to `Class:get_field_offset` for better clarity. As for `Assembly`, this struct is not exposed publicly - Code has been slightly modified and optimized, for example by using iterators instead of standard loops - Some functions have now a simpler syntax - Empty iterators now properly return as empty iterators, instead of returning Error - A `UnityPointer` struct has been added to allow for automatic pointer path resolution. It internally uses `DeepPointer`, wrapped into a `OnceCell` to allow for lazy evaluation of the pointer path with interior mutability when trying to `deref()` it. Example: ```rust let is_loading = UnityPointer::<5>::new("SceneManager", 1, &["s_sInstance", "m_bProcessing"]); ... watcher.is_loading.update(is_loading.deref::(&process, &module, &image).ok()); ``` --- src/game_engine/unity/il2cpp.rs | 488 ++++++++++++++++++++++++-------- src/game_engine/unity/mono.rs | 368 +++++++++++++++++------- 2 files changed, 644 insertions(+), 212 deletions(-) diff --git a/src/game_engine/unity/il2cpp.rs b/src/game_engine/unity/il2cpp.rs index 1f177f4..f794265 100644 --- a/src/game_engine/unity/il2cpp.rs +++ b/src/game_engine/unity/il2cpp.rs @@ -1,14 +1,21 @@ //! Support for attaching to Unity games that are using the IL2CPP backend. -use core::cmp::Ordering; +use core::{cell::OnceCell, cmp::Ordering}; use crate::{ - file_format::pe, future::retry, signature::Signature, string::ArrayCString, Address, Address32, - Address64, Error, Process, + deep_pointer::{DeepPointer, DerefType}, + file_format::pe, + future::retry, + signature::Signature, + string::ArrayCString, + Address, Address32, Address64, Error, Process, }; - +use arrayvec::{ArrayString, ArrayVec}; #[cfg(feature = "derive")] pub use asr_derive::Il2cppClass as Class; +use bytemuck::CheckedBitPattern; + +const CSTR: usize = 128; /// Represents access to a Unity game that is using the IL2CPP backend. pub struct Module { @@ -34,7 +41,12 @@ impl Module { /// correct for this function to work. If you don't know the version in /// advance, use [`attach_auto_detect`](Self::attach_auto_detect) instead. pub fn attach(process: &Process, version: Version) -> Option { - let mono_module = process.get_module_range("GameAssembly.dll").ok()?; + let mono_module = { + let address = process.get_module_address("GameAssembly.dll").ok()?; + let size = pe::read_size_of_image(process, address)? as u64; + (address, size) + }; + let is_64_bit = pe::MachineType::read(process, mono_module.0)? == pe::MachineType::X86_64; let assemblies_trg_addr = if is_64_bit { @@ -50,7 +62,7 @@ impl Module { process.read::(addr).ok()?.into() }; - let type_info_definition_table_trg_addr = if is_64_bit { + let type_info_definition_table_trg_addr: Address = if is_64_bit { const TYPE_INFO_DEFINITION_TABLE_TRG_SIG: Signature<10> = Signature::new("48 83 3C ?? 00 75 ?? 8B C? E8"); @@ -58,7 +70,10 @@ impl Module { .scan_process_range(process, mono_module)? .add_signed(-4); - addr + 0x4 + process.read::(addr).ok()? + process + .read::(addr + 0x4 + process.read::(addr).ok()?) + .ok()? + .into() } else { const TYPE_INFO_DEFINITION_TABLE_TRG_SIG: Signature<10> = Signature::new("C3 A1 ?? ?? ?? ?? 83 3C ?? 00"); @@ -66,58 +81,71 @@ impl Module { let addr = TYPE_INFO_DEFINITION_TABLE_TRG_SIG.scan_process_range(process, mono_module)? + 2; - process.read::(addr).ok()?.into() + process + .read::(process.read::(addr).ok()?) + .ok()? + .into() }; - Some(Self { - is_64_bit, - version, - offsets: Offsets::new(version, is_64_bit)?, - assemblies: assemblies_trg_addr, - type_info_definition_table: type_info_definition_table_trg_addr, + if type_info_definition_table_trg_addr.is_null() { + None + } else { + Some(Self { + is_64_bit, + version, + offsets: Offsets::new(version, is_64_bit)?, + assemblies: assemblies_trg_addr, + type_info_definition_table: type_info_definition_table_trg_addr, + }) + } + } + + fn assemblies<'a>( + &'a self, + process: &'a Process, + ) -> impl DoubleEndedIterator + 'a { + let (assemblies, nr_of_assemblies): (Address, u64) = if self.is_64_bit { + let [first, limit] = process + .read::<[u64; 2]>(self.assemblies) + .unwrap_or_default(); + let count = (limit - first) / self.size_of_ptr(); + (Address::new(first), count) + } else { + let [first, limit] = process + .read::<[u32; 2]>(self.assemblies) + .unwrap_or_default(); + let count = (limit - first) as u64 / self.size_of_ptr(); + (Address::new(first as _), count) + }; + + (0..nr_of_assemblies).filter_map(move |i| { + Some(Assembly { + assembly: self + .read_pointer(process, assemblies + i.wrapping_mul(self.size_of_ptr())) + .ok()?, + }) }) } /// Looks for the specified binary [image](Image) inside the target process. - /// An [image](Image), also called an assembly, is a .NET DLL that is loaded + /// An [image](Image) is a .NET DLL that is loaded /// by the game. The `Assembly-CSharp` [image](Image) is the main game /// assembly, and contains all the game logic. The /// [`get_default_image`](Self::get_default_image) function is a shorthand /// for this function that accesses the `Assembly-CSharp` [image](Image). pub fn get_image(&self, process: &Process, assembly_name: &str) -> Option { - let mut assemblies = self.read_pointer(process, self.assemblies).ok()?; - - let image = loop { - let mono_assembly = self.read_pointer(process, assemblies).ok()?; - if mono_assembly.is_null() { - return None; - } - - let name_addr = self - .read_pointer( - process, - mono_assembly - + self.offsets.monoassembly_aname - + self.offsets.monoassemblyname_name, - ) - .ok()?; - - let name = process.read::>(name_addr).ok()?; - - if name.matches(assembly_name) { - break self - .read_pointer(process, mono_assembly + self.offsets.monoassembly_image) - .ok()?; - } - assemblies = assemblies + self.size_of_ptr(); - }; - - Some(Image { image }) + self.assemblies(process) + .find(|assembly| { + assembly + .get_name::(process, self) + .is_ok_and(|name| name.matches(assembly_name)) + })? + .get_image(process, self) } /// Looks for the `Assembly-CSharp` binary [image](Image) inside the target - /// process. An [image](Image), also called an assembly, is a .NET DLL that - /// is loaded by the game. The `Assembly-CSharp` [image](Image) is the main + /// process. An [image](Image) is a .NET DLL that is loaded + /// by the game. The `Assembly-CSharp` [image](Image) is the main /// game assembly, and contains all the game logic. This function is a /// shorthand for [`get_image`](Self::get_image) that accesses the /// `Assembly-CSharp` [image](Image). @@ -149,7 +177,7 @@ impl Module { } /// Looks for the specified binary [image](Image) inside the target process. - /// An [image](Image), also called an assembly, is a .NET DLL that is loaded + /// An [image](Image) is a .NET DLL that is loaded /// by the game. The `Assembly-CSharp` [image](Image) is the main game /// assembly, and contains all the game logic. The /// [`wait_get_default_image`](Self::wait_get_default_image) function is a @@ -163,7 +191,7 @@ impl Module { } /// Looks for the `Assembly-CSharp` binary [image](Image) inside the target - /// process. An [image](Image), also called an assembly, is a .NET DLL that + /// process. An [image](Image) is a .NET DLL that /// is loaded by the game. The `Assembly-CSharp` [image](Image) is the main /// game assembly, and contains all the game logic. This function is a /// shorthand for [`wait_get_image`](Self::wait_get_image) that accesses the @@ -178,18 +206,43 @@ impl Module { #[inline] const fn size_of_ptr(&self) -> u64 { - if self.is_64_bit { - 8 - } else { - 4 + match self.is_64_bit { + true => 8, + false => 4, } } fn read_pointer(&self, process: &Process, address: Address) -> Result { - Ok(if self.is_64_bit { - process.read::(address)?.into() - } else { - process.read::(address)?.into() + Ok(match self.is_64_bit { + true => process.read::(address)?.into(), + false => process.read::(address)?.into(), + }) + } +} + +struct Assembly { + assembly: Address, +} + +impl Assembly { + fn get_name( + &self, + process: &Process, + module: &Module, + ) -> Result, Error> { + process.read(module.read_pointer( + process, + self.assembly + + module.offsets.monoassembly_aname + + module.offsets.monoassemblyname_name, + )?) + } + + fn get_image(&self, process: &Process, module: &Module) -> Option { + Some(Image { + image: module + .read_pointer(process, self.assembly + module.offsets.monoassembly_image) + .ok()?, }) } } @@ -206,47 +259,51 @@ impl Image { &self, process: &'a Process, module: &'a Module, - ) -> Result + 'a, Error> { - let type_count = process.read::(self.image + module.offsets.monoimage_typecount)?; + ) -> impl DoubleEndedIterator + 'a { + let type_count = process.read::(self.image + module.offsets.monoimage_typecount); - let metadata_handle = process.read::(match module.version { - Version::V2020 => module.read_pointer( - process, - self.image + module.offsets.monoimage_metadatahandle, - )?, - _ => self.image + module.offsets.monoimage_metadatahandle, - })? as u64; + let metadata_ptr = match type_count { + Ok(_) => match module.version { + Version::V2020 => module.read_pointer( + process, + self.image + module.offsets.monoimage_metadatahandle, + ), + _ => Ok(self.image + module.offsets.monoimage_metadatahandle), + }, + _ => Err(Error {}), + }; - let ptr = module.read_pointer(process, module.type_info_definition_table)? - + metadata_handle.wrapping_mul(module.size_of_ptr()); + let metadata_handle = match type_count { + Ok(0) => None, + Ok(_) => match metadata_ptr { + Ok(x) => process.read::(x).ok(), + _ => None, + }, + _ => None, + }; + + let ptr = metadata_handle.map(|val| { + module.type_info_definition_table + (val as u64).wrapping_mul(module.size_of_ptr()) + }); - Ok((0..type_count).filter_map(move |i| { + (0..type_count.unwrap_or_default() as u64).filter_map(move |i| { let class = module - .read_pointer(process, ptr + (i as u64).wrapping_mul(module.size_of_ptr())) + .read_pointer(process, ptr? + i.wrapping_mul(module.size_of_ptr())) .ok()?; - if !class.is_null() { - Some(Class { class }) - } else { - None + match class.is_null() { + false => Some(Class { class }), + true => None, } - })) + }) } /// Tries to find the specified [.NET class](struct@Class) in the image. pub fn get_class(&self, process: &Process, module: &Module, class_name: &str) -> Option { - self.classes(process, module).ok()?.find(|c| { - let Ok(name_addr) = - module.read_pointer(process, c.class + module.offsets.monoclass_name) - else { - return false; - }; - - let Ok(name) = process.read::>(name_addr) else { - return false; - }; - - name.matches(class_name) + self.classes(process, module).find(|class| { + class + .get_name::(process, module) + .is_ok_and(|name| name.matches(class_name)) }) } @@ -269,45 +326,50 @@ pub struct Class { } impl Class { - fn fields( + fn get_name( &self, process: &Process, module: &Module, - ) -> impl DoubleEndedIterator { - let field_count = process - .read::(self.class + module.offsets.monoclass_field_count) - .unwrap_or_default() as u64; + ) -> Result, Error> { + process.read(module.read_pointer(process, self.class + module.offsets.monoclass_name)?) + } - let fields = module - .read_pointer(process, self.class + module.offsets.monoclass_fields) - .unwrap_or_default(); + fn fields(&self, process: &Process, module: &Module) -> impl DoubleEndedIterator { + let field_count = process.read::(self.class + module.offsets.monoclass_field_count); + + let fields = match field_count { + Ok(_) => module + .read_pointer(process, self.class + module.offsets.monoclass_fields) + .ok(), + _ => None, + }; let monoclassfield_structsize = module.offsets.monoclassfield_structsize as u64; - (0..field_count).map(move |i| fields + i.wrapping_mul(monoclassfield_structsize)) + + (0..field_count.unwrap_or_default() as u64).filter_map(move |i| { + Some(Field { + field: fields? + i.wrapping_mul(monoclassfield_structsize), + }) + }) } /// Tries to find a field with the specified name in the class. This returns /// the offset of the field from the start of an instance of the class. If /// it's a static field, the offset will be from the start of the static /// table. - pub fn get_field(&self, process: &Process, module: &Module, field_name: &str) -> Option { - let found_field = self.fields(process, module).find(|&field| { - let Ok(name_addr) = - module.read_pointer(process, field + module.offsets.monoclassfield_name) - else { - return false; - }; - - let Ok(name) = process.read::>(name_addr) else { - return false; - }; - - name.matches(field_name) - })?; - - process - .read(found_field + module.offsets.monoclassfield_offset) - .ok() + pub fn get_field_offset( + &self, + process: &Process, + module: &Module, + field_name: &str, + ) -> Option { + self.fields(process, module) + .find(|field| { + field + .get_name::(process, module) + .is_ok_and(|name| name.matches(field_name)) + })? + .get_offset(process, module) } /// Tries to find the address of a static instance of the class based on its @@ -319,7 +381,9 @@ impl Class { field_name: &str, ) -> Address { let static_table = self.wait_get_static_table(process, module).await; - let field_offset = self.wait_get_field(process, module, field_name).await; + let field_offset = self + .wait_get_field_offset(process, module, field_name) + .await; let singleton_location = static_table + field_offset; retry(|| { @@ -357,8 +421,13 @@ impl Class { /// it's a static field, the offset will be from the start of the static /// table. This is the `await`able version of the /// [`get_field`](Self::get_field) function. - pub async fn wait_get_field(&self, process: &Process, module: &Module, name: &str) -> u32 { - retry(|| self.get_field(process, module, name)).await + pub async fn wait_get_field_offset( + &self, + process: &Process, + module: &Module, + name: &str, + ) -> u32 { + retry(|| self.get_field_offset(process, module, name)).await } /// Returns the address of the static table of the class. This contains the @@ -375,6 +444,178 @@ impl Class { } } +struct Field { + field: Address, +} + +impl Field { + fn get_name( + &self, + process: &Process, + module: &Module, + ) -> Result, Error> { + process.read(module.read_pointer(process, self.field + module.offsets.monoclassfield_name)?) + } + + fn get_offset(&self, process: &Process, module: &Module) -> Option { + process + .read(self.field + module.offsets.monoclassfield_offset) + .ok() + } +} + +/// An IL2CPP-specific implementation for automatic pointer path resolution +pub struct UnityPointer { + deep_pointer: OnceCell>, + class_name: ArrayString, + nr_of_parents: u8, + fields: ArrayVec, CAP>, +} + +impl UnityPointer { + /// Creates a new instance of the Pointer struct + /// + /// `CAP` must be higher or equal to the number of offsets defined in `fields`. + /// + /// If `CAP` is set to a value lower than the number of the offsets to be dereferenced, this function will ***Panic*** + pub fn new(class_name: &str, nr_of_parents: u8, fields: &[&str]) -> Self { + assert!(!fields.is_empty() && fields.len() <= CAP); + + Self { + deep_pointer: OnceCell::new(), + class_name: ArrayString::from(class_name).unwrap_or_default(), + nr_of_parents, + fields: fields + .iter() + .map(|&val| ArrayString::from(val).unwrap_or_default()) + .collect(), + } + } + + /// Tries to resolve the pointer path for the `IL2CPP` class specified + fn find_offsets(&self, process: &Process, module: &Module, image: &Image) -> Result<(), Error> { + // If the pointer path has already been found, there's no need to continue + if self.deep_pointer.get().is_some() { + return Ok(()); + } + + let mut current_class = image + .get_class(process, module, &self.class_name) + .ok_or(Error {})?; + + for _ in 0..self.nr_of_parents { + current_class = current_class.get_parent(process, module).ok_or(Error {})?; + } + + let static_table = current_class + .get_static_table(process, module) + .ok_or(Error {})?; + + let mut offsets: ArrayVec = ArrayVec::new(); + + for (i, &field_name) in self.fields.iter().enumerate() { + // Try to parse the offset, passed as a string, as an actual hex or decimal value + let offset_from_string = { + let mut temp_val = None; + + if field_name.starts_with("0x") && field_name.len() > 2 { + if let Some(hex_val) = field_name.get(2..field_name.len()) { + if let Ok(val) = u32::from_str_radix(hex_val, 16) { + temp_val = Some(val) + } + } + } else if let Ok(val) = field_name.parse::() { + temp_val = Some(val) + } + temp_val + }; + + // Then we try finding the MonoClassField of interest, which is needed if we only provided the name of the field, + // and will be needed anyway when looking for the next offset. + let target_field = current_class + .fields(process, module) + .find(|field| { + if let Some(val) = offset_from_string { + field + .get_offset(process, module) + .is_some_and(|offset| offset == val) + } else { + field + .get_name::(process, module) + .is_ok_and(|name| name.matches(field_name.as_ref())) + } + }) + .ok_or(Error {})?; + + offsets.push(if let Some(val) = offset_from_string { + val + } else { + target_field.get_offset(process, module).ok_or(Error {})? + } as u64); + + // In every iteration of the loop, except the last one, we then need to find the Class address for the next offset + if i != self.fields.len() - 1 { + let r#type = + module.read_pointer(process, target_field.field + module.size_of_ptr())?; + let type_definition = module.read_pointer(process, r#type)?; + + current_class = image + .classes(process, module) + .find(|c| { + module + .read_pointer( + process, + c.class + module.offsets.monoclass_type_definition, + ) + .is_ok_and(|val| val == type_definition) + }) + .ok_or(Error {})?; + } + } + + let pointer = DeepPointer::new( + static_table, + if module.is_64_bit { + DerefType::Bit64 + } else { + DerefType::Bit32 + }, + &offsets, + ); + let _ = self.deep_pointer.set(pointer); + Ok(()) + } + + /// Dereferences the pointer path, returning the memory address of the value of interest + pub fn deref_offsets( + &self, + process: &Process, + module: &Module, + image: &Image, + ) -> Result { + self.find_offsets(process, module, image)?; + self.deep_pointer.get().ok_or(Error {})?.deref_offsets(process) + } + + /// Dereferences the pointer path, returning the value stored at the final memory address + pub fn deref( + &self, + process: &Process, + module: &Module, + image: &Image, + ) -> Result { + self.find_offsets(process, module, image)?; + self.deep_pointer.get().ok_or(Error {})?.deref(process) + } + + /// Recovers the `DeepPointer` struct contained inside this `UnityPointer`, + /// if the offsets have been found + pub fn get_deep_pointer(&self, process: &Process, module: &Module, image: &Image) -> Option> { + self.find_offsets(process, module, image).ok()?; + self.deep_pointer.get().cloned() + } +} + struct Offsets { monoassembly_image: u8, monoassembly_aname: u8, @@ -382,6 +623,7 @@ struct Offsets { monoimage_typecount: u8, monoimage_metadatahandle: u8, monoclass_name: u8, + monoclass_type_definition: u8, monoclass_fields: u8, monoclass_field_count: u16, monoclass_static_fields: u8, @@ -407,6 +649,7 @@ impl Offsets { monoimage_typecount: 0x1C, monoimage_metadatahandle: 0x18, // MonoImage.typeStart monoclass_name: 0x10, + monoclass_type_definition: 0x68, monoclass_fields: 0x80, monoclass_field_count: 0x114, monoclass_static_fields: 0xB8, @@ -422,6 +665,7 @@ impl Offsets { monoimage_typecount: 0x1C, monoimage_metadatahandle: 0x18, // MonoImage.typeStart monoclass_name: 0x10, + monoclass_type_definition: 0x68, monoclass_fields: 0x80, monoclass_field_count: 0x11C, monoclass_static_fields: 0xB8, @@ -437,6 +681,7 @@ impl Offsets { monoimage_typecount: 0x18, monoimage_metadatahandle: 0x28, monoclass_name: 0x10, + monoclass_type_definition: 0x68, monoclass_fields: 0x80, monoclass_field_count: 0x120, monoclass_static_fields: 0xB8, @@ -462,7 +707,11 @@ pub enum Version { } fn detect_version(process: &Process) -> Option { - let unity_module = process.get_module_range("UnityPlayer.dll").ok()?; + let unity_module = { + let address = process.get_module_address("UnityPlayer.dll").ok()?; + let size = pe::read_size_of_image(process, address)? as u64; + (address, size) + }; if pe::MachineType::read(process, unity_module.0)? == pe::MachineType::X86 { return Some(Version::Base); @@ -492,13 +741,18 @@ fn detect_version(process: &Process) -> Option { Ordering::Equal => Version::V2019, Ordering::Greater => { const SIG_METADATA: Signature<9> = Signature::new("4C 8B 05 ?? ?? ?? ?? 49 63"); - let game_assembly = process.get_module_range("GameAssembly.dll").ok()?; + let game_assembly = { + let address = process.get_module_address("GameAssembly.dll").ok()?; + let size = pe::read_size_of_image(process, address)? as u64; + (address, size) + }; let Some(addr) = SIG_METADATA.scan_process_range(process, game_assembly) else { return Some(Version::V2019); }; let addr: Address = addr + 3; let addr: Address = addr + 0x4 + process.read::(addr).ok()?; + let addr = process.read::(addr).ok()?; let version = process.read::(addr + 4).ok()?; if version >= 27 { diff --git a/src/game_engine/unity/mono.rs b/src/game_engine/unity/mono.rs index 6aca45d..bc8bc63 100644 --- a/src/game_engine/unity/mono.rs +++ b/src/game_engine/unity/mono.rs @@ -2,13 +2,18 @@ //! backend. use crate::{ - file_format::pe, future::retry, signature::Signature, string::ArrayCString, Address, Address32, - Address64, Error, Process, + file_format::pe, + future::retry, + signature::Signature, + string::ArrayCString, + Address, Address32, Address64, Error, Process, deep_pointer::{DeepPointer, DerefType}, }; -use core::iter; +use core::{iter, cell::OnceCell}; +use arrayvec::{ArrayString, ArrayVec}; #[cfg(feature = "derive")] pub use asr_derive::MonoClass as Class; +use bytemuck::CheckedBitPattern; const CSTR: usize = 128; @@ -87,7 +92,7 @@ impl Module { } } - fn assemblies<'a>(&'a self, process: &'a Process) -> impl Iterator + 'a { + fn assemblies<'a>(&'a self, process: &'a Process) -> impl Iterator + 'a { let mut assembly = self.assemblies; let mut iter_break = assembly.is_null(); iter::from_fn(move || { @@ -111,7 +116,7 @@ impl Module { assembly = next_assembly; } - Some(data) + Some(Assembly { assembly: data }) } }) } @@ -123,25 +128,18 @@ impl Module { /// [`get_default_image`](Self::get_default_image) function is a shorthand /// for this function that accesses the `Assembly-CSharp` [image](Image). pub fn get_image(&self, process: &Process, assembly_name: &str) -> Option { - let assembly = self.assemblies(process).find(|&assembly| { - self.read_pointer(process, assembly + self.offsets.monoassembly_aname) - .is_ok_and(|name_ptr| { - process - .read::>(name_ptr) - .is_ok_and(|name| name.matches(assembly_name)) - }) - })?; - - Some(Image { - image: self - .read_pointer(process, assembly + self.offsets.monoassembly_image) - .ok()?, - }) + self.assemblies(process) + .find(|assembly| { + assembly + .get_name::(process, self) + .is_ok_and(|name| name.matches(assembly_name)) + })? + .get_image(process, self) } /// Looks for the `Assembly-CSharp` binary [image](Image) inside the target - /// process. An [image](Image), also called an assembly, is a .NET DLL that - /// is loaded by the game. The `Assembly-CSharp` [image](Image) is the main + /// process. An [image](Image) is a .NET DLL that is loaded + /// by the game. The `Assembly-CSharp` [image](Image) is the main /// game assembly, and contains all the game logic. This function is a /// shorthand for [`get_image`](Self::get_image) that accesses the /// `Assembly-CSharp` [image](Image). @@ -173,7 +171,7 @@ impl Module { } /// Looks for the specified binary [image](Image) inside the target process. - /// An [image](Image), also called an assembly, is a .NET DLL that is loaded + /// An [image](Image) is a .NET DLL that is loaded /// by the game. The `Assembly-CSharp` [image](Image) is the main game /// assembly, and contains all the game logic. The /// [`wait_get_default_image`](Self::wait_get_default_image) function is a @@ -187,7 +185,7 @@ impl Module { } /// Looks for the `Assembly-CSharp` binary [image](Image) inside the target - /// process. An [image](Image), also called an assembly, is a .NET DLL that + /// process. An [image](Image) is a .NET DLL that /// is loaded by the game. The `Assembly-CSharp` [image](Image) is the main /// game assembly, and contains all the game logic. This function is a /// shorthand for [`wait_get_image`](Self::wait_get_image) that accesses the @@ -216,6 +214,29 @@ impl Module { } } +struct Assembly { + assembly: Address, +} + +impl Assembly { + fn get_name( + &self, + process: &Process, + module: &Module, + ) -> Result, Error> { + process + .read(module.read_pointer(process, self.assembly + module.offsets.monoassembly_aname)?) + } + + fn get_image(&self, process: &Process, module: &Module) -> Option { + Some(Image { + image: module + .read_pointer(process, self.assembly + module.offsets.monoassembly_image) + .ok()?, + }) + } +} + /// An image is a .NET DLL that is loaded by the game. The `Assembly-CSharp` /// image is the main game assembly, and contains all the game logic. pub struct Image { @@ -229,22 +250,20 @@ impl Image { process: &'a Process, module: &'a Module, ) -> impl Iterator + 'a { - let class_cache_size = process - .read::( - self.image - + module.offsets.monoimage_class_cache - + module.offsets.monointernalhashtable_size, - ); + let class_cache_size = process.read::( + self.image + + module.offsets.monoimage_class_cache + + module.offsets.monointernalhashtable_size, + ); let table_addr = match class_cache_size { - Ok(_) => module - .read_pointer( - process, - self.image - + module.offsets.monoimage_class_cache - + module.offsets.monointernalhashtable_table, - ), - _ => Err(Error{}), + Ok(_) => module.read_pointer( + process, + self.image + + module.offsets.monoimage_class_cache + + module.offsets.monointernalhashtable_table, + ), + _ => Err(Error {}), }; (0..class_cache_size.unwrap_or_default()).flat_map(move |i| { @@ -277,8 +296,9 @@ impl Image { /// Tries to find the specified [.NET class](struct@Class) in the image. pub fn get_class(&self, process: &Process, module: &Module, class_name: &str) -> Option { self.classes(process, module).find(|class| { - class.get_name::(process, module) - .is_some_and(|name| name.matches(class_name)) + class + .get_name::(process, module) + .is_ok_and(|name| name.matches(class_name)) }) } @@ -305,21 +325,14 @@ impl Class { &self, process: &Process, module: &Module, - ) -> Option> { - let name_addr = module - .read_pointer( - process, - self.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_name, - ) - .ok()?; - process.read(name_addr).ok() + ) -> Result, Error> { + process.read(module.read_pointer( + process, + self.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_name, + )?) } - fn fields( - &self, - process: &Process, - module: &Module, - ) -> impl DoubleEndedIterator { + fn fields(&self, process: &Process, module: &Module) -> impl DoubleEndedIterator { let field_count = process .read::(self.class + module.offsets.monoclassdef_field_count) .ok(); @@ -337,28 +350,29 @@ impl Class { }; let monoclassfieldalignment = module.offsets.monoclassfieldalignment as u64; - (0..field_count.unwrap_or_default()) - .filter_map(move |i| Some(fields? + (i as u64).wrapping_mul(monoclassfieldalignment))) + (0..field_count.unwrap_or_default()).filter_map(move |i| { + Some(Field { + field: fields? + (i as u64).wrapping_mul(monoclassfieldalignment), + }) + }) } - /// Tries to find a field with the specified name in the class. This returns - /// the offset of the field from the start of an instance of the class. If - /// it's a static field, the offset will be from the start of the static + /// Tries to find the offset for a field with the specified name in the class. + /// If it's a static field, the offset will be from the start of the static /// table. - pub fn get_field(&self, process: &Process, module: &Module, field_name: &str) -> Option { - let field = self.fields(process, module).find(|&field| { - module - .read_pointer(process, field + module.offsets.monoclassfield_name) - .is_ok_and(|name_addr| { - process - .read::>(name_addr) - .is_ok_and(|name| name.matches(field_name)) - }) - })?; - - process - .read(field + module.offsets.monoclassfield_offset) - .ok() + pub fn get_field_offset( + &self, + process: &Process, + module: &Module, + field_name: &str, + ) -> Option { + self.fields(process, module) + .find(|field| { + field + .get_name::(process, module) + .is_ok_and(|name| name.matches(field_name)) + })? + .get_offset(process, module) } /// Tries to find the address of a static instance of the class based on its @@ -370,7 +384,9 @@ impl Class { field_name: &str, ) -> Address { let static_table = self.wait_get_static_table(process, module).await; - let field_offset = self.wait_get_field(process, module, field_name).await; + let field_offset = self + .wait_get_field_offset(process, module, field_name) + .await; let singleton_location = static_table + field_offset; retry(|| { @@ -438,9 +454,7 @@ impl Class { ) .ok()?; - let parent = module.read_pointer(process, parent_addr).ok()?; - - Some(Class { class: parent }) + Some(Class { class: module.read_pointer(process, parent_addr).ok()? }) } /// Tries to find a field with the specified name in the class. This returns @@ -448,8 +462,13 @@ impl Class { /// it's a static field, the offset will be from the start of the static /// table. This is the `await`able version of the /// [`get_field`](Self::get_field) function. - pub async fn wait_get_field(&self, process: &Process, module: &Module, name: &str) -> u32 { - retry(|| self.get_field(process, module, name)).await + pub async fn wait_get_field_offset( + &self, + process: &Process, + module: &Module, + name: &str, + ) -> u32 { + retry(|| self.get_field_offset(process, module, name)).await } /// Returns the address of the static table of the class. This contains the @@ -466,6 +485,170 @@ impl Class { } } +struct Field { + field: Address, +} + +impl Field { + fn get_name( + &self, + process: &Process, + module: &Module, + ) -> Result, Error> { + let name_addr = + module.read_pointer(process, self.field + module.offsets.monoclassfield_name)?; + process.read(name_addr) + } + + fn get_offset(&self, process: &Process, module: &Module) -> Option { + process + .read(self.field + module.offsets.monoclassfield_offset) + .ok() + } +} + +/// A Mono-specific implementation useful for automatic pointer path resolution +pub struct Pointer { + deep_pointer: OnceCell>, + class_name: ArrayString, + nr_of_parents: u8, + fields: ArrayVec, CAP>, +} + +impl Pointer { + /// Creates a new instance of the Pointer struct + /// + /// `CAP` must be higher or equal to the number of offsets defined in `fields`. + /// + /// If `CAP` is set to a value lower than the number of the offsets to be dereferenced, this function will ***Panic*** + pub fn new(class_name: &str, nr_of_parents: u8, fields: &[&str]) -> Self { + assert!(!fields.is_empty() && fields.len() <= CAP); + + Self { + deep_pointer: OnceCell::new(), + class_name: ArrayString::from(class_name).unwrap_or_default(), + nr_of_parents, + fields: fields + .iter() + .map(|&val| ArrayString::from(val).unwrap_or_default()) + .collect(), + } + } + + /// Tries to resolve the pointer path for the `Mono` class specified + fn find_offsets(&self, process: &Process, module: &Module, image: &Image) -> Result<(), Error> { + // If the pointer path has already been found, there's no need to continue + if self.deep_pointer.get().is_some() { + return Ok(()); + } + + let mut current_class = image + .get_class(process, module, &self.class_name) + .ok_or(Error {})?; + + for _ in 0..self.nr_of_parents { + current_class = current_class.get_parent(process, module).ok_or(Error {})?; + } + + let static_table = current_class + .get_static_table(process, module) + .ok_or(Error {})?; + + let mut offsets: ArrayVec = ArrayVec::new(); + + for (i, &field_name) in self.fields.iter().enumerate() { + // Try to parse the offset, passed as a string, as an actual hex or decimal value + let offset_from_string = { + let mut temp_val = None; + + if field_name.starts_with("0x") && field_name.len() > 2 { + if let Some(hex_val) = field_name.get(2..field_name.len()) { + if let Ok(val) = u32::from_str_radix(hex_val, 16) { + temp_val = Some(val) + } + } + } else if let Ok(val) = field_name.parse::() { + temp_val = Some(val) + } + temp_val + }; + + // Then we try finding the MonoClassField of interest, which is needed if we only provided the name of the field, + // and will be needed anyway when looking for the next offset. + let target_field = current_class + .fields(process, module) + .find(|field| { + if let Some(val) = offset_from_string { + field + .get_offset(process, module) + .is_some_and(|offset| offset == val) + } else { + field + .get_name::(process, module) + .is_ok_and(|name| name.matches(field_name.as_ref())) + } + }) + .ok_or(Error {})?; + + offsets.push(if let Some(val) = offset_from_string { + val + } else { + target_field.get_offset(process, module).ok_or(Error {})? + } as u64); + + // In every iteration of the loop, except the last one, we then need to find the Class address for the next offset + if i != self.fields.len() - 1 { + let vtable = module.read_pointer(process, target_field.field)?; + + current_class = Class { + class: module.read_pointer(process, vtable)?, + }; + } + } + + let pointer = DeepPointer::new( + static_table, + if module.is_64_bit { + DerefType::Bit64 + } else { + DerefType::Bit32 + }, + &offsets, + ); + let _ = self.deep_pointer.set(pointer); + Ok(()) + } + + /// Dereferences the pointer path, returning the memory address of the value of interest + pub fn deref_offsets( + &self, + process: &Process, + module: &Module, + image: &Image, + ) -> Result { + self.find_offsets(process, module, image)?; + self.deep_pointer.get().ok_or(Error {})?.deref_offsets(process) + } + + /// Dereferences the pointer path, returning the value stored at the final memory address + pub fn deref( + &self, + process: &Process, + module: &Module, + image: &Image, + ) -> Result { + self.find_offsets(process, module, image)?; + self.deep_pointer.get().ok_or(Error {})?.deref(process) + } + + /// Recovers the `DeepPointer` struct contained inside this `UnityPointer`, + /// if the offsets have been found + pub fn get_deep_pointer(&self, process: &Process, module: &Module, image: &Image) -> Option> { + self.find_offsets(process, module, image).ok()?; + self.deep_pointer.get().cloned() + } +} + struct Offsets { monoassembly_aname: u8, monoassembly_image: u8, @@ -635,27 +818,22 @@ fn detect_version(process: &Process) -> Option { return Some(Version::V1); } - let unity_module = process.get_module_address("UnityPlayer.dll").ok()?; - let mut unity_module_size = pe::read_size_of_image(process, unity_module)? as u64; - - const SIGG: Signature<8> = Signature::new("00 32 ?? ?? ?? 2E ?? 2E"); - const ZERO: u8 = b'0'; - const NINE: u8 = b'9'; - - let mut start_address = unity_module; - let mut scan_all = iter::from_fn(move || { - let found: Address = SIGG.scan_process_range(process, (start_address, unity_module_size))? + 1; - - unity_module_size = found.value() - start_address.value() - 1; - start_address = found + 1; + const SIG: Signature<25> = Signature::new( + "55 00 6E 00 69 00 74 00 79 00 20 00 56 00 65 00 72 00 73 00 69 00 6F 00 6E", + ); + const ZERO: u16 = b'0' as u16; + const NINE: u16 = b'9' as u16; - Some(found) - }); - let found = scan_all.find(|addr| addr.value() & 3 == 0)?; + let unity_module = { + let address = process.get_module_address("UnityPlayer.dll").ok()?; + let range = pe::read_size_of_image(process, address)? as u64; + (address, range) + }; - let version_string = process.read::<[u8; 6]>(found).ok()?; + let addr = SIG.scan_process_range(process, unity_module)? + 0x1E; + let version_string = process.read::<[u16; 6]>(addr).ok()?; let (before, after) = - version_string.split_at(version_string.iter().position(|&x| x == b'.')?); + version_string.split_at(version_string.iter().position(|&x| x == b'.' as u16)?); let mut unity: u32 = 0; for &val in before { From aa5eead7ebaaccc535ad38b2fc7434e53a02b506 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Sun, 15 Oct 2023 15:56:02 +0200 Subject: [PATCH 09/14] Formatting --- src/game_engine/unity/il2cpp.rs | 12 ++++++++++-- src/game_engine/unity/mono.rs | 23 +++++++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/game_engine/unity/il2cpp.rs b/src/game_engine/unity/il2cpp.rs index f794265..9e7f121 100644 --- a/src/game_engine/unity/il2cpp.rs +++ b/src/game_engine/unity/il2cpp.rs @@ -594,7 +594,10 @@ impl UnityPointer { image: &Image, ) -> Result { self.find_offsets(process, module, image)?; - self.deep_pointer.get().ok_or(Error {})?.deref_offsets(process) + self.deep_pointer + .get() + .ok_or(Error {})? + .deref_offsets(process) } /// Dereferences the pointer path, returning the value stored at the final memory address @@ -610,7 +613,12 @@ impl UnityPointer { /// Recovers the `DeepPointer` struct contained inside this `UnityPointer`, /// if the offsets have been found - pub fn get_deep_pointer(&self, process: &Process, module: &Module, image: &Image) -> Option> { + pub fn get_deep_pointer( + &self, + process: &Process, + module: &Module, + image: &Image, + ) -> Option> { self.find_offsets(process, module, image).ok()?; self.deep_pointer.get().cloned() } diff --git a/src/game_engine/unity/mono.rs b/src/game_engine/unity/mono.rs index bc8bc63..286560f 100644 --- a/src/game_engine/unity/mono.rs +++ b/src/game_engine/unity/mono.rs @@ -2,13 +2,14 @@ //! backend. use crate::{ + deep_pointer::{DeepPointer, DerefType}, file_format::pe, future::retry, signature::Signature, string::ArrayCString, - Address, Address32, Address64, Error, Process, deep_pointer::{DeepPointer, DerefType}, + Address, Address32, Address64, Error, Process, }; -use core::{iter, cell::OnceCell}; +use core::{cell::OnceCell, iter}; use arrayvec::{ArrayString, ArrayVec}; #[cfg(feature = "derive")] @@ -454,7 +455,9 @@ impl Class { ) .ok()?; - Some(Class { class: module.read_pointer(process, parent_addr).ok()? }) + Some(Class { + class: module.read_pointer(process, parent_addr).ok()?, + }) } /// Tries to find a field with the specified name in the class. This returns @@ -599,7 +602,7 @@ impl Pointer { // In every iteration of the loop, except the last one, we then need to find the Class address for the next offset if i != self.fields.len() - 1 { let vtable = module.read_pointer(process, target_field.field)?; - + current_class = Class { class: module.read_pointer(process, vtable)?, }; @@ -627,7 +630,10 @@ impl Pointer { image: &Image, ) -> Result { self.find_offsets(process, module, image)?; - self.deep_pointer.get().ok_or(Error {})?.deref_offsets(process) + self.deep_pointer + .get() + .ok_or(Error {})? + .deref_offsets(process) } /// Dereferences the pointer path, returning the value stored at the final memory address @@ -643,7 +649,12 @@ impl Pointer { /// Recovers the `DeepPointer` struct contained inside this `UnityPointer`, /// if the offsets have been found - pub fn get_deep_pointer(&self, process: &Process, module: &Module, image: &Image) -> Option> { + pub fn get_deep_pointer( + &self, + process: &Process, + module: &Module, + image: &Image, + ) -> Option> { self.find_offsets(process, module, image).ok()?; self.deep_pointer.get().cloned() } From c556b2d86ea929fcccb9eef1219858abfcf5b8f1 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Sun, 15 Oct 2023 16:01:10 +0200 Subject: [PATCH 10/14] docs --- src/game_engine/unity/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game_engine/unity/mod.rs b/src/game_engine/unity/mod.rs index 681ba22..1d4b970 100644 --- a/src/game_engine/unity/mod.rs +++ b/src/game_engine/unity/mod.rs @@ -23,7 +23,7 @@ //! //! // Once we have the address of the instance, we want to access one of its //! // fields, so we get the offset of the "currentTime" field. -//! let current_time_offset = timer_class.wait_get_field(&process, &module, "currentTime").await; +//! let current_time_offset = timer_class.wait_get_field_offset(&process, &module, "currentTime").await; //! //! // Now we can add it to the address of the instance and read the current time. //! if let Ok(current_time) = process.read::(instance + current_time_offset) { From 0b5b5ef023104794caf7d349f280790531c11edc Mon Sep 17 00:00:00 2001 From: Jujstme Date: Sun, 15 Oct 2023 18:35:27 +0200 Subject: [PATCH 11/14] MAYBE fixed IL2CPP version detection for good --- src/game_engine/unity/il2cpp.rs | 69 +++++++++++++-------------------- 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/src/game_engine/unity/il2cpp.rs b/src/game_engine/unity/il2cpp.rs index 9e7f121..13f97f9 100644 --- a/src/game_engine/unity/il2cpp.rs +++ b/src/game_engine/unity/il2cpp.rs @@ -725,49 +725,32 @@ fn detect_version(process: &Process) -> Option { return Some(Version::Base); } - const SIG: Signature<25> = Signature::new( - "55 00 6E 00 69 00 74 00 79 00 20 00 56 00 65 00 72 00 73 00 69 00 6F 00 6E", - ); - const ZERO: u16 = b'0' as u16; - const NINE: u16 = b'9' as u16; - - let addr = SIG.scan_process_range(process, unity_module)? + 0x1E; - let version_string = process.read::<[u16; 6]>(addr).ok()?; - let mut ver = version_string.split(|&b| b == b'.' as u16); - - let version = ver.next()?; - let mut il2cpp: u32 = 0; - for &val in version { - match val { - ZERO..=NINE => il2cpp = il2cpp * 10 + (val - ZERO) as u32, - _ => break, - } - } - - Some(match il2cpp.cmp(&2019) { - Ordering::Less => Version::Base, - Ordering::Equal => Version::V2019, - Ordering::Greater => { - const SIG_METADATA: Signature<9> = Signature::new("4C 8B 05 ?? ?? ?? ?? 49 63"); - let game_assembly = { - let address = process.get_module_address("GameAssembly.dll").ok()?; - let size = pe::read_size_of_image(process, address)? as u64; - (address, size) - }; + const SIG_202X: Signature<6> = Signature::new("00 32 30 32 ?? 2E"); + const SIG_2019: Signature<6> = Signature::new("00 32 30 31 39 2E"); - let Some(addr) = SIG_METADATA.scan_process_range(process, game_assembly) else { - return Some(Version::V2019); + if SIG_202X.scan_process_range(process, unity_module).is_some() { + let il2cpp_version = { + const SIG: Signature<14> = Signature::new("48 2B ?? 48 2B ?? ?? ?? ?? ?? 48 F7 ?? 48"); + let address = process.get_module_address("GameAssembly.dll").ok()?; + let size = pe::read_size_of_image(process, address)? as u64; + + let ptr = { + let addr = SIG.scan_process_range(process, (address, size))? + 6; + addr + 0x4 + process.read::(addr).ok()? }; - let addr: Address = addr + 3; - let addr: Address = addr + 0x4 + process.read::(addr).ok()?; - let addr = process.read::(addr).ok()?; - let version = process.read::(addr + 4).ok()?; - - if version >= 27 { - Version::V2020 - } else { - Version::V2019 - } - } - }) + + let addr = process.read::(ptr).ok()?; + process.read::(addr + 0x4).ok()? + }; + + Some(if il2cpp_version >= 27 { + Version::V2020 + } else { + Version::V2019 + }) + } else if SIG_2019.scan_process_range(process, unity_module).is_some() { + Some(Version::V2019) + } else { + Some(Version::Base) + } } From 3691ceb94aa2200453a12a487f2ade96b5d00938 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Sun, 15 Oct 2023 19:47:01 +0200 Subject: [PATCH 12/14] And hopefully Mono version detection is fixed as well --- src/game_engine/unity/il2cpp.rs | 8 ++++---- src/game_engine/unity/mono.rs | 26 ++++++++++++++------------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/game_engine/unity/il2cpp.rs b/src/game_engine/unity/il2cpp.rs index 13f97f9..ec37962 100644 --- a/src/game_engine/unity/il2cpp.rs +++ b/src/game_engine/unity/il2cpp.rs @@ -1,6 +1,6 @@ //! Support for attaching to Unity games that are using the IL2CPP backend. -use core::{cell::OnceCell, cmp::Ordering}; +use core::cell::OnceCell; use crate::{ deep_pointer::{DeepPointer, DerefType}, @@ -733,16 +733,16 @@ fn detect_version(process: &Process) -> Option { const SIG: Signature<14> = Signature::new("48 2B ?? 48 2B ?? ?? ?? ?? ?? 48 F7 ?? 48"); let address = process.get_module_address("GameAssembly.dll").ok()?; let size = pe::read_size_of_image(process, address)? as u64; - + let ptr = { let addr = SIG.scan_process_range(process, (address, size))? + 6; addr + 0x4 + process.read::(addr).ok()? }; - + let addr = process.read::(ptr).ok()?; process.read::(addr + 0x4).ok()? }; - + Some(if il2cpp_version >= 27 { Version::V2020 } else { diff --git a/src/game_engine/unity/mono.rs b/src/game_engine/unity/mono.rs index 286560f..59488dc 100644 --- a/src/game_engine/unity/mono.rs +++ b/src/game_engine/unity/mono.rs @@ -511,14 +511,14 @@ impl Field { } /// A Mono-specific implementation useful for automatic pointer path resolution -pub struct Pointer { +pub struct UnityPointer { deep_pointer: OnceCell>, class_name: ArrayString, nr_of_parents: u8, fields: ArrayVec, CAP>, } -impl Pointer { +impl UnityPointer { /// Creates a new instance of the Pointer struct /// /// `CAP` must be higher or equal to the number of offsets defined in `fields`. @@ -829,22 +829,24 @@ fn detect_version(process: &Process) -> Option { return Some(Version::V1); } - const SIG: Signature<25> = Signature::new( - "55 00 6E 00 69 00 74 00 79 00 20 00 56 00 65 00 72 00 73 00 69 00 6F 00 6E", - ); - const ZERO: u16 = b'0' as u16; - const NINE: u16 = b'9' as u16; - let unity_module = { let address = process.get_module_address("UnityPlayer.dll").ok()?; let range = pe::read_size_of_image(process, address)? as u64; (address, range) }; - let addr = SIG.scan_process_range(process, unity_module)? + 0x1E; - let version_string = process.read::<[u16; 6]>(addr).ok()?; - let (before, after) = - version_string.split_at(version_string.iter().position(|&x| x == b'.' as u16)?); + const SIG_202X: Signature<6> = Signature::new("00 32 30 32 ?? 2E"); + + let Some(addr) = SIG_202X.scan_process_range(process, unity_module) else { + return Some(Version::V2) + }; + + const ZERO: u8 = b'0'; + const NINE: u8 = b'9'; + + let version_string = process.read::<[u8; 6]>(addr + 1).ok()?; + + let (before, after) = version_string.split_at(version_string.iter().position(|&x| x == b'.')?); let mut unity: u32 = 0; for &val in before { From 6d39e0f8d09b8508eb22360628adb37ba280005b Mon Sep 17 00:00:00 2001 From: Jujstme Date: Mon, 16 Oct 2023 01:08:37 +0200 Subject: [PATCH 13/14] Use `saturating_sub()` instead of normal subtraction --- src/game_engine/unity/il2cpp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game_engine/unity/il2cpp.rs b/src/game_engine/unity/il2cpp.rs index ec37962..0be2dac 100644 --- a/src/game_engine/unity/il2cpp.rs +++ b/src/game_engine/unity/il2cpp.rs @@ -108,13 +108,13 @@ impl Module { let [first, limit] = process .read::<[u64; 2]>(self.assemblies) .unwrap_or_default(); - let count = (limit - first) / self.size_of_ptr(); + let count = (limit.saturating_sub(first)) / self.size_of_ptr(); (Address::new(first), count) } else { let [first, limit] = process .read::<[u32; 2]>(self.assemblies) .unwrap_or_default(); - let count = (limit - first) as u64 / self.size_of_ptr(); + let count = (limit.saturating_sub(first)) as u64 / self.size_of_ptr(); (Address::new(first as _), count) }; From a93ba71eb575ea050454a1d92c9d26447ac56a9a Mon Sep 17 00:00:00 2001 From: Jujstme Date: Mon, 16 Oct 2023 01:12:35 +0200 Subject: [PATCH 14/14] formatting --- src/game_engine/unity/il2cpp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game_engine/unity/il2cpp.rs b/src/game_engine/unity/il2cpp.rs index 0be2dac..47daa26 100644 --- a/src/game_engine/unity/il2cpp.rs +++ b/src/game_engine/unity/il2cpp.rs @@ -108,13 +108,13 @@ impl Module { let [first, limit] = process .read::<[u64; 2]>(self.assemblies) .unwrap_or_default(); - let count = (limit.saturating_sub(first)) / self.size_of_ptr(); + let count = limit.saturating_sub(first) / self.size_of_ptr(); (Address::new(first), count) } else { let [first, limit] = process .read::<[u32; 2]>(self.assemblies) .unwrap_or_default(); - let count = (limit.saturating_sub(first)) as u64 / self.size_of_ptr(); + let count = limit.saturating_sub(first) as u64 / self.size_of_ptr(); (Address::new(first as _), count) };