diff --git a/src/game_engine/unity/il2cpp.rs b/src/game_engine/unity/il2cpp.rs index 1f177f4..47daa26 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; 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.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(); + (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,186 @@ 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 +631,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 +657,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 +673,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 +689,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,50 +715,42 @@ 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); } - 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, - } - } + 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"); - 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 = process.get_module_range("GameAssembly.dll").ok()?; + 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 Some(addr) = SIG_METADATA.scan_process_range(process, game_assembly) else { - return Some(Version::V2019); + 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 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) + } } 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) { diff --git a/src/game_engine/unity/mono.rs b/src/game_engine/unity/mono.rs index 993bc7f..59488dc 100644 --- a/src/game_engine/unity/mono.rs +++ b/src/game_engine/unity/mono.rs @@ -2,13 +2,21 @@ //! backend. 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 core::iter; +use core::{cell::OnceCell, iter}; +use arrayvec::{ArrayString, ArrayVec}; #[cfg(feature = "derive")] pub use asr_derive::MonoClass as Class; +use bytemuck::CheckedBitPattern; + +const CSTR: usize = 128; /// Represents access to a Unity game that is using the standard Mono backend. pub struct Module { @@ -38,53 +46,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,70 +68,79 @@ 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(Assembly { assembly: 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()?; - }; - - 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). @@ -191,7 +172,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 @@ -205,7 +186,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 @@ -220,18 +201,39 @@ 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)?) + } + + fn get_image(&self, process: &Process, module: &Module) -> Option { + Some(Image { + image: module + .read_pointer(process, self.assembly + module.offsets.monoassembly_image) + .ok()?, }) } } @@ -248,71 +250,56 @@ impl Image { &self, process: &'a Process, module: &'a Module, - ) -> Result + 'a, Error> { - let Ok(class_cache_size) = process.read::( + ) -> impl Iterator + 'a { + let 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, - )?; + 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((0..class_cache_size).flat_map(move |i| { - let mut table = module - .read_pointer( - process, - table_addr + (i as u64).wrapping_mul(module.size_of_ptr()), - ) - .unwrap_or_default(); + (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_ok_and(|name| name.matches(class_name)) }) } @@ -335,44 +322,58 @@ pub struct Class { } impl Class { - fn fields(&self, process: &Process, module: &Module) -> impl Iterator { + fn get_name( + &self, + process: &Process, + module: &Module, + ) -> 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 { let field_count = process .read::(self.class + module.offsets.monoclassdef_field_count) - .unwrap_or_default(); + .ok(); - let fields = module - .read_pointer( - process, - self.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_fields, - ) - .unwrap_or_default(); + 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(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| { - 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(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 @@ -384,7 +385,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(|| { @@ -452,9 +455,9 @@ 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 @@ -462,8 +465,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 @@ -480,11 +488,181 @@ 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 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 `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, - monoassemblyname_name: u8, - glist_next: u8, monoimage_class_cache: u16, monointernalhashtable_table: u8, monointernalhashtable_size: u8, @@ -510,8 +688,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 +708,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 +728,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 +750,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 +770,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 +790,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 +812,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 +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) + }; + + 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 unity_module = process.get_module_range("UnityPlayer.dll").ok()?; + let version_string = process.read::<[u8; 6]>(addr + 1).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'.' as u16)?); + let (before, after) = version_string.split_at(version_string.iter().position(|&x| x == b'.')?); let mut unity: u32 = 0; for &val in before {