home..

UEVR: An Exploration of Advanced Game Hacking Techniques

The sphere of game hacking and modding has a rich history, tightly knit with a well-known practice called AOB (Array of Bytes) scanning, also known as signature scanning. This method, similarly employed within the realm of security, is a traditional approach used to detect malicious code or, in the context of modding and hacking, locate specific areas within a program’s memory.

In the realm of security, there exists a suite of techniques encompassing static, dynamic, and heuristic analysis. For the purpose of this discussion, we’ll refer to this group of methods as “binary analysis”. The question: could we leverage these techniques to create more robust code scanning techniques? This isn’t about superseding signature scanning; instead, it’s about broadening our capabilities, adding more depth to our strategies.

Automated binary analysis, static and dynamic, while not a novel approach, is sometimes applied within the game hacking community but is less frequently discussed. However, when used appropriately, these techniques can offer a powerful enhancement to - or even an alternative for - traditional AOB scanning. There are other reasons why these techniques would be used, but that is not the focus of this article.

Many of the techniques described here require the reader to have already identified areas of interest via something like IDA Pro, x64dbg, Cheat Engine, or other tools. From there, we will use automated techniques to locate these areas of interest, such as functions, global variables, or structure offsets.

Let’s start by introducing a practical project that demonstrates these advanced techniques in action. The Unreal Engine Virtual Reality project (UEVR) is a real-world initiative I’ve been working on that uses many of the techniques described in this article.

A Real World Project

Theory is great, but it’s not very useful without practical application.

That’s why there is a project I have been working on that uses many of these techniques. It’s meant to work on almost any Unreal Engine game. It adds 6DOF VR support to as many Unreal Engine games as possible, with little to no intervention required for the most part on the user’s end (kind of like an emulator in an ideal state).

Using many of the techniques described here, it is possible to make such a project. It’s called UEVR. This project has been my playground for experimenting with these techniques, and it’s been a great learning experience.

For information on just how many games (hundreds confirmed at the time of writing) it works on, and to what degree, see the compatibility list.

The core goals of UEVR

UEVR uses the following libraries:

Click here to see an overview of the techniques used in UEVR, and how it works in general


Piggybacks off of a built-in Unreal Engine command-line mechanism called -emulatestereo which “emulates” a stereo device, splitting the desktop screen in two with two cameras at separate positions

  • -emulatestereo uses a built-in class called FFakeStereoRendering which inherits from IStereoRendering, which UEVR aims to hook
  • For reference, IStereoRendering contains virtuals relevant to creating a proper stereoscopic image, these include:
    • A method for overriding the projection matrices
    • A method for overriding the view matrices (rotation via euler angles, position via vec3)
    • A method for modifying how the viewport is presented (left/right eye width/height/x/y)
    • A method for creating a texture as a viewport render target
      • The default FFakeStereoRendering interface does not implement this
      • This allows the VR headset to get a full resolution image at the runtime’s requested resolution
      • Using dynamic analysis from the return address of this virtual, we can locate the real function that creates textures
        • For context, if our function returns false, the engine falls back to its own texture creation method
        • This is also abused to create our UI render target at the same time


String reference analysis is used in several places to find the relevant structures and functions for this process

  • For its initial starting and anchor points, it uses string reference scanning to locate:
    • The GEngine global object
      • The "CALIBRATEMOTION" string has always been referenced near a reference to GEngine since early in UE4 development
      • Finds the function start where this string is referenced in and looks for global variables via static analysis
      • This is the UGameEngine instance
      • Contains the StereoRenderingDevice object among other things
      • Needed to call InitializeHMDDevice to create an FFakeStereoRendering instance
    • The vtable for FFakeStereoRendering
      • There are various configuration strings referenced within the constructor for FFakeStereoRendering
      • Finds the function start where these strings are referenced and looks for a LEA instruction via static analysis
      • There are some cases where it appears to have been #ifdef’d out, but this is extremely rare
        • If this happens, UEVR will manually construct a plausible IStereoRendering vtable and use that instead
    • The function for UGameEngine::InitializeHMDDevice
      • There is a reference to the string "emulatestereo" and a reference to the CVar for r.EnableStereoEmulation in this function
      • This allows us to create an FFakeStereoRendering instance without telling the user to use -emulatestereo
        • A few methods are used to achieve this, the main one is forcing r.EnableStereoEmulation to 1
        • The other is tricking the game into thinking -emulatestereo was passed in as a command-line argument via code-patching
      • After this is called, we can bruteforce scan for the FFakeStereoRendering vtable we found earlier inside of the global GEngine object to obtain the object and offset
      • Alternatives/fallbacks are in place to auto-locate the offset in UGameEngine for the StereoRenderingDevice for manual creation if either one of these processes fail


