Skip to content

Some Mac support for Unity/Mono #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions src/file_format/macho.rs
Original file line number Diff line number Diff line change
@@ -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<PointerSize> {
match process.read::<u32>(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<Address> {
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::<u32>(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<const CAP: usize>(
&self,
process: &Process,
) -> Result<ArrayCString<CAP>, 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<impl FusedIterator<Item = Symbol> + '_> {
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<u64, u64> = 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<u64, u64>, fileoff: u64) -> u64 {
map.iter()
.filter(|(&k, _)| k <= fileoff)
.max_by_key(|(&k, _)| k)
.map(|(&k, &v)| v + fileoff - k)
.unwrap_or(fileoff)
}
1 change: 1 addition & 0 deletions src/file_format/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Support for parsing various file formats.

pub mod elf;
pub mod macho;
pub mod pe;
102 changes: 102 additions & 0 deletions src/game_engine/unity/mono.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)))?;
Expand All @@ -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)?;
Expand All @@ -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) {
Expand All @@ -97,6 +115,14 @@ impl Module {
+ 3;
scan_address + 0x4 + process.read::<i32>(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::<i32>(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");
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -1083,6 +1180,7 @@ pub enum Version {
fn detect_version(process: &Process) -> Option<Version> {
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
Expand Down Expand Up @@ -1110,6 +1208,8 @@ fn detect_version(process: &Process) -> Option<Version> {
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 {
Expand All @@ -1119,6 +1219,8 @@ fn detect_version(process: &Process) -> Option<Version> {
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");
Expand Down
Loading
Loading