diff --git a/Cargo.toml b/Cargo.toml index bfd6be9..a3cb50d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ libm = { version = "0.2.7", optional = true } wasi = { version = "0.11.0+wasi-snapshot-preview1", default-features = false } [features] +std = ["alloc"] alloc = [] derive = ["asr-derive"] flags = ["bitflags"] diff --git a/src/file_format/macho.rs b/src/file_format/macho.rs new file mode 100644 index 0000000..0f6a7c0 --- /dev/null +++ b/src/file_format/macho.rs @@ -0,0 +1,123 @@ +//! Support for parsing MachO files + +use crate::{Address, PointerSize, Process}; + +use core::mem; + +// Magic mach-o header constants from: +// https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html +const MH_MAGIC_32: u32 = 0xfeedface; +const MH_CIGAM_32: u32 = 0xcefaedfe; +const MH_MAGIC_64: u32 = 0xfeedfacf; +const MH_CIGAM_64: u32 = 0xcffaedfe; + +struct MachOFormatOffsets { + number_of_commands: usize, + load_commands: usize, + command_size: usize, + symbol_table_offset: usize, + number_of_symbols: usize, + string_table_offset: usize, + nlist_value: usize, + size_of_nlist_item: usize, +} + +impl MachOFormatOffsets { + const fn new() -> Self { + // offsets taken from: + // - https://github.com/hackf5/unityspy/blob/master/src/HackF5.UnitySpy/Offsets/MachOFormatOffsets.cs + // - https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html + MachOFormatOffsets { + number_of_commands: 0x10, + load_commands: 0x20, + command_size: 0x04, + symbol_table_offset: 0x08, + number_of_symbols: 0x0c, + string_table_offset: 0x10, + nlist_value: 0x08, + size_of_nlist_item: 0x10, + } + } +} + +/// Scans the range for a page that begins with MachO Magic +pub fn scan_macho_page(process: &Process, range: (Address, u64)) -> Option
{ + const PAGE_SIZE: u64 = 0x1000; + let (addr, len) = range; + // negation mod PAGE_SIZE + let distance_to_page = (PAGE_SIZE - (addr.value() % PAGE_SIZE)) % PAGE_SIZE; + // round up to the next multiple of PAGE_SIZE + let first_page = addr + distance_to_page; + for i in 0..((len - distance_to_page) / PAGE_SIZE) { + let a = first_page + (i * PAGE_SIZE); + match process.read::(a) { + Ok(MH_MAGIC_64 | MH_CIGAM_64 | MH_MAGIC_32 | MH_CIGAM_32) => { + return Some(a); + } + _ => () + } + } + None +} + +/// Determines whether a MachO header at the address is 64-bit or 32-bit +pub fn pointer_size(process: &Process, address: Address) -> Option { + let magic: u32 = process.read(address).ok()?; + match magic { + MH_MAGIC_64 | MH_CIGAM_64 => Some(PointerSize::Bit64), + MH_MAGIC_32 | MH_CIGAM_32 => Some(PointerSize::Bit32), + _ => None + } +} + +/// Finds the address of a function from a MachO module range and file contents. +pub fn get_function_address(process: &Process, range: (Address, u64), macho_bytes: &[u8], function_name: &[u8]) -> Option
{ + let function_offset: u32 = get_function_offset(&macho_bytes, function_name)?; + let function_address = scan_macho_page(process, range)? + function_offset; + let actual: [u8; 0x100] = process.read(function_address).ok()?; + let expected: [u8; 0x100] = slice_read(&macho_bytes, function_offset as usize).ok()?; + if actual != expected { return None; } + Some(function_address) +} + +/// Finds the offset of a function in the bytes of a MachO file. +pub fn get_function_offset(macho_bytes: &[u8], function_name: &[u8]) -> Option { + let macho_offsets = MachOFormatOffsets::new(); + let number_of_commands: u32 = slice_read(macho_bytes, macho_offsets.number_of_commands).ok()?; + let function_name_len = function_name.len(); + + let mut offset_to_next_command: usize = macho_offsets.load_commands as usize; + for _i in 0..number_of_commands { + // Check if load command is LC_SYMTAB + let next_command: i32 = slice_read(macho_bytes, offset_to_next_command).ok()?; + if next_command == 2 { + let symbol_table_offset: u32 = slice_read(macho_bytes, offset_to_next_command + macho_offsets.symbol_table_offset).ok()?; + let number_of_symbols: u32 = slice_read(macho_bytes, offset_to_next_command + macho_offsets.number_of_symbols).ok()?; + let string_table_offset: u32 = slice_read(macho_bytes, offset_to_next_command + macho_offsets.string_table_offset).ok()?; + + for j in 0..(number_of_symbols as usize) { + let symbol_name_offset: u32 = slice_read(macho_bytes, symbol_table_offset as usize + (j * macho_offsets.size_of_nlist_item)).ok()?; + let string_offset = string_table_offset as usize + symbol_name_offset as usize; + let symbol_name: &[u8] = &macho_bytes[string_offset..(string_offset + function_name_len + 1)]; + + if symbol_name[function_name_len] == 0 && symbol_name.starts_with(function_name) { + return Some(slice_read(macho_bytes, symbol_table_offset as usize + (j * macho_offsets.size_of_nlist_item) + macho_offsets.nlist_value).ok()?); + } + } + + break; + } else { + let command_size: u32 = slice_read(macho_bytes, offset_to_next_command + macho_offsets.command_size).ok()?; + offset_to_next_command += command_size as usize; + } + } + None +} + +/// Reads a value of the type specified from the slice at the address +/// given. +pub fn slice_read(slice: &[u8], address: usize) -> Result { + let size = mem::size_of::(); + let slice_src = &slice[address..(address + size)]; + bytemuck::checked::try_from_bytes(slice_src).cloned() +} diff --git a/src/file_format/mod.rs b/src/file_format/mod.rs index 14b8a83..f26597d 100644 --- a/src/file_format/mod.rs +++ b/src/file_format/mod.rs @@ -2,3 +2,4 @@ pub mod elf; pub mod pe; +pub mod macho; diff --git a/src/game_engine/unity/mono.rs b/src/game_engine/unity/mono.rs index 3e63dc8..e7d0309 100644 --- a/src/game_engine/unity/mono.rs +++ b/src/game_engine/unity/mono.rs @@ -5,11 +5,19 @@ use crate::{ deep_pointer::DeepPointer, file_format::pe, future::retry, signature::Signature, string::ArrayCString, Address, Address32, Address64, Error, PointerSize, Process, }; +#[cfg(feature = "std")] +use crate::file_format::macho; use core::{array, cell::RefCell, iter}; +#[cfg(all(debug_assertions, feature = "alloc"))] +use alloc::collections::BTreeSet; +#[cfg(feature = "std")] +use alloc::vec::Vec; #[cfg(feature = "derive")] pub use asr_derive::MonoClass as Class; use bytemuck::CheckedBitPattern; +#[cfg(feature = "std")] +use std::{path::Path, fs::File, io, io::Read}; const CSTR: usize = 128; @@ -36,39 +44,72 @@ 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 module = ["mono.dll", "mono-2.0-bdwgc.dll"] - .iter() - .find_map(|&name| process.get_module_address(name).ok())?; - - let pointer_size = match pe::MachineType::read(process, module)? { - pe::MachineType::X86_64 => PointerSize::Bit64, - _ => PointerSize::Bit32, + #[allow(unused)] + let (module_name, module_range, format) = [ + ("mono.dll", BinaryFormat::PE), + ("mono-2.0-bdwgc.dll", BinaryFormat::PE), + #[cfg(feature = "std")] + ("libmono.0.dylib", BinaryFormat::MachO), + #[cfg(feature = "std")] + ("libmonobdwgc-2.0.dylib", BinaryFormat::MachO) + ].into_iter() + .find_map(|(name, format)| Some((name, process.get_module_range(name).ok()?, format)))?; + + let module = module_range.0; + + let pointer_size = match format { + BinaryFormat::PE => { + match pe::MachineType::read(process, module)? { + pe::MachineType::X86_64 => PointerSize::Bit64, + _ => PointerSize::Bit32, + } + } + #[cfg(feature = "std")] + BinaryFormat::MachO => macho::pointer_size(process, macho::scan_macho_page(process, module_range)?)?, + }; + let offsets = Offsets::new(version, pointer_size, format)?; + + let mono_assembly_foreach_address = match format { + BinaryFormat::PE => { + pe::symbols(process, module) + .find(|symbol| { + symbol + .get_name::<25>(process) + .is_ok_and(|name| name.matches("mono_assembly_foreach")) + })? + .address + }, + #[cfg(feature = "std")] + BinaryFormat::MachO => { + let mono_module_path = process.get_module_path(module_name).ok()?; + let mono_module_bytes = file_read_all_bytes(mono_module_path).ok()?; + macho::get_function_address(process, module_range, &mono_module_bytes, b"_mono_assembly_foreach")? + } }; - let offsets = Offsets::new(version, pointer_size)?; - - 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_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))? + let assemblies_pointer: Address = match (pointer_size, format) { + (PointerSize::Bit64, BinaryFormat::PE) => { + const SIG_MONO_64_PE: Signature<3> = Signature::new("48 8B 0D"); + let scan_address: Address = SIG_MONO_64_PE + .scan_process_range(process, (mono_assembly_foreach_address, 0x100))? + 3; scan_address + 0x4 + process.read::(scan_address).ok()? - } - PointerSize::Bit32 => { + }, + #[cfg(feature = "std")] + (PointerSize::Bit64, BinaryFormat::MachO) => { + const SIG_MONO_64_MACHO: Signature<3> = Signature::new("48 8B 3D"); + // RIP-relative addressing + // 3 is the offset to the next thing after the signature + let scan_address = SIG_MONO_64_MACHO.scan_process_range(process, (mono_assembly_foreach_address, 0x100))? + 3; + // 4 is the offset to the next instruction after relative + scan_address + 0x4 + process.read::(scan_address).ok()? + }, + (PointerSize::Bit32, BinaryFormat::PE) => { const SIG_32_1: Signature<2> = Signature::new("FF 35"); const SIG_32_2: Signature<2> = Signature::new("8B 0D"); let ptr = [SIG_32_1, SIG_32_2].iter().find_map(|sig| { - sig.scan_process_range(process, (root_domain_function_address, 0x100)) + sig.scan_process_range(process, (mono_assembly_foreach_address, 0x100)) })? + 2; process.read::(ptr).ok()?.into() @@ -266,6 +307,8 @@ impl Image { }; (0..class_cache_size.unwrap_or_default()).flat_map(move |i| { + #[cfg(all(debug_assertions, feature = "alloc"))] + let mut seen = BTreeSet::new(); let mut table = match table_addr { Ok(table_addr) => process .read_pointer( @@ -277,6 +320,8 @@ impl Image { }; iter::from_fn(move || { + #[cfg(all(debug_assertions, feature = "alloc"))] + if seen.replace(table?).is_some() { panic!("Image classes cycle detected"); } let class = process.read_pointer(table?, module.pointer_size).ok()?; table = process @@ -788,6 +833,13 @@ impl UnityPointer { } } +#[derive(Copy, Clone, PartialEq, Hash, Debug)] +enum BinaryFormat { + PE, + #[cfg(feature = "std")] + MachO, +} + struct Offsets { monoassembly_aname: u8, monoassembly_image: u8, @@ -811,9 +863,9 @@ struct Offsets { } impl Offsets { - const fn new(version: Version, pointer_size: PointerSize) -> Option<&'static Self> { - match pointer_size { - PointerSize::Bit64 => match version { + const fn new(version: Version, pointer_size: PointerSize, format: BinaryFormat) -> Option<&'static Self> { + match (pointer_size, format) { + (PointerSize::Bit64, BinaryFormat::PE) => match version { Version::V1 => Some(&Self { monoassembly_aname: 0x10, monoassembly_image: 0x58, @@ -835,25 +887,27 @@ impl Offsets { monovtable_vtable: 0x48, monoclassfieldalignment: 0x20, }), + // 64-bit PE V2 matches Unity2019_4_2020_3_x64_PE_Offsets from + // https://github.com/hackf5/unityspy/blob/master/src/HackF5.UnitySpy/Offsets/MonoLibraryOffsets.cs#L49 Version::V2 => Some(&Self { monoassembly_aname: 0x10, - monoassembly_image: 0x60, - monoimage_class_cache: 0x4C0, - monointernalhashtable_table: 0x20, - monointernalhashtable_size: 0x18, - monoclassdef_next_class_cache: 0x108, + monoassembly_image: 0x60, // AssemblyImage = 0x44 + 0x1c + monoimage_class_cache: 0x4C0, // ImageClassCache = 0x354 + 0x16c + monointernalhashtable_table: 0x20, // HashTableTable = 0x14 + 0xc + monointernalhashtable_size: 0x18, // HashTableSize = 0xc + 0xc + monoclassdef_next_class_cache: 0x108, // TypeDefinitionNextClassCache = 0xa8 + 0x34 + 0x10 + 0x18 + 0x4 monoclassdef_klass: 0x0, - monoclass_name: 0x48, - monoclass_name_space: 0x50, - monoclass_fields: 0x98, - monoclassdef_field_count: 0x100, - monoclass_runtime_info: 0xD0, - monoclass_vtable_size: 0x5C, - monoclass_parent: 0x30, + monoclass_name: 0x48, // TypeDefinitionName = 0x2c + 0x1c + monoclass_name_space: 0x50, // TypeDefinitionNamespace = 0x30 + 0x20 + monoclass_fields: 0x98, // TypeDefinitionFields = 0x60 + 0x20 + 0x18 + monoclassdef_field_count: 0x100, // TypeDefinitionFieldCount = 0xa4 + 0x34 + 0x10 + 0x18 + monoclass_runtime_info: 0xD0, // TypeDefinitionRuntimeInfo = 0x84 + 0x34 + 0x18 + monoclass_vtable_size: 0x5C, // TypeDefinitionVTableSize = 0x38 + 0x24 + monoclass_parent: 0x30, // TypeDefinitionParent = 0x20 + 0x10 monoclassfield_name: 0x8, monoclassfield_offset: 0x18, - monoclassruntimeinfo_domain_vtables: 0x8, - monovtable_vtable: 0x40, + monoclassruntimeinfo_domain_vtables: 0x8, // TypeDefinitionRuntimeInfoDomainVTables = 0x4 + 0x4 + monovtable_vtable: 0x40, // VTable = 0x28 + 0x18 monoclassfieldalignment: 0x20, }), Version::V3 => Some(&Self { @@ -878,7 +932,7 @@ impl Offsets { monoclassfieldalignment: 0x20, }), }, - PointerSize::Bit32 => match version { + (PointerSize::Bit32, BinaryFormat::PE) => match version { Version::V1 => Some(&Self { monoassembly_aname: 0x8, monoassembly_image: 0x40, @@ -899,26 +953,28 @@ impl Offsets { monoclassruntimeinfo_domain_vtables: 0x4, monovtable_vtable: 0x28, monoclassfieldalignment: 0x10, + // 32-bit PE V2 matches Unity2018_4_10_x86_PE_Offsets from + // https://github.com/hackf5/unityspy/blob/master/src/HackF5.UnitySpy/Offsets/MonoLibraryOffsets.cs#L12 }), Version::V2 => Some(&Self { monoassembly_aname: 0x8, - monoassembly_image: 0x44, - monoimage_class_cache: 0x354, - monointernalhashtable_table: 0x14, - monointernalhashtable_size: 0xC, - monoclassdef_next_class_cache: 0xA8, + monoassembly_image: 0x44, // AssemblyImage + monoimage_class_cache: 0x354, // ImageClassCache + monointernalhashtable_table: 0x14, // HashTableTable + monointernalhashtable_size: 0xC, // HashTableSize + monoclassdef_next_class_cache: 0xA8, // TypeDefinitionNextClassCache monoclassdef_klass: 0x0, - monoclass_name: 0x2C, - monoclass_name_space: 0x30, - monoclass_fields: 0x60, - monoclassdef_field_count: 0xA4, - monoclass_runtime_info: 0x84, - monoclass_vtable_size: 0x38, - monoclass_parent: 0x20, + monoclass_name: 0x2C, // TypeDefinitionName + monoclass_name_space: 0x30, // TypeDefinitionNamespace + monoclass_fields: 0x60, // TypeDefinitionFields + monoclassdef_field_count: 0xA4, // TypeDefinitionFieldCount + monoclass_runtime_info: 0x84, // TypeDefinitionRuntimeInfo + monoclass_vtable_size: 0x38, // TypeDefinitionVTableSize + monoclass_parent: 0x20, // TypeDefinitionParent monoclassfield_name: 0x4, monoclassfield_offset: 0xC, - monoclassruntimeinfo_domain_vtables: 0x4, - monovtable_vtable: 0x28, + monoclassruntimeinfo_domain_vtables: 0x4, // TypeDefinitionRuntimeInfoDomainVTables + monovtable_vtable: 0x28, // VTable monoclassfieldalignment: 0x10, }), Version::V3 => Some(&Self { @@ -943,6 +999,54 @@ impl Offsets { monoclassfieldalignment: 0x10, }), }, + #[cfg(feature = "std")] + (PointerSize::Bit64, BinaryFormat::MachO) => match version { + Version::V1 => Some(&Self { + monoassembly_aname: 0x10, + monoassembly_image: 0x58, // matches 64-bit PE V1 + monoimage_class_cache: 0x3D0, // matches 64-bit PE V1 + monointernalhashtable_table: 0x20, + monointernalhashtable_size: 0x18, + monoclassdef_next_class_cache: 0xF8, // 0x8 less than 64-bit PE V1 + monoclassdef_klass: 0x0, + monoclass_name: 0x40, // 0x8 less than 64-bit PE V1 + monoclass_name_space: 0x48, // 0x8 less than 64-bit PE V1 + monoclass_fields: 0xA0, // 0x8 less than 64-bit PE V1 + monoclassdef_field_count: 0x8C, // 0x8 less than 64-bit PE V1 + monoclass_runtime_info: 0xF0, // 0x8 less than 64-bit PE V1 + monoclass_vtable_size: 0x18, // MonoVtable.data + monoclass_parent: 0x28, // 0x8 less than 64-bit PE V1 + monoclassfield_name: 0x8, + monoclassfield_offset: 0x18, + monoclassruntimeinfo_domain_vtables: 0x8, + monovtable_vtable: 0x0, // UNUSED for V1 + monoclassfieldalignment: 0x20, + }), + // 64-bit MachO V2 matches Unity2019_4_2020_3_x64_MachO_Offsets from + // https://github.com/hackf5/unityspy/blob/master/src/HackF5.UnitySpy/Offsets/MonoLibraryOffsets.cs#L86 + Version::V2 => Some(&Self { + monoassembly_aname: 0x10, + monoassembly_image: 0x60, // AssemblyImage = 0x44 + 0x1c + monoimage_class_cache: 0x4C0, // ImageClassCache = 0x354 + 0x16c + monointernalhashtable_table: 0x20, // HashTableTable = 0x14 + 0xc + monointernalhashtable_size: 0x18, // HashTableSize = 0xc + 0xc + monoclassdef_next_class_cache: 0x100, // TypeDefinitionNextClassCache = 0xa8 + 0x34 + 0x10 + 0x18 + 0x4 - 0x8 + monoclassdef_klass: 0x0, + monoclass_name: 0x40, // TypeDefinitionName = 0x2c + 0x1c - 0x8 + monoclass_name_space: 0x48, // TypeDefinitionNamespace = 0x30 + 0x20 - 0x8 + monoclass_fields: 0x90, // TypeDefinitionFields = 0x60 + 0x20 + 0x18 - 0x8 + monoclassdef_field_count: 0xF8, // TypeDefinitionFieldCount = 0xa4 + 0x34 + 0x10 + 0x18 - 0x8 + monoclass_runtime_info: 0xC8, // TypeDefinitionRuntimeInfo = 0x84 + 0x34 + 0x18 - 0x8 + monoclass_vtable_size: 0x54, // TypeDefinitionVTableSize = 0x38 + 0x24 - 0x8 + monoclass_parent: 0x28, // TypeDefinitionParent = 0x20 + 0x10 - 0x8 + monoclassfield_name: 0x8, + monoclassfield_offset: 0x18, + monoclassruntimeinfo_domain_vtables: 0x8, // TypeDefinitionRuntimeInfoDomainVTables = 0x4 + 0x4 + monovtable_vtable: 0x40, // VTable = 0x28 + 0x18 + monoclassfieldalignment: 0x20, + }), + Version::V3 => None, + }, _ => None, } } @@ -964,41 +1068,40 @@ fn detect_version(process: &Process) -> Option { if process.get_module_address("mono.dll").is_ok() { return Some(Version::V1); } + if process.get_module_address("libmono.0.dylib").is_ok() { + return Some(Version::V1); + } - let unity_module = { - let address = process.get_module_address("UnityPlayer.dll").ok()?; - let range = pe::read_size_of_image(process, address)? as u64; - (address, range) - }; + let unity_module = [ + ("UnityPlayer.dll", BinaryFormat::PE), + #[cfg(feature = "std")] + ("UnityPlayer.dylib", BinaryFormat::MachO) + ].into_iter().find_map(|(name, format)| { + match format { + BinaryFormat::PE => { + let address = process.get_module_address(name).ok()?; + let range = pe::read_size_of_image(process, address)? as u64; + Some((address, range)) + }, + #[cfg(feature = "std")] + BinaryFormat::MachO => process.get_module_range(name).ok() + } + })?; + // null "202" wildcard "." const SIG_202X: Signature<6> = Signature::new("00 32 30 32 ?? 2E"); let Some(addr) = SIG_202X.scan_process_range(process, unity_module) else { return Some(Version::V2); }; - const ZERO: u8 = b'0'; - const NINE: u8 = b'9'; - let version_string = process.read::<[u8; 6]>(addr + 1).ok()?; let (before, after) = version_string.split_at(version_string.iter().position(|&x| x == b'.')?); - let mut unity: u32 = 0; - for &val in before { - match val { - ZERO..=NINE => unity = unity * 10 + (val - ZERO) as u32, - _ => break, - } - } + let unity: u32 = ascii_read_u32(before); - let mut unity_minor: u32 = 0; - for &val in &after[1..] { - match val { - ZERO..=NINE => unity_minor = unity_minor * 10 + (val - ZERO) as u32, - _ => break, - } - } + let unity_minor: u32 = ascii_read_u32(&after[1..]); Some(if (unity == 2021 && unity_minor >= 2) || (unity > 2021) { Version::V3 @@ -1006,3 +1109,27 @@ fn detect_version(process: &Process) -> Option { Version::V2 }) } + +fn ascii_read_u32(slice: &[u8]) -> u32 { + const ZERO: u8 = b'0'; + const NINE: u8 = b'9'; + + let mut result: u32 = 0; + for &val in slice { + match val { + ZERO..=NINE => result = result * 10 + (val - ZERO) as u32, + _ => break, + } + } + result +} + +// -------------------------------------------------------- + +#[cfg(feature = "std")] +fn file_read_all_bytes>(path: P) -> io::Result> { + let mut f = File::open(path)?; + let mut buffer: Vec = Vec::new(); + f.read_to_end(&mut buffer)?; + Ok(buffer) +} diff --git a/src/game_engine/unity/scene.rs b/src/game_engine/unity/scene.rs index 51c3214..7474d25 100644 --- a/src/game_engine/unity/scene.rs +++ b/src/game_engine/unity/scene.rs @@ -11,6 +11,7 @@ use core::{array, iter, mem::MaybeUninit}; use crate::{ file_format::pe, future::retry, signature::Signature, string::ArrayCString, Address, Address32, Address64, Error, PointerSize, Process, + file_format::macho, }; const CSTR: usize = 128; @@ -30,33 +31,58 @@ pub struct SceneManager { impl SceneManager { /// Attaches to the scene manager in the given process. pub fn attach(process: &Process) -> Option { - const SIG_64_BIT: Signature<13> = Signature::new("48 83 EC 20 4C 8B ?5 ???????? 33 F6"); + const SIG_64_BIT_PE: Signature<13> = Signature::new("48 83 EC 20 4C 8B ?5 ???????? 33 F6"); + const SIG_64_BIT_MACHO: Signature<13> = Signature::new("41 54 53 50 4C 8B ?5 ???????? 41 83"); const SIG_32_1: Signature<12> = Signature::new("55 8B EC 51 A1 ???????? 53 33 DB"); const SIG_32_2: Signature<6> = Signature::new("53 8D 41 ?? 33 DB"); const SIG_32_3: Signature<14> = Signature::new("55 8B EC 83 EC 18 A1 ???????? 33 C9 53"); - let unity_player = process.get_module_range("UnityPlayer.dll").ok()?; + let (unity_player, format) = [("UnityPlayer.dll", BinaryFormat::PE), ("UnityPlayer.dylib", BinaryFormat::MachO)] + .into_iter() + .find_map(|(name, format)| Some((process.get_module_range(name).ok()?, format)))?; - let pointer_size = match pe::MachineType::read(process, unity_player.0)? { - pe::MachineType::X86_64 => PointerSize::Bit64, - _ => PointerSize::Bit32, + let pointer_size = match format { + BinaryFormat::PE => { + match pe::MachineType::read(process, unity_player.0)? { + pe::MachineType::X86_64 => PointerSize::Bit64, + _ => PointerSize::Bit32, + } + } + BinaryFormat::MachO => macho::pointer_size(process, macho::scan_macho_page(process, unity_player)?)?, }; - 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 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) { - process.read::(addr + 5).ok()?.into() - } else if let Some(addr) = SIG_32_2.scan_process_range(process, unity_player) { - process.read::(addr.add_signed(-4)).ok()?.into() - } else if let Some(addr) = SIG_32_3.scan_process_range(process, unity_player) { - process.read::(addr + 7).ok()?.into() - } else { - return None; + let base_address: Address = match (pointer_size, format) { + (PointerSize::Bit64, BinaryFormat::PE) => { + let addr = SIG_64_BIT_PE.scan_process_range(process, unity_player)? + 7; + addr + 0x4 + process.read::(addr).ok()? + }, + (PointerSize::Bit64, BinaryFormat::MachO) => { + // RIP-relative addressing + // 7 is the offset to the ???????? question marks in the signature + let addr = SIG_64_BIT_MACHO.scan_process_range(process, unity_player)? + 7; + // 4 is the offset to the next instruction after the question marks + addr + 0x4 + process.read::(addr).ok()? + }, + (PointerSize::Bit32, BinaryFormat::PE) => { + if let Some(addr) = SIG_32_1.scan_process_range(process, unity_player) { + process.read::(addr + 5).ok()?.into() + } else if let Some(addr) = SIG_32_2.scan_process_range(process, unity_player) { + process.read::(addr.add_signed(-4)).ok()?.into() + } else if let Some(addr) = SIG_32_3.scan_process_range(process, unity_player) { + process.read::(addr + 7).ok()?.into() + } else { + return None; + } + }, + (PointerSize::Bit32, BinaryFormat::MachO) => { + return None; + }, + (PointerSize::Bit16, _) => { + return None; + }, }; let offsets = Offsets::new(pointer_size); @@ -429,6 +455,12 @@ impl Transform { } } +#[derive(Copy, Clone, PartialEq, Hash, Debug)] +enum BinaryFormat { + PE, + MachO, +} + struct Offsets { scene_count: u8, active_scene: u8, diff --git a/src/lib.rs b/src/lib.rs index d22d79b..8f1d560 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![no_std] +#![cfg_attr(not(feature = "std"), no_std)] #![warn( clippy::complexity, clippy::correctness,