Skip to main content

asr/game_engine/unreal/
mod.rs

1//! Support for attaching to games using the Unreal Engine
2
3use core::{
4    array,
5    cell::RefCell,
6    iter::{self, FusedIterator},
7    mem::size_of,
8};
9
10use bytemuck::{CheckedBitPattern, Pod, Zeroable};
11
12use crate::{
13    file_format::pe, future::retry, signature::Signature, string::ArrayCString, Address, Error,
14    PointerSize, Process,
15};
16
17const CSTR: usize = 128;
18
19/// Represents access to a Unreal Engine game.
20///
21/// This struct gives immediate access to 2 important structs present in every UE game:
22/// - GEngine: a static object that persists throughout the process' lifetime
23/// - GWorld: a pointer to the currently loaded UWorld object
24pub struct Module {
25    pointer_size: PointerSize,
26    //version: Version,
27    offsets: &'static Offsets,
28    g_engine: Address,
29    g_world: Address,
30    fname_base: Address,
31}
32
33impl Module {
34    /// Tries attaching to a UE game. The UE version needs to be correct for this
35    /// function to work.
36    pub fn attach(
37        process: &Process,
38        version: Version,
39        main_module_address: Address,
40    ) -> Option<Self> {
41        let pointer_size = pe::MachineType::read(process, main_module_address)?.pointer_size()?;
42        let offsets = Offsets::new(version, pointer_size)?;
43        let module_size = pe::read_size_of_image(process, main_module_address)? as u64;
44        let module_range = (main_module_address, module_size);
45
46        let g_engine = {
47            const GENGINE: &[(Signature<7>, u8)] = &[
48                (Signature::new("A8 01 75 ?? 48 C7 05"), 7),
49                (Signature::new("A8 01 75 ?? C7 05 ??"), 6),
50            ];
51
52            let addr = GENGINE.iter().find_map(|(sig, offset)| {
53                Some(sig.scan_process_range(process, module_range)? + *offset)
54            })?;
55            addr + 0x8 + process.read::<i32>(addr).ok()?
56        };
57
58        let g_world = {
59            const GWORLD: &[(Signature<22>, u8)] = &[
60                (
61                    Signature::new(
62                        "80 7C 24 ?? 00 ?? ?? 48 8B 3D ?? ?? ?? ?? 48 ?? ?? ?? ?? ?? ?? ??",
63                    ),
64                    10,
65                ),
66                (
67                    Signature::new(
68                        "48 8B 05 ?? ?? ?? ?? 48 3B ?? 48 0F 44 ?? 48 89 05 ?? ?? ?? ?? E8",
69                    ),
70                    3,
71                ),
72            ];
73
74            let addr = GWORLD.iter().find_map(|(sig, offset)| {
75                Some(sig.scan_process_range(process, module_range)? + *offset)
76            })?;
77            addr + 0x4 + process.read::<i32>(addr).ok()?
78        };
79
80        let fname_base = {
81            const FNAME_POOL: &[(Signature<13>, u8)] = &[
82                (Signature::new("74 09 48 8D 15 ?? ?? ?? ?? EB 16 ?? ??"), 5),
83                (Signature::new("89 5C 24 ?? 89 44 24 ?? 74 ?? 48 8D 15"), 13),
84                (Signature::new("57 0F B7 F8 74 ?? B8 ?? ?? ?? ?? 8B 44"), 7),
85            ];
86
87            let addr = FNAME_POOL.iter().find_map(|(sig, offset)| {
88                Some(sig.scan_process_range(process, module_range)? + *offset)
89            })?;
90            addr + 0x4 + process.read::<i32>(addr).ok()?
91        };
92
93        Some(Self {
94            pointer_size,
95            //version,
96            offsets,
97            g_engine,
98            g_world,
99            fname_base,
100        })
101    }
102
103    /// Tries attaching to a UE game. The UE version needs to be correct for this
104    /// function to work.
105    pub async fn wait_attach(
106        process: &Process,
107        version: Version,
108        main_module_address: Address,
109    ) -> Self {
110        retry(|| Self::attach(process, version, main_module_address)).await
111    }
112
113    /// Returns the memory pointer to GWorld
114    pub const fn g_world(&self) -> Address {
115        self.g_world
116    }
117
118    /// Returns the memory pointer to GEngine
119    pub const fn g_engine(&self) -> Address {
120        self.g_engine
121    }
122
123    /// Returns the current instance of GWorld
124    pub fn get_g_world_uobject(&self, process: &Process) -> Option<UObject> {
125        match process.read_pointer(self.g_world, self.pointer_size) {
126            Ok(Address::NULL) | Err(_) => None,
127            Ok(val) => Some(UObject { object: val }),
128        }
129    }
130
131    /// Returns the current instance of GEngine
132    pub fn get_g_engine_uobject(&self, process: &Process) -> Option<UObject> {
133        match process.read_pointer(self.g_engine, self.pointer_size) {
134            Ok(Address::NULL) | Err(_) => None,
135            Ok(val) => Some(UObject { object: val }),
136        }
137    }
138
139    /// Returns string associated with provided FNameKey
140    pub fn get_fname<const N: usize>(
141        &self,
142        process: &Process,
143        key: FNameKey,
144    ) -> Result<ArrayCString<N>, Error> {
145        let addr = process.read_pointer(
146            self.fname_base + (self.pointer_size as u64).wrapping_mul(key.chunk_offset as u64 + 2),
147            self.pointer_size,
148        )? + (key.name_offset as u64).wrapping_mul(size_of::<u16>() as u64);
149
150        let string_size = process
151            .read::<u16>(addr)?
152            .checked_shr(6)
153            .unwrap_or_default() as usize;
154
155        let mut string = process.read::<ArrayCString<N>>(addr + size_of::<u16>() as u64)?;
156        string.set_len(string_size);
157
158        Ok(string)
159    }
160}
161
162/// An `UObject` is the base class of every Unreal Engine object,
163/// from which every other class in the UE engine inherits from.
164///
165/// This struct represents a currently running instance of any UE class,
166/// from which it's possible to perform introspection in order to return
167/// various information, such as the class' `FName`, property names, offsets, etc.
168///
169// Docs:
170// - https://docs.unrealengine.com/4.27/en-US/API/Runtime/CoreUObject/UObject/UObject/
171// - https://gist.github.com/apple1417/b23f91f7a9e3b834d6d052d35a0010ff#object-structure
172#[derive(Copy, Clone)]
173pub struct UObject {
174    object: Address,
175}
176
177impl From<Address> for UObject {
178    fn from(addr: Address) -> Self {
179        UObject { object: addr }
180    }
181}
182
183impl UObject {
184    /// Create an arbitrary `UObject` from an address
185    pub fn new(address: impl Into<Address>) -> Self {
186        Self {
187            object: address.into(),
188        }
189    }
190
191    /// Returns the address of the current `UObject`
192    pub fn get_address(&self) -> Address {
193        self.object
194    }
195
196    /// Reads the `FName` of the current `UObject`
197    pub fn get_fname<const N: usize>(
198        &self,
199        process: &Process,
200        module: &Module,
201    ) -> Result<ArrayCString<N>, Error> {
202        let key = process.read::<FNameKey>(self.object + module.offsets.uobject_fname)?;
203
204        module.get_fname(process, key)
205    }
206
207    /// Returns the underlying class definition for the current `UObject`
208    fn get_uclass(&self, process: &Process, module: &Module) -> Result<UClass, Error> {
209        match process.read_pointer(
210            self.object + module.offsets.uobject_class,
211            module.pointer_size,
212        ) {
213            Ok(Address::NULL) | Err(_) => Err(Error {}),
214            Ok(val) => Ok(UClass { class: val }),
215        }
216    }
217
218    /// Tries to find a field with the specified name in the current UObject and returns
219    /// the offset of the field from the start of an instance of the class.
220    pub fn get_field_offset(
221        &self,
222        process: &Process,
223        module: &Module,
224        field_name: &str,
225    ) -> Option<u32> {
226        self.get_uclass(process, module)
227            .ok()?
228            .get_field_offset(process, module, field_name)
229    }
230}
231
232/// An UClass / UStruct is the object class relative to a specific UObject.
233/// It essentially represents the class definition for any given UObject,
234/// containing information about its properties, parent and children classes,
235/// and much more.
236///
237/// It's always referred by an UObject and it's used for recover data about
238/// its properties and offsets.
239///
240// Source: https://github.com/bl-sdk/unrealsdk/blob/master/src/unrealsdk/unreal/classes/ustruct.h
241#[derive(Copy, Clone)]
242struct UClass {
243    class: Address,
244}
245
246impl UClass {
247    fn properties<'a>(
248        &'a self,
249        process: &'a Process,
250        module: &'a Module,
251    ) -> impl FusedIterator<Item = UProperty> + 'a {
252        // Logic: properties are contained in a linked list that can be accessed directly
253        // through the `property_link` field, from the most derived to the least derived class.
254        // Source: https://gist.github.com/apple1417/b23f91f7a9e3b834d6d052d35a0010ff#object-structure
255        //
256        // However, if you are in a class with no additional fields other than the ones it inherits from,
257        // `property_link` results in a null pointer. In this case, we access the parent class
258        // through the `super_field` offset.
259        let mut current_property = {
260            let mut val = None;
261            let mut current_class = *self;
262
263            while val.is_none() {
264                match process.read_pointer(
265                    current_class.class + module.offsets.uclass_property_link,
266                    module.pointer_size,
267                ) {
268                    Ok(Address::NULL) => match process.read_pointer(
269                        current_class.class + module.offsets.uclass_super_field,
270                        module.pointer_size,
271                    ) {
272                        Ok(Address::NULL) | Err(_) => break,
273                        Ok(super_field) => {
274                            current_class = UClass { class: super_field };
275                        }
276                    },
277                    Ok(current_property_address) => {
278                        val = Some(UProperty {
279                            property: current_property_address,
280                        });
281                    }
282                    _ => break,
283                }
284            }
285
286            val
287        };
288
289        iter::from_fn(move || match current_property {
290            Some(prop) => match process.read_pointer(
291                prop.property + module.offsets.uproperty_property_link_next,
292                module.pointer_size,
293            ) {
294                Ok(val) => {
295                    current_property = match val {
296                        Address::NULL => None,
297                        _ => Some(UProperty { property: val }),
298                    };
299                    Some(prop)
300                }
301                _ => None,
302            },
303            _ => None,
304        })
305        .fuse()
306    }
307
308    /// Returns the offset for the specified named property.
309    /// Returns `None` on case of failure.
310    fn get_field_offset(
311        &self,
312        process: &Process,
313        module: &Module,
314        field_name: &str,
315    ) -> Option<u32> {
316        self.properties(process, module)
317            .find(|field| {
318                field
319                    .get_fname::<CSTR>(process, module)
320                    .is_ok_and(|name| name.matches(field_name))
321            })?
322            .get_offset(process, module)
323    }
324}
325
326/// Definition for a property used in a certain UClass.
327///
328/// Used mostly just to recover field names and offsets.
329// Source: https://github.com/bl-sdk/unrealsdk/blob/master/src/unrealsdk/unreal/classes/uproperty.h
330#[derive(Copy, Clone)]
331struct UProperty {
332    property: Address,
333}
334
335impl UProperty {
336    fn get_fname<const N: usize>(
337        &self,
338        process: &Process,
339        module: &Module,
340    ) -> Result<ArrayCString<N>, Error> {
341        let key = process.read::<FNameKey>(self.property + module.offsets.uproperty_fname)?;
342
343        module.get_fname(process, key)
344    }
345
346    fn get_offset(&self, process: &Process, module: &Module) -> Option<u32> {
347        process
348            .read(self.property + module.offsets.uproperty_offset_internal)
349            .ok()
350    }
351}
352
353/// A key to an `FName`, whose associated string can be retrieved from the FName pool
354#[derive(Pod, Zeroable, Copy, Clone, PartialEq, Eq, Debug)]
355#[repr(C)]
356pub struct FNameKey {
357    name_offset: u16,
358    chunk_offset: u16,
359}
360
361impl FNameKey {
362    /// Returns `true` if the key's values are 0
363    pub fn is_null(&self) -> bool {
364        self.chunk_offset == 0 && self.name_offset == 0
365    }
366}
367
368/// An implementation for automatic pointer path resolution
369#[derive(Clone)]
370pub struct UnrealPointer<const CAP: usize> {
371    cache: RefCell<UnrealPointerCache<CAP>>,
372    base_address: Address,
373    fields: [&'static str; CAP],
374    depth: usize,
375}
376
377#[derive(Clone, Copy)]
378struct UnrealPointerCache<const CAP: usize> {
379    offsets: [u64; CAP],
380    resolved_offsets: usize,
381}
382
383impl<const CAP: usize> UnrealPointer<CAP> {
384    /// Creates a new instance of the Pointer struct
385    ///
386    /// `CAP` should be higher or equal to the number of offsets defined in `fields`.
387    ///
388    /// If a higher number of offsets is provided, the pointer path will be truncated
389    /// according to the value of `CAP`.
390    pub fn new(base_address: Address, fields: &[&'static str]) -> Self {
391        let this_fields: [&str; CAP] = {
392            let mut iter = fields.iter();
393            array::from_fn(|_| iter.next().copied().unwrap_or_default())
394        };
395
396        let cache = RefCell::new(UnrealPointerCache {
397            offsets: [u64::default(); CAP],
398            resolved_offsets: usize::default(),
399        });
400
401        Self {
402            cache,
403            base_address,
404            fields: this_fields,
405            depth: fields.len().min(CAP),
406        }
407    }
408
409    /// Tries to resolve the pointer path
410    fn find_offsets(&self, process: &Process, module: &Module) -> Result<(), Error> {
411        let mut cache = self.cache.borrow_mut();
412
413        // If the pointer path has already been found, there's no need to continue
414        if cache.resolved_offsets == self.depth {
415            return Ok(());
416        }
417
418        // If we already resolved some offsets, we need to traverse them again starting from the base address
419        // (usually GWorld of GEngine) in order to recalculate the address of the farthest UObject we can reach.
420        // If no offsets have been resolved yet, we just need to read the base address instead.
421        let mut current_uobject = UObject {
422            object: match cache.resolved_offsets {
423                0 => process.read_pointer(self.base_address, module.pointer_size)?,
424                x => {
425                    let mut addr = process.read_pointer(self.base_address, module.pointer_size)?;
426                    for &i in &cache.offsets[..x] {
427                        addr = process.read_pointer(addr + i, module.pointer_size)?;
428                    }
429                    addr
430                }
431            },
432        };
433
434        for i in cache.resolved_offsets..self.depth {
435            let offset_from_string = match self.fields[i].strip_prefix("0x") {
436                Some(rem) => u32::from_str_radix(rem, 16).ok(),
437                _ => self.fields[i].parse().ok(),
438            };
439
440            let current_offset = match offset_from_string {
441                Some(offset) => offset as u64,
442                _ => current_uobject
443                    .get_field_offset(process, module, self.fields[i])
444                    .ok_or(Error {})? as u64,
445            };
446
447            cache.offsets[i] = current_offset;
448            cache.resolved_offsets += 1;
449
450            current_uobject = UObject {
451                object: process
452                    .read_pointer(current_uobject.object + current_offset, module.pointer_size)?,
453            };
454        }
455        Ok(())
456    }
457
458    /// Dereferences the pointer path, returning the memory address at the end of the path
459    pub fn deref_offsets(&self, process: &Process, module: &Module) -> Result<Address, Error> {
460        self.find_offsets(process, module)?;
461        let cache = self.cache.borrow();
462        let (&last, path) = cache.offsets[..self.depth].split_last().ok_or(Error {})?;
463        let mut address = process.read_pointer(self.base_address, module.pointer_size)?;
464        for &offset in path {
465            address = process.read_pointer(address + offset, module.pointer_size)?;
466        }
467        Ok(address + last)
468    }
469
470    /// Dereferences the pointer path, returning the value stored at the final memory address
471    pub fn deref<T: CheckedBitPattern>(
472        &self,
473        process: &Process,
474        module: &Module,
475    ) -> Result<T, Error> {
476        self.find_offsets(process, module)?;
477        let cache = self.cache.borrow();
478        process.read_pointer_path(
479            process.read_pointer(self.base_address, module.pointer_size)?,
480            module.pointer_size,
481            &cache.offsets[..self.depth],
482        )
483    }
484}
485
486struct Offsets {
487    uobject_fname: u8,
488    uobject_class: u8,
489    uclass_super_field: u8,
490    uclass_property_link: u8,
491    uproperty_fname: u8,
492    uproperty_offset_internal: u8,
493    uproperty_property_link_next: u8,
494}
495
496impl Offsets {
497    const fn new(version: Version, pointer_size: PointerSize) -> Option<&'static Self> {
498        match pointer_size {
499            PointerSize::Bit64 => Some(match version {
500                // Tested on: Sonic Omens
501                Version::V4_23 | Version::V4_24 => &Self {
502                    uobject_fname: 0x18,
503                    uobject_class: 0x10,
504                    uclass_super_field: 0x40,
505                    uclass_property_link: 0x48,
506                    uproperty_fname: 0x18,
507                    uproperty_offset_internal: 0x44,
508                    uproperty_property_link_next: 0x50,
509                },
510                // Tested on: Tetris Effect / Kao the Kangaroo
511                Version::V4_25
512                | Version::V4_26
513                | Version::V4_27
514                | Version::V5_0
515                | Version::V5_1
516                | Version::V5_2 => &Self {
517                    uobject_fname: 0x18,
518                    uobject_class: 0x10,
519                    uclass_super_field: 0x40,
520                    uclass_property_link: 0x50,
521                    uproperty_fname: 0x28,
522                    uproperty_offset_internal: 0x4C,
523                    uproperty_property_link_next: 0x58,
524                },
525                // Tested on Unreal Physics
526                Version::V5_3 | Version::V5_4 => &Self {
527                    uobject_fname: 0x18,
528                    uobject_class: 0x10,
529                    uclass_super_field: 0x40,
530                    uclass_property_link: 0x50,
531                    uproperty_fname: 0x20,
532                    uproperty_offset_internal: 0x44,
533                    uproperty_property_link_next: 0x48,
534                },
535            }),
536            _ => None,
537        }
538    }
539}
540
541#[non_exhaustive]
542#[derive(Copy, Clone, PartialEq, Hash, Debug, PartialOrd)]
543#[allow(missing_docs)]
544/// The version of Unreal Engine used by the game
545pub enum Version {
546    V4_23,
547    V4_24,
548    V4_25,
549    V4_26,
550    V4_27,
551    V5_0,
552    V5_1,
553    V5_2,
554    V5_3,
555    V5_4,
556}