Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions src/game_engine/unity/managed/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ pub struct Il2CppRuntime {
}

impl MonoRuntime {
/// Reads the address of a class's vtable in the first domain, which every
/// instance of the class begins with a pointer to.
pub fn vtable_address(
&self,
process: &Process,
pointer_size: PointerSize,
class: ClassRef,
) -> Option<Address> {
let runtime_info = process
.read_pointer(class.address + self.runtime_info, pointer_size)
.ok()
.filter(|address| !address.is_null())?;

process
.read_pointer(runtime_info + pointer_size as u64, pointer_size)
.ok()
.filter(|address| !address.is_null())
}

// The class whose count slot holds this class's count: a generic instance
// carries the inflated fields itself but no count, so the definition it
// was made from answers, reached through the instantiation descriptor.
Expand Down Expand Up @@ -174,15 +193,7 @@ impl Runtime {
) -> Option<Address> {
let slot = match self {
Self::Mono(mono) => {
let runtime_info = process
.read_pointer(class.address + mono.runtime_info, pointer_size)
.ok()
.filter(|address| !address.is_null())?;

let vtables = process
.read_pointer(runtime_info + pointer_size as u64, pointer_size)
.ok()
.filter(|address| !address.is_null())?;
let vtables = mono.vtable_address(process, pointer_size, class)?;

if mono.statics_in_vtable_data {
vtables + mono.vtable_size
Expand Down
51 changes: 50 additions & 1 deletion src/game_engine/unity/mono/class.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::super::managed::ClassRef;
use super::super::managed::{self, ClassRef};
use super::Module;
use crate::{future::retry, Address, Process};

Expand Down Expand Up @@ -51,6 +51,55 @@ impl Class {
.await
}

/// Reads the class of an object found in memory.
///
/// Every managed object begins with a pointer to its class's vtable, and a
/// vtable begins with a pointer to its class, so an object can be resolved
/// back to a [`Class`] whose fields are then available by name.
///
/// This is the only way to reach a generic instantiation such as
/// `HashSet<string>`. Each instantiation is a distinct class with its own
/// field offsets, and none of them can be looked up in an
/// [`Image`](super::Image) by name -- but any instance points at its own.
///
/// # Inflated generics may have no field names
///
/// Mono fills a class's field table in lazily, and for an inflated generic
/// nothing necessarily has. [`get_field_offset`](Self::get_field_offset)
/// can therefore return [`None`] for every field of a class resolved this
/// way, against an object that is plainly an instance of it -- and the
/// same lookup may succeed against another process running the same build,
/// so it is runtime state rather than anything a version check could
/// predict. Code that must not fail on a collection needs a fallback to
/// the known layout, guarded by a check that the object agrees with it.
pub fn of_object(process: &Process, module: &Module, object: Address) -> Option<Self> {
module
.walk()
.object_class(process, object)
.map(|class| Class {
class: class.address,
})
}

/// Returns the address of this class's `MonoVTable` in the first domain.
///
/// Every managed object begins with a pointer to the vtable of its class,
/// so this doubles as an identity handle for the class: an object at
/// `addr` is an instance of this exact class if and only if the pointer at
/// `addr` equals this value.
///
/// This is useful for games where the object of interest cannot be reached
/// by walking static fields. Games built around constructor-injection
/// dependency injection often have no static roots at all, which makes
/// [`UnityPointer`](super::UnityPointer) inapplicable, and the only way to
/// find a service is to scan the heap for the instance of its class.
pub fn get_vtable(&self, process: &Process, module: &Module) -> Option<Address> {
let managed::Runtime::Mono(mono) = module.walk().runtime else {
return None;
};
mono.vtable_address(process, module.pointer_size, ClassRef::new(self.class))
}

/// Returns the address of the static table of the class. This contains the
/// values of all the static fields.
pub fn get_static_table(&self, process: &Process, module: &Module) -> Option<Address> {
Expand Down
33 changes: 32 additions & 1 deletion src/game_engine/unity/mono/walk_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use super::offsets::{
AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, HashTableOffsets,
ImageOffsets, MonoVTableOffsets, TypeOffsets,
};
use super::{profiles, Library, Module, Profile, UnityPointer};
use super::{profiles, Class, Library, Module, Profile, UnityPointer};
use crate::runtime::mock::{poll_once, with_process};
use crate::{Address, PointerSize, Process};

Expand Down Expand Up @@ -571,6 +571,37 @@ fn statics_resolve_through_the_vtable() {
});
}

// The vtable is the one the statics were found through, and a class whose
// runtime info is unset has none.
#[test]
fn vtables_resolve_through_the_runtime_info() {
on_fixture(measured(), |process, module| {
let image = module.get_default_image(process).unwrap();
let game_manager = image.get_class(process, module, "GameManager").unwrap();
assert_eq!(
game_manager.get_vtable(process, module),
Some(Address::new(BASE + 0x1700)),
);

let outer = image.get_class(process, module, "Game.Outer").unwrap();
assert!(outer.get_vtable(process, module).is_none());
});
}

// A live object resolves to a class whose fields read by name, and an object
// whose vtable pointer is null resolves to nothing.
#[test]
fn objects_resolve_to_their_class() {
on_fixture(measured(), |process, module| {
let object = Address::new(BASE + 0x1900);
let class = Class::of_object(process, module, object).unwrap();
let points = class.get_field_offset(process, module, "points").unwrap();
assert_eq!(process.read::<u32>(object + points).ok(), Some(777));

assert!(Class::of_object(process, module, Address::new(BASE + 0x1908)).is_none());
});
}

// A static field found on a parent measures into the parent's own static
// table, not the table of the class the lookup started at.
#[test]
Expand Down
Loading