Tries to minimize use of plain AOBs (signatures)

  • Only uses them when necessary
  • Only uses them in a localized scenario (such as within the bounds of a function or near an anchor point)
  • Instead relies mainly on string refs, and prefers disassembly
    • Unique string references are preferred, but not required (strings that are referenced in only one place)
    • Strings are human readable, easier to maintain, more likely to reside within the binary through updates, and are less likely to change


Using several of these techniques, UEVR analyzes the FFakeStereoRendering vtable and identifies the relevant vtable indices (which has changed a lot over the years of Unreal Engine development)

  • A few of the ways it finds relevant virtual functions to replace:
    • Emulation
      • This is necessary to deal with obfuscation, it has been seen in the wild on these virtuals
      • Obfuscation seen ranged anywhere from a simple jmp wrapper to multiple layers of control flow obfuscation to simply return true
    • General disassembly
    • Signature scanning
    • Presence of references to specific strings
    • Presence of calls to imports like tanf or tan
    • Presence of a large amount of xmm registers
      • This also detects UE5 based on usage of double precision xmm instructions
    • Heuristic analysis and anchor points
      • Like assuming that a certain virtual lies a specific distance from another if things like:
        • Multiple or single functions return nullptr in a row preceding it
        • Multiple or single functions return true or false in a row preceding it
        • Multiple functions mix nullptr and true/false returns, forming a unique pattern


It replaces the relevant vtable indices with hooks to its own functions for proper output to a VR headset (using the OpenVR/OpenXR APIs, as well as D3D11/D3D12)


It goes even further by using these techniques in several different ways such as

  • Using the return addresses or call stack of some of the virtual functions as anchor points to:
    • Find other functions, variables, and offsets of interest
    • Find the local player object by using call stack analysis, data flow analysis and mid-function hooks
      • This can be found via other methods, but I did not want to construct a completely new method for this
      • Relied instead on methods I already had in place, taking advantage of the fact that some of the virtuals in the vtable are called from functions where the local player is the this pointer
    • After the local player is found, its vtable is bruteforced to locate the PostInitProperties function
      • Using path exhaustive disassembly, it checks for references to the GEngine global, it’s the only function that does this within the vtable
        • This method is necessary because it doesn’t always reside within the first call stack level
      • When calling this function, a vectored exception handler must be setup to catch exceptions that occur on development/debug builds of games
        • It also goes even further and tries to patch out any exception-causing calls and instructions beforehand through path exhaustion
        • This is because the engine will assert if a view state is already created for any eye (so, if this function was called already)
      • Calling this function will create the second view state for the other eye, so we don’t crash when doing stereo rendering
      • This function is normally called by the engine when enabling VR or using -emulatestereo immediately when the game starts, but not at any point afterwards
    • Find and hook the texture creation function in combination with mid hooks
      • In particular, this is achieved by performing emulation and disassembly after the return address of IStereoRenderTargetManager::AllocateRenderTargetTexture, placing the mid hook right before the call to the creation function, as well as after
      • This is used not only to create our viewport render target, but to create our UI render target as well
    • Find the calling function or walk the callstack
    • Insert other hooks on areas of interest (inline and mid hooks)
  • Locates and hooks UGameEngine::Tick in a few ways:
    • Multiple string reference options
    • Vtable analysis after locating UEngine::Tick pure virtual as a reference point
    • In extreme cases it can try hooking it via call stack analysis from the UGameViewportClient::Draw hook if all fails
    • Code path exhaustion via static analysis to locate specific strings buried deep within the function like "causeevent="
      • Does this by disassembling all of UGameEngine’s virtual functions (after we’ve found GEngine)
      • Follows all branches that are possible from a static perspective (hence, path exhaustion)
      • This is necessary sometimes because:
        • Compiler optimizations or obfuscation
        • Sometimes the string identifying the function is buried beneath several jmps or calls (direct and indirect)
        • Locating the start of the function after finding the stringref sometimes fails
        • Attempting to use emulation may not cover the specific branch path the string resides in
    • This function is very important which necessitates multiple fallbacks
  • Locates and hooks UGameViewportClient::Draw
    • Looks for a reference to the string "CanvasObject" in the function
    • There can be multiple references to this "CanvasObject" string because:
      • There can be multiple viewport types
      • We only need one specific one, so we have to analyze each function that this string is contained in
  • Locates and hooks FViewport::Draw by analyzing the call stack from a UGameViewportClient::Draw hook
  • Locates and hooks FSlateRHIRenderer::DrawWindow_RenderThread by:
    • Analyzing where the Slate.DrawToVRRenderTarget CVar is referenced
    • If this fails, analyzes the return address of IStereoRendering::RenderTexture_RenderThread which is directly called within FSlateRHIRenderer::DrawWindow_RenderThread
    • This is used to force the game to render the UI to our UI render target instead of the viewport render target
      • Done by replacing one of the argument’s member fields with our UI render target temporarily
        • This particular field offset is obtained by bruteforce and emulation of all vtables residing within the object
  • Locates the ViewExtensions member of UGameEngine:
    • This is done by assuming that it’s fixed distance ahead of the StereoRenderingDevice member offset
      • This can be seen as using the StereoRenderingDevice offset as an anchor point
      • This assumption was made by seeing that it’s held true for years of Unreal Engine development
    • This particular interface is very powerful and allows us to insert hooks at various points in the rendering pipeline
  • After locating the ViewExtensions member:
    • It inserts a dummy vtable that has many variants of the same function using template metaprogramming
    • The purpose of this is to locate various functions of interest, based on how the virtual functions are being called, and how often
    • It also uses this to bruteforce the offset for the frame count within the ViewFamily passed to BeginRenderViewFamily and others
      • Detects which offsets that have integer-like values that are increasing by 1 every call
      • The frame count is used by UEVR to track the flow of rendering data until DXGI presents the frame
    • After these steps are done, it inserts the final vtable with the indices found from the analysis
  • Locates and hooks the FSceneView constructor
    • There are two strings in this function, "r.TranslucentSortPolicy" and "vr.InstancedStereo"
    • This is a bit complex though, because "r.TranslucentSortPolicy" is actually referenced in multiple functions
    • Also, the "vr.InstancedStereo" string isn’t guaranteed to be at the same call stack level as the other string
      • It’s sometimes nested within a call inside the constructor
    • Because of this, we need to locate all "r.TranslucentSortPolicy" references, and then analyze each function to see if it contains the "vr.InstancedStereo" string
      • We do this by exhaustively disassembling each function and following all possible branches until we find an instruction that references the "vr.InstancedStereo" string
  • Uses static and dynamic analysis to locate virtual function indices for the IConsoleVariable vtable
    • After locating a CVar, the vtable is bruteforced and analyzed to find the indices of functions that are used by the engine
      • Set
      • GetInt
      • GetFloat
      • Execute (For IConsoleCommand)
    • After locating destructor-like function(s), these functions are known to be ahead of them
    • Through behavior analysis and heuristic guesswork, the indices of the functions are found


