From 0b02b426520953b8a11f212d52f29dc9696876c5 Mon Sep 17 00:00:00 2001 From: ero-qt Date: Tue, 15 Sep 2026 00:28:48 +0200 Subject: [PATCH 1/3] replace version fallback with nearest build --- src/game_engine/unity/il2cpp/builds.rs | 132 +++++++++------------ src/game_engine/unity/il2cpp/mod.rs | 96 ++++----------- src/game_engine/unity/il2cpp/walk_tests.rs | 70 +++-------- 3 files changed, 95 insertions(+), 203 deletions(-) diff --git a/src/game_engine/unity/il2cpp/builds.rs b/src/game_engine/unity/il2cpp/builds.rs index 028cf043..1b7c2163 100644 --- a/src/game_engine/unity/il2cpp/builds.rs +++ b/src/game_engine/unity/il2cpp/builds.rs @@ -1,6 +1,6 @@ -//! Known IL2CPP builds. Each entry is one measured player. The version of -//! its `global-metadata.dat` and the full Unity version say which player. -//! The offsets come from that player's `GameAssembly.pdb`. +//! Known IL2CPP builds. Each entry is one measured player. The full Unity +//! version says which player. The offsets come from that player's +//! `GameAssembly.pdb`. use super::offsets::{ AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, IL2CPPOffsets, ImageOffsets, @@ -11,34 +11,33 @@ use crate::PointerSize; /// One measured IL2CPP player and the offsets from its PDB. pub(super) struct Build { - pub(super) metadata: u32, pub(super) unity: (u16, u16, u16, u16), pub(super) pointer_size: PointerSize, pub(super) version: Version, pub(super) offsets: IL2CPPOffsets, } -/// Finds the build measured on this exact metadata version, Unity version -/// and width. `GameAssembly.dll` is compiled per game, so the Unity version -/// stands in for the binary. The Unity version is the four parts of -/// `UnityPlayer.dll`'s file version, and the last part tells `f1` from `f2` -/// and an alpha from its release. A player nobody measured gets no answer. -pub(super) fn find( - metadata: u32, +/// Finds the build for a player at its width. The Unity version is the four +/// parts of `UnityPlayer.dll`'s file version, and the last part tells `f1` +/// from `f2` and an alpha from its release. A measured player gets its own +/// build. Any other player gets the newest build whose major.minor is at or +/// below its own, since patch releases of one major.minor share their +/// layouts, or the oldest build when nothing is below. The table reads +/// oldest to newest, so the newest match is the last one. +pub(super) fn nearest( unity: (u16, u16, u16, u16), pointer_size: PointerSize, ) -> Option<&'static Build> { - BUILDS.iter().find(|build| { - build.metadata == metadata && build.unity == unity && build.pointer_size == pointer_size - }) -} + let at_width = || { + BUILDS + .iter() + .filter(move |build| build.pointer_size == pointer_size) + }; -/// Whether some build was measured on this Unity version at this width, -/// whatever its metadata says. -pub(super) fn measured(unity: (u16, u16, u16, u16), pointer_size: PointerSize) -> bool { - BUILDS - .iter() - .any(|build| build.unity == unity && build.pointer_size == pointer_size) + at_width() + .find(|build| build.unity == unity) + .or_else(|| at_width().rfind(|build| (build.unity.0, build.unity.1) <= (unity.0, unity.1))) + .or_else(|| at_width().next()) } // The table reads from the oldest player to the newest. @@ -46,7 +45,6 @@ static BUILDS: &[Build] = &[ // Unity 2018.4.36f1, metadata version 24, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2018, 4, 36, 54151), pointer_size: PointerSize::Bit64, version: Version::Base, @@ -88,7 +86,6 @@ static BUILDS: &[Build] = &[ // Unity 2018.4.36f1, metadata version 24, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2018, 4, 36, 54151), pointer_size: PointerSize::Bit32, version: Version::Base, @@ -130,7 +127,6 @@ static BUILDS: &[Build] = &[ // Unity 2019.4.41f2, metadata version 24, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2019, 4, 41, 9172), pointer_size: PointerSize::Bit64, version: Version::V2019, @@ -172,7 +168,6 @@ static BUILDS: &[Build] = &[ // Unity 2019.4.41f2, metadata version 24, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2019, 4, 41, 9172), pointer_size: PointerSize::Bit32, version: Version::V2019, @@ -214,7 +209,6 @@ static BUILDS: &[Build] = &[ // Unity 2020.1.18f1, metadata version 24, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2020, 1, 18, 38512), pointer_size: PointerSize::Bit64, version: Version::V2019, @@ -256,7 +250,6 @@ static BUILDS: &[Build] = &[ // Unity 2020.1.18f1, metadata version 24, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2020, 1, 18, 38512), pointer_size: PointerSize::Bit32, version: Version::V2019, @@ -298,7 +291,6 @@ static BUILDS: &[Build] = &[ // Unity 2021.3.11f1, metadata version 29, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2021, 3, 11, 23713), pointer_size: PointerSize::Bit64, version: Version::V2020, @@ -340,7 +332,6 @@ static BUILDS: &[Build] = &[ // Unity 2021.3.11f1, metadata version 29, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2021, 3, 11, 23713), pointer_size: PointerSize::Bit32, version: Version::V2020, @@ -382,7 +373,6 @@ static BUILDS: &[Build] = &[ // Unity 2022.3.0f1, metadata version 29, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2022, 3, 0, 4507), pointer_size: PointerSize::Bit64, version: Version::V2022, @@ -424,7 +414,6 @@ static BUILDS: &[Build] = &[ // Unity 2022.3.0f1, metadata version 29, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2022, 3, 0, 4507), pointer_size: PointerSize::Bit32, version: Version::V2022, @@ -466,7 +455,6 @@ static BUILDS: &[Build] = &[ // Unity 2023.1.0f1, metadata version 29, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2023, 1, 0, 2298), pointer_size: PointerSize::Bit64, version: Version::V2022, @@ -508,7 +496,6 @@ static BUILDS: &[Build] = &[ // Unity 2023.1.0f1, metadata version 29, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2023, 1, 0, 2298), pointer_size: PointerSize::Bit32, version: Version::V2022, @@ -550,7 +537,6 @@ static BUILDS: &[Build] = &[ // Unity 2023.1.22f1, metadata version 29, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2023, 1, 22, 16744), pointer_size: PointerSize::Bit64, version: Version::V2022, @@ -592,7 +578,6 @@ static BUILDS: &[Build] = &[ // Unity 2023.1.22f1, metadata version 29, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2023, 1, 22, 16744), pointer_size: PointerSize::Bit32, version: Version::V2022, @@ -634,7 +619,6 @@ static BUILDS: &[Build] = &[ // Unity 6000.2.12f1, metadata version 31, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 31, unity: (6000, 2, 12, 40285), pointer_size: PointerSize::Bit64, version: Version::V2022, @@ -676,7 +660,6 @@ static BUILDS: &[Build] = &[ // Unity 6000.2.12f1, metadata version 31, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 31, unity: (6000, 2, 12, 40285), pointer_size: PointerSize::Bit32, version: Version::V2022, @@ -718,7 +701,6 @@ static BUILDS: &[Build] = &[ // Unity 6000.3.21f1, metadata version 39, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 39, unity: (6000, 3, 21, 9777), pointer_size: PointerSize::Bit64, version: Version::V2022, @@ -760,7 +742,6 @@ static BUILDS: &[Build] = &[ // Unity 6000.3.21f1, metadata version 39, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 39, unity: (6000, 3, 21, 9777), pointer_size: PointerSize::Bit32, version: Version::V2022, @@ -802,7 +783,6 @@ static BUILDS: &[Build] = &[ // Unity 6000.5.10f1, metadata version 107, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 107, unity: (6000, 5, 10, 54518), pointer_size: PointerSize::Bit64, version: Version::V2022, @@ -844,7 +824,6 @@ static BUILDS: &[Build] = &[ // Unity 6000.5.10f1, metadata version 107, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 107, unity: (6000, 5, 10, 54518), pointer_size: PointerSize::Bit32, version: Version::V2022, @@ -886,7 +865,6 @@ static BUILDS: &[Build] = &[ // Unity 6000.7.0a3, metadata version 110, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 110, unity: (6000, 7, 0, 5476), pointer_size: PointerSize::Bit64, version: Version::V2022, @@ -928,7 +906,6 @@ static BUILDS: &[Build] = &[ // Unity 6000.7.0a3, metadata version 110, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 110, unity: (6000, 7, 0, 5476), pointer_size: PointerSize::Bit32, version: Version::V2022, @@ -971,18 +948,14 @@ static BUILDS: &[Build] = &[ #[cfg(all(test, not(target_family = "wasm")))] mod tests { - use super::{find, IL2CPPOffsets, BUILDS}; + use super::{nearest, IL2CPPOffsets, BUILDS}; use crate::PointerSize; #[test] fn table_reads_oldest_to_newest() { - assert!(BUILDS - .windows(2) - .all(|pair| (pair[0].metadata, pair[0].unity) <= (pair[1].metadata, pair[1].unity))); + assert!(BUILDS.windows(2).all(|pair| pair[0].unity <= pair[1].unity)); } - const UNITY_6000_3: (u16, u16, u16, u16) = (6000, 3, 21, 9777); - // Every player was measured at both widths, so each version in the // table has an x64 entry and an x86 entry. #[test] @@ -992,38 +965,46 @@ mod tests { PointerSize::Bit64 => PointerSize::Bit32, _ => PointerSize::Bit64, }; - assert!( - find(build.metadata, build.unity, other).is_some(), + let twin = nearest(build.unity, other).unwrap(); + assert_eq!( + twin.unity, build.unity, "{:?} at {:?} has no twin", - build.unity, - other + build.unity, other ); } } + // A player nobody measured takes the newest build at or below its + // major.minor, or the oldest build when nothing is below. A measured + // player takes its own build even when a newer patch was measured. #[test] - fn finds_exact_builds() { - let build = find(39, UNITY_6000_3, PointerSize::Bit64).unwrap(); - assert_eq!(build.metadata, 39); - assert_eq!(build.unity, UNITY_6000_3); + fn nearest_takes_the_newest_build_at_or_below_the_major_minor() { + let x64 = PointerSize::Bit64; + let unity = |player| nearest(player, x64).unwrap().unity; + assert_eq!(unity((2021, 3, 5, 1)), (2021, 3, 11, 23713)); + assert_eq!(unity((2021, 3, 45, 1)), (2021, 3, 11, 23713)); + assert_eq!(unity((2023, 1, 10, 1)), (2023, 1, 22, 16744)); + assert_eq!(unity((2023, 1, 0, 2298)), (2023, 1, 0, 2298)); + assert_eq!(unity((2022, 1, 0, 1)), (2021, 3, 11, 23713)); + assert_eq!(unity((7000, 0, 0, 0)), (6000, 7, 0, 5476)); + assert_eq!(unity((5, 6, 7, 0)), (2018, 4, 36, 54151)); + } - let narrow = find(39, UNITY_6000_3, PointerSize::Bit32).unwrap(); - assert_eq!(narrow.metadata, 39); - assert_eq!(narrow.pointer_size, PointerSize::Bit32); + #[test] + fn nearest_is_the_build_itself_on_a_measured_version() { + for build in BUILDS { + let found = nearest(build.unity, build.pointer_size).unwrap(); + assert_eq!(found.unity, build.unity); + assert_eq!(found.pointer_size, build.pointer_size); + } } - // A build is one measured player. An identity off it in any part, the - // metadata version, any part of the Unity version, or the width, is a - // player nobody measured and gets no answer. #[test] - fn identities_off_a_measured_build_answer_nothing() { - let (major, minor, patch, build) = UNITY_6000_3; - assert!(find(40, UNITY_6000_3, PointerSize::Bit64).is_none()); - assert!(find(39, (major, minor, patch, build + 1), PointerSize::Bit64).is_none()); - assert!(find(39, (major, minor, patch + 1, build), PointerSize::Bit64).is_none()); - assert!(find(39, (major, minor + 1, patch, build), PointerSize::Bit64).is_none()); - assert!(find(200, (7000, 0, 0, 0), PointerSize::Bit64).is_none()); - assert!(find(16, (5, 6, 7, 0), PointerSize::Bit64).is_none()); + fn nearest_keeps_the_width() { + let build = nearest((2021, 3, 5, 1), PointerSize::Bit32).unwrap(); + assert_eq!(build.unity, (2021, 3, 11, 23713)); + assert_eq!(build.pointer_size, PointerSize::Bit32); + assert!(nearest((2021, 3, 5, 1), PointerSize::Bit16).is_none()); } // A version table's value for any of the grown members must match every @@ -1061,17 +1042,14 @@ mod tests { // too. #[test] fn unity_6000_5_builds_diverge_from_their_version_table_on_statics() { - for (metadata, unity, static_fields) in [ - (107, (6000, 5, 10, 54518), 0xA0), - (110, (6000, 7, 0, 5476), 0x98), - ] { - let build = find(metadata, unity, PointerSize::Bit64).unwrap(); + for (unity, static_fields) in [((6000, 5, 10, 54518), 0xA0), ((6000, 7, 0, 5476), 0x98)] { + let build = nearest(unity, PointerSize::Bit64).unwrap(); let table = IL2CPPOffsets::new(build.version, build.pointer_size).unwrap(); assert_eq!(build.offsets.class.static_fields, static_fields); assert_ne!(build.offsets.class.static_fields, table.class.static_fields); } - let build = find(110, (6000, 7, 0, 5476), PointerSize::Bit64).unwrap(); + let build = nearest((6000, 7, 0, 5476), PointerSize::Bit64).unwrap(); let table = IL2CPPOffsets::new(build.version, build.pointer_size).unwrap(); assert_ne!(build.offsets.class.field_count, table.class.field_count); } diff --git a/src/game_engine/unity/il2cpp/mod.rs b/src/game_engine/unity/il2cpp/mod.rs index 7112bb93..7db21fbf 100644 --- a/src/game_engine/unity/il2cpp/mod.rs +++ b/src/game_engine/unity/il2cpp/mod.rs @@ -38,48 +38,36 @@ pub struct Module { } impl Module { - /// Tries attaching to a Unity game that is using the IL2CPP backend. If - /// the game's metadata and Unity versions name a measured build, this - /// function uses that build's offsets. Otherwise this function detects - /// the [IL2CPP version](Version). If you know the version in advance or - /// it fails detecting it, use [`attach`](Self::attach) instead. + /// Tries attaching to a Unity game that is using the IL2CPP backend. The + /// game gets the offsets of the measured build nearest to its Unity + /// version, its own build when someone measured that version. If you + /// know the [IL2CPP version](Version) in advance, use + /// [`attach`](Self::attach) instead. pub fn attach_auto_detect(process: &Process) -> Option { let il2cpp_module = Self::find_runtime_module(process)?; let pointer_size = pe::MachineType::read(process, il2cpp_module.0)?.pointer_size()?; + let unity = Self::unity_version(process)?; + let build = builds::nearest(unity, pointer_size)?; - let unity = Self::unity_version(process); - let metadata = Self::metadata_version(process); - - match (unity, metadata) { - (Some(unity), Some(metadata)) => { - if let Some(build) = builds::find(metadata, unity, pointer_size) { - let module = Self::attach_with( - process, - il2cpp_module, - pointer_size, - build.version, - &build.offsets, - )?; - print_limited::<128>(&format_args!( - "known il2cpp build: metadata {metadata}, unity {}.{}.{}.{}", - unity.0, unity.1, unity.2, unity.3, - )); - return Some(module); - } - print_limited::<128>(&format_args!( - "unknown il2cpp build: metadata {metadata}, unity {}.{}.{}.{}", - unity.0, unity.1, unity.2, unity.3, - )); - } - // The game maps its metadata after GameAssembly.dll. A player - // with a measured build waits for it, since attaching now would - // take the version table's offsets and keep them. - (Some(unity), None) if builds::measured(unity, pointer_size) => return None, - _ => {} - } - - let version = Version::detect(process)?; - Self::attach(process, version) + let module = Self::attach_with( + process, + il2cpp_module, + pointer_size, + build.version, + &build.offsets, + )?; + print_limited::<128>(&format_args!( + "il2cpp: unity {}.{}.{}.{} takes the build measured on {}.{}.{}.{}", + unity.0, + unity.1, + unity.2, + unity.3, + build.unity.0, + build.unity.1, + build.unity.2, + build.unity.3, + )); + Some(module) } /// Tries attaching to a Unity game that is using the IL2CPP backend with @@ -113,17 +101,6 @@ impl Module { )) } - /// Reads the version of the game's metadata. The mapped - /// `global-metadata.dat` starts with a sanity value, then the version. - fn metadata_version(process: &Process) -> Option { - process.memory_ranges().find_map(|range| { - let [sanity, version] = process.read::<[u32; 2]>(range.address().ok()?).ok()?; - // Versions are small numbers. Unity 6 renumbered them and reaches - // the low hundreds. - (sanity == 0xFAB1_1BAF && (16..=999).contains(&version)).then_some(version) - }) - } - fn attach_with( process: &Process, il2cpp_module: (Address, u64), @@ -640,24 +617,3 @@ impl Module { retry(|| self.get_hash_set_offsets(process, at)).await } } - -#[cfg(all(test, not(target_family = "wasm")))] -mod tests { - use super::Module; - use crate::runtime::mock::with_process; - - #[test] - fn reads_the_metadata_version_off_the_mapped_file() { - let mapped = [0xAF_u8, 0x1B, 0xB1, 0xFA, 39, 0, 0, 0]; - // The sanity value with nothing sane behind it must not answer. - let stray = [0xAF_u8, 0x1B, 0xB1, 0xFA, 0, 0, 0, 0]; - - with_process(&[(0x10000, &stray), (0x20000, &mapped)], |process| { - assert_eq!(Module::metadata_version(process), Some(39)); - }); - - with_process(&[(0x10000, &stray)], |process| { - assert!(Module::metadata_version(process).is_none()); - }); - } -} diff --git a/src/game_engine/unity/il2cpp/walk_tests.rs b/src/game_engine/unity/il2cpp/walk_tests.rs index 3f31d9aa..ec29755b 100644 --- a/src/game_engine/unity/il2cpp/walk_tests.rs +++ b/src/game_engine/unity/il2cpp/walk_tests.rs @@ -21,37 +21,24 @@ fn put(image: &mut [u8], at: u64, bytes: &[u8]) { } // A player as `attach_auto_detect` sees it: `GameAssembly.dll` with a PE -// header and the x64 code that points at both globals, `UnityPlayer.dll` with a -// PE header and a version resource, and the mapped metadata file when it is -// there yet. +// header and the x64 code that points at both globals, and `UnityPlayer.dll` +// with a PE header and a version resource. struct Player { unity: (u16, u16, u16, u16), - metadata: Option, } const GAME_ASSEMBLY: u64 = 0x1_8000_0000; const UNITY_PLAYER: u64 = 0x1900_0000; -const METADATA: u64 = 0x2000_0000; impl Player { fn attach(&self) -> Option { let game_assembly = Self::game_assembly(); let unity_player = Self::unity_player(self.unity); - let metadata = self.metadata.map(|version| { - let mut i = vec![0; 0x100]; - put(&mut i, 0x0, &0xFAB1_1BAF_u32.to_le_bytes()); - put(&mut i, 0x4, &version.to_le_bytes()); - i - }); - let mut regions = vec![ - (GAME_ASSEMBLY, &game_assembly[..]), - (UNITY_PLAYER, &unity_player[..]), - ]; - if let Some(metadata) = &metadata { - regions.push((METADATA, &metadata[..])); - } crate::runtime::mock::with_modules( - ®ions, + &[ + (GAME_ASSEMBLY, &game_assembly[..]), + (UNITY_PLAYER, &unity_player[..]), + ], &[ ("GameAssembly.dll", GAME_ASSEMBLY, 0x1000), ("UnityPlayer.dll", UNITY_PLAYER, 0x1000), @@ -129,13 +116,12 @@ impl Player { const MEASURED_6000_5: (u16, u16, u16, u16) = (6000, 5, 10, 54518); -// A game on a measured player, with its metadata mapped, attaches with the -// offsets measured on that player. +// A game on a measured player attaches with the offsets measured on that +// player. #[test] fn attach_auto_detect_uses_a_measured_build() { let module = Player { unity: MEASURED_6000_5, - metadata: Some(107), } .attach() .unwrap(); @@ -147,44 +133,16 @@ fn attach_auto_detect_uses_a_measured_build() { assert_eq!(module.offsets.class.static_fields, 0xA0); } -// The metadata file is mapped after `GameAssembly.dll`. Until it is, a game -// on a measured player has to wait rather than attach with the version -// table's offsets and keep them. +// A player nobody measured takes the nearest build. #[test] -fn attach_auto_detect_waits_for_the_metadata_of_a_measured_player() { +fn attach_auto_detect_takes_the_nearest_build_for_an_unmeasured_player() { let module = Player { - unity: MEASURED_6000_5, - metadata: None, - } - .attach(); - assert!(module.is_none()); -} - -// A player nobody measured takes the version table, whether or not its -// metadata is mapped yet. -#[test] -fn attach_auto_detect_falls_back_for_an_unmeasured_player() { - for metadata in [None, Some(107)] { - let module = Player { - unity: (6000, 5, 11, 1), - metadata, - } - .attach() - .unwrap(); - assert_eq!(module.offsets.class.static_fields, 0xB8); - } -} - -// A measured player whose metadata says another version is not that build. -#[test] -fn attach_auto_detect_falls_back_when_the_metadata_disagrees() { - let module = Player { - unity: MEASURED_6000_5, - metadata: Some(110), + unity: (2021, 3, 5, 1), } .attach() .unwrap(); - assert_eq!(module.offsets.class.static_fields, 0xB8); + let nearest = super::builds::nearest((2021, 3, 11, 23713), PointerSize::Bit64).unwrap(); + assert!(core::ptr::eq(module.offsets, &nearest.offsets)); } // The x86 code that points at both globals. The assemblies loop reads the vector's @@ -417,7 +375,7 @@ fn x64_scanner_refuses_an_x86_image() { // own name field, where the version tables read it off the assembly. #[test] fn assembly_names_resolve_through_the_image() { - let build = super::builds::find(107, (6000, 5, 10, 54518), PointerSize::Bit64).unwrap(); + let build = super::builds::nearest(MEASURED_6000_5, PointerSize::Bit64).unwrap(); let offsets = &build.offsets; assert!(offsets.assembly.aname.is_none()); let name_at = offsets.image.assembly_name.unwrap() as u64; From 3d7dc9dc37a5f3dc95f50a800f16527f963a18e1 Mon Sep 17 00:00:00 2001 From: ero-qt Date: Tue, 15 Sep 2026 00:28:53 +0200 Subject: [PATCH 2/3] remove il2cpp version tables --- src/game_engine/unity/il2cpp/builds.rs | 119 +++--------- .../unity/il2cpp/collections_tests.rs | 57 +++--- src/game_engine/unity/il2cpp/mod.rs | 57 ++---- src/game_engine/unity/il2cpp/offsets.rs | 154 ++------------- src/game_engine/unity/il2cpp/readers_tests.rs | 7 +- src/game_engine/unity/il2cpp/version.rs | 47 ----- src/game_engine/unity/il2cpp/walk_tests.rs | 183 ++++++++++++------ src/game_engine/unity/mod.rs | 6 +- 8 files changed, 197 insertions(+), 433 deletions(-) delete mode 100644 src/game_engine/unity/il2cpp/version.rs diff --git a/src/game_engine/unity/il2cpp/builds.rs b/src/game_engine/unity/il2cpp/builds.rs index 1b7c2163..bcecb8ad 100644 --- a/src/game_engine/unity/il2cpp/builds.rs +++ b/src/game_engine/unity/il2cpp/builds.rs @@ -4,16 +4,14 @@ use super::offsets::{ AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, IL2CPPOffsets, ImageOffsets, - TypeOffsets, + TypeOffsets, TypeStart, }; -use super::Version; use crate::PointerSize; /// One measured IL2CPP player and the offsets from its PDB. pub(super) struct Build { pub(super) unity: (u16, u16, u16, u16), pub(super) pointer_size: PointerSize, - pub(super) version: Version, pub(super) offsets: IL2CPPOffsets, } @@ -47,7 +45,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2018, 4, 36, 54151), pointer_size: PointerSize::Bit64, - version: Version::Base, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -56,7 +53,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x1c, - metadata_handle: 0x18, + type_start: TypeStart::Inline(0x18), }, class: ClassOffsets { name: 0x10, @@ -88,7 +85,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2018, 4, 36, 54151), pointer_size: PointerSize::Bit32, - version: Version::Base, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -97,7 +93,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0x10, - metadata_handle: 0xc, + type_start: TypeStart::Inline(0xc), }, class: ClassOffsets { name: 0x8, @@ -129,7 +125,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2019, 4, 41, 9172), pointer_size: PointerSize::Bit64, - version: Version::V2019, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -138,7 +133,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x1c, - metadata_handle: 0x18, + type_start: TypeStart::Inline(0x18), }, class: ClassOffsets { name: 0x10, @@ -170,7 +165,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2019, 4, 41, 9172), pointer_size: PointerSize::Bit32, - version: Version::V2019, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -179,7 +173,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0x10, - metadata_handle: 0xc, + type_start: TypeStart::Inline(0xc), }, class: ClassOffsets { name: 0x8, @@ -211,7 +205,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2020, 1, 18, 38512), pointer_size: PointerSize::Bit64, - version: Version::V2019, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -220,7 +213,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x1c, - metadata_handle: 0x18, + type_start: TypeStart::Inline(0x18), }, class: ClassOffsets { name: 0x10, @@ -252,7 +245,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2020, 1, 18, 38512), pointer_size: PointerSize::Bit32, - version: Version::V2019, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -261,7 +253,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0x10, - metadata_handle: 0xc, + type_start: TypeStart::Inline(0xc), }, class: ClassOffsets { name: 0x8, @@ -293,7 +285,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2021, 3, 11, 23713), pointer_size: PointerSize::Bit64, - version: Version::V2020, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -302,7 +293,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -334,7 +325,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2021, 3, 11, 23713), pointer_size: PointerSize::Bit32, - version: Version::V2020, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -343,7 +333,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -375,7 +365,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2022, 3, 0, 4507), pointer_size: PointerSize::Bit64, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -384,7 +373,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -416,7 +405,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2022, 3, 0, 4507), pointer_size: PointerSize::Bit32, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -425,7 +413,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -457,7 +445,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2023, 1, 0, 2298), pointer_size: PointerSize::Bit64, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -466,7 +453,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -498,7 +485,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2023, 1, 0, 2298), pointer_size: PointerSize::Bit32, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -507,7 +493,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -539,7 +525,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2023, 1, 22, 16744), pointer_size: PointerSize::Bit64, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -548,7 +533,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -580,7 +565,6 @@ static BUILDS: &[Build] = &[ Build { unity: (2023, 1, 22, 16744), pointer_size: PointerSize::Bit32, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -589,7 +573,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -621,7 +605,6 @@ static BUILDS: &[Build] = &[ Build { unity: (6000, 2, 12, 40285), pointer_size: PointerSize::Bit64, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -630,7 +613,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -662,7 +645,6 @@ static BUILDS: &[Build] = &[ Build { unity: (6000, 2, 12, 40285), pointer_size: PointerSize::Bit32, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -671,7 +653,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -703,7 +685,6 @@ static BUILDS: &[Build] = &[ Build { unity: (6000, 3, 21, 9777), pointer_size: PointerSize::Bit64, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -712,7 +693,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -744,7 +725,6 @@ static BUILDS: &[Build] = &[ Build { unity: (6000, 3, 21, 9777), pointer_size: PointerSize::Bit32, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -753,7 +733,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -785,7 +765,6 @@ static BUILDS: &[Build] = &[ Build { unity: (6000, 5, 10, 54518), pointer_size: PointerSize::Bit64, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -794,7 +773,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -826,7 +805,6 @@ static BUILDS: &[Build] = &[ Build { unity: (6000, 5, 10, 54518), pointer_size: PointerSize::Bit32, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -835,7 +813,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -867,7 +845,6 @@ static BUILDS: &[Build] = &[ Build { unity: (6000, 7, 0, 5476), pointer_size: PointerSize::Bit64, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -876,7 +853,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -908,7 +885,6 @@ static BUILDS: &[Build] = &[ Build { unity: (6000, 7, 0, 5476), pointer_size: PointerSize::Bit32, - version: Version::V2022, offsets: IL2CPPOffsets { assembly: AssemblyOffsets { image: 0x0, @@ -917,7 +893,7 @@ static BUILDS: &[Build] = &[ image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -948,7 +924,7 @@ static BUILDS: &[Build] = &[ #[cfg(all(test, not(target_family = "wasm")))] mod tests { - use super::{nearest, IL2CPPOffsets, BUILDS}; + use super::{nearest, BUILDS}; use crate::PointerSize; #[test] @@ -1006,51 +982,4 @@ mod tests { assert_eq!(build.pointer_size, PointerSize::Bit32); assert!(nearest((2021, 3, 5, 1), PointerSize::Bit16).is_none()); } - - // A version table's value for any of the grown members must match every - // measured build it stands in for, or say nothing. - #[test] - fn version_tables_never_contradict_a_measured_build() { - fn agrees(table: Option, measured: Option) -> bool { - table.is_none() || table == measured - } - - for build in BUILDS { - let Some(table) = IL2CPPOffsets::new(build.version, build.pointer_size) else { - continue; - }; - assert!(agrees( - table.class.declaring_type, - build.offsets.class.declaring_type - )); - assert!(agrees( - table.class.instance_size, - build.offsets.class.instance_size - )); - assert!(agrees( - table.generic.cached_class, - build.offsets.generic.cached_class - )); - assert!(agrees(table.type_words.data, build.offsets.type_words.data)); - assert!(agrees(table.type_words.kind, build.offsets.type_words.kind)); - assert!(agrees(table.field.type_, build.offsets.field.type_)); - } - } - - // The version table for 6000.5 and 6000.7 puts static_fields where 2022.3 - // had it. Both measured players put it lower, and 6000.7 moves field_count - // too. - #[test] - fn unity_6000_5_builds_diverge_from_their_version_table_on_statics() { - for (unity, static_fields) in [((6000, 5, 10, 54518), 0xA0), ((6000, 7, 0, 5476), 0x98)] { - let build = nearest(unity, PointerSize::Bit64).unwrap(); - let table = IL2CPPOffsets::new(build.version, build.pointer_size).unwrap(); - assert_eq!(build.offsets.class.static_fields, static_fields); - assert_ne!(build.offsets.class.static_fields, table.class.static_fields); - } - - let build = nearest((6000, 7, 0, 5476), PointerSize::Bit64).unwrap(); - let table = IL2CPPOffsets::new(build.version, build.pointer_size).unwrap(); - assert_ne!(build.offsets.class.field_count, table.class.field_count); - } } diff --git a/src/game_engine/unity/il2cpp/collections_tests.rs b/src/game_engine/unity/il2cpp/collections_tests.rs index 33dfa132..c575c69a 100644 --- a/src/game_engine/unity/il2cpp/collections_tests.rs +++ b/src/game_engine/unity/il2cpp/collections_tests.rs @@ -4,7 +4,7 @@ //! is the route every corlib dictionary takes. use super::super::managed::DictionaryShape; -use super::{IL2CPPOffsets, Module, Version}; +use super::Module; use crate::runtime::mock::with_process; use crate::{Address, PointerSize, Process}; @@ -12,6 +12,7 @@ use std::vec; use std::vec::Vec; const BASE: u64 = 0x60_0000; +const MEASURED_2019: (u16, u16, u16, u16) = (2019, 4, 41, 9172); fn put(image: &mut [u8], at: u64, bytes: &[u8]) { let at = at as usize; @@ -32,11 +33,12 @@ fn field(image: &mut [u8], at: u64, name: u64, type_: u64, offset: i32) { put(image, at + 0x18, &offset.to_le_bytes()); } -fn image(version: Version) -> Vec { - let field_count_at = match version { - Version::V2019 => 0x11C, - _ => 0x124, - }; +fn image(unity: (u16, u16, u16, u16)) -> Vec { + let field_count_at = super::builds::nearest(unity, PointerSize::Bit64) + .unwrap() + .offsets + .class + .field_count as u64; let mut i = vec![0; 0x3000]; let strings = [ @@ -198,25 +200,26 @@ fn image(version: Version) -> Vec { i } -fn module(version: Version) -> Module { +fn module(unity: (u16, u16, u16, u16)) -> Module { Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x40), - version, - offsets: IL2CPPOffsets::new(version, PointerSize::Bit64).unwrap(), + offsets: &super::builds::nearest(unity, PointerSize::Bit64) + .unwrap() + .offsets, pointer_size: PointerSize::Bit64, } } -fn on_fixture(version: Version, test: impl FnOnce(&Process, &Module)) { - with_process(&[(BASE, &image(version))], |process| { - test(process, &module(version)); +fn on_fixture(unity: (u16, u16, u16, u16), test: impl FnOnce(&Process, &Module)) { + with_process(&[(BASE, &image(unity))], |process| { + test(process, &module(unity)); }); } #[test] fn dictionaries_resolve_through_the_cached_class() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let offsets = module .get_dictionary_offsets(process, Address::new(BASE)) .unwrap(); @@ -240,12 +243,9 @@ fn dictionaries_resolve_through_the_cached_class() { }); } -// The 2022.2-and-later fallback table carries no cached class, because the -// measured builds inside that stretch disagree. A fallback attach misses -// cleanly; known builds carry their own value. #[test] fn dictionaries_read_their_live_pairs() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE); let offsets = module.get_dictionary_offsets(process, slot).unwrap(); let pairs = module @@ -257,7 +257,7 @@ fn dictionaries_read_their_live_pairs() { #[test] fn dictionaries_derived_from_corlibs_dictionary_resolve() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x8); let offsets = module.get_dictionary_offsets(process, slot).unwrap(); let pairs = module @@ -269,7 +269,7 @@ fn dictionaries_derived_from_corlibs_dictionary_resolve() { #[test] fn dictionary_shaped_objects_are_not_dictionaries() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { assert!(module .get_dictionary_offsets(process, Address::new(BASE + 0x10)) .is_none()); @@ -278,7 +278,7 @@ fn dictionary_shaped_objects_are_not_dictionaries() { #[test] fn dictionaries_without_allocated_backing_read_empty() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x18); let offsets = module.get_dictionary_offsets(process, slot).unwrap(); let pairs = module @@ -290,7 +290,7 @@ fn dictionaries_without_allocated_backing_read_empty() { #[test] fn hash_sets_read_their_live_values() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x20); let offsets = module.get_hash_set_offsets(process, slot).unwrap(); let values = module @@ -302,7 +302,7 @@ fn hash_sets_read_their_live_values() { #[test] fn hash_sets_derived_from_corlibs_hash_set_resolve() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x28); let offsets = module.get_hash_set_offsets(process, slot).unwrap(); let values = module @@ -314,7 +314,7 @@ fn hash_sets_derived_from_corlibs_hash_set_resolve() { #[test] fn hash_set_shaped_objects_are_not_hash_sets() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { assert!(module .get_hash_set_offsets(process, Address::new(BASE + 0x30)) .is_none()); @@ -323,7 +323,7 @@ fn hash_set_shaped_objects_are_not_hash_sets() { #[test] fn hash_sets_without_allocated_backing_read_empty() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x38); let offsets = module.get_hash_set_offsets(process, slot).unwrap(); let values = module @@ -332,12 +332,3 @@ fn hash_sets_without_allocated_backing_read_empty() { assert!(values.is_empty()); }); } - -#[test] -fn fallback_tables_without_a_cached_class_answer_nothing() { - on_fixture(Version::V2022, |process, module| { - assert!(module - .get_dictionary_offsets(process, Address::new(BASE)) - .is_none()); - }); -} diff --git a/src/game_engine/unity/il2cpp/mod.rs b/src/game_engine/unity/il2cpp/mod.rs index 7db21fbf..2b42eb7d 100644 --- a/src/game_engine/unity/il2cpp/mod.rs +++ b/src/game_engine/unity/il2cpp/mod.rs @@ -13,12 +13,10 @@ mod image; pub use image::Image; mod class; pub use class::Class; -mod version; -pub use version::Version; mod pointer; pub use pointer::UnityPointer; mod offsets; -use offsets::IL2CPPOffsets; +use offsets::{IL2CPPOffsets, TypeStart}; #[cfg(all(test, not(target_family = "wasm")))] mod collections_tests; #[cfg(all(test, not(target_family = "wasm")))] @@ -32,7 +30,6 @@ use super::{managed, DictionaryOffsets, HashSetOffsets, ListOffsets, ManagedStri pub struct Module { assemblies: Address, type_info_definition_table: Address, - version: Version, offsets: &'static IL2CPPOffsets, pointer_size: PointerSize, } @@ -40,22 +37,14 @@ pub struct Module { impl Module { /// Tries attaching to a Unity game that is using the IL2CPP backend. The /// game gets the offsets of the measured build nearest to its Unity - /// version, its own build when someone measured that version. If you - /// know the [IL2CPP version](Version) in advance, use - /// [`attach`](Self::attach) instead. + /// version, its own build when someone measured that version. pub fn attach_auto_detect(process: &Process) -> Option { let il2cpp_module = Self::find_runtime_module(process)?; let pointer_size = pe::MachineType::read(process, il2cpp_module.0)?.pointer_size()?; let unity = Self::unity_version(process)?; let build = builds::nearest(unity, pointer_size)?; - let module = Self::attach_with( - process, - il2cpp_module, - pointer_size, - build.version, - &build.offsets, - )?; + let module = Self::attach_with(process, il2cpp_module, pointer_size, &build.offsets)?; print_limited::<128>(&format_args!( "il2cpp: unity {}.{}.{}.{} takes the build measured on {}.{}.{}.{}", unity.0, @@ -70,18 +59,6 @@ impl Module { Some(module) } - /// Tries attaching to a Unity game that is using the IL2CPP backend with - /// the [IL2CPP version](Version) provided. The version needs to be - /// 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 il2cpp_module = Self::find_runtime_module(process)?; - let pointer_size = pe::MachineType::read(process, il2cpp_module.0)?.pointer_size()?; - let offsets = IL2CPPOffsets::new(version, pointer_size)?; - - Self::attach_with(process, il2cpp_module, pointer_size, version, offsets) - } - fn find_runtime_module(process: &Process) -> Option<(Address, u64)> { let address = process.get_module_address("GameAssembly.dll").ok()?; let size = pe::read_size_of_image(process, address)? as u64; @@ -105,7 +82,6 @@ impl Module { process: &Process, il2cpp_module: (Address, u64), pointer_size: PointerSize, - version: Version, offsets: &'static IL2CPPOffsets, ) -> Option { let (assemblies, type_info_definition_table) = match pointer_size { @@ -117,7 +93,6 @@ impl Module { Some(Self { assemblies, type_info_definition_table, - version, offsets, pointer_size, }) @@ -250,13 +225,18 @@ impl Module { } fn walk(&self) -> managed::Walk { + let (metadata_handle, handle_is_inline) = match self.offsets.image.type_start { + TypeStart::Inline(at) => (at, true), + TypeStart::Handle(at) => (at, false), + }; + managed::Walk { runtime: managed::Runtime::Il2Cpp(managed::Il2CppRuntime { assemblies: self.assemblies, type_info_definition_table: self.type_info_definition_table, type_count: self.offsets.image.type_count.into(), - metadata_handle: self.offsets.image.metadata_handle.into(), - handle_is_inline: matches!(self.version, Version::Base | Version::V2019), + metadata_handle: metadata_handle.into(), + handle_is_inline, field_count: self.offsets.class.field_count, static_fields: self.offsets.class.static_fields.into(), cached_class: self.offsets.generic.cached_class, @@ -522,10 +502,8 @@ impl Module { managed::read_reference_list(process, self.pointer_size, offsets, at) } - /// Attaches to a Unity game that is using the IL2CPP backend. This function - /// automatically detects the [IL2CPP version](Version). If you know the - /// version in advance or it fails detecting it, use - /// [`wait_attach`](Self::wait_attach) instead. + /// Attaches to a Unity game that is using the IL2CPP backend. The game + /// gets the offsets of the measured build nearest to its Unity version. /// /// This is the `await`able version of the /// [`attach_auto_detect`](Self::attach_auto_detect) function, yielding back @@ -534,17 +512,6 @@ impl Module { retry(|| Self::attach_auto_detect(process)).await } - /// Attaches to a Unity game that is using the IL2CPP backend with the - /// [IL2CPP version](Version) provided. The version needs to be correct - /// for this function to work. If you don't know the version in advance, use - /// [`wait_attach_auto_detect`](Self::wait_attach_auto_detect) instead. - /// - /// This is the `await`able version of the [`attach`](Self::attach) - /// function, yielding back to the runtime between each try. - pub async fn wait_attach(process: &Process, version: Version) -> Module { - retry(|| Self::attach(process, version)).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 diff --git a/src/game_engine/unity/il2cpp/offsets.rs b/src/game_engine/unity/il2cpp/offsets.rs index a1f1df05..35bec88f 100644 --- a/src/game_engine/unity/il2cpp/offsets.rs +++ b/src/game_engine/unity/il2cpp/offsets.rs @@ -1,5 +1,3 @@ -use crate::{game_engine::unity::il2cpp::Version, PointerSize}; - pub(super) struct IL2CPPOffsets { pub(super) assembly: AssemblyOffsets, pub(super) image: ImageOffsets, @@ -9,146 +7,6 @@ pub(super) struct IL2CPPOffsets { pub(super) field: FieldInfoOffsets, } -impl IL2CPPOffsets { - pub(super) fn new(version: Version, pointer_size: PointerSize) -> Option<&'static Self> { - match pointer_size { - PointerSize::Bit64 => Some(match version { - Version::V2022 => &Self { - assembly: AssemblyOffsets { - image: 0x0, - aname: Some(0x18), - }, - image: ImageOffsets { - assembly_name: None, - type_count: 0x18, - metadata_handle: 0x28, - }, - class: ClassOffsets { - name: 0x10, - namespace: 0x18, - declaring_type: Some(0x50), // 2023.1 through 6000.7 - parent: 0x58, - fields: 0x80, - static_fields: 0xB8, - instance_size: None, - field_count: 0x124, - }, - generic: GenericOffsets { cached_class: None }, - type_words: TypeOffsets { - data: Some(0x0), // 2023.1 through 6000.7 - kind: Some(0xA), // 2023.1 through 6000.7 - }, - field: FieldInfoOffsets { - name: 0x0, - type_: Some(0x8), // 2023.1 through 6000.7 - offset: 0x18, - struct_size: 0x20, - }, - }, - Version::V2020 => &Self { - assembly: AssemblyOffsets { - image: 0x0, - aname: Some(0x18), - }, - image: ImageOffsets { - assembly_name: None, - type_count: 0x18, - metadata_handle: 0x28, - }, - class: ClassOffsets { - name: 0x10, - namespace: 0x18, - declaring_type: None, - parent: 0x58, - fields: 0x80, - static_fields: 0xB8, - instance_size: None, - field_count: 0x120, - }, - generic: GenericOffsets { cached_class: None }, - type_words: TypeOffsets { - data: None, - kind: None, - }, - field: FieldInfoOffsets { - name: 0x0, - type_: None, - offset: 0x18, - struct_size: 0x20, - }, - }, - Version::V2019 => &Self { - assembly: AssemblyOffsets { - image: 0x0, - aname: Some(0x18), - }, - image: ImageOffsets { - assembly_name: None, - type_count: 0x1C, - metadata_handle: 0x18, - }, - class: ClassOffsets { - name: 0x10, - namespace: 0x18, - declaring_type: Some(0x50), // 2019.4, 2020.1 - parent: 0x58, - fields: 0x80, - static_fields: 0xB8, - instance_size: Some(0xF4), // 2019.4, 2020.1 - field_count: 0x11C, - }, - generic: GenericOffsets { - cached_class: Some(0x18), // 2019.4, 2020.1 - }, - type_words: TypeOffsets { - data: Some(0x0), // 2019.4, 2020.1 - kind: Some(0xA), // 2019.4, 2020.1 - }, - field: FieldInfoOffsets { - name: 0x0, - type_: Some(0x8), // 2019.4, 2020.1 - offset: 0x18, - struct_size: 0x20, - }, - }, - Version::Base => &Self { - assembly: AssemblyOffsets { - image: 0x0, - aname: Some(0x18), - }, - image: ImageOffsets { - assembly_name: None, - type_count: 0x1C, - metadata_handle: 0x18, - }, - class: ClassOffsets { - name: 0x10, - namespace: 0x18, - declaring_type: None, - parent: 0x58, - fields: 0x80, - static_fields: 0xB8, - instance_size: None, - field_count: 0x114, - }, - generic: GenericOffsets { cached_class: None }, - type_words: TypeOffsets { - data: None, - kind: None, - }, - field: FieldInfoOffsets { - name: 0x0, - type_: None, - offset: 0x18, - struct_size: 0x20, - }, - }, - }), - _ => None, - } - } -} - pub(super) struct AssemblyOffsets { pub(super) image: u8, pub(super) aname: Option, // Either this or ImageOffsets::assembly_name locates the name @@ -157,7 +15,17 @@ pub(super) struct AssemblyOffsets { pub(super) struct ImageOffsets { pub(super) assembly_name: Option, // Either this or AssemblyOffsets::aname locates the name pub(super) type_count: u8, - pub(super) metadata_handle: u8, + pub(super) type_start: TypeStart, +} + +/// Where an image keeps the index of its first type in the type table. +#[derive(Copy, Clone)] +pub(super) enum TypeStart { + /// The index sits in the image, at this offset. + Inline(u8), + /// A pointer sits in the image at this offset. The index sits where it + /// points. + Handle(u8), } pub(super) struct ClassOffsets { diff --git a/src/game_engine/unity/il2cpp/readers_tests.rs b/src/game_engine/unity/il2cpp/readers_tests.rs index b3ea61af..7279547f 100644 --- a/src/game_engine/unity/il2cpp/readers_tests.rs +++ b/src/game_engine/unity/il2cpp/readers_tests.rs @@ -2,7 +2,7 @@ //! implementation is shared, so this pins the mirror surface and the one //! behavior whose rationale is IL2CPP's: the full-width length judgment. -use super::{IL2CPPOffsets, Module, Version}; +use super::Module; use crate::runtime::mock::with_process; use crate::{Address, PointerSize, Process}; @@ -52,8 +52,9 @@ fn on_fixture(test: impl FnOnce(&Process, &Module)) { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - version: Version::V2022, - offsets: IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + offsets: &super::builds::nearest((2022, 3, 0, 4507), PointerSize::Bit64) + .unwrap() + .offsets, pointer_size: PointerSize::Bit64, }; test(process, &module); diff --git a/src/game_engine/unity/il2cpp/version.rs b/src/game_engine/unity/il2cpp/version.rs deleted file mode 100644 index b3a29efb..00000000 --- a/src/game_engine/unity/il2cpp/version.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::{file_format::pe, PointerSize, Process}; - -/// The version of IL2CPP that was used for the game. -#[non_exhaustive] -#[derive(Copy, Clone, PartialEq, Hash, Debug)] -pub enum Version { - /// The base version. - Base, - /// The version used starting from Unity 2019.0 - V2019, - /// The version used starting from Unity 2020.2 - V2020, - /// The version used starting from Unity 2022.2 - V2022, -} - -impl Version { - pub(crate) fn detect(process: &Process) -> Option { - let unity_module = process.get_module_address("UnityPlayer.dll").ok()?; - - if pe::MachineType::pointer_size(pe::MachineType::read(process, unity_module)?)? - == PointerSize::Bit32 - { - return Some(Self::Base); - } - - let file_version = pe::FileVersion::read(process, unity_module)?; - - return Some( - if file_version.major_version > 2022 - || (file_version.major_version == 2022 && file_version.minor_version >= 2) - { - Self::V2022 - } else if (file_version.major_version >= 2021 && file_version.major_version < 2023) - || (file_version.major_version == 2020 && file_version.minor_version >= 2) - { - Self::V2020 - } else if (file_version.major_version == 2020 && file_version.minor_version < 2) - || file_version.major_version == 2019 - { - Self::V2019 - } else { - Self::Base - }, - ); - } -} diff --git a/src/game_engine/unity/il2cpp/walk_tests.rs b/src/game_engine/unity/il2cpp/walk_tests.rs index ec29755b..1df923cb 100644 --- a/src/game_engine/unity/il2cpp/walk_tests.rs +++ b/src/game_engine/unity/il2cpp/walk_tests.rs @@ -4,7 +4,7 @@ //! the literal numbers of the Unity 2019.4 and 6000.3 layouts, copied by hand, //! so the walk is checked against the layout rather than against itself. -use super::{IL2CPPOffsets, Module, UnityPointer, Version}; +use super::{IL2CPPOffsets, Module, UnityPointer}; use crate::runtime::mock::{poll_once, with_process}; use crate::{Address, PointerSize, Process}; @@ -114,8 +114,14 @@ impl Player { } } +const MEASURED_2019: (u16, u16, u16, u16) = (2019, 4, 41, 9172); +const MEASURED_2022: (u16, u16, u16, u16) = (2022, 3, 0, 4507); const MEASURED_6000_5: (u16, u16, u16, u16) = (6000, 5, 10, 54518); +fn measured(unity: (u16, u16, u16, u16), pointer_size: PointerSize) -> &'static IL2CPPOffsets { + &super::builds::nearest(unity, pointer_size).unwrap().offsets +} + // A game on a measured player attaches with the offsets measured on that // player. #[test] @@ -196,16 +202,14 @@ fn x86_image(store: &'static [u8], operand_at: u64) -> vec::Vec { X86Image::with_store(store, operand_at).lay() } -// The scan reads no offsets, and the version tables carry no 32 bit arm, so -// the x64 offsets stand in. The module is the first 0x1000 bytes of the +// The scan reads no offsets. The module is the first 0x1000 bytes of the // image, so the image can hold bytes past the module's end. fn attach_x86(process: &crate::Process) -> Option { Module::attach_with( process, (Address::new(BASE), 0x1000), PointerSize::Bit32, - Version::V2022, - IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + measured(MEASURED_6000_5, PointerSize::Bit32), ) } @@ -343,8 +347,7 @@ fn x64_globals_resolve_from_a_mapped_image() { process, (Address::new(BASE), 0x1000), PointerSize::Bit64, - Version::V2022, - IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + measured(MEASURED_6000_5, PointerSize::Bit64), ) .unwrap(); assert_eq!(module.assemblies, Address::new(BASE + 0x900)); @@ -364,15 +367,14 @@ fn x64_scanner_refuses_an_x86_image() { process, (Address::new(BASE), 0x1000), PointerSize::Bit64, - Version::V2022, - IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + measured(MEASURED_6000_5, PointerSize::Bit64), ) .is_none()); }); } // Every measured build names an assembly through its image, at the image's -// own name field, where the version tables read it off the assembly. +// own name field. #[test] fn assembly_names_resolve_through_the_image() { let build = super::builds::nearest(MEASURED_6000_5, PointerSize::Bit64).unwrap(); @@ -395,7 +397,6 @@ fn assembly_names_resolve_through_the_image() { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - version: Version::V2022, offsets, pointer_size: PointerSize::Bit64, }; @@ -411,12 +412,12 @@ fn ptr(image: &mut [u8], at: u64, target: u64) { // The target's structures, hand-laid: the assemblies vector, the type info // definition table sliced by the image's handle, a parent chain reaching a // UnityEngine class, a static table, and a live object heading with its class. -fn image(version: Version) -> Vec { - let (type_count_at, handle_at, field_count_at) = match version { - Version::V2019 => (0x1C, 0x18, 0x11C), - Version::V2020 => (0x18, 0x28, 0x120), - _ => (0x18, 0x28, 0x124), - }; +fn image(unity: (u16, u16, u16, u16)) -> Vec { + use super::offsets::TypeStart; + + let offsets = measured(unity, PointerSize::Bit64); + let type_count_at = offsets.image.type_count as u64; + let field_count_at = offsets.class.field_count as u64; let mut i = vec![0; 0x5000]; @@ -454,19 +455,26 @@ fn image(version: Version) -> Vec { ptr(&mut i, 0x40, BASE + 0x80); ptr(&mut i, 0x48, BASE + 0xC0); - // Il2CppAssembly: the image at 0x0, the name at 0x18. - ptr(&mut i, 0x80, BASE + 0x140); - ptr(&mut i, 0x80 + 0x18, BASE + 0x2000); - ptr(&mut i, 0xC0, BASE + 0x300); - ptr(&mut i, 0xC0 + 0x18, BASE + 0x2080); + // Each assembly points at its image. Depending on the measured player, + // the assembly name lives either on the assembly or on that image. + let assembly = |i: &mut [u8], at: u64, image: u64, name: u64| { + ptr(i, at + offsets.assembly.image as u64, BASE + image); + if let Some(name_at) = offsets.assembly.aname { + ptr(i, at + name_at as u64, BASE + name); + } else if let Some(name_at) = offsets.image.assembly_name { + ptr(i, image + name_at as u64, BASE + name); + } + }; + assembly(&mut i, 0x80, 0x140, 0x2000); + assembly(&mut i, 0xC0, 0x300, 0x2080); // The default image: three classes, reached through the handle. The older // lineage stores the handle inline where the newer one points at it. put(&mut i, 0x300 + type_count_at, &5_u32.to_le_bytes()); - match version { - Version::V2019 => put(&mut i, 0x300 + handle_at, &5_u32.to_le_bytes()), - _ => { - ptr(&mut i, 0x300 + handle_at, BASE + 0x400); + match offsets.image.type_start { + TypeStart::Inline(at) => put(&mut i, 0x300 + at as u64, &5_u32.to_le_bytes()), + TypeStart::Handle(at) => { + ptr(&mut i, 0x300 + at as u64, BASE + 0x400); put(&mut i, 0x400, &5_u32.to_le_bytes()); } } @@ -617,26 +625,25 @@ fn image(version: Version) -> Vec { i } -fn module(version: Version) -> Module { +fn module(unity: (u16, u16, u16, u16)) -> Module { Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - version, - offsets: IL2CPPOffsets::new(version, PointerSize::Bit64).unwrap(), + offsets: measured(unity, PointerSize::Bit64), pointer_size: PointerSize::Bit64, } } -fn on_fixture(version: Version, test: impl FnOnce(&Process, &Module)) { - with_process(&[(BASE, &image(version))], |process| { - test(process, &module(version)); +fn on_fixture(unity: (u16, u16, u16, u16), test: impl FnOnce(&Process, &Module)) { + with_process(&[(BASE, &image(unity))], |process| { + test(process, &module(unity)); }); } #[test] fn images_resolve_by_name_in_both_lineages() { - for version in [Version::V2019, Version::V2022] { - on_fixture(version, |process, module| { + for unity in [MEASURED_2019, MEASURED_2022] { + on_fixture(unity, |process, module| { assert!(module.get_default_image(process).is_some()); assert!(module.get_image(process, "mscorlib").is_some()); assert!(module.get_image(process, "Assembly-DoesNotExist").is_none()); @@ -646,8 +653,8 @@ fn images_resolve_by_name_in_both_lineages() { #[test] fn classes_resolve_by_name_and_namespace() { - for version in [Version::V2019, Version::V2022] { - on_fixture(version, |process, module| { + for unity in [MEASURED_2019, MEASURED_2022] { + on_fixture(unity, |process, module| { let image = module.get_default_image(process).unwrap(); assert!(image.get_class(process, module, "GameManager").is_some()); assert!(image.get_class(process, module, "Game.Boss").is_some()); @@ -660,7 +667,7 @@ fn classes_resolve_by_name_and_namespace() { #[test] fn field_offsets_resolve_declared_and_inherited() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let game_manager = image.get_class(process, module, "GameManager").unwrap(); assert_eq!( @@ -676,7 +683,7 @@ fn field_offsets_resolve_declared_and_inherited() { #[test] fn nested_classes_resolve_by_their_written_name() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); assert!(image .get_class(process, module, "Game.Outer+Inner") @@ -693,25 +700,11 @@ fn nested_classes_resolve_by_their_written_name() { }); } -// V2020's table never measured where a class keeps its declaring type, so a -// nested lookup on it must miss cleanly rather than answer with whichever -// class carries the leaf name. -#[test] -fn nested_lookups_without_a_measured_offset_answer_nothing() { - on_fixture(Version::V2020, |process, module| { - let image = module.get_default_image(process).unwrap(); - assert!(image - .get_class(process, module, "Game.Outer+Inner") - .is_none()); - assert!(image.get_class(process, module, "GameManager").is_some()); - }); -} - // The climb stops at UnityEngine's namespace, so an engine field never // resolves. #[test] fn field_climbs_stop_at_the_engine() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let game_manager = image.get_class(process, module, "GameManager").unwrap(); assert!(game_manager @@ -722,7 +715,7 @@ fn field_climbs_stop_at_the_engine() { #[test] fn statics_resolve_from_the_class() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let game_manager = image.get_class(process, module, "GameManager").unwrap(); assert_eq!( @@ -736,7 +729,7 @@ fn statics_resolve_from_the_class() { // table, not the table of the class the lookup started at. #[test] fn static_instances_resolve_through_the_declaring_class() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let boss = image.get_class(process, module, "Boss").unwrap(); assert_eq!( @@ -756,7 +749,7 @@ fn static_instances_resolve_through_the_declaring_class() { // and the read returns the live count's elements, never the backing capacity's. #[test] fn lists_resolve_through_their_own_class() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let at = Address::new(BASE + 0x18); let offsets = module.get_list_offsets(process, at).unwrap(); let read = module.read_list::(process, offsets, at).unwrap(); @@ -766,7 +759,7 @@ fn lists_resolve_through_their_own_class() { #[test] fn lists_derived_from_corlibs_list_resolve() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let at = Address::new(BASE + 0x20); let offsets = module.get_list_offsets(process, at).unwrap(); let read = module.read_list::(process, offsets, at).unwrap(); @@ -776,7 +769,7 @@ fn lists_derived_from_corlibs_list_resolve() { #[test] fn list_shaped_objects_are_not_lists() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { assert!(module .get_list_offsets(process, Address::new(BASE + 0x28)) .is_none()); @@ -787,7 +780,7 @@ fn list_shaped_objects_are_not_lists() { // element preserved at its position. #[test] fn reference_lists_resolve_their_element_addresses() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let at = Address::new(BASE + 0x30); let offsets = module.get_list_offsets(process, at).unwrap(); let read = module @@ -804,7 +797,7 @@ fn reference_lists_resolve_their_element_addresses() { // resolved against the object's own class read off its head. #[test] fn pointers_dereference_through_a_static_root() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let pointer = UnityPointer::<2>::new("GameManager", 0, &["instance", "points"]); assert_eq!(pointer.deref::(process, module, &image).unwrap(), 888); @@ -824,7 +817,7 @@ fn public_types_keep_their_properties() { is_copy::(); is_copy::(); - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let _ = double_ended(image.classes(process, module)); }); @@ -833,6 +826,8 @@ fn public_types_keep_their_properties() { // A 32 bit target lays the assemblies vector and its pointers at four bytes. #[test] fn images_resolve_on_32_bit_targets() { + let offsets = measured(MEASURED_6000_5, PointerSize::Bit32); + let name_at = offsets.image.assembly_name.unwrap() as u64; let mut i = vec![0; 0x1000]; let narrow = |i: &mut [u8], at: u64, target: u64| { put(i, at, &(target as u32).to_le_bytes()); @@ -843,16 +838,76 @@ fn images_resolve_on_32_bit_targets() { narrow(&mut i, 0x4, BASE + 0x44); // and end, one assembly along narrow(&mut i, 0x40, BASE + 0x80); narrow(&mut i, 0x80, BASE + 0x100); // Il2CppAssembly.image - narrow(&mut i, 0x80 + 0x18, BASE + 0x800); // Il2CppAssembly.aname + narrow(&mut i, 0x100 + name_at, BASE + 0x800); // Il2CppImage.nameNoExt with_process(&[(BASE, &i)], |process| { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - version: Version::V2022, - offsets: IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + offsets, pointer_size: PointerSize::Bit32, }; assert!(module.get_default_image(process).is_some()); }); } + +// An image says where its first type sits in the type table. Players up to +// 2020.1 keep that index in the image. Later players keep a handle there, +// and the index sits behind it. +#[test] +fn class_walk_reads_the_type_start_where_the_build_keeps_it() { + use super::offsets::TypeStart; + + let walk = |build: &'static super::builds::Build| { + let offsets = &build.offsets; + let mut i = vec![0; 0x1000]; + let ptr = |i: &mut [u8], at: u64, target: u64| { + put(i, at, &target.to_le_bytes()); + }; + put(&mut i, 0x800, b"Timer"); + ptr(&mut i, 0x10, BASE + 0x200); // the type table + ptr(&mut i, 0x210, BASE + 0x300); // its entry 2 + ptr(&mut i, 0x300 + offsets.class.name as u64, BASE + 0x800); + put( + &mut i, + 0x100 + offsets.image.type_count as u64, + &1_u32.to_le_bytes(), + ); + match offsets.image.type_start { + TypeStart::Inline(at) => put(&mut i, 0x100 + at as u64, &2_u32.to_le_bytes()), + TypeStart::Handle(at) => { + ptr(&mut i, 0x100 + at as u64, BASE + 0x180); + put(&mut i, 0x180, &2_u32.to_le_bytes()); + } + } + + with_process(&[(BASE, &i)], |process| { + let module = Module { + assemblies: Address::new(BASE), + type_info_definition_table: Address::new(BASE + 0x10), + offsets, + pointer_size: PointerSize::Bit64, + }; + let image = super::Image { + image: Address::new(BASE + 0x100), + }; + image + .get_class(process, &module, "Timer") + .map(|class| class.class) + }) + }; + + let inline = super::builds::nearest((2018, 4, 36, 54151), PointerSize::Bit64).unwrap(); + assert!(matches!( + inline.offsets.image.type_start, + TypeStart::Inline(0x18) + )); + assert_eq!(walk(inline), Some(Address::new(BASE + 0x300))); + + let handle = super::builds::nearest(MEASURED_6000_5, PointerSize::Bit64).unwrap(); + assert!(matches!( + handle.offsets.image.type_start, + TypeStart::Handle(0x28) + )); + assert_eq!(walk(handle), Some(Address::new(BASE + 0x300))); +} diff --git a/src/game_engine/unity/mod.rs b/src/game_engine/unity/mod.rs index 820e763f..b452c3f9 100644 --- a/src/game_engine/unity/mod.rs +++ b/src/game_engine/unity/mod.rs @@ -6,12 +6,12 @@ //! # async fn example(process: asr::Process) { //! use asr::{ //! future::retry, -//! game_engine::unity::il2cpp::{Module, Version}, +//! game_engine::unity::il2cpp::Module, //! Address, Address64, //! }; //! -//! // We first attach to the Mono module. Here we know that the game is using IL2CPP 2020. -//! let module = Module::wait_attach(&process, Version::V2020).await; +//! // We first attach to the IL2CPP module. +//! let module = Module::wait_attach_auto_detect(&process).await; //! // We access the .NET DLL that the game code is in. //! let image = module.wait_get_default_image(&process).await; //! From ef0f1b18a1026a03aab474af080bfb98e227fd4f Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Fri, 18 Sep 2026 16:57:05 +0200 Subject: [PATCH 3/3] expose measured il2cpp profiles --- src/game_engine/unity/il2cpp/builds.rs | 238 +++++++++--------- .../unity/il2cpp/collections_tests.rs | 6 +- src/game_engine/unity/il2cpp/mod.rs | 80 ++++-- src/game_engine/unity/il2cpp/offsets.rs | 178 ++++++++++--- src/game_engine/unity/il2cpp/profiles.rs | 147 +++++++++++ src/game_engine/unity/il2cpp/readers_tests.rs | 4 +- src/game_engine/unity/il2cpp/walk_tests.rs | 73 ++++-- 7 files changed, 509 insertions(+), 217 deletions(-) create mode 100644 src/game_engine/unity/il2cpp/profiles.rs diff --git a/src/game_engine/unity/il2cpp/builds.rs b/src/game_engine/unity/il2cpp/builds.rs index bcecb8ad..76b32340 100644 --- a/src/game_engine/unity/il2cpp/builds.rs +++ b/src/game_engine/unity/il2cpp/builds.rs @@ -3,16 +3,16 @@ //! `GameAssembly.pdb`. use super::offsets::{ - AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, IL2CPPOffsets, ImageOffsets, + AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, ImageOffsets, Profile, TypeOffsets, TypeStart, }; use crate::PointerSize; /// One measured IL2CPP player and the offsets from its PDB. +#[derive(Copy, Clone)] pub(super) struct Build { pub(super) unity: (u16, u16, u16, u16), - pub(super) pointer_size: PointerSize, - pub(super) offsets: IL2CPPOffsets, + pub(super) profile: Profile, } /// Finds the build for a player at its width. The Unity version is the four @@ -29,7 +29,7 @@ pub(super) fn nearest( let at_width = || { BUILDS .iter() - .filter(move |build| build.pointer_size == pointer_size) + .filter(move |build| build.profile.pointer_size == pointer_size) }; at_width() @@ -39,16 +39,16 @@ pub(super) fn nearest( } // The table reads from the oldest player to the newest. -static BUILDS: &[Build] = &[ +pub(super) const BUILDS: &[Build] = &[ // Unity 2018.4.36f1, metadata version 24, x64. // Offsets from the player's own GameAssembly.pdb. Build { unity: (2018, 4, 36, 54151), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -68,7 +68,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -76,7 +76,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -84,11 +84,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2018, 4, 36, 54151), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -108,7 +108,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -116,7 +116,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -124,11 +124,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2019, 4, 41, 9172), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -148,7 +148,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -156,7 +156,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -164,11 +164,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2019, 4, 41, 9172), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -188,7 +188,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -196,7 +196,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -204,11 +204,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2020, 1, 18, 38512), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -228,7 +228,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -236,7 +236,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -244,11 +244,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2020, 1, 18, 38512), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -268,7 +268,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -276,7 +276,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -284,11 +284,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2021, 3, 11, 23713), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -308,7 +308,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -316,7 +316,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -324,11 +324,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2021, 3, 11, 23713), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -348,7 +348,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -356,7 +356,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -364,11 +364,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2022, 3, 0, 4507), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -388,7 +388,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -396,7 +396,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -404,11 +404,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2022, 3, 0, 4507), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -428,7 +428,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -436,7 +436,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -444,11 +444,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2023, 1, 0, 2298), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -468,7 +468,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -476,7 +476,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -484,11 +484,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2023, 1, 0, 2298), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -508,7 +508,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -516,7 +516,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -524,11 +524,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2023, 1, 22, 16744), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -548,7 +548,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -556,7 +556,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -564,11 +564,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (2023, 1, 22, 16744), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -588,7 +588,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -596,7 +596,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -604,11 +604,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (6000, 2, 12, 40285), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -628,7 +628,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -636,7 +636,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -644,11 +644,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (6000, 2, 12, 40285), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -668,7 +668,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -676,7 +676,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -684,11 +684,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (6000, 3, 21, 9777), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -708,7 +708,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -716,7 +716,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -724,11 +724,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (6000, 3, 21, 9777), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -748,7 +748,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -756,7 +756,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -764,11 +764,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (6000, 5, 10, 54518), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -788,7 +788,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -796,7 +796,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -804,11 +804,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (6000, 5, 10, 54518), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -828,7 +828,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -836,7 +836,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -844,11 +844,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (6000, 7, 0, 5476), - pointer_size: PointerSize::Bit64, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), @@ -868,7 +868,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x10), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -876,7 +876,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, @@ -884,11 +884,11 @@ static BUILDS: &[Build] = &[ // Offsets from the player's own GameAssembly.pdb. Build { unity: (6000, 7, 0, 5476), - pointer_size: PointerSize::Bit32, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), @@ -908,7 +908,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x8), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -916,7 +916,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -937,7 +937,7 @@ mod tests { #[test] fn every_version_is_measured_at_both_widths() { for build in BUILDS { - let other = match build.pointer_size { + let other = match build.profile.pointer_size { PointerSize::Bit64 => PointerSize::Bit32, _ => PointerSize::Bit64, }; @@ -969,9 +969,9 @@ mod tests { #[test] fn nearest_is_the_build_itself_on_a_measured_version() { for build in BUILDS { - let found = nearest(build.unity, build.pointer_size).unwrap(); + let found = nearest(build.unity, build.profile.pointer_size).unwrap(); assert_eq!(found.unity, build.unity); - assert_eq!(found.pointer_size, build.pointer_size); + assert_eq!(found.profile.pointer_size, build.profile.pointer_size); } } @@ -979,7 +979,7 @@ mod tests { fn nearest_keeps_the_width() { let build = nearest((2021, 3, 5, 1), PointerSize::Bit32).unwrap(); assert_eq!(build.unity, (2021, 3, 11, 23713)); - assert_eq!(build.pointer_size, PointerSize::Bit32); + assert_eq!(build.profile.pointer_size, PointerSize::Bit32); assert!(nearest((2021, 3, 5, 1), PointerSize::Bit16).is_none()); } } diff --git a/src/game_engine/unity/il2cpp/collections_tests.rs b/src/game_engine/unity/il2cpp/collections_tests.rs index c575c69a..ee011e65 100644 --- a/src/game_engine/unity/il2cpp/collections_tests.rs +++ b/src/game_engine/unity/il2cpp/collections_tests.rs @@ -36,7 +36,7 @@ fn field(image: &mut [u8], at: u64, name: u64, type_: u64, offset: i32) { fn image(unity: (u16, u16, u16, u16)) -> Vec { let field_count_at = super::builds::nearest(unity, PointerSize::Bit64) .unwrap() - .offsets + .profile .class .field_count as u64; let mut i = vec![0; 0x3000]; @@ -204,9 +204,9 @@ fn module(unity: (u16, u16, u16, u16)) -> Module { Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x40), - offsets: &super::builds::nearest(unity, PointerSize::Bit64) + profile: super::builds::nearest(unity, PointerSize::Bit64) .unwrap() - .offsets, + .profile, pointer_size: PointerSize::Bit64, } } diff --git a/src/game_engine/unity/il2cpp/mod.rs b/src/game_engine/unity/il2cpp/mod.rs index 2b42eb7d..8ed937e2 100644 --- a/src/game_engine/unity/il2cpp/mod.rs +++ b/src/game_engine/unity/il2cpp/mod.rs @@ -16,9 +16,13 @@ pub use class::Class; mod pointer; pub use pointer::UnityPointer; mod offsets; -use offsets::{IL2CPPOffsets, TypeStart}; +pub use offsets::{ + AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, ImageOffsets, Profile, + TypeOffsets, TypeStart, +}; #[cfg(all(test, not(target_family = "wasm")))] mod collections_tests; +pub mod profiles; #[cfg(all(test, not(target_family = "wasm")))] mod readers_tests; #[cfg(all(test, not(target_family = "wasm")))] @@ -30,7 +34,7 @@ use super::{managed, DictionaryOffsets, HashSetOffsets, ListOffsets, ManagedStri pub struct Module { assemblies: Address, type_info_definition_table: Address, - offsets: &'static IL2CPPOffsets, + profile: Profile, pointer_size: PointerSize, } @@ -44,7 +48,7 @@ impl Module { let unity = Self::unity_version(process)?; let build = builds::nearest(unity, pointer_size)?; - let module = Self::attach_with(process, il2cpp_module, pointer_size, &build.offsets)?; + let module = Self::attach_with(process, il2cpp_module, build.profile)?; print_limited::<128>(&format_args!( "il2cpp: unity {}.{}.{}.{} takes the build measured on {}.{}.{}.{}", unity.0, @@ -59,6 +63,20 @@ impl Module { Some(module) } + /// Tries attaching to a Unity game that is using the IL2CPP backend with + /// the provided measured [`Profile`]. The profile's pointer width must + /// match the target process. Use a built-in [`profiles`] constant for a + /// known player, or construct a custom profile for another measured + /// layout. If the target is not known in advance, use + /// [`attach_auto_detect`](Self::attach_auto_detect) instead. + pub fn attach(process: &Process, profile: Profile) -> Option { + let il2cpp_module = Self::find_runtime_module(process)?; + let pointer_size = pe::MachineType::read(process, il2cpp_module.0)?.pointer_size()?; + (pointer_size == profile.pointer_size).then_some(())?; + + Self::attach_with(process, il2cpp_module, profile) + } + fn find_runtime_module(process: &Process) -> Option<(Address, u64)> { let address = process.get_module_address("GameAssembly.dll").ok()?; let size = pe::read_size_of_image(process, address)? as u64; @@ -81,9 +99,9 @@ impl Module { fn attach_with( process: &Process, il2cpp_module: (Address, u64), - pointer_size: PointerSize, - offsets: &'static IL2CPPOffsets, + profile: Profile, ) -> Option { + let pointer_size = profile.pointer_size; let (assemblies, type_info_definition_table) = match pointer_size { PointerSize::Bit64 => Self::globals_x64(process, il2cpp_module)?, PointerSize::Bit32 => Self::globals_x86(process, il2cpp_module)?, @@ -93,7 +111,7 @@ impl Module { Some(Self { assemblies, type_info_definition_table, - offsets, + profile, pointer_size, }) } @@ -225,7 +243,7 @@ impl Module { } fn walk(&self) -> managed::Walk { - let (metadata_handle, handle_is_inline) = match self.offsets.image.type_start { + let (metadata_handle, handle_is_inline) = match self.profile.image.type_start { TypeStart::Inline(at) => (at, true), TypeStart::Handle(at) => (at, false), }; @@ -234,34 +252,34 @@ impl Module { runtime: managed::Runtime::Il2Cpp(managed::Il2CppRuntime { assemblies: self.assemblies, type_info_definition_table: self.type_info_definition_table, - type_count: self.offsets.image.type_count.into(), + type_count: self.profile.image.type_count.into(), metadata_handle: metadata_handle.into(), handle_is_inline, - field_count: self.offsets.class.field_count, - static_fields: self.offsets.class.static_fields.into(), - cached_class: self.offsets.generic.cached_class, - type_data: self.offsets.type_words.data, - type_kind: self.offsets.type_words.kind, + field_count: self.profile.class.field_count, + static_fields: self.profile.class.static_fields.into(), + cached_class: self.profile.generic.cached_class, + type_data: self.profile.type_.data, + type_kind: self.profile.type_.kind, }), offsets: managed::WalkOffsets { assembly: managed::AssemblyOffsets { - name_in_image: self.offsets.image.assembly_name.map(u16::from), - name_in_assembly: self.offsets.assembly.aname.map(u16::from), - image: self.offsets.assembly.image.into(), + name_in_image: self.profile.image.assembly_name.map(u16::from), + name_in_assembly: self.profile.assembly.name.map(u16::from), + image: self.profile.assembly.image.into(), }, class: managed::ClassOffsets { - name: self.offsets.class.name.into(), - namespace: self.offsets.class.namespace.into(), - parent: self.offsets.class.parent.into(), - declaring: self.offsets.class.declaring_type, - instance_size: self.offsets.class.instance_size, - fields: self.offsets.class.fields.into(), + name: self.profile.class.name.into(), + namespace: self.profile.class.namespace.into(), + parent: self.profile.class.parent.into(), + declaring: self.profile.class.declaring_type, + instance_size: self.profile.class.instance_size, + fields: self.profile.class.fields.into(), }, field: managed::FieldOffsets { - name: self.offsets.field.name.into(), - type_: self.offsets.field.type_, - offset: self.offsets.field.offset.into(), - stride: self.offsets.field.struct_size.into(), + name: self.profile.field.name.into(), + type_: self.profile.field.type_, + offset: self.profile.field.offset.into(), + stride: self.profile.field.size.into(), }, }, stop: managed::ClimbStop::UNITY, @@ -512,6 +530,16 @@ impl Module { retry(|| Self::attach_auto_detect(process)).await } + /// Attaches to a Unity game that is using the IL2CPP backend with the + /// provided measured [`Profile`]. The profile's pointer width must match + /// the target process. + /// + /// This is the `await`able version of [`attach`](Self::attach), yielding + /// back to the runtime between each try. + pub async fn wait_attach(process: &Process, profile: Profile) -> Module { + retry(|| Self::attach(process, 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 diff --git a/src/game_engine/unity/il2cpp/offsets.rs b/src/game_engine/unity/il2cpp/offsets.rs index 35bec88f..88f1e873 100644 --- a/src/game_engine/unity/il2cpp/offsets.rs +++ b/src/game_engine/unity/il2cpp/offsets.rs @@ -1,59 +1,157 @@ -pub(super) struct IL2CPPOffsets { - pub(super) assembly: AssemblyOffsets, - pub(super) image: ImageOffsets, - pub(super) class: ClassOffsets, - pub(super) generic: GenericOffsets, - pub(super) type_words: TypeOffsets, - pub(super) field: FieldInfoOffsets, +use crate::PointerSize; + +/// A complete measured IL2CPP runtime layout. +/// +/// Profiles are deliberately exhaustive. If a future ASR release needs more +/// layout information, custom profiles must provide it rather than silently +/// inheriting an offset that may be wrong. Built-in measured profiles are +/// available in [`profiles`](super::profiles). +/// +/// A custom profile lists the complete layout. Every field is written out +/// intentionally: using struct-update syntax would opt the custom profile in +/// to silently inheriting fields added to the source profile in the future. +/// +/// ``` +/// use asr::{ +/// game_engine::unity::il2cpp::{ +/// AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, +/// ImageOffsets, Profile, TypeOffsets, TypeStart, +/// }, +/// PointerSize, +/// }; +/// +/// const CUSTOM_PROFILE: Profile = Profile { +/// pointer_size: PointerSize::Bit64, +/// assembly: AssemblyOffsets { +/// image: 0x0, +/// name: None, +/// }, +/// image: ImageOffsets { +/// assembly_name: Some(0x8), +/// type_count: 0x18, +/// type_start: TypeStart::Handle(0x28), +/// }, +/// class: ClassOffsets { +/// name: 0x10, +/// namespace: 0x18, +/// declaring_type: Some(0x50), +/// parent: 0x58, +/// fields: 0x80, +/// static_fields: 0xb8, +/// instance_size: Some(0xf8), +/// field_count: 0x124, +/// }, +/// generic: GenericOffsets { +/// cached_class: Some(0x18), +/// }, +/// type_: TypeOffsets { +/// data: Some(0x0), +/// kind: Some(0xa), +/// }, +/// field: FieldInfoOffsets { +/// name: 0x0, +/// type_: Some(0x8), +/// offset: 0x18, +/// size: 0x20, +/// }, +/// }; +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Profile { + /// The pointer width this profile was measured for. + pub pointer_size: PointerSize, + /// Offsets within `Il2CppAssembly`. + pub assembly: AssemblyOffsets, + /// Offsets within `Il2CppImage`. + pub image: ImageOffsets, + /// Offsets within `Il2CppClass`. + pub class: ClassOffsets, + /// Offsets within `Il2CppGenericClass`. + pub generic: GenericOffsets, + /// Offsets within `Il2CppType`. + pub type_: TypeOffsets, + /// Offsets and size of `FieldInfo`. + pub field: FieldInfoOffsets, } -pub(super) struct AssemblyOffsets { - pub(super) image: u8, - pub(super) aname: Option, // Either this or ImageOffsets::assembly_name locates the name +/// Offsets within `Il2CppAssembly`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct AssemblyOffsets { + /// The image pointer. + pub image: u8, + /// The assembly-name pointer, when the name is stored on the assembly. + /// Otherwise [`ImageOffsets::assembly_name`] locates it. + pub name: Option, } -pub(super) struct ImageOffsets { - pub(super) assembly_name: Option, // Either this or AssemblyOffsets::aname locates the name - pub(super) type_count: u8, - pub(super) type_start: TypeStart, +/// Offsets within `Il2CppImage`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ImageOffsets { + /// The assembly-name pointer, when the name is stored on the image. + /// Otherwise [`AssemblyOffsets::name`] locates it. + pub assembly_name: Option, + /// The number of types in the image. + pub type_count: u8, + /// Where the image stores the index of its first type. + pub type_start: TypeStart, } /// Where an image keeps the index of its first type in the type table. -#[derive(Copy, Clone)] -pub(super) enum TypeStart { - /// The index sits in the image, at this offset. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum TypeStart { + /// The index sits directly in the image at this offset. Inline(u8), - /// A pointer sits in the image at this offset. The index sits where it + /// A pointer sits in the image at this offset; the index sits where it /// points. Handle(u8), } -pub(super) struct ClassOffsets { - pub(super) name: u8, - pub(super) namespace: u8, - pub(super) declaring_type: Option, // Where a class keeps the one declaring it - pub(super) parent: u8, - pub(super) fields: u8, - pub(super) static_fields: u8, - pub(super) instance_size: Option, // What one instance occupies, boxed header included - pub(super) field_count: u16, +/// Offsets within `Il2CppClass`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ClassOffsets { + /// The class-name pointer. + pub name: u8, + /// The namespace-name pointer. + pub namespace: u8, + /// The declaring class, when measured. + pub declaring_type: Option, + /// The parent-class pointer. + pub parent: u8, + /// The field table pointer. + pub fields: u8, + /// The static-field storage pointer. + pub static_fields: u8, + /// The boxed instance size, when measured. + pub instance_size: Option, + /// The number of fields declared by the class. + pub field_count: u16, } -// Il2CppGenericClass keeps the class an instantiation resolved to. -pub(super) struct GenericOffsets { - pub(super) cached_class: Option, +/// Offsets within `Il2CppGenericClass`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct GenericOffsets { + /// The class cached for a resolved generic instantiation, when measured. + pub cached_class: Option, } -// Il2CppType's own words: the data pointer and the element kind byte. -pub(super) struct TypeOffsets { - pub(super) data: Option, - pub(super) kind: Option, +/// Offsets within `Il2CppType`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct TypeOffsets { + /// The type-data pointer, when measured. + pub data: Option, + /// The byte describing the element kind, when measured. + pub kind: Option, } -pub(super) struct FieldInfoOffsets { - pub(super) name: u8, - pub(super) type_: Option, // Where a field keeps its Il2CppType - - pub(super) offset: u8, - pub(super) struct_size: u8, +/// Offsets and size of `FieldInfo`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct FieldInfoOffsets { + /// The field-name pointer. + pub name: u8, + /// The field's `Il2CppType` pointer, when measured. + pub type_: Option, + /// The field offset within its object or static storage. + pub offset: u8, + /// The size of one `FieldInfo` entry. + pub size: u8, } diff --git a/src/game_engine/unity/il2cpp/profiles.rs b/src/game_engine/unity/il2cpp/profiles.rs new file mode 100644 index 00000000..50253966 --- /dev/null +++ b/src/game_engine/unity/il2cpp/profiles.rs @@ -0,0 +1,147 @@ +//! Complete IL2CPP layouts measured from specific Unity players. +//! +//! Each constant describes one architecture of one measured player. Pass a +//! profile directly to [`Module::attach`](super::Module::attach) or +//! [`Module::wait_attach`](super::Module::wait_attach) when the target game is +//! known. This avoids linking the automatic lookup table and its other +//! profiles into the final auto splitter. + +use super::{builds::BUILDS, Profile}; + +macro_rules! profiles { + ($(#[$meta:meta] $name:ident = $index:literal;)+) => { + $( + #[$meta] + pub const $name: Profile = BUILDS[$index].profile; + )+ + }; +} + +profiles! { + /// Unity 2018.4.36f1, file version `2018.4.36.54151`, x86-64. + UNITY_2018_4_36F1_X86_64 = 0; + /// Unity 2018.4.36f1, file version `2018.4.36.54151`, x86. + UNITY_2018_4_36F1_X86 = 1; + /// Unity 2019.4.41f2, file version `2019.4.41.9172`, x86-64. + UNITY_2019_4_41F2_X86_64 = 2; + /// Unity 2019.4.41f2, file version `2019.4.41.9172`, x86. + UNITY_2019_4_41F2_X86 = 3; + /// Unity 2020.1.18f1, file version `2020.1.18.38512`, x86-64. + UNITY_2020_1_18F1_X86_64 = 4; + /// Unity 2020.1.18f1, file version `2020.1.18.38512`, x86. + UNITY_2020_1_18F1_X86 = 5; + /// Unity 2021.3.11f1, file version `2021.3.11.23713`, x86-64. + UNITY_2021_3_11F1_X86_64 = 6; + /// Unity 2021.3.11f1, file version `2021.3.11.23713`, x86. + UNITY_2021_3_11F1_X86 = 7; + /// Unity 2022.3.0f1, file version `2022.3.0.4507`, x86-64. + UNITY_2022_3_0F1_X86_64 = 8; + /// Unity 2022.3.0f1, file version `2022.3.0.4507`, x86. + UNITY_2022_3_0F1_X86 = 9; + /// Unity 2023.1.0f1, file version `2023.1.0.2298`, x86-64. + UNITY_2023_1_0F1_X86_64 = 10; + /// Unity 2023.1.0f1, file version `2023.1.0.2298`, x86. + UNITY_2023_1_0F1_X86 = 11; + /// Unity 2023.1.22f1, file version `2023.1.22.16744`, x86-64. + UNITY_2023_1_22F1_X86_64 = 12; + /// Unity 2023.1.22f1, file version `2023.1.22.16744`, x86. + UNITY_2023_1_22F1_X86 = 13; + /// Unity 6000.2.12f1, file version `6000.2.12.40285`, x86-64. + UNITY_6000_2_12F1_X86_64 = 14; + /// Unity 6000.2.12f1, file version `6000.2.12.40285`, x86. + UNITY_6000_2_12F1_X86 = 15; + /// Unity 6000.3.21f1, file version `6000.3.21.9777`, x86-64. + UNITY_6000_3_21F1_X86_64 = 16; + /// Unity 6000.3.21f1, file version `6000.3.21.9777`, x86. + UNITY_6000_3_21F1_X86 = 17; + /// Unity 6000.5.10f1, file version `6000.5.10.54518`, x86-64. + UNITY_6000_5_10F1_X86_64 = 18; + /// Unity 6000.5.10f1, file version `6000.5.10.54518`, x86. + UNITY_6000_5_10F1_X86 = 19; + /// Unity 6000.7.0a3, file version `6000.7.0.5476`, x86-64. + UNITY_6000_7_0A3_X86_64 = 20; + /// Unity 6000.7.0a3, file version `6000.7.0.5476`, x86. + UNITY_6000_7_0A3_X86 = 21; +} + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::*; + use crate::PointerSize; + + #[test] + fn constants_name_the_expected_profiles() { + let profiles = [ + ( + (2018, 4, 36, 54151), + UNITY_2018_4_36F1_X86_64, + UNITY_2018_4_36F1_X86, + ), + ( + (2019, 4, 41, 9172), + UNITY_2019_4_41F2_X86_64, + UNITY_2019_4_41F2_X86, + ), + ( + (2020, 1, 18, 38512), + UNITY_2020_1_18F1_X86_64, + UNITY_2020_1_18F1_X86, + ), + ( + (2021, 3, 11, 23713), + UNITY_2021_3_11F1_X86_64, + UNITY_2021_3_11F1_X86, + ), + ( + (2022, 3, 0, 4507), + UNITY_2022_3_0F1_X86_64, + UNITY_2022_3_0F1_X86, + ), + ( + (2023, 1, 0, 2298), + UNITY_2023_1_0F1_X86_64, + UNITY_2023_1_0F1_X86, + ), + ( + (2023, 1, 22, 16744), + UNITY_2023_1_22F1_X86_64, + UNITY_2023_1_22F1_X86, + ), + ( + (6000, 2, 12, 40285), + UNITY_6000_2_12F1_X86_64, + UNITY_6000_2_12F1_X86, + ), + ( + (6000, 3, 21, 9777), + UNITY_6000_3_21F1_X86_64, + UNITY_6000_3_21F1_X86, + ), + ( + (6000, 5, 10, 54518), + UNITY_6000_5_10F1_X86_64, + UNITY_6000_5_10F1_X86, + ), + ( + (6000, 7, 0, 5476), + UNITY_6000_7_0A3_X86_64, + UNITY_6000_7_0A3_X86, + ), + ]; + + for (unity, x86_64, x86) in profiles { + assert_eq!( + super::super::builds::nearest(unity, PointerSize::Bit64) + .unwrap() + .profile, + x86_64, + ); + assert_eq!( + super::super::builds::nearest(unity, PointerSize::Bit32) + .unwrap() + .profile, + x86, + ); + } + } +} diff --git a/src/game_engine/unity/il2cpp/readers_tests.rs b/src/game_engine/unity/il2cpp/readers_tests.rs index 7279547f..b51d2e3d 100644 --- a/src/game_engine/unity/il2cpp/readers_tests.rs +++ b/src/game_engine/unity/il2cpp/readers_tests.rs @@ -52,9 +52,9 @@ fn on_fixture(test: impl FnOnce(&Process, &Module)) { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - offsets: &super::builds::nearest((2022, 3, 0, 4507), PointerSize::Bit64) + profile: super::builds::nearest((2022, 3, 0, 4507), PointerSize::Bit64) .unwrap() - .offsets, + .profile, pointer_size: PointerSize::Bit64, }; test(process, &module); diff --git a/src/game_engine/unity/il2cpp/walk_tests.rs b/src/game_engine/unity/il2cpp/walk_tests.rs index 1df923cb..b7b7bfa9 100644 --- a/src/game_engine/unity/il2cpp/walk_tests.rs +++ b/src/game_engine/unity/il2cpp/walk_tests.rs @@ -4,7 +4,7 @@ //! the literal numbers of the Unity 2019.4 and 6000.3 layouts, copied by hand, //! so the walk is checked against the layout rather than against itself. -use super::{IL2CPPOffsets, Module, UnityPointer}; +use super::{Module, Profile, UnityPointer}; use crate::runtime::mock::{poll_once, with_process}; use crate::{Address, PointerSize, Process}; @@ -32,6 +32,14 @@ const UNITY_PLAYER: u64 = 0x1900_0000; impl Player { fn attach(&self) -> Option { + self.with_process(Module::attach_auto_detect) + } + + fn attach_profile(&self, profile: Profile) -> Option { + self.with_process(|process| Module::attach(process, profile)) + } + + fn with_process(&self, f: impl FnOnce(&Process) -> T) -> T { let game_assembly = Self::game_assembly(); let unity_player = Self::unity_player(self.unity); crate::runtime::mock::with_modules( @@ -43,7 +51,7 @@ impl Player { ("GameAssembly.dll", GAME_ASSEMBLY, 0x1000), ("UnityPlayer.dll", UNITY_PLAYER, 0x1000), ], - Module::attach_auto_detect, + f, ) } @@ -118,8 +126,8 @@ const MEASURED_2019: (u16, u16, u16, u16) = (2019, 4, 41, 9172); const MEASURED_2022: (u16, u16, u16, u16) = (2022, 3, 0, 4507); const MEASURED_6000_5: (u16, u16, u16, u16) = (6000, 5, 10, 54518); -fn measured(unity: (u16, u16, u16, u16), pointer_size: PointerSize) -> &'static IL2CPPOffsets { - &super::builds::nearest(unity, pointer_size).unwrap().offsets +fn measured(unity: (u16, u16, u16, u16), pointer_size: PointerSize) -> Profile { + super::builds::nearest(unity, pointer_size).unwrap().profile } // A game on a measured player attaches with the offsets measured on that @@ -136,7 +144,21 @@ fn attach_auto_detect_uses_a_measured_build() { module.type_info_definition_table, Address::new(GAME_ASSEMBLY + 0x910) ); - assert_eq!(module.offsets.class.static_fields, 0xA0); + assert_eq!(module.profile.class.static_fields, 0xA0); +} + +#[test] +fn attach_uses_the_explicit_profile_and_checks_its_width() { + let player = Player { + unity: MEASURED_6000_5, + }; + let module = player + .attach_profile(super::profiles::UNITY_6000_5_10F1_X86_64) + .unwrap(); + assert_eq!(module.profile.class.static_fields, 0xa0); + assert!(player + .attach_profile(super::profiles::UNITY_6000_5_10F1_X86) + .is_none()); } // A player nobody measured takes the nearest build. @@ -148,7 +170,7 @@ fn attach_auto_detect_takes_the_nearest_build_for_an_unmeasured_player() { .attach() .unwrap(); let nearest = super::builds::nearest((2021, 3, 11, 23713), PointerSize::Bit64).unwrap(); - assert!(core::ptr::eq(module.offsets, &nearest.offsets)); + assert_eq!(module.profile, nearest.profile); } // The x86 code that points at both globals. The assemblies loop reads the vector's @@ -208,7 +230,6 @@ fn attach_x86(process: &crate::Process) -> Option { Module::attach_with( process, (Address::new(BASE), 0x1000), - PointerSize::Bit32, measured(MEASURED_6000_5, PointerSize::Bit32), ) } @@ -346,7 +367,6 @@ fn x64_globals_resolve_from_a_mapped_image() { let module = Module::attach_with( process, (Address::new(BASE), 0x1000), - PointerSize::Bit64, measured(MEASURED_6000_5, PointerSize::Bit64), ) .unwrap(); @@ -366,7 +386,6 @@ fn x64_scanner_refuses_an_x86_image() { assert!(Module::attach_with( process, (Address::new(BASE), 0x1000), - PointerSize::Bit64, measured(MEASURED_6000_5, PointerSize::Bit64), ) .is_none()); @@ -378,9 +397,9 @@ fn x64_scanner_refuses_an_x86_image() { #[test] fn assembly_names_resolve_through_the_image() { let build = super::builds::nearest(MEASURED_6000_5, PointerSize::Bit64).unwrap(); - let offsets = &build.offsets; - assert!(offsets.assembly.aname.is_none()); - let name_at = offsets.image.assembly_name.unwrap() as u64; + let profile = build.profile; + assert!(profile.assembly.name.is_none()); + let name_at = profile.image.assembly_name.unwrap() as u64; let mut i = vec![0; 0x1000]; let ptr = |i: &mut [u8], at: u64, target: u64| { @@ -397,7 +416,7 @@ fn assembly_names_resolve_through_the_image() { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - offsets, + profile, pointer_size: PointerSize::Bit64, }; let image = module.get_default_image(process).unwrap(); @@ -413,7 +432,7 @@ fn ptr(image: &mut [u8], at: u64, target: u64) { // definition table sliced by the image's handle, a parent chain reaching a // UnityEngine class, a static table, and a live object heading with its class. fn image(unity: (u16, u16, u16, u16)) -> Vec { - use super::offsets::TypeStart; + use super::TypeStart; let offsets = measured(unity, PointerSize::Bit64); let type_count_at = offsets.image.type_count as u64; @@ -459,7 +478,7 @@ fn image(unity: (u16, u16, u16, u16)) -> Vec { // the assembly name lives either on the assembly or on that image. let assembly = |i: &mut [u8], at: u64, image: u64, name: u64| { ptr(i, at + offsets.assembly.image as u64, BASE + image); - if let Some(name_at) = offsets.assembly.aname { + if let Some(name_at) = offsets.assembly.name { ptr(i, at + name_at as u64, BASE + name); } else if let Some(name_at) = offsets.image.assembly_name { ptr(i, image + name_at as u64, BASE + name); @@ -629,7 +648,7 @@ fn module(unity: (u16, u16, u16, u16)) -> Module { Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - offsets: measured(unity, PointerSize::Bit64), + profile: measured(unity, PointerSize::Bit64), pointer_size: PointerSize::Bit64, } } @@ -826,8 +845,8 @@ fn public_types_keep_their_properties() { // A 32 bit target lays the assemblies vector and its pointers at four bytes. #[test] fn images_resolve_on_32_bit_targets() { - let offsets = measured(MEASURED_6000_5, PointerSize::Bit32); - let name_at = offsets.image.assembly_name.unwrap() as u64; + let profile = measured(MEASURED_6000_5, PointerSize::Bit32); + let name_at = profile.image.assembly_name.unwrap() as u64; let mut i = vec![0; 0x1000]; let narrow = |i: &mut [u8], at: u64, target: u64| { put(i, at, &(target as u32).to_le_bytes()); @@ -844,7 +863,7 @@ fn images_resolve_on_32_bit_targets() { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - offsets, + profile, pointer_size: PointerSize::Bit32, }; assert!(module.get_default_image(process).is_some()); @@ -856,10 +875,10 @@ fn images_resolve_on_32_bit_targets() { // and the index sits behind it. #[test] fn class_walk_reads_the_type_start_where_the_build_keeps_it() { - use super::offsets::TypeStart; + use super::TypeStart; let walk = |build: &'static super::builds::Build| { - let offsets = &build.offsets; + let profile = build.profile; let mut i = vec![0; 0x1000]; let ptr = |i: &mut [u8], at: u64, target: u64| { put(i, at, &target.to_le_bytes()); @@ -867,13 +886,13 @@ fn class_walk_reads_the_type_start_where_the_build_keeps_it() { put(&mut i, 0x800, b"Timer"); ptr(&mut i, 0x10, BASE + 0x200); // the type table ptr(&mut i, 0x210, BASE + 0x300); // its entry 2 - ptr(&mut i, 0x300 + offsets.class.name as u64, BASE + 0x800); + ptr(&mut i, 0x300 + profile.class.name as u64, BASE + 0x800); put( &mut i, - 0x100 + offsets.image.type_count as u64, + 0x100 + profile.image.type_count as u64, &1_u32.to_le_bytes(), ); - match offsets.image.type_start { + match profile.image.type_start { TypeStart::Inline(at) => put(&mut i, 0x100 + at as u64, &2_u32.to_le_bytes()), TypeStart::Handle(at) => { ptr(&mut i, 0x100 + at as u64, BASE + 0x180); @@ -885,7 +904,7 @@ fn class_walk_reads_the_type_start_where_the_build_keeps_it() { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - offsets, + profile, pointer_size: PointerSize::Bit64, }; let image = super::Image { @@ -899,14 +918,14 @@ fn class_walk_reads_the_type_start_where_the_build_keeps_it() { let inline = super::builds::nearest((2018, 4, 36, 54151), PointerSize::Bit64).unwrap(); assert!(matches!( - inline.offsets.image.type_start, + inline.profile.image.type_start, TypeStart::Inline(0x18) )); assert_eq!(walk(inline), Some(Address::new(BASE + 0x300))); let handle = super::builds::nearest(MEASURED_6000_5, PointerSize::Bit64).unwrap(); assert!(matches!( - handle.offsets.image.type_start, + handle.profile.image.type_start, TypeStart::Handle(0x28) )); assert_eq!(walk(handle), Some(Address::new(BASE + 0x300)));