Hacking, Coding and Gaming | @[email protected]

My previous "Hacking Unity Games" post explored a few tools and methods for hacking Unity games. These methods all involved patching the game logic - either in the game's files on disk or code in memory. An update to the game could break all of these methods by replacing the files on disk or causing the bytes/offset searched for in memory to change.

With the use of Frida (again) we can inject some custom Javascript into the game and access Mono (which Unity games are compiled with) functions for better hacking. This is done with "frida-inject" to inject our code and the "frida-mono-api" package to interact with Mono.

Once again I'm going to be hacking the game "198X", and re-creating the previous "invulnerability" hack as well as some others. After a few iterations of my script to hack the mini-games and hours of trial and error, googling, and staring and offsets and memory values, I've finally come up with a Javascript library to do some of the heavy lifting.

It's unimaginatively called "enumerator.js" and is used to enumerate all the function names and properties of a given Unity/Mono class, as well as provide "getter" and "setter" methods for easily manipulating an instance of that class. Unfortunately the class names still need to be discovered with dnSpy (which you'd probably still want to use anyway to find the game logic you want to hack) - this is mostly due to some of the needed Mono functionality not yet being implemented in the "frida-mono-api" library.

It's worth noting that output from the injected script (eg: console.log()) are printed in the console window the injector script is run from.

  1. Installation/Setup
  2. Hacking "Beating Heart", "Out of the Void", and "Shadowplay"
  3. Hacking "The Runaway"
  4. Hacking "Kill Screen"
  5. Conclusion

Installation/Setup

To use my "Enumerator" or hacks, you'll need to clone my "unity-frida-hacks" repo, install NodeJS 13 (NodeJS 14 LTS also worked but NodeJS 15 had issues), Frida and install the NPM libraries:

npm install frida-mono-api
npm install frida-inject

But... it doesn't end there :/ You'll also need two files from a pending pull request (but using the full pull request branch gave me issues)...

Hopefully that's enough to get everything working.

As mentioned above, this all works by injecting Javascript into the game - for which an "injector" script is needed, one can be found in my repo that supports command line arguments for easy reuse: injector.js.

A simple script, like below, will print out all the information of the "TakeDamage" class the previous post focused on:

import Enumerator from './enumerator.js'

// mono class we want to enumerate
var takeDamage = Enumerator.enumerateClass('TakeDamage');

// print it out
Enumerator.prettyPrint(takeDamage);

(run it with node injector.js 198X.exe enumerator-test.js)

Which prints out the "TakeDamage" class's information:

{
    "address": "0xe8733e0",
    "methods": {
        ...
        "Damage": {
            "address": "0xe887610",
            "jit_address": "0x1082bd20"
        },
        ...
    },
    "fields": {
        ...
        "isPlayerCharacter": {
            "address": "0xe8873d0",
            "offset": "0x1c",
            "type": "boolean"
        }
    }
}

The function's "jit_address" is of special mention as it's this value we needed in the previous post - either in CheatEngine script, or searching for specific bytes in memory - in order to patch. We now have a more programmatic, non-CheatEngine, way of finding this address IF we really wanted to do things that way... but we're still getting to the good bit.

Some of the code in my "Enumerator" is definitely going to buggy or unreliable, and the code contains some magical offsets based purely upon luck and assumptions - use with caution :P

Hacking "Beating Heart", "Out of the Void", and "Shadowplay"

