Game Hacking with Binary Ninja

by

Ben R

July

2022

Most searches for game hacking will return with a plethora of articles about using CheatEngine to make alterations in memory. While this is a valid option, I wanted to challenge myself to use a different method to implement some common game cheat functionality. I decided to use a reverse engineering framework to implement patches to the game files. Not only does it avoid using the clunky CheatEngine UI, it’s also much more portable and repeatable to make permanent changes to the game file, rather than in memory.

As the never-ending battle between IDA and Ghidra rages, Binary Ninja has slowly been improving in reliability and performance. Behind the aesthetic UI and intuitive keybindings is a powerful scripting API. Gone are the days of writing Java or Jython (looking at you Ghidra) – Binary Ninja supports Python and C++ by default, with Rust binding available too. This article is essentially going to be a demo of how I use the scripting API to write patch automation.

The target for the article is going to be the PwnAdventure3 Windows Client. PwnAdventure is a deliberately vulnerable video game, written for the Ghost in a Shell 2015 event, so feel free to follow along, or dive in and create some cheats for yourself.

PwnAdventure can be hosted on a server or played entirely from the game client. For the purposes of this article the game will be played entirely from the game client (offline mode).

The three most interesting files are PwnAdventure3.exe, GameLogic.dll, GameLogic.pdb

A Basic Patch

The first goal I set was to implement a speed hack, to make navigating the map less painful. This can be done in PwnAdventure by patching the GameLogic.dll file. The DLL comes with a PowerBasic Library (PBL) file too (a file containing symbols and data structures inside the DLL). Since speed is related to the player, looking in the Player class, there is a method GetSprintMultiplier

Game code
The method is simple enough, it retrieves a floating point number from a memory address and returns.

float __convention("thiscall") Player::GetSprintMultiplier(class RubicksCube* const this)

fld     st0, dword [0x10078b34] # 0x70dbca0a
retn     {__return_addr}

By increasing the value at address 0x10078b34, we can increase the rate the player increases speed, and also their maximum speed. We could automate this by writing a bigger number to the address 0x10078b34. This could be done in Binary Ninja by simply entering the following command into the Python interpreter:

bv.write(0x10078b37,b"\x7f")

This sets the highest byte of the 4 bit (little endian) signed integer to the max possible positive value. Saving the newly patched DLL and running the PwnAdventure.exe file, we now observe our speed increases rapidly until we can cross the entire map in just a few seconds.

Improving the Reliability

This works fine, but if a game update was released, the memory address that holds this value could have changed so if we ran the patch command it would just corrupt the file, which pretty much defeats the point of automating it in the first place. By dynamically identifying the memory address through scanning Player::GetSprintMultiplier for a pointer we eliminate this risk.

# get the address of the Player::GetSprintMultiplier function 
address_of_multi_func = bv.get_symbols_by_name("Player::GetSprintMultiplier")[0].address
# get the Player::GetSprintMultiplier function object
multi_func = bv.get_function_at(address_of_multi_func)
# list of all the instructions in the function, store the first one
multi_func_instr =[i for i in multi_func.llil_instructions][0]

# find the address constant in the list of tokens
for token in multi_func_instr.tokens:
    if type(token.value) == int and (token.value != 0):
        pointer_to_speed_multiplier = token.value
        break
    else:
        pass
        
# write to the address    
bv.write(pointer_to_speed_multiplier + 3, b "\x7f")

So that’s a fair way to increase the reliability. We could also increase the portability by removing the reliance on the PBL file.

Improving the Portability

In PwnAdventure, a player can collect spells as the progress through the game. The first spell a player gets is called Great Balls of Fire, and it costs 4 mana to perform.

Being able to perform the spell without worrying about mana levels would be pretty cool, let’s patch it out. This can be done without the PBL file by dynamically locating interesting data that exists directly inside the DLL, rather than symbols imported from the PBL file. Strings are a useful place to start here.

With the PBL file imported, the GreatBallsOfFire::GetManaCost pseudocode for the function looks like this:

enum ItemRarity __convention("thiscall") GreatBallsOfFire::GetManaCost(class Flag* const this)
{
	return 4;
}

The GetManaCost method belongs to the GreatBallsOfFire class, if we find the class vtable, we should find the method. We could locate the class by finding another method belonging to the same class, that handles a unique string. The method GreatBallsOfFire::GetFlavorText uses the string Many balls. Very fire. Ow. The process for finding GreatBallsOfFire::GetManaCost becomes:

  1. Find references to the unique string by scanning memory and finding xrefs to it. Through this, the GreatBallsOfFire::GetFlavorText can be located
  2. Parse the GreatBallsOfFire class vtable to find the GetManaCost method
  3. Patch GreatBallsOfFire::GetManaCost

So to find references to the string, the find_all_data method can be used. This takes a bytestring and address range as arguments, and searches for consecutive bytes that match the specified bytestring. This returns a generator object that can be parsed into a list.