Utilizes several fallback techniques in-case the main techniques fail such as:

  • Hardcoded vtable indices in extreme cases based on PDB dumps of compiled release builds of Unreal Engine
  • Detection of different texture creation methods and accounting for them
  • Detection of older UE games based on how the virtual functions are laid out
    • This can manifest in several ways
      • Missing virtual functions
      • Repeating virtual functions (such as multiple nullptr returns in a row followed by a valid function)


  • Bruteforces through the texture vtable to find certain functions (such as those that return an ID3D12Resource pointer)
    • It does this by checking if the returned pointer’s vtable resides within any of the DirectX DLLs
  • Bruteforces through certain structures to find offsets based on what they point to
  • All of these analysis techniques are used instead of using PDB dumps because various factors can cause differences in shipped code and PDB dumps
    • This includes compiler optimizations, obfuscation, and other factors such as adjustments made by the game developers


Finds other non-essential things such as:

  • The GUObjectArray global variable
    • Searches for two strings that must reside in the same function
      • This is an extremely precise heuristic, especially when dealing with multiple string refs for one string
    • These strings have been present in the engine for years
    • Fully disassembles the function, looking for displacements
    • Uses structure analysis against each displacement to probe for the GUObjectArray global variable
      • Checks that integer values in the structure look like integers
      • Checks that the pointer to the objects is a valid pointer
      • Checks that the first element of the array is a valid pointer and points to a valid vtable
    • Once a suitable candidate has been identified:
      • Goes even further and detects <= UE 4.10 (which is an inlined array) through similar structure analysis
      • Goes even further and checks for the chunked variant of the array through similar structure analysis
      • Detects the distance between FUObjectItem elements via bruteforce scanning for valid pointers to UObjectBase
  • Bruteforces offsets for various UObject, FField, FProperty, etc… structures
    • Uses known structure relationships to probe for offsets
  • The FName::ToString() function
    • Easily found via multiple string references
    • Can sometimes be inlined, so there are fallbacks in place

There are various other techniques used, but they are too numerous to list here or the intricacies are too complex to explain in a short summary


