diff --git a/Cargo.toml b/Cargo.toml index 0866b3e..bfd6be9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ time = { version = "0.3.5", default-features = false } [target.'cfg(target_os = "wasi")'.dependencies] libm = { version = "0.2.7", optional = true } -wasi = { version = "0.11.0", default-features = false } +wasi = { version = "0.11.0+wasi-snapshot-preview1", default-features = false } [features] alloc = [] diff --git a/src/deep_pointer.rs b/src/deep_pointer.rs index 253d2b2..407af8c 100644 --- a/src/deep_pointer.rs +++ b/src/deep_pointer.rs @@ -1,20 +1,24 @@ //! Support for storing pointer paths for easy dereferencing inside the autosplitter logic. -use arrayvec::ArrayVec; +use core::array; + use bytemuck::CheckedBitPattern; -use crate::{Address, Address32, Address64, Error, Process}; +use crate::{Address, Error, PointerSize, Process}; /// An abstraction of a pointer path, usable for easy dereferencing inside an autosplitter logic. /// /// The maximum depth of the pointer path is given by the generic parameter `CAP`. -/// Of note, `CAP` must be higher or equal to the number of offsets provided in `path`, -/// otherwise calling `new()` on this struct will trigger a ***Panic***. -#[derive(Clone)] +/// +/// `CAP` should be higher or equal to the number of offsets provided in `path`. +/// If a higher number of offsets is provided, the pointer path will be truncated +/// according to the value of `CAP`. +#[derive(Copy, Clone)] pub struct DeepPointer { base_address: Address, - path: ArrayVec, - deref_type: DerefType, + path: [u64; CAP], + depth: usize, + pointer_size: PointerSize, } impl Default for DeepPointer { @@ -23,8 +27,9 @@ impl Default for DeepPointer { fn default() -> Self { Self { base_address: Address::default(), - path: ArrayVec::default(), - deref_type: DerefType::default(), + path: [u64::default(); CAP], + depth: usize::default(), + pointer_size: PointerSize::Bit64, } } } @@ -32,50 +37,46 @@ impl Default for DeepPointer { impl DeepPointer { /// Creates a new DeepPointer and specify the pointer size dereferencing #[inline] - pub fn new(base_address: Address, deref_type: DerefType, path: &[u64]) -> Self { - assert!(CAP != 0 && CAP >= path.len()); + pub fn new(base_address: impl Into
, pointer_size: PointerSize, path: &[u64]) -> Self { + let this_path = { + let mut iter = path.iter(); + array::from_fn(|_| iter.next().copied().unwrap_or_default()) + }; + Self { - base_address, - path: path.iter().cloned().collect(), - deref_type, + base_address: base_address.into(), + path: this_path, + depth: path.len().min(CAP), + pointer_size, } } /// Creates a new DeepPointer with 32bit pointer size dereferencing - pub fn new_32bit(base_address: Address, path: &[u64]) -> Self { - Self::new(base_address, DerefType::Bit32, path) + pub fn new_32bit(base_address: impl Into
, path: &[u64]) -> Self { + Self::new(base_address, PointerSize::Bit32, path) } /// Creates a new DeepPointer with 64bit pointer size dereferencing - pub fn new_64bit(base_address: Address, path: &[u64]) -> Self { - Self::new(base_address, DerefType::Bit64, path) + pub fn new_64bit(base_address: impl Into
, path: &[u64]) -> Self { + Self::new(base_address, PointerSize::Bit64, path) } /// Dereferences the pointer path, returning the memory address of the value of interest pub fn deref_offsets(&self, process: &Process) -> Result { let mut address = self.base_address; - let (&last, path) = self.path.split_last().ok_or(Error {})?; + let (&last, path) = self.path[..self.depth].split_last().ok_or(Error {})?; for &offset in path { - address = match self.deref_type { - DerefType::Bit32 => process.read::(address + offset)?.into(), - DerefType::Bit64 => process.read::(address + offset)?.into(), - }; + address = process.read_pointer(address + offset, self.pointer_size)?; } Ok(address + last) } /// Dereferences the pointer path, returning the value stored at the final memory address pub fn deref(&self, process: &Process) -> Result { - process.read(self.deref_offsets(process)?) + process.read_pointer_path( + self.base_address, + self.pointer_size, + &self.path[..self.depth], + ) } } - -/// Describes the pointer size that should be used while deferecencing a pointer path -#[derive(Copy, Clone, Default)] -pub enum DerefType { - /// 4-byte pointer size, used in 32bit processes - Bit32, - /// 8-byte pointer size, used in 64bit processes - #[default] - Bit64, -} diff --git a/src/emulator/gba/mod.rs b/src/emulator/gba/mod.rs index 71847b3..df97ad6 100644 --- a/src/emulator/gba/mod.rs +++ b/src/emulator/gba/mod.rs @@ -114,13 +114,8 @@ impl Emulator { self.state.set(state); - if success { - self.ram_base.set(ram_base); - true - } else { - self.ram_base.set(None); - false - } + self.ram_base.set(if success { ram_base } else { None }); + success } /// Reads any value from the emulated RAM. diff --git a/src/emulator/sms/retroarch.rs b/src/emulator/sms/retroarch.rs index 1ad53d7..81db41d 100644 --- a/src/emulator/sms/retroarch.rs +++ b/src/emulator/sms/retroarch.rs @@ -1,4 +1,6 @@ -use crate::{file_format::pe, signature::Signature, Address, Address32, Address64, Process}; +use crate::{ + file_format::pe, signature::Signature, Address, Address32, Address64, PointerSize, Process, +}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct State { @@ -94,8 +96,9 @@ impl State { .read::(ptr + 13 + 0x4 + game.read::(ptr + 13).ok()? + 3) .ok()?; let addr = game - .read_pointer_path64::( + .read_pointer_path::( ptr + 0x4 + game.read::(ptr).ok()?, + PointerSize::Bit64, &[0x0, 0x0, offset as _], ) .ok()?; @@ -111,7 +114,11 @@ impl State { .read::(ptr + 12 + 0x4 + game.read::(ptr + 12).ok()? + 2) .ok()?; let addr = game - .read_pointer_path32::(ptr, &[0x0, 0x0, 0x0, offset as _]) + .read_pointer_path::( + ptr, + PointerSize::Bit32, + &[0x0, 0x0, 0x0, offset as _], + ) .ok()?; if addr.is_null() { return None; diff --git a/src/file_format/elf.rs b/src/file_format/elf.rs index b2cbf32..e5a28dd 100644 --- a/src/file_format/elf.rs +++ b/src/file_format/elf.rs @@ -8,7 +8,7 @@ use core::{ use bytemuck::{Pod, Zeroable}; -use crate::{string::ArrayCString, Address, Endian, Error, FromEndian, Process}; +use crate::{string::ArrayCString, Address, Endian, Error, FromEndian, PointerSize, Process}; // Based on: // https://refspecs.linuxfoundation.org/elf/elf.pdf @@ -99,6 +99,15 @@ impl Bitness { pub fn is_64(self) -> bool { self == Self::BITNESS_64 } + + /// Returns the pointer size for the given bitness. + pub const fn pointer_size(self) -> Option { + Some(match self { + Self::BITNESS_64 => PointerSize::Bit64, + Self::BITNESS_32 => PointerSize::Bit32, + _ => return None, + }) + } } /// Segment type identifier for the ELF program header diff --git a/src/file_format/pe.rs b/src/file_format/pe.rs index 9b0781c..e2fb0d2 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, Error, FromEndian, Process}; +use crate::{string::ArrayCString, Address, Error, FromEndian, PointerSize, Process}; // Reference: // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format @@ -236,6 +236,16 @@ impl MachineType { pub const THUMB: Self = Self(0x1c2); /// MIPS little-endian WCE v2 pub const WCEMIPSV2: Self = Self(0x169); + + /// Returns the pointer size for the given machine type. Only the most + /// common machine types are supported. + pub const fn pointer_size(self) -> Option { + Some(match self { + Self::AMD64 | Self::ARM64 | Self::IA64 => PointerSize::Bit64, + Self::I386 | Self::ARM => PointerSize::Bit32, + _ => return None, + }) + } } /// Reads the size of the image of a module (`exe` or `dll`) from the given diff --git a/src/future/mod.rs b/src/future/mod.rs index 1fa1c0e..d325122 100644 --- a/src/future/mod.rs +++ b/src/future/mod.rs @@ -276,15 +276,19 @@ pub struct UntilProcessCloses<'a, F> { future: F, } -impl> Future for UntilProcessCloses<'_, F> { - type Output = (); +impl> Future for UntilProcessCloses<'_, F> { + type Output = Option; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { if !self.process.is_open() { - return Poll::Ready(()); + return Poll::Ready(None); } // SAFETY: We are simply projecting the Pin. - unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().future).poll(cx) } + unsafe { + Pin::new_unchecked(&mut self.get_unchecked_mut().future) + .poll(cx) + .map(Some) + } } } diff --git a/src/game_engine/unity/il2cpp.rs b/src/game_engine/unity/il2cpp.rs index 4361ad0..f6e55b8 100644 --- a/src/game_engine/unity/il2cpp.rs +++ b/src/game_engine/unity/il2cpp.rs @@ -1,16 +1,12 @@ //! Support for attaching to Unity games that are using the IL2CPP backend. -use core::cell::OnceCell; +use core::{array, cell::RefCell, iter}; use crate::{ - deep_pointer::{DeepPointer, DerefType}, - file_format::pe, - future::retry, - signature::Signature, - string::ArrayCString, - Address, Address32, Address64, Error, Process, + deep_pointer::DeepPointer, file_format::pe, future::retry, signature::Signature, + string::ArrayCString, Address, Address64, Error, PointerSize, Process, }; -use arrayvec::{ArrayString, ArrayVec}; + #[cfg(feature = "derive")] pub use asr_derive::Il2cppClass as Class; use bytemuck::CheckedBitPattern; @@ -19,7 +15,7 @@ const CSTR: usize = 128; /// Represents access to a Unity game that is using the IL2CPP backend. pub struct Module { - is_64_bit: bool, + pointer_size: PointerSize, version: Version, offsets: &'static Offsets, assemblies: Address, @@ -47,22 +43,32 @@ impl Module { (address, size) }; - let is_64_bit = pe::MachineType::read(process, mono_module.0)? == pe::MachineType::X86_64; + let pointer_size = match pe::MachineType::read(process, mono_module.0)? { + pe::MachineType::X86_64 => PointerSize::Bit64, + _ => PointerSize::Bit32, + }; - let assemblies_trg_addr = if is_64_bit { - const ASSEMBLIES_TRG_SIG: Signature<12> = - Signature::new("48 FF C5 80 3C ?? 00 75 ?? 48 8B 1D"); + let offsets = Offsets::new(version, pointer_size)?; - let addr = ASSEMBLIES_TRG_SIG.scan_process_range(process, mono_module)? + 12; - addr + 0x4 + process.read::(addr).ok()? - } else { - const ASSEMBLIES_TRG_SIG: Signature<9> = Signature::new("8A 07 47 84 C0 75 ?? 8B 35"); + let assemblies = match pointer_size { + PointerSize::Bit64 => { + const ASSEMBLIES_TRG_SIG: Signature<12> = + Signature::new("48 FF C5 80 3C ?? 00 75 ?? 48 8B 1D"); + + let addr = ASSEMBLIES_TRG_SIG.scan_process_range(process, mono_module)? + 12; + addr + 0x4 + process.read::(addr).ok()? + } + PointerSize::Bit32 => { + const ASSEMBLIES_TRG_SIG: Signature<9> = + Signature::new("8A 07 47 84 C0 75 ?? 8B 35"); - let addr = ASSEMBLIES_TRG_SIG.scan_process_range(process, mono_module)? + 9; - process.read::(addr).ok()?.into() + let addr = ASSEMBLIES_TRG_SIG.scan_process_range(process, mono_module)? + 9; + process.read_pointer(addr, pointer_size).ok()? + } + _ => return None, }; - let type_info_definition_table_trg_addr: Address = if is_64_bit { + let type_info_definition_table = if pointer_size == PointerSize::Bit64 { const TYPE_INFO_DEFINITION_TABLE_TRG_SIG: Signature<10> = Signature::new("48 83 3C ?? 00 75 ?? 8B C? E8"); @@ -71,9 +77,9 @@ impl Module { .add_signed(-4); process - .read::(addr + 0x4 + process.read::(addr).ok()?) - .ok()? - .into() + .read_pointer(addr + 0x4 + process.read::(addr).ok()?, pointer_size) + .ok() + .filter(|val| !val.is_null())? } else { const TYPE_INFO_DEFINITION_TABLE_TRG_SIG: Signature<10> = Signature::new("C3 A1 ?? ?? ?? ?? 83 3C ?? 00"); @@ -82,46 +88,48 @@ impl Module { TYPE_INFO_DEFINITION_TABLE_TRG_SIG.scan_process_range(process, mono_module)? + 2; process - .read::(process.read::(addr).ok()?) - .ok()? - .into() + .read_pointer(process.read_pointer(addr, pointer_size).ok()?, pointer_size) + .ok() + .filter(|val| !val.is_null())? }; - 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, - }) - } + Some(Self { + pointer_size, + version, + offsets, + assemblies, + type_info_definition_table, + }) } 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) + let (assemblies, nr_of_assemblies): (Address, u64) = match self.pointer_size { + PointerSize::Bit64 => { + 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) + } + _ => { + 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())) + assembly: process + .read_pointer( + assemblies + i.wrapping_mul(self.size_of_ptr()), + self.pointer_size, + ) .ok()?, }) }) @@ -206,20 +214,11 @@ impl Module { #[inline] const fn size_of_ptr(&self) -> u64 { - match self.is_64_bit { - true => 8, - false => 4, - } - } - - fn read_pointer(&self, process: &Process, address: Address) -> Result { - Ok(match self.is_64_bit { - true => process.read::(address)?.into(), - false => process.read::(address)?.into(), - }) + self.pointer_size as u64 } } +#[derive(Copy, Clone)] struct Assembly { assembly: Address, } @@ -230,18 +229,21 @@ impl Assembly { process: &Process, module: &Module, ) -> Result, Error> { - process.read(module.read_pointer( - process, + process.read(process.read_pointer( self.assembly + module.offsets.monoassembly_aname + module.offsets.monoassemblyname_name, + module.pointer_size, )?) } fn get_image(&self, process: &Process, module: &Module) -> Option { Some(Image { - image: module - .read_pointer(process, self.assembly + module.offsets.monoassembly_image) + image: process + .read_pointer( + self.assembly + module.offsets.monoassembly_image, + module.pointer_size, + ) .ok()?, }) } @@ -249,6 +251,7 @@ impl Assembly { /// 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. +#[derive(Copy, Clone)] pub struct Image { image: Address, } @@ -264,9 +267,9 @@ impl Image { let metadata_ptr = match type_count { Ok(_) => match module.version { - Version::V2020 => module.read_pointer( - process, + Version::V2020 => process.read_pointer( self.image + module.offsets.monoimage_metadatahandle, + module.pointer_size, ), _ => Ok(self.image + module.offsets.monoimage_metadatahandle), }, @@ -287,14 +290,14 @@ impl Image { }); (0..type_count.unwrap_or_default() as u64).filter_map(move |i| { - let class = module - .read_pointer(process, ptr? + i.wrapping_mul(module.size_of_ptr())) - .ok()?; - - match class.is_null() { - false => Some(Class { class }), - true => None, - } + let class = process + .read_pointer( + ptr? + i.wrapping_mul(module.size_of_ptr()), + module.pointer_size, + ) + .ok() + .filter(|val| !val.is_null())?; + Some(Class { class }) }) } @@ -320,7 +323,8 @@ impl Image { } } -/// A .NET class that is part of an [`Image`]. +/// A .NET class that is part of an [`Image`](Image). +#[derive(Copy, Clone)] pub struct Class { class: Address, } @@ -331,26 +335,79 @@ impl Class { process: &Process, module: &Module, ) -> Result, Error> { - process.read(module.read_pointer(process, self.class + module.offsets.monoclass_name)?) + process.read_pointer_path( + self.class, + module.pointer_size, + &[module.offsets.monoclass_name.into(), 0x0], + ) + //process.read(module.read_pointer(process, self.class + module.offsets.monoclass_name)?) } - fn fields(&self, process: &Process, module: &Module) -> impl DoubleEndedIterator { - let field_count = process.read::(self.class + module.offsets.monoclass_field_count); + fn get_name_space( + &self, + process: &Process, + module: &Module, + ) -> Result, Error> { + process.read_pointer_path( + self.class, + module.pointer_size, + &[module.offsets.monoclass_name_space.into(), 0x0], + ) + } - let fields = match field_count { - Ok(_) => module - .read_pointer(process, self.class + module.offsets.monoclass_fields) - .ok(), - _ => None, - }; + fn fields<'a>( + &'a self, + process: &'a Process, + module: &'a Module, + ) -> impl Iterator + '_ { + let mut this_class = Class { class: self.class }; + let mut iter_break = this_class.class.is_null(); - let monoclassfield_structsize = module.offsets.monoclassfield_structsize as u64; + iter::from_fn(move || { + if iter_break { + None + } else if !this_class.class.is_null() + && this_class + .get_name::(process, module) + .is_ok_and(|name| !name.matches("Object")) + && this_class + .get_name_space::(process, module) + .is_ok_and(|name| !name.matches("UnityEngine")) + { + let field_count = + process.read::(this_class.class + module.offsets.monoclass_field_count); + + let fields = match field_count { + Ok(_) => process + .read_pointer( + this_class.class + module.offsets.monoclass_fields, + module.pointer_size, + ) + .ok(), + _ => None, + }; + + let monoclassfield_structsize = module.offsets.monoclassfield_structsize as u64; + + if let Some(x) = this_class.get_parent(process, module) { + this_class = x; + } else { + iter_break = true; + } - (0..field_count.unwrap_or_default() as u64).filter_map(move |i| { - Some(Field { - field: fields? + i.wrapping_mul(monoclassfield_structsize), - }) + Some( + (0..field_count.unwrap_or_default() as u64).filter_map(move |i| { + Some(Field { + field: fields? + i.wrapping_mul(monoclassfield_structsize), + }) + }), + ) + } else { + iter_break = true; + None + } }) + .flatten() } /// Tries to find a field with the specified name in the class. This returns @@ -387,32 +444,36 @@ impl Class { let singleton_location = static_table + field_offset; retry(|| { - let addr = module.read_pointer(process, singleton_location).ok()?; - - if addr.is_null() { - None - } else { - Some(addr) - } + process + .read_pointer(singleton_location, module.pointer_size) + .ok() + .filter(|val| !val.is_null()) }) .await } + fn get_static_table_pointer(&self, module: &Module) -> Address { + self.class + module.offsets.monoclass_static_fields + } + /// Returns the address of the static table of the class. This contains the /// values of all the static fields. pub fn get_static_table(&self, process: &Process, module: &Module) -> Option
{ - module - .read_pointer(process, self.class + module.offsets.monoclass_static_fields) + process + .read_pointer(self.get_static_table_pointer(module), module.pointer_size) .ok() - .filter(|a| !a.is_null()) + .filter(|val| !val.is_null()) } /// Tries to find the parent class. pub fn get_parent(&self, process: &Process, module: &Module) -> Option { - let parent = module - .read_pointer(process, self.class + module.offsets.monoclass_parent) - .ok()?; - + let parent = process + .read_pointer( + self.class + module.offsets.monoclass_parent, + module.pointer_size, + ) + .ok() + .filter(|val| !val.is_null())?; Some(Class { class: parent }) } @@ -444,6 +505,7 @@ impl Class { } } +#[derive(Copy, Clone)] struct Field { field: Address, } @@ -454,7 +516,11 @@ impl Field { process: &Process, module: &Module, ) -> Result, Error> { - process.read(module.read_pointer(process, self.field + module.offsets.monoclassfield_name)?) + process.read_pointer_path( + self.field, + module.pointer_size, + &[module.offsets.monoclassfield_name.into(), 0x0], + ) } fn get_offset(&self, process: &Process, module: &Module) -> Option { @@ -465,124 +531,153 @@ impl Field { } /// An IL2CPP-specific implementation for automatic pointer path resolution +#[derive(Clone)] pub struct UnityPointer { - deep_pointer: OnceCell>, - class_name: ArrayString, - nr_of_parents: u8, - fields: ArrayVec, CAP>, + cache: RefCell>, + class_name: &'static str, + nr_of_parents: usize, + fields: [&'static str; CAP], + depth: usize, +} + +#[derive(Clone, Copy)] +struct UnityPointerCache { + base_address: Address, + offsets: [u64; CAP], + resolved_offsets: usize, + current_instance_pointer: Option
, + starting_class: Option, } impl UnityPointer { /// Creates a new instance of the Pointer struct /// - /// `CAP` must be higher or equal to the number of offsets defined in `fields`. + /// `CAP` should 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); + /// If a higher number of offsets is provided, the pointer path will be truncated + /// according to the value of `CAP`. + pub fn new(class_name: &'static str, nr_of_parents: usize, fields: &[&'static str]) -> Self { + let this_fields: [&str; CAP] = { + let mut iter = fields.iter(); + array::from_fn(|_| iter.next().copied().unwrap_or_default()) + }; + + let cache = RefCell::new(UnityPointerCache { + base_address: Address::default(), + offsets: [u64::default(); CAP], + current_instance_pointer: None, + starting_class: None, + resolved_offsets: usize::default(), + }); Self { - deep_pointer: OnceCell::new(), - class_name: ArrayString::from(class_name).unwrap_or_default(), + cache, + class_name, nr_of_parents, - fields: fields - .iter() - .map(|&val| ArrayString::from(val).unwrap_or_default()) - .collect(), + fields: this_fields, + depth: fields.len().min(CAP), } } /// Tries to resolve the pointer path for the `IL2CPP` class specified fn find_offsets(&self, process: &Process, module: &Module, image: &Image) -> Result<(), Error> { + let mut cache = self.cache.borrow_mut(); + // If the pointer path has already been found, there's no need to continue - if self.deep_pointer.get().is_some() { + if cache.resolved_offsets == self.depth { return Ok(()); } - let mut current_class = image - .get_class(process, module, &self.class_name) - .ok_or(Error {})?; + // Logic: the starting class can be recovered with the get_class() function, + // and parent class can be recovered if needed. However, this is a VERY + // intensive process because it involves looping through all the main classes + // in the game. For this reason, once the class is found, we want to store it + // into the cache, where it can be recovered if this function need to be run again + // (for example if a previous attempt at pointer path resolution failed) + let starting_class = match cache.starting_class { + Some(starting_class) => starting_class, + _ => { + 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 {})?; - } + 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 {})?; + cache.starting_class = Some(current_class); + current_class + } + }; - let mut offsets: ArrayVec = ArrayVec::new(); + // Recovering the address of the static table is not very CPU intensive, + // but it might be worth caching it as well + if cache.base_address.is_null() { + let s_table = starting_class + .get_static_table(process, module) + .ok_or(Error {})?; + cache.base_address = s_table; + }; - 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; + // As we need to be able to find instances in a more reliable way, + // instead of the Class itself, we need the address pointing to an + // instance of that Class. If the cache is empty, we start from the + // pointer to the static table of the first class. + let mut current_instance_pointer = match cache.current_instance_pointer { + Some(val) => val, + _ => starting_class.get_static_table_pointer(module), + }; + + // We keep track of the already resolved offsets in order to skip resolving them again + for i in cache.resolved_offsets..self.depth { + let class_instance = process + .read_pointer(current_instance_pointer, module.pointer_size) + .ok() + .filter(|val| !val.is_null()) + .ok_or(Error {})?; - 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) + // Try to parse the offset, passed as a string, as an actual hex or decimal value + let offset_from_string = super::value_from_string(self.fields[i]); + + let current_offset = match offset_from_string { + Some(offset) => offset as u64, + _ => { + let current_class = match i { + 0 => starting_class, + _ => { + let class = process + .read_pointer(class_instance, module.pointer_size) + .ok() + .filter(|val| !val.is_null()) + .ok_or(Error {})?; + Class { class } } - } - } else if let Ok(val) = field_name.parse::() { - temp_val = Some(val) + }; + + let val = current_class + .fields(process, module) + .find(|field| { + field + .get_name::(process, module) + .is_ok_and(|name| name.matches(self.fields[i])) + }) + .ok_or(Error {})? + .get_offset(process, module) + .ok_or(Error {})? as u64; + + // Explicitly allowing this clippy because of borrowing rules shenanigans + #[allow(clippy::let_and_return)] + 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 {})?; + cache.offsets[i] = current_offset; - 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 {})?; - } + current_instance_pointer = class_instance + current_offset; + cache.current_instance_pointer = Some(current_instance_pointer); + cache.resolved_offsets += 1; } - let pointer = DeepPointer::new( - static_table, - if module.is_64_bit { - DerefType::Bit64 - } else { - DerefType::Bit32 - }, - &offsets, - ); - let _ = self.deep_pointer.set(pointer); Ok(()) } @@ -594,10 +689,13 @@ impl UnityPointer { image: &Image, ) -> Result { self.find_offsets(process, module, image)?; - self.deep_pointer - .get() - .ok_or(Error {})? - .deref_offsets(process) + let cache = self.cache.borrow(); + let mut address = cache.base_address; + let (&last, path) = cache.offsets[..self.depth].split_last().ok_or(Error {})?; + for &offset in path { + address = process.read_pointer(address + offset, module.pointer_size)?; + } + Ok(address + last) } /// Dereferences the pointer path, returning the value stored at the final memory address @@ -608,11 +706,16 @@ impl UnityPointer { image: &Image, ) -> Result { self.find_offsets(process, module, image)?; - self.deep_pointer.get().ok_or(Error {})?.deref(process) + let cache = self.cache.borrow(); + process.read_pointer_path( + cache.base_address, + module.pointer_size, + &cache.offsets[..self.depth], + ) } - /// Recovers the `DeepPointer` struct contained inside this `UnityPointer`, - /// if the offsets have been found + /// Generates a `DeepPointer` struct based on the offsets + /// recovered from this `UnityPointer`. pub fn get_deep_pointer( &self, process: &Process, @@ -620,7 +723,12 @@ impl UnityPointer { image: &Image, ) -> Option> { self.find_offsets(process, module, image).ok()?; - self.deep_pointer.get().cloned() + let cache = self.cache.borrow(); + Some(DeepPointer::::new( + cache.base_address, + module.pointer_size, + &cache.offsets[..self.depth], + )) } } @@ -631,7 +739,7 @@ struct Offsets { monoimage_typecount: u8, monoimage_metadatahandle: u8, monoclass_name: u8, - monoclass_type_definition: u8, + monoclass_name_space: u8, monoclass_fields: u8, monoclass_field_count: u16, monoclass_static_fields: u8, @@ -642,63 +750,62 @@ struct Offsets { } impl Offsets { - const fn new(version: Version, is_64_bit: bool) -> Option<&'static Self> { - if !is_64_bit { - // Il2Cpp on 32-bit is unsupported. Although there are some games - // using Il2Cpp_base, there are known issues with its offsets. - return None; + const fn new(version: Version, pointer_size: PointerSize) -> Option<&'static Self> { + match pointer_size { + PointerSize::Bit64 => { + Some(match version { + Version::Base => &Self { + monoassembly_image: 0x0, + monoassembly_aname: 0x18, + monoassemblyname_name: 0x0, + monoimage_typecount: 0x1C, + monoimage_metadatahandle: 0x18, // MonoImage.typeStart + monoclass_name: 0x10, + monoclass_name_space: 0x18, + monoclass_fields: 0x80, + monoclass_field_count: 0x114, + monoclass_static_fields: 0xB8, + monoclass_parent: 0x58, + monoclassfield_structsize: 0x20, + monoclassfield_name: 0x0, + monoclassfield_offset: 0x18, + }, + Version::V2019 => &Self { + monoassembly_image: 0x0, + monoassembly_aname: 0x18, + monoassemblyname_name: 0x0, + monoimage_typecount: 0x1C, + monoimage_metadatahandle: 0x18, // MonoImage.typeStart + monoclass_name: 0x10, + monoclass_name_space: 0x18, + monoclass_fields: 0x80, + monoclass_field_count: 0x11C, + monoclass_static_fields: 0xB8, + monoclass_parent: 0x58, + monoclassfield_structsize: 0x20, + monoclassfield_name: 0x0, + monoclassfield_offset: 0x18, + }, + Version::V2020 => &Self { + monoassembly_image: 0x0, + monoassembly_aname: 0x18, + monoassemblyname_name: 0x0, + monoimage_typecount: 0x18, + monoimage_metadatahandle: 0x28, + monoclass_name: 0x10, + monoclass_name_space: 0x18, + monoclass_fields: 0x80, + monoclass_field_count: 0x120, + monoclass_static_fields: 0xB8, + monoclass_parent: 0x58, + monoclassfield_structsize: 0x20, + monoclassfield_name: 0x0, + monoclassfield_offset: 0x18, + }, + }) + } + _ => None, } - - Some(match version { - Version::Base => &Self { - monoassembly_image: 0x0, - monoassembly_aname: 0x18, - monoassemblyname_name: 0x0, - 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, - monoclass_parent: 0x58, - monoclassfield_structsize: 0x20, - monoclassfield_name: 0x0, - monoclassfield_offset: 0x18, - }, - Version::V2019 => &Self { - monoassembly_image: 0x0, - monoassembly_aname: 0x18, - monoassemblyname_name: 0x0, - 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, - monoclass_parent: 0x58, - monoclassfield_structsize: 0x20, - monoclassfield_name: 0x0, - monoclassfield_offset: 0x18, - }, - Version::V2020 => &Self { - monoassembly_image: 0x0, - monoassembly_aname: 0x18, - monoassemblyname_name: 0x0, - monoimage_typecount: 0x18, - monoimage_metadatahandle: 0x28, - monoclass_name: 0x10, - monoclass_type_definition: 0x68, - monoclass_fields: 0x80, - monoclass_field_count: 0x120, - monoclass_static_fields: 0xB8, - monoclass_parent: 0x58, - monoclassfield_structsize: 0x20, - monoclassfield_name: 0x0, - monoclassfield_offset: 0x18, - }, - }) } } diff --git a/src/game_engine/unity/mod.rs b/src/game_engine/unity/mod.rs index 1d4b970..277f97f 100644 --- a/src/game_engine/unity/mod.rs +++ b/src/game_engine/unity/mod.rs @@ -88,3 +88,11 @@ pub mod mono; mod scene; pub use self::scene::*; + +fn value_from_string(value: &str) -> Option { + if let Some(rem) = value.strip_prefix("0x") { + u32::from_str_radix(rem, 16).ok() + } else { + value.parse().ok() + } +} diff --git a/src/game_engine/unity/mono.rs b/src/game_engine/unity/mono.rs index 18afc44..3e63dc8 100644 --- a/src/game_engine/unity/mono.rs +++ b/src/game_engine/unity/mono.rs @@ -2,16 +2,11 @@ //! backend. use crate::{ - deep_pointer::{DeepPointer, DerefType}, - file_format::pe, - future::retry, - signature::Signature, - string::ArrayCString, - Address, Address32, Address64, Error, Process, + deep_pointer::DeepPointer, file_format::pe, future::retry, signature::Signature, + string::ArrayCString, Address, Address32, Address64, Error, PointerSize, Process, }; -use core::{cell::OnceCell, iter}; +use core::{array, cell::RefCell, iter}; -use arrayvec::{ArrayString, ArrayVec}; #[cfg(feature = "derive")] pub use asr_derive::MonoClass as Class; use bytemuck::CheckedBitPattern; @@ -20,7 +15,7 @@ const CSTR: usize = 128; /// Represents access to a Unity game that is using the standard Mono backend. pub struct Module { - is_64_bit: bool, + pointer_size: PointerSize, version: Version, offsets: &'static Offsets, assemblies: Address, @@ -45,8 +40,12 @@ impl Module { .iter() .find_map(|&name| process.get_module_address(name).ok())?; - let is_64_bit = pe::MachineType::read(process, module)? == pe::MachineType::X86_64; - let offsets = Offsets::new(version, is_64_bit); + let pointer_size = match pe::MachineType::read(process, module)? { + pe::MachineType::X86_64 => PointerSize::Bit64, + _ => PointerSize::Bit32, + }; + + let offsets = Offsets::new(version, pointer_size)?; let root_domain_function_address = pe::symbols(process, module) .find(|symbol| { @@ -56,15 +55,15 @@ impl Module { })? .address; - let assemblies_pointer: Address = match is_64_bit { - true => { + let assemblies_pointer: Address = match pointer_size { + PointerSize::Bit64 => { const SIG_MONO_64: Signature<3> = Signature::new("48 8B 0D"); let scan_address: Address = SIG_MONO_64 .scan_process_range(process, (root_domain_function_address, 0x100))? + 3; scan_address + 0x4 + process.read::(scan_address).ok()? } - false => { + PointerSize::Bit32 => { const SIG_32_1: Signature<2> = Signature::new("FF 35"); const SIG_32_2: Signature<2> = Signature::new("8B 0D"); @@ -74,23 +73,20 @@ impl Module { process.read::(ptr).ok()?.into() } + _ => return None, }; - let assemblies: Address = match is_64_bit { - true => process.read::(assemblies_pointer).ok()?.into(), - false => process.read::(assemblies_pointer).ok()?.into(), - }; + let assemblies = process + .read_pointer(assemblies_pointer, pointer_size) + .ok() + .filter(|val| !val.is_null())?; - if assemblies.is_null() { - None - } else { - Some(Self { - is_64_bit, - version, - offsets, - assemblies, - }) - } + Some(Self { + pointer_size, + version, + offsets, + assemblies, + }) } fn assemblies<'a>(&'a self, process: &'a Process) -> impl Iterator + 'a { @@ -100,12 +96,12 @@ impl Module { if iter_break { None } else { - let [data, next_assembly]: [Address; 2] = match self.is_64_bit { - true => process + let [data, next_assembly]: [Address; 2] = match self.pointer_size { + PointerSize::Bit64 => process .read::<[Address64; 2]>(assembly) .ok()? .map(|item| item.into()), - false => process + _ => process .read::<[Address32; 2]>(assembly) .ok()? .map(|item| item.into()), @@ -201,20 +197,11 @@ impl Module { #[inline] const fn size_of_ptr(&self) -> u64 { - match self.is_64_bit { - true => 8, - false => 4, - } - } - - fn read_pointer(&self, process: &Process, address: Address) -> Result { - Ok(match self.is_64_bit { - true => process.read::(address)?.into(), - false => process.read::(address)?.into(), - }) + self.pointer_size as u64 } } +#[derive(Copy, Clone)] struct Assembly { assembly: Address, } @@ -225,21 +212,29 @@ impl Assembly { process: &Process, module: &Module, ) -> Result, Error> { - process - .read(module.read_pointer(process, self.assembly + module.offsets.monoassembly_aname)?) + process.read_pointer_path( + self.assembly, + module.pointer_size, + &[module.offsets.monoassembly_aname.into(), 0x0], + ) } fn get_image(&self, process: &Process, module: &Module) -> Option { Some(Image { - image: module - .read_pointer(process, self.assembly + module.offsets.monoassembly_image) - .ok()?, + image: process + .read_pointer( + self.assembly + module.offsets.monoassembly_image, + module.pointer_size, + ) + .ok() + .filter(|val| !val.is_null())?, }) } } /// 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. +#[derive(Copy, Clone)] pub struct Image { image: Address, } @@ -251,43 +246,46 @@ 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, + ) + .ok() + .filter(|&val| val != 0); let table_addr = match class_cache_size { - Ok(_) => module.read_pointer( - process, + Some(_) => process.read_pointer( self.image + module.offsets.monoimage_class_cache + module.offsets.monointernalhashtable_table, + module.pointer_size, ), _ => Err(Error {}), }; (0..class_cache_size.unwrap_or_default()).flat_map(move |i| { - let mut table = if let Ok(table_addr) = table_addr { - module + let mut table = match table_addr { + Ok(table_addr) => process .read_pointer( - process, table_addr + (i as u64).wrapping_mul(module.size_of_ptr()), + module.pointer_size, ) - .ok() - } else { - None + .ok(), + _ => None, }; iter::from_fn(move || { - let class = module.read_pointer(process, table?).ok()?; + let class = process.read_pointer(table?, module.pointer_size).ok()?; - table = module + table = process .read_pointer( - process, table? + module.offsets.monoclassdef_next_class_cache, + module.pointer_size, ) - .ok(); + .ok() + .filter(|val| !val.is_null()); Some(Class { class }) }) @@ -316,7 +314,8 @@ impl Image { } } -/// A .NET class that is part of an [`Image`]. +/// A .NET class that is part of an [`Image`](Image). +#[derive(Copy, Clone)] pub struct Class { class: Address, } @@ -327,35 +326,89 @@ impl Class { process: &Process, module: &Module, ) -> Result, Error> { - process.read(module.read_pointer( - process, - self.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_name, - )?) + process.read_pointer_path( + self.class, + module.pointer_size, + &[ + module.offsets.monoclassdef_klass as u64 + module.offsets.monoclass_name as u64, + 0x0, + ], + ) } - fn fields(&self, process: &Process, module: &Module) -> impl DoubleEndedIterator { - let field_count = process - .read::(self.class + module.offsets.monoclassdef_field_count) - .ok(); + fn get_name_space( + &self, + process: &Process, + module: &Module, + ) -> Result, Error> { + process.read_pointer_path( + self.class, + module.pointer_size, + &[ + module.offsets.monoclassdef_klass as u64 + + module.offsets.monoclass_name_space as u64, + 0x0, + ], + ) + } + + fn fields<'a>( + &'a self, + process: &'a Process, + module: &'a Module, + ) -> impl Iterator + 'a { + let mut this_class = Class { class: self.class }; + let mut iter_break = this_class.class.is_null(); + + iter::from_fn(move || { + if iter_break { + None + } else if !this_class.class.is_null() + && this_class + .get_name::(process, module) + .is_ok_and(|name| !name.matches("Object")) + && this_class + .get_name_space::(process, module) + .is_ok_and(|name| !name.matches("UnityEngine")) + { + let field_count = process + .read::(this_class.class + module.offsets.monoclassdef_field_count) + .ok() + .filter(|&val| val != 0); + + let fields = match field_count { + Some(_) => process + .read_pointer( + this_class.class + + module.offsets.monoclassdef_klass + + module.offsets.monoclass_fields, + module.pointer_size, + ) + .ok(), + _ => None, + }; - 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; - let monoclassfieldalignment = module.offsets.monoclassfieldalignment as u64; - (0..field_count.unwrap_or_default()).filter_map(move |i| { - Some(Field { - field: fields? + (i as u64).wrapping_mul(monoclassfieldalignment), - }) + if let Some(x) = this_class.get_parent(process, module) { + this_class = x; + } else { + iter_break = true; + } + + Some( + (0..field_count.unwrap_or_default() as u64).filter_map(move |i| { + Some(Field { + field: fields? + i.wrapping_mul(monoclassfieldalignment), + }) + }), + ) + } else { + iter_break = true; + None + } }) + .flatten() } /// Tries to find the offset for a field with the specified name in the class. @@ -391,7 +444,9 @@ impl Class { let singleton_location = static_table + field_offset; retry(|| { - let addr = module.read_pointer(process, singleton_location).ok()?; + let addr = process + .read_pointer(singleton_location, module.pointer_size) + .ok()?; if addr.is_null() { None @@ -402,61 +457,69 @@ impl Class { .await } - /// Returns the address of the static table of the class. This contains the - /// values of all the static fields. - pub fn get_static_table(&self, process: &Process, module: &Module) -> Option
{ - let runtime_info = module + fn get_static_table_pointer(&self, process: &Process, module: &Module) -> Option
{ + let runtime_info = process .read_pointer( - process, self.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_runtime_info, + module.pointer_size, ) .ok()?; - let mut vtables = module + let mut vtables = process .read_pointer( - process, runtime_info + module.offsets.monoclassruntimeinfo_domain_vtables, + module.pointer_size, ) .ok()?; // Mono V1 behaves differently when it comes to recover the static table - if module.version == Version::V1 { - module - .read_pointer(process, vtables + module.offsets.monoclass_vtable_size) - .ok() - } else { - vtables = vtables + module.offsets.monovtable_vtable; - - let vtable_size = process - .read::( - self.class - + module.offsets.monoclassdef_klass - + module.offsets.monoclass_vtable_size, - ) - .ok()?; + match module.version { + Version::V1 => Some(vtables + module.offsets.monoclass_vtable_size), + _ => { + vtables = vtables + module.offsets.monovtable_vtable; + + let vtable_size = process + .read::( + self.class + + module.offsets.monoclassdef_klass + + module.offsets.monoclass_vtable_size, + ) + .ok()?; - module - .read_pointer( - process, - vtables + (vtable_size as u64).wrapping_mul(module.size_of_ptr()), - ) - .ok() + Some(vtables + (vtable_size as u64).wrapping_mul(module.size_of_ptr())) + } } } + /// Returns the address of the static table of the class. This contains the + /// values of all the static fields. + pub fn get_static_table(&self, process: &Process, module: &Module) -> Option
{ + process + .read_pointer( + self.get_static_table_pointer(process, module)?, + module.pointer_size, + ) + .ok() + .filter(|val| !val.is_null()) + } + /// Tries to find the parent class. pub fn get_parent(&self, process: &Process, module: &Module) -> Option { - let parent_addr = module + let parent_addr = process .read_pointer( - process, self.class + module.offsets.monoclassdef_klass + module.offsets.monoclass_parent, + module.pointer_size, ) - .ok()?; + .ok() + .filter(|val| !val.is_null())?; Some(Class { - class: module.read_pointer(process, parent_addr).ok()?, + class: process + .read_pointer(parent_addr, module.pointer_size) + .ok() + .filter(|val| !val.is_null())?, }) } @@ -488,6 +551,7 @@ impl Class { } } +#[derive(Copy, Clone)] struct Field { field: Address, } @@ -498,9 +562,11 @@ impl Field { process: &Process, module: &Module, ) -> Result, Error> { - let name_addr = - module.read_pointer(process, self.field + module.offsets.monoclassfield_name)?; - process.read(name_addr) + process.read_pointer_path( + self.field, + module.pointer_size, + &[module.offsets.monoclassfield_name.into(), 0x0], + ) } fn get_offset(&self, process: &Process, module: &Module) -> Option { @@ -510,115 +576,164 @@ impl Field { } } -/// A Mono-specific implementation useful for automatic pointer path resolution +/// A Mono-specific implementation for automatic pointer path resolution +#[derive(Clone)] pub struct UnityPointer { - deep_pointer: OnceCell>, - class_name: ArrayString, - nr_of_parents: u8, - fields: ArrayVec, CAP>, + cache: RefCell>, + class_name: &'static str, + nr_of_parents: usize, + fields: [&'static str; CAP], + depth: usize, +} + +#[derive(Clone, Copy)] +struct UnityPointerCache { + base_address: Address, + offsets: [u64; CAP], + resolved_offsets: usize, + current_instance_pointer: Option
, + starting_class: Option, } impl UnityPointer { /// Creates a new instance of the Pointer struct /// - /// `CAP` must be higher or equal to the number of offsets defined in `fields`. + /// `CAP` should 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); + /// If a higher number of offsets is provided, the pointer path will be truncated + /// according to the value of `CAP`. + pub fn new(class_name: &'static str, nr_of_parents: usize, fields: &[&'static str]) -> Self { + let this_fields: [&str; CAP] = { + let mut iter = fields.iter(); + array::from_fn(|_| iter.next().copied().unwrap_or_default()) + }; + + let cache = RefCell::new(UnityPointerCache { + base_address: Address::default(), + offsets: [u64::default(); CAP], + current_instance_pointer: None, + starting_class: None, + resolved_offsets: usize::default(), + }); Self { - deep_pointer: OnceCell::new(), - class_name: ArrayString::from(class_name).unwrap_or_default(), + cache, + class_name, nr_of_parents, - fields: fields - .iter() - .map(|&val| ArrayString::from(val).unwrap_or_default()) - .collect(), + fields: this_fields, + depth: fields.len().min(CAP), } } /// Tries to resolve the pointer path for the `Mono` class specified fn find_offsets(&self, process: &Process, module: &Module, image: &Image) -> Result<(), Error> { + let mut cache = self.cache.borrow_mut(); + // If the pointer path has already been found, there's no need to continue - if self.deep_pointer.get().is_some() { + if cache.resolved_offsets == self.depth { return Ok(()); } - let mut current_class = image - .get_class(process, module, &self.class_name) - .ok_or(Error {})?; + // Logic: the starting class can be recovered with the get_class() function, + // and parent class can be recovered if needed. However, this is a VERY + // intensive process because it involves looping through all the main classes + // in the game. For this reason, once the class is found, we want to store it + // into the cache, where it can be recovered if this function need to be run again + // (for example if a previous attempt at pointer path resolution failed) + let starting_class = match cache.starting_class { + Some(starting_class) => starting_class, + _ => { + 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 {})?; + } - for _ in 0..self.nr_of_parents { - current_class = current_class.get_parent(process, module).ok_or(Error {})?; - } + cache.starting_class = Some(current_class); + current_class + } + }; - let static_table = current_class - .get_static_table(process, module) - .ok_or(Error {})?; + // Recovering the address of the static table is not very CPU intensive, + // but it might be worth caching it as well + if cache.base_address.is_null() { + let s_table = starting_class + .get_static_table(process, module) + .ok_or(Error {})?; + cache.base_address = s_table; + }; - let mut offsets: ArrayVec = ArrayVec::new(); + // As we need to be able to find instances in a more reliable way, + // instead of the Class itself, we need the address pointing to an + // instance of that Class. If the cache is empty, we start from the + // pointer to the static table of the first class. + let mut current_instance_pointer = match cache.current_instance_pointer { + Some(val) => val, + _ => starting_class + .get_static_table_pointer(process, module) + .ok_or(Error {})?, + }; - 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; + // We keep track of the already resolved offsets in order to skip resolving them again + for i in cache.resolved_offsets..self.depth { + let class_instance = process + .read_pointer(current_instance_pointer, module.pointer_size) + .ok() + .filter(|val| !val.is_null()) + .ok_or(Error {})?; - 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) + // Try to parse the offset, passed as a string, as an actual hex or decimal value + let offset_from_string = super::value_from_string(self.fields[i]); + + let current_offset = match offset_from_string { + Some(offset) => offset as u64, + _ => { + let current_class = match i { + 0 => starting_class, + _ => { + let class = process + .read_pointer( + process + .read_pointer(class_instance, module.pointer_size) + .ok() + .filter(|val| !val.is_null()) + .ok_or(Error {})?, + module.pointer_size, + ) + .ok() + .filter(|val| !val.is_null()) + .ok_or(Error {})?; + + Class { class } } - } - } else if let Ok(val) = field_name.parse::() { - temp_val = Some(val) + }; + + let val = current_class + .fields(process, module) + .find(|field| { + field + .get_name::(process, module) + .is_ok_and(|name| name.matches(self.fields[i])) + }) + .ok_or(Error {})? + .get_offset(process, module) + .ok_or(Error {})? as u64; + + // Explicitly allowing this clippy because of borrowing rules shenanigans + #[allow(clippy::let_and_return)] + 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)?; + cache.offsets[i] = current_offset; - current_class = Class { - class: module.read_pointer(process, vtable)?, - }; - } + current_instance_pointer = class_instance + current_offset; + cache.current_instance_pointer = Some(current_instance_pointer); + cache.resolved_offsets += 1; } - let pointer = DeepPointer::new( - static_table, - if module.is_64_bit { - DerefType::Bit64 - } else { - DerefType::Bit32 - }, - &offsets, - ); - let _ = self.deep_pointer.set(pointer); Ok(()) } @@ -630,10 +745,13 @@ impl UnityPointer { image: &Image, ) -> Result { self.find_offsets(process, module, image)?; - self.deep_pointer - .get() - .ok_or(Error {})? - .deref_offsets(process) + let cache = self.cache.borrow(); + let mut address = cache.base_address; + let (&last, path) = cache.offsets[..self.depth].split_last().ok_or(Error {})?; + for &offset in path { + address = process.read_pointer(address + offset, module.pointer_size)?; + } + Ok(address + last) } /// Dereferences the pointer path, returning the value stored at the final memory address @@ -644,11 +762,16 @@ impl UnityPointer { image: &Image, ) -> Result { self.find_offsets(process, module, image)?; - self.deep_pointer.get().ok_or(Error {})?.deref(process) + let cache = self.cache.borrow(); + process.read_pointer_path( + cache.base_address, + module.pointer_size, + &cache.offsets[..self.depth], + ) } - /// Recovers the `DeepPointer` struct contained inside this `UnityPointer`, - /// if the offsets have been found + /// Generates a `DeepPointer` struct based on the offsets + /// recovered from this `UnityPointer`. pub fn get_deep_pointer( &self, process: &Process, @@ -656,7 +779,12 @@ impl UnityPointer { image: &Image, ) -> Option> { self.find_offsets(process, module, image).ok()?; - self.deep_pointer.get().cloned() + let cache = self.cache.borrow(); + Some(DeepPointer::::new( + cache.base_address, + module.pointer_size, + &cache.offsets[..self.depth], + )) } } @@ -669,6 +797,7 @@ struct Offsets { monoclassdef_next_class_cache: u16, monoclassdef_klass: u8, monoclass_name: u8, + monoclass_name_space: u8, monoclass_fields: u8, monoclassdef_field_count: u16, monoclass_runtime_info: u8, @@ -682,10 +811,10 @@ struct Offsets { } impl Offsets { - const fn new(version: Version, is_64_bit: bool) -> &'static Self { - match is_64_bit { - true => match version { - Version::V1 => &Self { + const fn new(version: Version, pointer_size: PointerSize) -> Option<&'static Self> { + match pointer_size { + PointerSize::Bit64 => match version { + Version::V1 => Some(&Self { monoassembly_aname: 0x10, monoassembly_image: 0x58, monoimage_class_cache: 0x3D0, @@ -694,6 +823,7 @@ impl Offsets { monoclassdef_next_class_cache: 0x100, monoclassdef_klass: 0x0, monoclass_name: 0x48, + monoclass_name_space: 0x50, monoclass_fields: 0xA8, monoclassdef_field_count: 0x94, monoclass_runtime_info: 0xF8, @@ -704,8 +834,8 @@ impl Offsets { monoclassruntimeinfo_domain_vtables: 0x8, monovtable_vtable: 0x48, monoclassfieldalignment: 0x20, - }, - Version::V2 => &Self { + }), + Version::V2 => Some(&Self { monoassembly_aname: 0x10, monoassembly_image: 0x60, monoimage_class_cache: 0x4C0, @@ -714,6 +844,7 @@ impl Offsets { monoclassdef_next_class_cache: 0x108, monoclassdef_klass: 0x0, monoclass_name: 0x48, + monoclass_name_space: 0x50, monoclass_fields: 0x98, monoclassdef_field_count: 0x100, monoclass_runtime_info: 0xD0, @@ -724,8 +855,8 @@ impl Offsets { monoclassruntimeinfo_domain_vtables: 0x8, monovtable_vtable: 0x40, monoclassfieldalignment: 0x20, - }, - Version::V3 => &Self { + }), + Version::V3 => Some(&Self { monoassembly_aname: 0x10, monoassembly_image: 0x60, monoimage_class_cache: 0x4D0, @@ -734,6 +865,7 @@ impl Offsets { monoclassdef_next_class_cache: 0x108, monoclassdef_klass: 0x0, monoclass_name: 0x48, + monoclass_name_space: 0x50, monoclass_fields: 0x98, monoclassdef_field_count: 0x100, monoclass_runtime_info: 0xD0, @@ -744,10 +876,10 @@ impl Offsets { monoclassruntimeinfo_domain_vtables: 0x8, monovtable_vtable: 0x48, monoclassfieldalignment: 0x20, - }, + }), }, - false => match version { - Version::V1 => &Self { + PointerSize::Bit32 => match version { + Version::V1 => Some(&Self { monoassembly_aname: 0x8, monoassembly_image: 0x40, monoimage_class_cache: 0x2A0, @@ -756,6 +888,7 @@ impl Offsets { monoclassdef_next_class_cache: 0xA8, monoclassdef_klass: 0x0, monoclass_name: 0x30, + monoclass_name_space: 0x34, monoclass_fields: 0x74, monoclassdef_field_count: 0x64, monoclass_runtime_info: 0xA4, @@ -766,8 +899,8 @@ impl Offsets { monoclassruntimeinfo_domain_vtables: 0x4, monovtable_vtable: 0x28, monoclassfieldalignment: 0x10, - }, - Version::V2 => &Self { + }), + Version::V2 => Some(&Self { monoassembly_aname: 0x8, monoassembly_image: 0x44, monoimage_class_cache: 0x354, @@ -776,6 +909,7 @@ impl Offsets { monoclassdef_next_class_cache: 0xA8, monoclassdef_klass: 0x0, monoclass_name: 0x2C, + monoclass_name_space: 0x30, monoclass_fields: 0x60, monoclassdef_field_count: 0xA4, monoclass_runtime_info: 0x84, @@ -786,8 +920,8 @@ impl Offsets { monoclassruntimeinfo_domain_vtables: 0x4, monovtable_vtable: 0x28, monoclassfieldalignment: 0x10, - }, - Version::V3 => &Self { + }), + Version::V3 => Some(&Self { monoassembly_aname: 0x8, monoassembly_image: 0x48, monoimage_class_cache: 0x35C, @@ -796,6 +930,7 @@ impl Offsets { monoclassdef_next_class_cache: 0xA0, monoclassdef_klass: 0x0, monoclass_name: 0x2C, + monoclass_name_space: 0x30, monoclass_fields: 0x60, monoclassdef_field_count: 0x9C, monoclass_runtime_info: 0x7C, @@ -806,8 +941,9 @@ impl Offsets { monoclassruntimeinfo_domain_vtables: 0x4, monovtable_vtable: 0x2C, monoclassfieldalignment: 0x10, - }, + }), }, + _ => None, } } } diff --git a/src/game_engine/unity/scene.rs b/src/game_engine/unity/scene.rs index ec99d8d..51c3214 100644 --- a/src/game_engine/unity/scene.rs +++ b/src/game_engine/unity/scene.rs @@ -10,7 +10,7 @@ use core::{array, iter, mem::MaybeUninit}; use crate::{ file_format::pe, future::retry, signature::Signature, string::ArrayCString, Address, Address32, - Address64, Error, Process, + Address64, Error, PointerSize, Process, }; const CSTR: usize = 128; @@ -21,7 +21,7 @@ const CSTR: usize = 128; /// It can be useful to identify splitting conditions or as an alternative to /// the traditional class lookup in games with no useful static references. pub struct SceneManager { - is_64_bit: bool, + pointer_size: PointerSize, is_il2cpp: bool, address: Address, offsets: &'static Offsets, @@ -37,12 +37,16 @@ impl SceneManager { let unity_player = process.get_module_range("UnityPlayer.dll").ok()?; - let is_64_bit = pe::MachineType::read(process, unity_player.0)? == pe::MachineType::X86_64; + let pointer_size = match pe::MachineType::read(process, unity_player.0)? { + pe::MachineType::X86_64 => PointerSize::Bit64, + _ => PointerSize::Bit32, + }; + let is_il2cpp = process.get_module_address("GameAssembly.dll").is_ok(); // There are multiple signatures that can be used, depending on the version of Unity // used in the target game. - let base_address: Address = if is_64_bit { + let base_address: Address = if pointer_size == PointerSize::Bit64 { let addr = SIG_64_BIT.scan_process_range(process, unity_player)? + 7; addr + 0x4 + process.read::(addr).ok()? } else if let Some(addr) = SIG_32_1.scan_process_range(process, unity_player) { @@ -55,25 +59,21 @@ impl SceneManager { return None; }; - let offsets = Offsets::new(is_64_bit); + let offsets = Offsets::new(pointer_size); // Dereferencing one level because this pointer never changes as long as the game is open. // It might not seem a lot, but it helps make things a bit faster when querying for scene stuff. - let address: Address = match is_64_bit { - true => process.read::(base_address).ok()?.into(), - false => process.read::(base_address).ok()?.into(), - }; - - if address.is_null() { - None - } else { - Some(Self { - is_64_bit, - is_il2cpp, - address, - offsets, - }) - } + let address = process + .read_pointer(base_address, pointer_size) + .ok() + .filter(|val| !val.is_null())?; + + Some(Self { + pointer_size, + is_il2cpp, + address, + offsets, + }) } /// Attaches to the scene manager in the given process. @@ -84,25 +84,19 @@ impl SceneManager { retry(|| Self::attach(process)).await } - fn read_pointer(&self, process: &Process, address: Address) -> Result { - Ok(match self.is_64_bit { - true => process.read::(address)?.into(), - false => process.read::(address)?.into(), - }) - } - #[inline] - const fn pointer_size(&self) -> u8 { - match self.is_64_bit { - true => 8, - false => 4, - } + const fn size_of_ptr(&self) -> u64 { + self.pointer_size as u64 } /// Tries to retrieve the current active scene. fn get_current_scene(&self, process: &Process) -> Result { Ok(Scene { - address: self.read_pointer(process, self.address + self.offsets.active_scene)?, + address: process + .read_pointer(self.address + self.offsets.active_scene, self.pointer_size) + .ok() + .filter(|val| !val.is_null()) + .ok_or(Error {})?, }) } @@ -143,26 +137,30 @@ impl SceneManager { &'a self, process: &'a Process, ) -> impl DoubleEndedIterator + 'a { - let (num_scenes, addr): (usize, Address) = if self.is_64_bit { - let [first, _, third] = process - .read::<[u64; 3]>(self.address + self.offsets.scene_count) - .unwrap_or_default(); - (first as usize, Address::new(third)) - } else { - let [first, _, third] = process - .read::<[u32; 3]>(self.address + self.offsets.scene_count) - .unwrap_or_default(); - (first as usize, Address::new(third as _)) + let (num_scenes, addr): (usize, Address) = match self.pointer_size { + PointerSize::Bit64 => { + let [first, _, third] = process + .read::<[u64; 3]>(self.address + self.offsets.scene_count) + .unwrap_or_default(); + (first as usize, Address::new(third)) + } + _ => { + let [first, _, third] = process + .read::<[u32; 3]>(self.address + self.offsets.scene_count) + .unwrap_or_default(); + (first as usize, Address::new(third as _)) + } }; (0..num_scenes).filter_map(move |index| { Some(Scene { - address: self + address: process .read_pointer( - process, - addr + (index as u64).wrapping_mul(self.pointer_size() as _), + addr + (index as u64).wrapping_mul(self.size_of_ptr()), + self.pointer_size, ) - .ok()?, + .ok() + .filter(|val| !val.is_null())?, }) }) } @@ -180,8 +178,11 @@ impl SceneManager { process: &'a Process, scene: &Scene, ) -> impl Iterator + 'a { - let list_first = self - .read_pointer(process, scene.address + self.offsets.root_storage_container) + let list_first = process + .read_pointer( + scene.address + self.offsets.root_storage_container, + self.pointer_size, + ) .unwrap_or_default(); let mut current_list = list_first; @@ -191,12 +192,12 @@ impl SceneManager { if iter_break { None } else { - let [first, _, third]: [Address; 3] = match self.is_64_bit { - true => process + let [first, _, third]: [Address; 3] = match self.pointer_size { + PointerSize::Bit64 => process .read::<[Address64; 3]>(current_list) .ok()? .map(|a| a.into()), - false => process + _ => process .read::<[Address32; 3]>(current_list) .ok()? .map(|a| a.into()), @@ -254,13 +255,15 @@ impl Transform { process: &Process, scene_manager: &SceneManager, ) -> Result, Error> { - let game_object = scene_manager - .read_pointer(process, self.address + scene_manager.offsets.game_object)?; - let name_ptr = scene_manager.read_pointer( - process, - game_object + scene_manager.offsets.game_object_name, - )?; - process.read(name_ptr) + process.read_pointer_path( + self.address, + scene_manager.pointer_size, + &[ + scene_manager.offsets.game_object as u64, + scene_manager.offsets.game_object_name as u64, + 0x0, + ], + ) } /// Iterates over the classes referred to in the current `Transform`. @@ -269,17 +272,23 @@ impl Transform { process: &'a Process, scene_manager: &'a SceneManager, ) -> Result + 'a, Error> { - let game_object = scene_manager - .read_pointer(process, self.address + scene_manager.offsets.game_object)?; + let game_object = process.read_pointer( + self.address + scene_manager.offsets.game_object, + scene_manager.pointer_size, + )?; - let (number_of_components, main_object): (usize, Address) = if scene_manager.is_64_bit { - let array = - process.read::<[Address64; 3]>(game_object + scene_manager.offsets.game_object)?; - (array[2].value() as usize, array[0].into()) - } else { - let array = - process.read::<[Address32; 3]>(game_object + scene_manager.offsets.game_object)?; - (array[2].value() as usize, array[0].into()) + let (number_of_components, main_object): (usize, Address) = match scene_manager.pointer_size + { + PointerSize::Bit64 => { + let array = process + .read::<[Address64; 3]>(game_object + scene_manager.offsets.game_object)?; + (array[2].value() as usize, array[0].into()) + } + _ => { + let array = process + .read::<[Address32; 3]>(game_object + scene_manager.offsets.game_object)?; + (array[2].value() as usize, array[0].into()) + } }; if number_of_components == 0 { @@ -288,34 +297,41 @@ impl Transform { const ARRAY_SIZE: usize = 128; - let components: [Address; ARRAY_SIZE] = if scene_manager.is_64_bit { - let mut buf = [MaybeUninit::<[Address64; 2]>::uninit(); ARRAY_SIZE]; - let slice = - process.read_into_uninit_slice(main_object, &mut buf[..number_of_components])?; - - let mut iter = slice.iter_mut(); - array::from_fn(|_| { - iter.next() - .map(|&mut [_, second]| second.into()) - .unwrap_or_default() - }) - } else { - let mut buf = [MaybeUninit::<[Address32; 2]>::uninit(); ARRAY_SIZE]; - let slice = - process.read_into_uninit_slice(main_object, &mut buf[..number_of_components])?; - - let mut iter = slice.iter_mut(); - array::from_fn(|_| { - iter.next() - .map(|&mut [_, second]| second.into()) - .unwrap_or_default() - }) + let components: [Address; ARRAY_SIZE] = match scene_manager.pointer_size { + PointerSize::Bit64 => { + let mut buf = [MaybeUninit::<[Address64; 2]>::uninit(); ARRAY_SIZE]; + let slice = process + .read_into_uninit_slice(main_object, &mut buf[..number_of_components])?; + + let mut iter = slice.iter_mut(); + array::from_fn(|_| { + iter.next() + .map(|&mut [_, second]| second.into()) + .unwrap_or_default() + }) + } + _ => { + let mut buf = [MaybeUninit::<[Address32; 2]>::uninit(); ARRAY_SIZE]; + let slice = process + .read_into_uninit_slice(main_object, &mut buf[..number_of_components])?; + + let mut iter = slice.iter_mut(); + array::from_fn(|_| { + iter.next() + .map(|&mut [_, second]| second.into()) + .unwrap_or_default() + }) + } }; Ok((1..number_of_components).filter_map(move |m| { - scene_manager - .read_pointer(process, components[m] + scene_manager.offsets.klass) + process + .read_pointer( + components[m] + scene_manager.offsets.klass, + scene_manager.pointer_size, + ) .ok() + .filter(|val| !val.is_null()) })) } @@ -327,42 +343,21 @@ impl Transform { name: &str, ) -> Result { self.classes(process, scene_manager)? - .find(|&c| { - let Ok(vtable) = scene_manager.read_pointer(process, c) else { - return false; + .find(|&addr| { + let val: Result, Error> = match scene_manager.is_il2cpp { + true => process.read_pointer_path( + addr, + scene_manager.pointer_size, + &[0x0, scene_manager.size_of_ptr().wrapping_mul(2), 0x0], + ), + false => process.read_pointer_path( + addr, + scene_manager.pointer_size, + &[0x0, 0x0, scene_manager.offsets.klass_name as u64, 0x0], + ), }; - let name_ptr = { - match scene_manager.is_il2cpp { - true => { - let Ok(name_ptr) = scene_manager.read_pointer( - process, - vtable + 2_u64.wrapping_mul(scene_manager.pointer_size() as _), - ) else { - return false; - }; - - name_ptr - } - false => { - let Ok(vtable) = scene_manager.read_pointer(process, vtable) else { - return false; - }; - - let Ok(name_ptr) = scene_manager - .read_pointer(process, vtable + scene_manager.offsets.klass_name) - else { - return false; - }; - - name_ptr - } - } - }; - - process - .read::>(name_ptr) - .is_ok_and(|class_name| class_name.matches(name)) + val.is_ok_and(|class_name| class_name.matches(name)) }) .ok_or(Error {}) } @@ -373,14 +368,17 @@ impl Transform { process: &'a Process, scene_manager: &'a SceneManager, ) -> Result + 'a, Error> { - let (child_count, child_pointer): (usize, Address) = if scene_manager.is_64_bit { - let [first, _, third] = - process.read::<[u64; 3]>(self.address + scene_manager.offsets.children_pointer)?; - (third as usize, Address::new(first)) - } else { - let [first, _, third] = - process.read::<[u32; 3]>(self.address + scene_manager.offsets.children_pointer)?; - (third as usize, Address::new(first as _)) + let (child_count, child_pointer): (usize, Address) = match scene_manager.pointer_size { + PointerSize::Bit64 => { + let [first, _, third] = process + .read::<[u64; 3]>(self.address + scene_manager.offsets.children_pointer)?; + (third as usize, Address::new(first)) + } + _ => { + let [first, _, third] = process + .read::<[u32; 3]>(self.address + scene_manager.offsets.children_pointer)?; + (third as usize, Address::new(first as _)) + } }; // Define an empty array and fill it later with the addresses of all child classes found for the current Transform. @@ -391,18 +389,23 @@ impl Transform { return Err(Error {}); } - let children: [Address; ARRAY_SIZE] = if scene_manager.is_64_bit { - let mut buf = [MaybeUninit::::uninit(); ARRAY_SIZE]; - let slice = process.read_into_uninit_slice(child_pointer, &mut buf[..child_count])?; + let children: [Address; ARRAY_SIZE] = match scene_manager.pointer_size { + PointerSize::Bit64 => { + let mut buf = [MaybeUninit::::uninit(); ARRAY_SIZE]; + let slice = + process.read_into_uninit_slice(child_pointer, &mut buf[..child_count])?; - let mut iter = slice.iter_mut(); - array::from_fn(|_| iter.next().copied().map(Into::into).unwrap_or_default()) - } else { - let mut buf = [MaybeUninit::::uninit(); ARRAY_SIZE]; - let slice = process.read_into_uninit_slice(child_pointer, &mut buf[..child_count])?; + let mut iter = slice.iter_mut(); + array::from_fn(|_| iter.next().copied().map(Into::into).unwrap_or_default()) + } + _ => { + let mut buf = [MaybeUninit::::uninit(); ARRAY_SIZE]; + let slice = + process.read_into_uninit_slice(child_pointer, &mut buf[..child_count])?; - let mut iter = slice.iter_mut(); - array::from_fn(|_| iter.next().copied().map(Into::into).unwrap_or_default()) + let mut iter = slice.iter_mut(); + array::from_fn(|_| iter.next().copied().map(Into::into).unwrap_or_default()) + } }; Ok((0..child_count).map(move |f| Self { @@ -441,9 +444,9 @@ struct Offsets { } impl Offsets { - pub const fn new(is_64_bit: bool) -> &'static Self { - match is_64_bit { - true => &Self { + pub const fn new(pointer_size: PointerSize) -> &'static Self { + match pointer_size { + PointerSize::Bit64 => &Self { scene_count: 0x18, active_scene: 0x48, dont_destroy_on_load_scene: 0x70, @@ -456,7 +459,7 @@ impl Offsets { klass_name: 0x48, children_pointer: 0x70, }, - false => &Self { + _ => &Self { scene_count: 0x10, active_scene: 0x28, dont_destroy_on_load_scene: 0x40, @@ -502,9 +505,11 @@ impl Scene { process: &Process, scene_manager: &SceneManager, ) -> Result, Error> { - let addr = - scene_manager.read_pointer(process, self.address + scene_manager.offsets.asset_path)?; - process.read(addr) + process.read_pointer_path( + self.address, + scene_manager.pointer_size, + &[scene_manager.offsets.asset_path as u64, 0x0], + ) } } diff --git a/src/primitives/mod.rs b/src/primitives/mod.rs index 6b57526..3b0879f 100644 --- a/src/primitives/mod.rs +++ b/src/primitives/mod.rs @@ -2,3 +2,16 @@ mod address; mod endian; pub use self::{address::*, endian::*}; + +/// Pointer size represents the width (in bytes) of memory addresses used +/// in a certain process. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[repr(u8)] +pub enum PointerSize { + /// A 16-bit (2 byte wide) pointer size + Bit16 = 0x2, + /// A 32-bit (4 byte wide) pointer size + Bit32 = 0x4, + /// A 64-bit (8 byte wide) pointer size + Bit64 = 0x8, +} diff --git a/src/runtime/process.rs b/src/runtime/process.rs index 976d2e4..ccbfb31 100644 --- a/src/runtime/process.rs +++ b/src/runtime/process.rs @@ -4,7 +4,7 @@ use core::{ slice, }; -use crate::{Address, Address32, Address64}; +use crate::{Address, Address16, Address32, Address64, PointerSize}; use super::{sys, Error, MemoryRange}; @@ -414,36 +414,31 @@ impl Process { Ok(buf) } - /// Follows a path of pointers from the address given and reads a value of - /// the type specified from the process at the end of the pointer path. This - /// method is specifically for dealing with processes that use 64-bit - /// pointers. - pub fn read_pointer_path64( + /// Reads a pointer address from the process at the address given. + pub fn read_pointer( &self, address: impl Into
, - path: &[u64], - ) -> Result { - let mut address = address.into(); - let (&last, path) = path.split_last().ok_or(Error {})?; - for &offset in path { - address = self.read::(address + offset)?.into(); - } - self.read(address + last) + pointer_size: PointerSize, + ) -> Result { + Ok(match pointer_size { + PointerSize::Bit16 => self.read::(address)?.into(), + PointerSize::Bit32 => self.read::(address)?.into(), + PointerSize::Bit64 => self.read::(address)?.into(), + }) } /// Follows a path of pointers from the address given and reads a value of - /// the type specified from the process at the end of the pointer path. This - /// method is specifically for dealing with processes that use 32-bit - /// pointers. - pub fn read_pointer_path32( + /// the type specified from the process at the end of the pointer path. + pub fn read_pointer_path( &self, address: impl Into
, - path: &[u32], + pointer_size: PointerSize, + path: &[u64], ) -> Result { let mut address = address.into(); let (&last, path) = path.split_last().ok_or(Error {})?; for &offset in path { - address = self.read::(address + offset)?.into(); + address = self.read_pointer(address + offset, pointer_size)?; } self.read(address + last) } diff --git a/src/runtime/settings/gui.rs b/src/runtime/settings/gui.rs index faaecb6..469560c 100644 --- a/src/runtime/settings/gui.rs +++ b/src/runtime/settings/gui.rs @@ -55,17 +55,17 @@ pub fn add_title(key: &str, description: &str, heading_level: u32) { /// of settings. The description is what's shown to the user. The key of the /// default option to show needs to be specified. #[inline] -pub fn add_choice(key: &str, description: &str, default_item_key: &str) { +pub fn add_choice(key: &str, description: &str, default_option_key: &str) { // SAFETY: We provide valid pointers and lengths to key, description and - // default_item_key. They are also guaranteed to be valid UTF-8 strings. + // default_option_key. They are also guaranteed to be valid UTF-8 strings. unsafe { sys::user_settings_add_choice( key.as_ptr(), key.len(), description.as_ptr(), description.len(), - default_item_key.as_ptr(), - default_item_key.len(), + default_option_key.as_ptr(), + default_option_key.len(), ) } }