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
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 tokensfor token in multi_func_instr.tokens:
if type(token.value) == int and (token.value != 0):
pointer_to_speed_multiplier = token.valuebreakelse:
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:
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:
Find references to the unique string by scanning memory and finding xrefs to it. Through this, the GreatBallsOfFire::GetFlavorText can be located
Parse the GreatBallsOfFire class vtable to find the GetManaCost method
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 structdef 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")returnelse:
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 vftableiflen(GreatBallsOfFire_ref) != 1:
log_error(f"Invalid number of references found to GreatBallsOfFire::GetFlavorText. Expected 1. Exiting")returnelse:
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 functionwhile 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 = Trueelif 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 failedif 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 issueif 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.