Overall, in essence: tries very hard to recreate what the official VR plugins for the Unreal Engine do, without requiring the game to include them

For context: the official VR plugins are what allow the engine to render to the HMD properly in the first place, they act as a bridge between the engine and the HMD SDKs


A Brief History of Signatures

AOB scanning, or signature scanning, have long been used as a means to locate specific areas in a program’s memory. The technique consists of identifying unique sequences of bytes that correspond to certain operations or data. This practice, which started in the field of antivirus software, quickly found its way to the modding/hacking community, where it became an essential tool. These sequence of bytes can be a function prologue, a magic number, a specific string, and many other things.

One such industry tool that employs signatures is YARA, a versatile malware identification and classification tool. YARA leverages signatures as part of its sets of rules that identify malware based on binary or textual patterns. The use of signatures, while effective in some scenarios, often encounters issues when dealing with complex game updates, function changes, engine differences, or compiler changes, mainly due to its rigidity and specificity.

Binary Analysis in the Security Industry

Unlike AOBs, binary analysis isn’t dependent on specific byte patterns. Instead, it examines the structural and behavioral patterns in a program. In cybersecurity, binary analysis is used to detect unknown viruses or malware by looking at how they behave or how they are structured, instead of looking for a known virus signature. It’s also used to perform formal verification of software, which is the process of proving or disproving the correctness of a program with respect to a certain formal specification or property. Software like BAP is used to perform formal verification of software.

The implementation of binary analysis is broad and ranges from simple static binary analysis to more sophisticated techniques such as emulation, symbolic execution, taint analysis, control flow analysis, and more. These techniques can be used to locate specific areas of interest in a way that is resistant to updates and changes.

Bridging the Gap

Historically, the game hacking community is well-known for using techniques seen in the wild well before research from the security industry is released. However, despite the potential benefits, the adoption of automated binary analysis for locating key areas has been slow: it is complex, and some may see it as overkill.

The community often prefers AOBs for their simplicity, but advanced modders might find binary analysis a potent addition to their arsenal, especially if working with a prolific game engine, or if the game is frequently updated and more robust techniques are needed to locate areas of interest.

A particularly compelling use case for automated binary analysis is with specific game engines that power many games. The technique’s ability to locate shared engine functions can exceed that of a signature if used correctly. Automated binary analysis is generally more resilient to game updates in general, as it’s not dependent on specific byte patterns. However, it’s not a silver bullet, and it’s not without its own set of challenges. It can also be quite slow if not done with care.

Localized emulation of functions, symbolic execution, general disassembly (static analysis) using libraries, data flow analysis, and string reference analysis, offer a solid addition to the modder’s arsenal.

While these techniques are powerful on their own, when some, or all of these techniques are combined, they become powerful tools for locating functions, addresses, and structure offsets that can withstand the test of time.

The Techniques

Emulation

The first thought that usually comes to mind when thinking about emulation is the use of a virtual machine to run a program, or a program to emulate a game console. However, emulation can also be used to run a function in a controlled environment and observe its behavior. This technique is particularly useful when dealing with functions that are difficult to analyze statically.

Given an anchor instruction to start from (this assumes you know how to find this instruction in the first place, for example with an AOB or string reference lookup, even the entry point of the program), we can emulate the function (or set of instructions) and observe its behavior. Along the way, we can look for function calls to other functions of interest, look for specific memory operations (such as accesses of global variables), look for specific values in registers, look for accesses into structures to find specific field offsets, or just trace the flow of data in general.

One can argue that some of those things can just be found simply with a disassembly pass, but here are some counterpoints:

In C/C++, libraries such as Triton, Unicorn, and bddisasm’s bdshemu can be used to emulate functions.

In Python, libraries such as miasm, Unicorn, qiling, or Triton can be used to emulate functions.

Symbolic Execution

Symbolic execution is similar to emulation in that it runs a function in a controlled environment. However, instead of running the function, it analyzes the function’s code and generates a set of constraints that represent the function’s behavior. This particular technique is not used here, but worth mentioning.

The simplest example I can give here is that symbolic execution has the ability to figure out what inputs can drive a specific function or program to end up at a certain instruction. Example being, product key activation functions, where you can figure out what key will make the program think it’s valid.

The only similar technique we use to this is the enhancement to static analysis to explore all paths.

In C++, libraries such as Triton can be used to symbolically execute functions.

In Python, miasm, angr can be used to symbolically execute functions.

Data Flow Analysis

Data flow analysis is the process of tracking the flow of data in a program. This technique is commonly used in the security industry to track user input and identify potential vulnerabilities. However, it can also be used to track the flow of data in a program and identify specific operations or functions of interest.