Conveniently all three of these mini-games use the same logic, making them easy to hack at once. In the previous post I patched a if (this.dead ... logic check, to use a different field causing the damage logic to be bypassed. Rather than patching the game code, my script is going to dynamically modify the game object receiving damage to bypass the logic.

var takeDamage = Enumerator.enumerateClass('TakeDamage');
MonoApiHelper.Intercept(takeDamage.address, 'Damage', {
  onEnter: function(args) {
    this.instance = args[0];

    // check if the player is receiving damage, and if so then set "dead" flag
    // (damage code is skipped if the object receiving it is flagged as "dead")
    var playerCheck1 = takeDamage.getValue(this.instance, 'isPlayerCharacter'); // for "beating heart" and "shadowplay"
    var playerCheck2 = (takeDamage.getValue(this.instance, 'maxHealth') === 3); // for "out of the void"

    if (playerCheck1 || playerCheck2) {
      takeDamage.setValue(this.instance, 'dead', true);
      this.resetDeadFlag = true; // tell "onLeave" (below) to reset this
    }
  },

  onLeave: function(retval) {
    if (this.resetDeadFlag) {
      takeDamage.setValue(this.instance, 'dead', false);
    }
  }
});

The code "enumerates" the class (getting the field offsets, that I previously needed to get from CheatEngine, and other info) and then sets up a Frida interceptor on the "Damage" function. The arguments passed to the "Damage" function are available in the local "args" variable, with the first element being a pointer to the instance of the "TakeDamage" class.

Unfortunately, unlike regular Frida interception, changing the "args" values (or "retval" in the "onLeave") of a Mono function results in an error rather than affecting the game's code - so we can't do that. Instead the "dead" property of the object can be changed, when the object is the player's character, to bypass the game logic. This is all done dynamically, no hardcoded addresses or offsets here, so the hack should survive general updates to the game that don't change it's logic too much. The "this" variable is shared between the "onEnter" and "onLeave" functions making for an easy way to share some state.

The Enumerator's "getValue()" and "setValue()" functions determine the relevant offset from the instance base address, and the data type, to handle reading and writing values easily.

Hacking "The Runaway"

This is a racing mini-game dealing with time and speed rather than damage. It also revealed a fairly major shortcoming of my "Enumerator" code... I want to modify a property of a sub-class ("RoadRenderer.Sprite") which my code can't find or lookup the offsets for, so I've had to resort to a hardcoded offset :/

For this hack I bypass deceleration logic applied when you go off the road, prevent a speed loss when colliding with another car or obstacle, and prevent obstacles from causing a "wipeout". This logic, and work-arounds, were found looking through the game's code in dnSpy (which the previous post covers).

var carController = Enumerator.enumerateClass('CarController');
MonoApiHelper.Intercept(carController.address, 'SetSpeed', {
  onEnter: function(args) {
    this.instance = args[0];

    // prevent going "off-road" from reducing speed
    // (the offRoadDeceleration value is subtracted from current speed)
    carController.setValue(this.instance, 'offRoadDeceleration', 0.0);
  }
});

MonoApiHelper.Intercept(carController.address, 'OnCollision', {
  onEnter: function(args) {
    this.instance = args[0];

    // prevent collisions with other cars from reducing speed
    // (current speed is multiplied by collisionSpeedLoss, set it to 1 to prevent it from changing)
    carController.setValue(this.instance, 'collisionSpeedLoss', 1.0);

    // prevent collisions with objects form causing a "wipeout"
    // (the "shouldCauseWipeout" property of the sprite is checked to determine this, set it to false to prevent wipeouts)
    //
    // NOTE: a "RoadRenderer.Sprite" object is passed in to "OnCollision"
    // this "Enumerator" can't find the nested sprite class, so this has to be done manually...
    // the "0x44" offset is from CheatEngine, and we add it to the sprite address to reference "shouldCauseWipeout"
    var spriteAddr = parseInt(args[1]);
    var wipeoutAddr = spriteAddr + 0x44;
    Enumerator.setFieldValue(wipeoutAddr, 'boolean', false);
  }
});

Note: this does NOT make the "Complete The Runaway without a single collision" achievement any easier, the collisions still happen and are counted, you just aren't slowed down.

Hacking "Kill Screen"

This mini-game is an RPG, dungeon-crawler, style of game. Damage is dealt to the player by an "EnemyAttack" function in the "RPGController" class. The first argument passed into this function is the amount of damage being dealt but unfortunately, as mentioned above, this value can't just be set to zero to prevent the damage.

Instead of "preventing" the damage I decided to just "undo" it... reading the player's health value before the damage and setting the value back after the damage. Setting the health back in the "onLeave" (of the "EnemyAttack" function) has the desired effect, and the game believes the player's still at full health, but before the function completes the screen is updated and the damaged-health value is shown. To work around this I decided to reset the player's health in the screen update function ("UpdateStatusText" in the "Status") class... this meant that I couldn't use the "this" variable as scope is not shared between "onEnter" functions, so I used a global variable for this.

var rpgController = Enumerator.enumerateClass('RPGController');
var status = Enumerator.enumerateClass('Status'); // used to update on-screen RGP text (eg: health)

MonoApiHelper.Intercept(rpgController.address, 'EnemyAttack', {
  onEnter: function(args) {
    this.instance = args[0];
    var damage = parseInt(args[1]);

    // get the current health value, to set health back to after damage
    // (we can't change the incoming damage value to 0 unfortunately)
    var health = rpgController.getValue(this.instance, 'health');

    // we could set the health back in the "onLeave" for "EnemyAttack", but then the health displayed in-game looks like we took damage
    // instead we'll reset the health before the UI (and displayed health) is updated so it can stay at full health
    globalState.contollerAddress = this.instance;
    globalState.healthWas = health;
    globalState.updateHealth = true;
  }
});

MonoApiHelper.Intercept(status.address, 'UpdateStatusText', {
  onEnter: function(args) {
    // make sure we want to update health (NOT during game start or level up)
    if (globalState.contollerAddress && globalState.updateHealth) {
      // set the health back to what it was previously - before the UI update
      // using the RPG Controller's address, rather than this "Status" object's instance address!
      var health = rpgController.setValue(globalState.contollerAddress, 'health', globalState.healthWas);

      // clear the flag for future level-ups or game restarts
      globalState.resetHealth = false;
    }
  }
});

Conclusion

All of the above hacks are available in my "198X-hacks.js" script on GitHub. Also included is a hook of the game's main menu, detecting when the player starts one of the games:

Hooking game functions like this could make it easier to write bots - relying on events happening in-game to trigger bot logic, and providing easier access to game state - or be used in "speedruns" needing to detect when a level (mini-game) is completed.