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,