diff --git a/src/file_format/macho.rs b/src/file_format/macho.rs new file mode 100644 index 00000000..4a7c7ee7 --- /dev/null +++ b/src/file_format/macho.rs @@ -0,0 +1,177 @@ +//! Support for parsing Mach-O format + +#[cfg(feature = "alloc")] +use core::iter::FusedIterator; + +#[cfg(feature = "alloc")] +use alloc::collections::BTreeMap; + +#[cfg(feature = "alloc")] +use crate::{string::ArrayCString, Error}; +use crate::{Address, PointerSize, Process}; + +// 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; + +/// Checks if a given Mach-O module is 64-bit or 32-bit +pub fn pointer_size(process: &Process, range: (Address, u64)) -> Option { + match process.read::(scan_macho_page(process, range)?).ok()? { + MH_MAGIC_64 | MH_CIGAM_64 => Some(PointerSize::Bit64), + MH_MAGIC_32 | MH_CIGAM_32 => Some(PointerSize::Bit32), + _ => None, + } +} + +/// Scans the range for a page that begins with Mach-O Magic +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 +} + +// Constants for the cmd field of load commands, the type +// https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html +/// link-edit stab symbol table info +#[cfg(feature = "alloc")] +const LC_SYMTAB: u32 = 0x2; +/// 64-bit segment of this file to be mapped +#[cfg(feature = "alloc")] +const LC_SEGMENT_64: u32 = 0x19; + +#[cfg(feature = "alloc")] +struct MachOFormatOffsets { + number_of_commands: u32, + load_commands: u32, + command_size: u32, + symtab_offset: u32, + number_of_symbols: u32, + strtab_offset: u32, + nlist_value: u32, + size_of_nlist_item: u32, + segcmd64_vmaddr: u32, + segcmd64_fileoff: u32, +} + +#[cfg(feature = "alloc")] +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, + symtab_offset: 0x08, + number_of_symbols: 0x0c, + strtab_offset: 0x10, + nlist_value: 0x08, + size_of_nlist_item: 0x10, + segcmd64_vmaddr: 0x18, + segcmd64_fileoff: 0x28, + } + } +} + +/// A symbol exported into the current module. +#[cfg(feature = "alloc")] +pub struct Symbol { + /// The address associated with the current function + pub address: Address, + /// The address storing the name of the current function + name_addr: Address, +} + +#[cfg(feature = "alloc")] +impl Symbol { + /// Tries to retrieve the name of the current function + pub fn get_name( + &self, + process: &Process, + ) -> Result, Error> { + process.read(self.name_addr) + } +} + +/// Iterates over the exported symbols for a given module. +/// Only 64-bit Mach-O format is supported +#[cfg(feature = "alloc")] +pub fn symbols( + process: &Process, + range: (Address, u64), +) -> Option + '_> { + let page = scan_macho_page(process, range)?; + let offsets = MachOFormatOffsets::new(); + let number_of_commands: u32 = process.read(page + offsets.number_of_commands).ok()?; + + let mut symtab_fileoff: u32 = 0; + let mut number_of_symbols: u32 = 0; + let mut strtab_fileoff: u32 = 0; + let mut map_fileoff_to_vmaddr: BTreeMap = BTreeMap::new(); + + let mut next: u32 = offsets.load_commands; + for _i in 0..number_of_commands { + let cmdtype: u32 = process.read(page + next).ok()?; + if cmdtype == LC_SYMTAB { + symtab_fileoff = process.read(page + next + offsets.symtab_offset).ok()?; + number_of_symbols = process.read(page + next + offsets.number_of_symbols).ok()?; + strtab_fileoff = process.read(page + next + offsets.strtab_offset).ok()?; + } else if cmdtype == LC_SEGMENT_64 { + let vmaddr: u64 = process.read(page + next + offsets.segcmd64_vmaddr).ok()?; + let fileoff: u64 = process.read(page + next + offsets.segcmd64_fileoff).ok()?; + map_fileoff_to_vmaddr.insert(fileoff, vmaddr); + } + let command_size: u32 = process.read(page + next + offsets.command_size).ok()?; + next += command_size; + } + + if symtab_fileoff == 0 || number_of_symbols == 0 || strtab_fileoff == 0 { + return None; + } + + let symtab_vmaddr = fileoff_to_vmaddr(&map_fileoff_to_vmaddr, symtab_fileoff as u64); + let strtab_vmaddr = fileoff_to_vmaddr(&map_fileoff_to_vmaddr, strtab_fileoff as u64); + + Some( + (0..number_of_symbols) + .filter_map(move |j| { + let nlist_item = page + symtab_vmaddr + (j * offsets.size_of_nlist_item); + let symname_offset: u32 = process.read(nlist_item).ok()?; + let string_address = page + strtab_vmaddr + symname_offset; + let symbol_fileoff = process.read(nlist_item + offsets.nlist_value).ok()?; + let symbol_vmaddr = fileoff_to_vmaddr(&map_fileoff_to_vmaddr, symbol_fileoff); + let symbol_address = page + symbol_vmaddr; + Some(Symbol { + address: symbol_address, + name_addr: string_address, + }) + }) + .fuse(), + ) +} + +#[cfg(feature = "alloc")] +fn fileoff_to_vmaddr(map: &BTreeMap, fileoff: u64) -> u64 { + map.iter() + .filter(|(&k, _)| k <= fileoff) + .max_by_key(|(&k, _)| k) + .map(|(&k, &v)| v + fileoff - k) + .unwrap_or(fileoff) +} diff --git a/src/file_format/mod.rs b/src/file_format/mod.rs index 14b8a83d..8ca31222 100644 --- a/src/file_format/mod.rs +++ b/src/file_format/mod.rs @@ -1,4 +1,5 @@ //! Support for parsing various file formats. pub mod elf; +pub mod macho; pub mod pe; diff --git a/src/game_engine/unity/mono.rs b/src/game_engine/unity/mono.rs index fa442f55..87a5a0e4 100644 --- a/src/game_engine/unity/mono.rs +++ b/src/game_engine/unity/mono.rs @@ -1,6 +1,8 @@ //! Support for attaching to Unity games that are using the standard Mono //! backend. +#[cfg(feature = "alloc")] +use crate::file_format::macho; use crate::{ file_format::{elf, pe}, future::retry, @@ -47,8 +49,12 @@ impl Module { let (module_range, format) = [ ("mono.dll", BinaryFormat::PE), ("libmono.so", BinaryFormat::ELF), + #[cfg(feature = "alloc")] + ("libmono.0.dylib", BinaryFormat::MachO), ("mono-2.0-bdwgc.dll", BinaryFormat::PE), ("libmonobdwgc-2.0.so", BinaryFormat::ELF), + #[cfg(feature = "alloc")] + ("libmonobdwgc-2.0.dylib", BinaryFormat::MachO), ] .into_iter() .find_map(|(name, format)| Some((process.get_module_range(name).ok()?, format)))?; @@ -57,6 +63,8 @@ impl Module { let pointer_size = match format { BinaryFormat::PE => pe::MachineType::read(process, module)?.pointer_size()?, BinaryFormat::ELF => elf::pointer_size(process, module)?, + #[cfg(feature = "alloc")] + BinaryFormat::MachO => macho::pointer_size(process, module_range)?, }; let offsets = Offsets::new(version, pointer_size, format)?; @@ -80,6 +88,16 @@ impl Module { })? .address } + #[cfg(feature = "alloc")] + BinaryFormat::MachO => { + macho::symbols(process, module_range)? + .find(|symbol| { + symbol + .get_name::<26>(process) + .is_ok_and(|name| name.matches("_mono_assembly_foreach")) + })? + .address + } }; let assemblies: Address = match (pointer_size, format) { @@ -97,6 +115,14 @@ impl Module { + 3; scan_address + 0x4 + process.read::(scan_address).ok()? } + #[cfg(feature = "alloc")] + (PointerSize::Bit64, BinaryFormat::MachO) => { + const SIG_MONO_64_MACHO: Signature<3> = Signature::new("48 8B 3D"); + let scan_address: Address = SIG_MONO_64_MACHO + .scan_process_range(process, (root_domain_function_address, 0x100))? + + 3; + 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"); @@ -966,6 +992,75 @@ impl Offsets { monoclassfieldalignment: 0x20, }), }, + #[cfg(feature = "alloc")] + (PointerSize::Bit64, BinaryFormat::MachO) => match version { + Version::V1 => Some(&Self { + monoassembly_aname: 0x10, + monoassembly_image: 0x58, + monoimage_class_cache: 0x3D0, + monointernalhashtable_table: 0x20, + monointernalhashtable_size: 0x18, + monoclassdef_next_class_cache: 0xF8, + monoclassdef_klass: 0x0, + monoclass_name: 0x40, + monoclass_name_space: 0x48, + monoclass_fields: 0xA0, + monoclassdef_field_count: 0x8C, + monoclass_runtime_info: 0xF0, + monoclass_vtable_size: 0x18, // MonoVtable.data + monoclass_parent: 0x28, + monoclassfield_name: 0x8, + monoclassfield_offset: 0x18, + monoclassruntimeinfo_domain_vtables: 0x8, + monovtable_vtable: 0x48, + monoclassfieldalignment: 0x20, + }), + Version::V1Cattrs => Some(&Self { + monoassembly_aname: 0x10, + monoassembly_image: 0x58, + monoimage_class_cache: 0x3D0, + monointernalhashtable_table: 0x20, + monointernalhashtable_size: 0x18, + 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, + monoclass_vtable_size: 0x18, // MonoVtable.data + monoclass_parent: 0x28, + monoclassfield_name: 0x8, + monoclassfield_offset: 0x18, + monoclassruntimeinfo_domain_vtables: 0x8, + monovtable_vtable: 0x48, + 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, + monoimage_class_cache: 0x4C0, + monointernalhashtable_table: 0x20, + monointernalhashtable_size: 0x18, + monoclassdef_next_class_cache: 0x100, + monoclassdef_klass: 0x0, + monoclass_name: 0x40, + monoclass_name_space: 0x48, + monoclass_fields: 0x90, + monoclassdef_field_count: 0xF8, + monoclass_runtime_info: 0xC8, + monoclass_vtable_size: 0x54, + monoclass_parent: 0x28, + monoclassfield_name: 0x8, + monoclassfield_offset: 0x18, + monoclassruntimeinfo_domain_vtables: 0x8, + monovtable_vtable: 0x40, + monoclassfieldalignment: 0x20, + }), + _ => None, + }, (PointerSize::Bit32, BinaryFormat::PE) => match version { Version::V1 => Some(&Self { monoassembly_aname: 0x8, @@ -1064,6 +1159,8 @@ impl Offsets { enum BinaryFormat { PE, ELF, + #[cfg(feature = "alloc")] + MachO, } /// The version of Mono that was used for the game. These don't correlate to the @@ -1083,6 +1180,7 @@ pub enum Version { fn detect_version(process: &Process) -> Option { if process.get_module_address("mono.dll").is_ok() || process.get_module_address("libmono.so").is_ok() + || process.get_module_address("libmono.0.dylib").is_ok() { // If the module mono.dll is present, then it's either V1 or V1Cattrs. // In order to distinguish between them, we check the first class listed in the @@ -1110,6 +1208,8 @@ fn detect_version(process: &Process) -> Option { let unity_module = [ ("UnityPlayer.dll", BinaryFormat::PE), ("UnityPlayer.so", BinaryFormat::ELF), + #[cfg(feature = "alloc")] + ("UnityPlayer.dylib", BinaryFormat::MachO), ] .into_iter() .find_map(|(name, format)| match format { @@ -1119,6 +1219,8 @@ fn detect_version(process: &Process) -> Option { Some((address, range)) } BinaryFormat::ELF => process.get_module_range(name).ok(), + #[cfg(feature = "alloc")] + BinaryFormat::MachO => process.get_module_range(name).ok(), })?; const SIG_202X: Signature<6> = Signature::new("00 32 30 32 ?? 2E"); diff --git a/src/game_engine/unity/scene.rs b/src/game_engine/unity/scene.rs index 60e1a69a..58b20425 100644 --- a/src/game_engine/unity/scene.rs +++ b/src/game_engine/unity/scene.rs @@ -13,7 +13,7 @@ use core::{ }; use crate::{ - file_format::{elf, pe}, + file_format::{elf, macho, pe}, future::retry, signature::Signature, string::ArrayCString, @@ -39,6 +39,8 @@ impl SceneManager { pub fn attach(process: &Process) -> Option { const SIG_64_BIT_PE: Signature<13> = Signature::new("48 83 EC 20 4C 8B ?5 ???????? 33 F6"); const SIG_64_BIT_ELF: Signature<13> = Signature::new("41 54 53 50 4C 8B ?5 ???????? 41 83"); + 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"); @@ -46,6 +48,7 @@ impl SceneManager { let (unity_player, format) = [ ("UnityPlayer.dll", BinaryFormat::PE), ("UnityPlayer.so", BinaryFormat::ELF), + ("UnityPlayer.dylib", BinaryFormat::MachO), ] .into_iter() .find_map(|(name, format)| match format { @@ -62,6 +65,7 @@ impl SceneManager { let pointer_size = match format { BinaryFormat::PE => pe::MachineType::read(process, unity_player.0)?.pointer_size()?, BinaryFormat::ELF => elf::pointer_size(process, unity_player.0)?, + BinaryFormat::MachO => macho::pointer_size(process, unity_player)?, }; let is_il2cpp = process.get_module_address("GameAssembly.dll").is_ok(); @@ -77,6 +81,10 @@ impl SceneManager { let addr = SIG_64_BIT_ELF.scan_process_range(process, unity_player)? + 7; addr + 0x4 + process.read::(addr).ok()? } + (PointerSize::Bit64, BinaryFormat::MachO) => { + let addr = SIG_64_BIT_MACHO.scan_process_range(process, unity_player)? + 7; + 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() @@ -467,6 +475,7 @@ impl Transform { enum BinaryFormat { PE, ELF, + MachO, } struct Offsets {