In this particular context, given an anchor point (such as a function or instruction), we can “taint” a register or memory location with a magic number and track its flow throughout the code using emulation. This can be used for a few things, such as:

The same libraries used for emulation can be used for data flow and/or taint analysis.

Structural Analysis

Structural analysis, in this context, refers to the analysis of structures (or classes) in a program. This is particularly useful when dealing with C++ programs, as C++ classes are often represented as structures in the binary. This technique is used to locate specific fields in a structure, such as offsets, sizes, and types. It can also just be used to verify that a structure is what we expect it to be.

If we know the general layout of a structure beforehand, we can probe specific areas of memory to see if the structure layout matches. If a structure is supposed to have a valid pointer at 0x10, we can check the offset at 0x10 from a specific pointer, check if it points to valid memory. Combining this with several other checks, like making sure integers look like valid integers, and strings look like valid strings, we can verify that the structure is what we expect it to be.

This technique also works if you know the general layout of the structure, but not the exact offsets within the structure. Using that information, we can perform bruteforce-like checks against the entire structure to obtain the offsets of the fields we are looking for.

An example being, in the UStruct structure in Unreal Engine 4/5, there is a field called SuperStruct. If we have full access to the GUObjectArray already, we can find the:

Using the pointers to these classes, we can scan over the UStruct structure until we see a pointer to the UField class. If we do, we perform the same check on the UField structure until we see a pointer to the UObject class. If we do, we can assume that the SuperStruct field is at the given offset, because all of these classes are supposed to point to eachother via the SuperStruct field.

Static Analysis (Disassembly at Runtime)

Static analysis is the process of analyzing a program’s code without running it. Disassemblers such as IDA, Ghidra, or Binary Ninja are commonly used to perform static analysis. However, in the context of runtime binary analysis, static analysis can be used to analyze a function’s code and look for specific patterns or behavior at runtime using libraries such as:

Using libraries such as these, we can not only perform simple linear disassembly, we can also perform more complex disassembly techniques such as recursive traversal of code paths.

An example, we could disassemble forward, and by using a virtual instruction pointer and list of branches to analyze:

This traversal technique is particularly useful because:

String Reference Scanning

String reference scanning is a technique that involves combing through the program’s memory for references to specific strings. In conventional software analysis tools like IDA or x64dbg, cross-references to strings are often used as a reliable method for locating functions during manual analysis. However, leveraging string references within the context of automated runtime binary analysis in the game hacking community is less frequently discussed.

String reference scanning is a form of signature scanning, but due to its reliance on human-readable data, it is more reliable than traditional signature scanning, which usually looks for byte patterns of instructions which may change for a variety of reasons that the original programmer was not even aware of.

Deploying string reference scanning can accomplish several goals:

Disassemblers such as IDA commonly employ this technique to display cross-references to strings. The difference here is it would be performed at runtime. We wouldn’t have the luxury of disassembling the entire program though; this would be too slow, it would have to be a purpose-built scan that employs localized disassembly.

There are some complexities involved because string references on x86-64 are usually relative as part of an instruction operand. So, a special scanning technique that scans the memory byte-by-byte, treating 4 bytes as the “relative” offset, would be required.

// This would need to be used at every address that is scanned for a string reference
// where customOffset is 4
// This assumes that the immediate is the last operand, which is sometimes not the case
// but it's the most common case so it's not really a big deal
uintptr_t calculate_absolute(uintptr_t address, uint8_t customOffset /*= 4*/) {
    auto offset = *(int32_t*)address;

    return address + customOffset + offset;
}

There’s also the issue that this would only give you the address within the middle of the instruction, where the relative operand is. If you need the start of the instruction, you need to get creative. For example, you could have a list of valid prefixes for address references, and scan backwards until you find one of those prefixes, or you could use the technique(s) described earlier that can get you the start of the containing function. From there, you can disassemble forward until the bounds of the instruction encapsulate the address reference.

This same technique can also just be applied to references to any data in general, not just strings. But, since strings are the easiest to scan for from the get-go, it’s a good starting point. If trying to find references to data in general, you would need to know how to find that data in the first place (such as by using or combining the other techniques talked about here).

There is an IDA plugin I wrote a while back which allows the user to click on any address, and it will look for any string references that are near a reference to that particular address. It will only display areas where only the first occurrence of the string reference is near the address. It orders them and displays them in a list based on distance to the target address reference.

IDA String Reference Locator

Result of using this plugin on the FName constructor in the Unreal Engine

