Skip to content
Open
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
33 changes: 33 additions & 0 deletions src/game_engine/unity/il2cpp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,25 @@ impl Module {
Self::attach_with(process, il2cpp_module, format, profile)
}

/// Tries attaching to a Unity game that is using the IL2CPP backend, in
/// the module at `il2cpp_module`, with the provided measured [`Profile`].
/// Use this when the runtime sits in a module [`attach`](Self::attach)
/// doesn't look for, such as a renamed `GameAssembly.dll`.
/// [`Process::get_module_address`] finds a module by its name without
/// allocating. The module must be a PE or an ELF image whose pointer width
/// matches the profile.
pub fn attach_to_module(
process: &Process,
il2cpp_module: Address,
profile: Profile,
) -> Option<Self> {
let (il2cpp_module, format) = super::module_at(process, il2cpp_module)?;
let pointer_size = Self::pointer_size(process, il2cpp_module, format)?;
(pointer_size == profile.pointer_size).then_some(())?;

Self::attach_with(process, il2cpp_module, format, profile)
}

/// Finds the module the runtime is compiled into, which is
/// `GameAssembly.dll` on Windows and `GameAssembly.so` on Linux.
fn find_runtime_module(process: &Process) -> Option<((Address, u64), BinaryFormat)> {
Expand Down Expand Up @@ -480,6 +499,20 @@ impl Module {
retry(|| Self::attach(process, profile)).await
}

/// Attaches to a Unity game that is using the IL2CPP backend, in the
/// module at `il2cpp_module`, with the provided measured [`Profile`].
///
/// This is the `await`able version of
/// [`attach_to_module`](Self::attach_to_module), yielding back to the
/// runtime between each try.
pub async fn wait_attach_to_module(
process: &Process,
il2cpp_module: Address,
profile: Profile,
) -> Module {
retry(|| Self::attach_to_module(process, il2cpp_module, profile)).await
}

/// Looks for the specified binary [image](Image) inside the target process.
/// An [image](Image) is a .NET DLL that is loaded
/// by the game. The `Assembly-CSharp` [image](Image) is the main game
Expand Down
24 changes: 24 additions & 0 deletions src/game_engine/unity/il2cpp/walk_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,30 @@ fn attach_auto_detect_uses_a_measured_build() {
assert_eq!(module.profile.class.static_fields, 0xA0);
}

// A runtime under a file name `attach` doesn't look for still attaches
// through the module the auto splitter hands in. An address that holds no
// module doesn't attach.
#[test]
fn a_given_module_attaches_under_any_name() {
let image = Player::game_assembly();
with_modules(
&[(GAME_ASSEMBLY, &image)],
&[("GameAssembly_x64.dll", GAME_ASSEMBLY, 0x1000)],
|process| {
let profile = super::profiles::UNITY_6000_5_0F1_X86_64;
assert!(Module::attach(process, profile).is_none());
let module = process.get_module_address("GameAssembly_x64.dll").unwrap();
let attached = Module::attach_to_module(process, module, profile).unwrap();
assert_eq!(attached.profile, profile);
assert!(Module::attach_to_module(process, module + 0x800, profile).is_none());
assert!(matches!(
poll_once(Module::wait_attach_to_module(process, module, profile)),
Poll::Ready(_)
));
},
);
}

#[test]
fn attach_uses_the_explicit_profile_and_checks_its_width() {
let player = Player {
Expand Down
22 changes: 21 additions & 1 deletion src/game_engine/unity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ pub use managed::{DictionaryOffsets, HashSetOffsets, ListOffsets, ManagedString}
pub mod mono;
pub mod scene_manager;

use crate::{signature::Signature, Address, Process};
#[cfg(all(test, not(target_family = "wasm")))]
mod module_tests;

use crate::{
file_format::{elf, pe},
signature::Signature,
Address, Process,
};

const CSTR: usize = 128;

Expand All @@ -102,6 +109,19 @@ enum BinaryFormat {
MachO,
}

/// Reads the format and the size of the module at `address` from its
/// header. A PE image spans the size of image its optional header gives and
/// an ELF image runs to the end of its last load segment. asr has no reader
/// for a Mach-O image's size, so a Mach-O image gives `None` like any header
/// it doesn't know.
fn module_at(process: &Process, address: Address) -> Option<((Address, u64), BinaryFormat)> {
if let Some(size) = pe::read_size_of_image(process, address) {
return Some(((address, size as u64), BinaryFormat::PE));
}
let size = elf::read_size_of_image(process, address)?;
Some(((address, size), BinaryFormat::ELF))
}

/// Finds the Unity version string in a player, `2021.3.11f1` for
/// example, and returns its three numbers. The fourth part of a file
/// version has no equal in the string, so it is 0. A NUL heads the
Expand Down
61 changes: 61 additions & 0 deletions src/game_engine/unity/module_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! Tests reading a module's format and size from its header, for auto
//! splitters that hand in the module themselves.

use super::{module_at, BinaryFormat};
use crate::{runtime::mock::with_process, Address};

use std::vec;

const BASE: u64 = 0x7FF6_1000_0000;

fn put(image: &mut [u8], at: usize, bytes: &[u8]) {
image[at..at + bytes.len()].copy_from_slice(bytes);
}

#[test]
fn pe_module_spans_its_size_of_image() {
// The DOS header points at the COFF header, which is followed by a PE32+
// optional header that says the image is 0x5000 bytes.
let mut image = vec![0; 0x400];
put(&mut image, 0, b"MZ");
put(&mut image, 0x3C, &0x80_u32.to_le_bytes());
put(&mut image, 0x80, b"PE\0\0");
put(&mut image, 0x84, &0x8664_u16.to_le_bytes());
put(&mut image, 0x94, &0xF0_u16.to_le_bytes());
put(&mut image, 0x98, &0x20B_u16.to_le_bytes());
put(&mut image, 0x98 + 0x38, &0x5000_u32.to_le_bytes());
with_process(&[(BASE, &image)], |process| {
assert_eq!(
module_at(process, Address::new(BASE)),
Some(((Address::new(BASE), 0x5000), BinaryFormat::PE))
);
});
}

#[test]
fn elf_module_spans_to_its_last_load_segment() {
// An ELF64 header with one program header, a load segment that runs to
// 0x3000 in memory.
let mut image = vec![0; 0x400];
put(&mut image, 0, b"\x7fELF\x02\x01\x01");
put(&mut image, 0x10, &3_u16.to_le_bytes());
put(&mut image, 0x20, &0x40_u64.to_le_bytes());
put(&mut image, 0x36, &56_u16.to_le_bytes());
put(&mut image, 0x38, &1_u16.to_le_bytes());
put(&mut image, 0x40, &1_u32.to_le_bytes());
put(&mut image, 0x68, &0x3000_u64.to_le_bytes());
with_process(&[(BASE, &image)], |process| {
assert_eq!(
module_at(process, Address::new(BASE)),
Some(((Address::new(BASE), 0x3000), BinaryFormat::ELF))
);
});
}

#[test]
fn unknown_header_gives_no_module() {
let image = vec![0; 0x400];
with_process(&[(BASE, &image)], |process| {
assert_eq!(module_at(process, Address::new(BASE)), None);
});
}
26 changes: 26 additions & 0 deletions src/game_engine/unity/mono/attachment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,29 @@ fn an_unknown_legacy_runtime_uses_the_executables_version() {
},
);
}

// A runtime under a file name `attach` doesn't look for still attaches
// through the module the auto splitter hands in, which doesn't need `alloc`.
// An address that holds no module doesn't attach.
#[test]
fn a_given_module_attaches_under_any_name() {
let runtime = runtime();
with_modules(
&[(RUNTIME, &runtime)],
&[("libmono-game.so", RUNTIME, runtime.len() as u64)],
|process| {
let profile = profiles::UNITY_5_6_0F1_LINUX_MONO_X86_64;
assert!(Module::attach(process, profile).is_none());
let module = process.get_module_address("libmono-game.so").unwrap();
let attached = Module::attach_to_module(process, module, profile).unwrap();
assert_eq!(attached.profile, profile);
assert!(Module::attach_to_module(process, module + 0x1000, profile).is_none());
let Poll::Ready(attached) =
poll_once(Module::wait_attach_to_module(process, module, profile))
else {
panic!("the given module should attach immediately");
};
assert_eq!(attached.profile, profile);
},
);
}
40 changes: 39 additions & 1 deletion src/game_engine/unity/mono/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
//! a separate player module can be identified through its executable. A
//! `no_std` auto splitter using allocation must provide a global allocator. Use
//! [`Module::attach`] or [`Module::wait_attach`] with a known [`Profile`] to
//! attach without allocation on Windows and Linux. macOS support requires
//! attach without allocation on Windows and Linux.
//! [`Module::attach_to_module`] does the same for a runtime the auto splitter
//! finds itself. macOS support requires
//! `alloc` for Mach-O symbol lookup even with an explicit profile.

#[cfg(feature = "alloc")]
Expand Down Expand Up @@ -228,6 +230,28 @@ impl Module {
Self::attach_with(process, module_range, format, profile)
}

/// Tries attaching to a Unity game that is using the standard Mono
/// backend, in the module at `mono_module`, with the provided measured
/// [`Profile`]. Use this when the runtime sits in a module
/// [`attach`](Self::attach) doesn't look for, such as a renamed runtime
/// library. [`Process::get_module_address`] finds a module by its name
/// without allocating. The module must be a PE or an ELF image whose
/// pointer width matches the profile and the profile's library must be
/// the runtime the module holds.
pub fn attach_to_module(
process: &Process,
mono_module: Address,
profile: Profile,
) -> Option<Self> {
let (module_range, format) = super::module_at(process, mono_module)?;
let pointer_size = Self::pointer_size(process, module_range, format)?;
if pointer_size != profile.pointer_size {
return None;
}

Self::attach_with(process, module_range, format, profile)
}

fn find_runtime_module(
process: &Process,
) -> Option<((Address, u64), BinaryFormat, &'static str, Library)> {
Expand Down Expand Up @@ -739,6 +763,20 @@ impl Module {
retry(|| Self::attach(process, profile)).await
}

/// Attaches to a Unity game that is using the standard Mono backend, in
/// the module at `mono_module`, with the provided measured [`Profile`].
///
/// This is the `await`able version of
/// [`attach_to_module`](Self::attach_to_module), yielding back to the
/// runtime between each try.
pub async fn wait_attach_to_module(
process: &Process,
mono_module: Address,
profile: Profile,
) -> Self {
retry(|| Self::attach_to_module(process, mono_module, profile)).await
}

/// Looks for the specified binary [image](Image) inside the target process.
/// An [image](Image) is a .NET DLL that is loaded
/// by the game. The `Assembly-CSharp` [image](Image) is the main game
Expand Down
Loading
Loading