fire_string: bytes = b"Many balls. Very fire. Ow."
fire_refs: list = list(bv.find_all_data(bv.start, bv.end, fire_string))

A Binary Ninja plugin for something like this could look like the following. The code has been commented heavily to describe the functionality of the Binary Ninja specific functions:

 
from binaryninja import *
import struct


def mana_hack(bv) -> None:
    """
    Patch the GreatBallsOfFire::GetManaCost function to return 0, providing unlimited mana
    """
    fire_string: bytes =  b"Many balls. Very fire. Ow."
    # find_all_data returns a generator object containing tuples of (address, binaryninja.databuffer.DataBuffer object)
    fire_refs: list = list(bv.find_all_data(bv.start, bv.end, fire_string))
 
    if len(fire_refs) != 1:
        log_error(f"Invalid number of strings ({len(fire_refs)}) found for string {fire_string}. Was expecting to be unique. Exiting")
        return
 
    # Use get_code_refs to find code xrefs. The address of the referencing instruction is stored in field 0
    # returns a generator of binaryninja.binaryview.ReferenceSource objects, which have an address attribute
    refs_to_string: list = [i.address for i in bv.get_code_refs(fire_refs[0][0])]
 
    if len(refs_to_string) != 1:
        log_error(f"Invalid number of references found for string {fire_string}. Expected 1. Exiting")
        return
    else:
        log_info(f"Got the following reference to '{fire_string}': {hex(refs_to_string[0])}")
 
    # pass an address, returns a list of binaryninja.function.Function objects
    GetFlavorText_func = bv.get_functions_containing(refs_to_string[0])[0]
    # get xrefs to the start address of the function from the data segment.
    # Returns a generator of BinaryView.get_data_refs objects
    GreatBallsOfFire_ref: list = list(bv.get_data_refs(GetFlavorText_func.start))# inside the GreatBallsOfFire class vftable
 
    if len(GreatBallsOfFire_ref) != 1:
        log_error(f"Invalid number of references found to GreatBallsOfFire::GetFlavorText. Expected 1. Exiting")
        return
    else: 
        start_addr: int = GreatBallsOfFire_ref[0]
        log_info(f"Got the following reference to GreatBallsOfFire::GetFlavorText: {hex(start_addr)}")
 
    function_found: bool = False
    # iterate through the vtable looking for a function that matches our expected function
    while function_found is not True:
        # read 4 bytes of data from the start_addr
        pointer: int = struct.unpack("I", bv.read(start_addr, 4))[0]
        # returns a binaryninja.function.Function object that the provided address belongs to.
        function_ptr  =  bv.get_function_at(pointer)
        # create a list of hlil instructions. This will make it easier to identify the interesting
        # function without comparing individual assembly code instructions
        instruction: list = list(function_ptr.hlil.instructions)[0]
        if str(instruction) == "return 4":
            function_found = True
        elif function_ptr is None:
            # if its none, we've gone past the end of the vtable so should just give up
            log_warn("Parsed GreatBallsOfFire class but could not find GetManaCost. Exiting")
        else:
            start_addr += 4
 
    # read the vtable pointer to get address of GreatBallsOfFire::GetManaCost
    
GetManaCost_func: int = struct.unpack("I", bv.read(start_addr, 4))[0]
    log_info(f"Patching at location: {hex(GetManaCost_func)}")
 
    # the first instruction of the function, so we can just write to the addr directly
    # bv.write returns the number of bytes written, so if 0 then write failed
    if bv.write(GetManaCost_func,  function_ptr.arch.assemble("mov eax, 0")) == 0:
        log_info(f"Writing patch to location {GetFlavorText_func} failed.")
 
    # Check save file operation returns True ,to alert the user if there is a save issue
    if bv.save(bv.file.filename) is True:
        log_info("Patch complete. Spell 'Great Balls Of Fire' should now cost 0 Mana ")
    else:
        log_info("Could not save automatically. Attempt to save manually")
    return
 
 
PluginCommand.register("Hack Mana", "Implement a Patch for Unlimited Mana", mana_hack)

The spell now costs no Mana. This works on dll files independently of the PBL file too making the script more portable. This could be extended further by reworking the script to work independently of the Binary Ninja, as a headless script.

For anyone following along, you may notice that the fireballs now also cause zero damage. This is because the calculation for spell damage in PwnAdventure is based on Mana cost. So, my challenge to you is to write a new Binary Ninja plugin to provide unlimited (or a huge amount of) Mana by patching the Player::UseMana or Player::GetMana functions. The snippets plugin can be useful for writing these kinds of UI scripts if you don’t want to make them full plugins. Feel free to tweet @InterruptLabs to let us know how you solved it!

Please click on "Preferences" to confirm your cookie preferences. By default, the essential cookies are always activated. View our Cookie Policy for more information.