/*
4.27:
[address hidden] + 0x8 NumNetGUIDsPending
[address hidden] + 0x7 SkeletalMeshComponentClothTick
[address hidden] + 0x7 DefaultModulationPlugin
[address hidden] + 0x4 Oculus Quest2
[address hidden] + 0x4 FSequencerPlayerAnimSequence
[address hidden] + 0x4 EndPhysicsTick
[address hidden] + 0x4 StartPhysicsTick
[address hidden] + 0x4 TickAnimationSharing
[address hidden] + 0x4 LakeCollisionComponent
[address hidden] + 0x4 SkeletalMeshComponentEndPhysicsTick
[address hidden] + 0x4 Behavior
[address hidden] + 0x4 FSlateMouseEventsMetaData
[address hidden] + 0x4 FSlateCursorMetaData
[address hidden] + 0x4 SoundExporterWAV
[address hidden] + 0x4 FReflectionMetaData
[address hidden] + 0x4 GameDefaultMap
[address hidden] + 0x4 Test FName
[address hidden] + 0x4 WaterBodyCollision
[address hidden] + 0x4 WidgetTree
[address hidden] + 0x4 FTagMetaData
[address hidden] + 0x4 FSlateToolTipMetaData
[address hidden] + 0x4 ParticleSystemManager
[address hidden] + 0x4 Plugins
[address hidden] + 0x4 FNavigationMetaData
[address hidden] + 0x4 FSceneViewExtensionContext
*/

/*
4.14:
[address hidden] + 0x8 bIsPlaying
[address hidden] + 0x8 FLandscapeUniformShaderParameters
[address hidden] + 0x8 STAT_ColorList
[address hidden] + 0x7 OPUS
[address hidden] + 0x4 LightComponent
[address hidden] + 0x4 FPostProcessMaterialNode
[address hidden] + 0x4 SoundExporterWAV
[address hidden] + 0x4 Component
[address hidden] + 0x4 STextBlock
[address hidden] + 0x4 FLightPropagationVolumeSettings
[address hidden] + 0x4 CraneCameraMount
[address hidden] + 0x4 Priority
[address hidden] + 0x4 FTagMetaData
[address hidden] + 0x4 FNavigationMetaData
*/

/*
Present in both (and 5.0.3+):
+ 0x4 FTagMetaData
+ 0x4 FNavigationMetaData
*/


Heuristic Analysis

Heuristic analysis is essentially inferring something about a program given specific patterns or behavior. This is a very broad term, and can be used to describe a lot of things, and some of the techniques talked about so far can fall under this umbrella, or be used in the process of heuristic analysis. It can be used to describe rule of thumb techniques which make educated assumptions, can have a good chance of working, though are not guaranteed to work.

For example, a heuristic for locating a specific global variable in a game engine could be to look for a function that contains a specific string reference, and checking the most accessed global variable inside the function using static analysis, and assuming this is what we’re looking for. We came up with these specific rules by analyzing a few different sample binaries, and the source code of the engine.

This can describe the offline process (such as manual analysis in IDA) AND the automated techniques we will need to use at the same time, providing instructions to two different groups of reverse engineers. This very specific example will be explored down below.

Call Stack Analysis

Another powerful technique that is especially useful for finding anchors to use for other techniques is call stack analysis. We can use Windows functions like RtlCaptureStackBackTrace to do this.

The call stack shows us the chain of calls that got us to a particular function. It’s a list of return addresses. Note, there must be hook on this function or the thread must be intercepted in some way to get the call stack.

Using these return addresses we can:

Anchors

Anchors, or reference points, in the context of both exploit development and game hacking, refer to specific points or references within a codebase or memory space that serve as starting points or ‘fixed’ locations for further code analysis or manipulation. These anchors could be functions, global variables, return addresses, or essentially any identifiable piece of code or data.

In the realm of exploit development, anchor points play a crucial role, especially when dealing with mitigations like Address Space Layout Randomization (ASLR). ASLR randomizes the memory locations of key data areas, making it more difficult for attackers to predict where in memory their payloads or shellcode will reside. Here, the concept of an anchor or ‘fixed’ point is invaluable, allowing an attacker to navigate the memory space in a deterministic manner, despite the randomization.

When talking about signatures in the context of game hacking, they are usually used to scan through an entire module. If scanning from an anchor point, and limiting the size, it can drastically reduce false positives, and can also allow the signature to be smaller.

Anchor points can also apply to offsets or virtuals. We can sometimes assume that an offset to a member, or a virtual index can sometimes precede or be ahead of another offset/virtual index based on previous analysis.

One may ask: “Well, how do I find an anchor in the first place?”

There’s a few different ways:

An interesting note: when enough anchors are gathered, the need for signatures is reduced. This is because the anchors can be used to find other anchors, and so on. This is a very powerful concept, and is the basis for a lot of the techniques described here.

A Practical Example: String Reference Scanning for Anchors, Static and Heuristic Analysis

Take this function for example, which is robust enough to work across hundreds of UE4/5 games. It’s a function that finds the IConsoleManager interface, which is used to interact with the game’s console.

namespace detail {
sdk::FConsoleManager** try_find_console_manager(const std::wstring& string_candidate) {
    SPDLOG_INFO("Finding IConsoleManager...");

    const auto now = std::chrono::steady_clock::now();

    const auto core_module = sdk::get_ue_module(L"Core");
    const auto candidate_string = utility::scan_string(core_module, string_candidate.c_str());

    if (!candidate_string) {
        SPDLOG_ERROR("Failed to find {} string", utility::narrow(string_candidate));
        return nullptr;
    }

    const auto candidate_stringref = utility::scan_displacement_reference(core_module, *candidate_string);

    if (!candidate_stringref) {
        SPDLOG_ERROR("Failed to find {} stringref", utility::narrow(string_candidate));
        return nullptr;
    }

    SPDLOG_INFO("Found {} stringref: {:x}", utility::narrow(string_candidate), (uintptr_t)*candidate_stringref);

    // This might need to be improved a bit, it can be insanely slow on obfuscated builds
    const auto containing_function = utility::find_function_start_with_call(*candidate_stringref);

    if (!containing_function) {
        SPDLOG_ERROR("Failed to find containing function");
        return nullptr;
    }
    
    // Disassemble the function and look for references to global variables
    std::unordered_map<uintptr_t, size_t> global_variable_references{};
    std::optional<std::tuple<uintptr_t, size_t>> highest_global_variable_reference{};

    utility::exhaustive_decode((uint8_t*)*containing_function, 20, [&](INSTRUX& ix, uintptr_t ip) -> utility::ExhaustionResult {
        if (std::string_view{ix.Mnemonic}.starts_with("CALL")) {
            return utility::ExhaustionResult::STEP_OVER;
        }

        // Checks if the instruction can possibly return a displacement, and if it does, resolve it
        // an example could be "mov rax, [rip + 0x12345678]" where the displacement is rip + 0x12345678 (an absolute address)
        const auto displacement = utility::resolve_displacement(ip);

        if (!displacement) {
            return utility::ExhaustionResult::CONTINUE;
        }

        // hinges on IConsoleManager actually being constructed and assigned
        if (!IsBadReadPtr((void*)*displacement, sizeof(void*)) && !IsBadReadPtr(*(void**)*displacement, sizeof(void*))) {
            global_variable_references[*displacement]++;

            if (!highest_global_variable_reference || global_variable_references[*displacement] > std::get<1>(*highest_global_variable_reference)) {
                highest_global_variable_reference = std::make_tuple(*displacement, global_variable_references[*displacement]);
            }
        }

        return utility::ExhaustionResult::CONTINUE;
    });

    if (!highest_global_variable_reference) {
        SPDLOG_ERROR("Failed to find any references to global variables");
        return nullptr;
    }

    SPDLOG_INFO("Found IConsoleManager**: {:x}", (uintptr_t)std::get<0>(*highest_global_variable_reference));
    SPDLOG_INFO("Points to IConsoleManager*: {:x}", *(uintptr_t*)std::get<0>(*highest_global_variable_reference));

    const auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - now).count();
    SPDLOG_INFO("Took {}ms to find IConsoleManager", diff);

    return (sdk::FConsoleManager**)std::get<0>(*highest_global_variable_reference);
}
}

namespace sdk {
FConsoleManager* FConsoleManager::get() {
    static auto result = []() -> FConsoleManager** {
        std::vector<std::wstring> candidates {
            L"r.DumpingMovie",
            L"vr.pixeldensity"
        };

        const auto now = std::chrono::steady_clock::now();

        for (const auto& candidate : candidates) {
            SPDLOG_INFO("Trying to find IConsoleManager with candidate: {}", utility::narrow(candidate));
            auto result = detail::try_find_console_manager(candidate);

            if (result) {
                SPDLOG_INFO("Took {}ms to search through all candidates", std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - now).count());
                return result;
            }
        }

        SPDLOG_ERROR("Failed to find IConsoleManager");

        return nullptr;
    }();

    if (result == nullptr) {
        return nullptr;
    }

    return *result;
}
}


Let’s break this down:

  1. It scans for the string data inside the Core module that’s passed in as an argument ("r.DumpingMovie" or "vr.pixeldensity" in this case)
  2. It scans for a reference to that string data
  3. It locates the containing function of that reference
  4. It disassembles the containing function and looks for references to global variables
  5. It finds the global variable reference that’s referenced the most, this is likely to be the global variable that holds the pointer to the IConsoleManager instance. This can be considered a form of heuristic analysis, as it’s not guaranteed to work, but it’s likely to work in most cases based on sample binaries analyzed manually prior to writing this function.

From a manual perspective, recreating this entire process from the perspective of a reverse engineer would look something like this:

Phase 1: Find the string data (utility::scan_string in the code)

stringref

Phase 2: Head to the cross-reference shown in the UI (utility::scan_displacement_reference in the code)

crossref

Phase 3: Locate the start of the function (scroll up a bit) (utility::find_function_start_with_call in the code)

function_start

Phase 4: Scan through the disassembly listing looking for the most referenced global variable (utility::exhaustive_decode in the code)

global_count1

global_count2

Phase 5: Rename the global variable to something meaningful (returning the result in the code)

global_rename

This kind of approach is going for more of a human-oriented approach, where we’re trying to replicate the process a human would go through to find the IConsoleManager instance. Doing it like this allows us to easily maintain the code.

While more complex than an AOB, this set of techniques can be more robust, as it’s not reliant on a specific set of instruction bytes being present in the game’s executable other than the string data itself. Rather, it’s dependent on these strings being referenced by the game’s code, which is much more likely to remain consistent across different versions of the game, no matter how they’re referenced, whether with a LEA instruction, a MOV instruction, or something else entirely. The same goes for the global variables: we’re checking for global displacement references, which can be several different kinds of instructions.

If it fails at any point, we have multiple options to use as the IConsoleManager instance is used in many, many other places near unique strings, and we can simply try again with a different string.

In contrast, while an AOB could work here, it may require multiple variants to be written, not necessarily for each version of the game, but to account for function changes, edge cases, different compiler versions, obfuscated code, and more. It’s also much more likely to break, as it’s dependent on a specific set of bytes (a specific instruction) being present in the game’s executable, which is more likely to change for the reasons stated, and sometimes updates to the game in general can break AOBs. I would argue for using an AOB though if there was a magic constant being used in the function though.

To explain how I came up with these strings to look for, I looked through the source code, and through the disassembly listing in IDA in several samples. I found that these strings were referenced in the same function that the IConsoleManager instance was referenced in.

In Defense of Signatures

While it may seem as if this article is trying to dissuade from using signatures, that’s not the case. Signatures are still a very useful tool in the reverse engineer’s arsenal, and can be used in conjunction with the techniques discussed in this article to improve their effectiveness.

I would still very much advocate for signatures if there was a magic constant being used somewhere in an instruction as well. An example being:

mov eax, 0xdeadbeef

While it’s possible the mnemonic may change, or the bytes may partially change, the constant referenced within it may remain the same, and can be used as a signature. I could see a form of obfuscation breaking this assumption, however.

The techniques described here are usually going to be slower than a straight signature scan, and may not be feasible in some cases. The degree varies, and can range anywhere from a few milliseconds to a few seconds, depending on the complexity of the binary. Moreso if it’s obfuscated in some way, which not only bloats the size of the executuable, but also messes up the code, and other things like the Windows exception directory. However, this may be a tradeoff between speed and robustness.

Signatures may also be the only option if the target encrypts all of its strings, has very minimal strings in the first place, there’s no feasible anchor points to work with, or doing deep automated analysis may eat up too much time. These methods are in no way easy to come up with an attack plan either, it may just be easier and quicker to write a signature, it may be the case that there’s not enough samples to come up with a broad set of heuristics either.

There’s also more advanced signature scanning techniques which operate on “nibbles” (4 bits) instead of whole bytes, which can be used to make signatures more robust, and less likely to break. Tools like YARA also have “jump” sequences for their signatures, which can be used to skip over N amount of bytes, within a specific range, which can be used to make signatures more robust as well.

In Closing

The entire point I am trying to get across is that the process of locating certain functions or data structure by a reverse engineer manually in IDA, can be automated in the form of runtime code. If the process taken to get to a specific location manually is broken down by how it was achieved, the same can be achieved in code.

Case in point: If a reverse engineer finds a string reference in IDA that leads them to the function they want, this same exact process can be recreated in code. Even going an extreme step further: If they had to step through the code in a debugger to find the function they want, this same exact process can be recreated in code in the form of emulation or disassembly.

The techniques discussed in this article aren’t intended to displace traditional tools such as AOB scanning, which undeniably hold their unique advantages and a place of importance in the hacker’s toolkit.

© 2023 praydog   •  Theme  Moonwalk