Easier Grand Theft Auto cheats
This blog post is also available as a YouTube video:
I could never remember all the cheat codes in the Grand Theft Auto games by heart. Sure, one or two I could remember, but for the others, I had to refer to all the cheat code lists that were floating around. The goal is therefore simple, see if I can hack the games and replace the long cheat codes with a much simpler combination of inputs.
The games I will be reverse engineering are Grand Theft Auto III, Vice City and San Andreas. As you will see over the progress of this blog post, with each of these three games Rockstar put progressively more effort in hiding the cheat codes in the games.
If you're not really interested in the technical details of how the cheat codes work, you can skip straight to the cheat code remapper tool I've built, or the list of San Andreas cheat codes and all the alternative input sequences you can use:
- Remap cheat codes in GTA III, Vice City and San Andreas
- List of all GTA San Andreas cheat codes and alternative inputs
The tools I will be using for this are PCSX2, the PS2 emulator with its built-in debugger, and Ghidra, to assist with the decompilation and assembly interpretation. The games addressed in this blog post are the EU variants, though the information of course applies to the other regions as well, just with different memory offsets.
Table of contents:
- Hooking into the game
- Grand Theft Auto III
- Grand Theft Auto - Vice City
- Grand Theft Auto - San Andreas
- An online tool to remap the cheat codes
- Brute-forcing the San Andreas hashes
Hooking into the game
We need an entry point in the game, something we have knowledge of, that we can trigger a breakpoint on. Luckily, the list of cheat codes for the three games are known, we don't have to start from scratch. For example, we know that when we activate one of the weapons cheats, we are given multiple weapons, along with ammo.
Ammo is a good target, because we can easily shoot a single bullet to influence the value in memory that contains our current ammo count. The cheat code on the other hand, will increase that value. Once we have the memory address of the ammo value, we trigger the cheat code, and can look at the call stack to figure out which function handles the cheat code.
As you can see in the image above, the HUD of GTA III shows we have 12 bullets ready to shoot. Now we use the 'Memory Search' tab in the debugger of PCSX2, to find the value 12 as the initial search. Don't forget to turn off the 'Hex' checkbox, or you will be searching for the value 0x12, which is 18 decimal.
Note that we have over 125000 results, which is of course to be expected. Now, we simply shoot one bullet and
use the 'Filter Search' button to search for 11, 10, etc. until we reduce the amount of results to 1. As you can
see in the image below, it only took me 2 attempts to filter down the search in order to end up with one
address, namely 0x00B653EC.
It's always good to double check the memory address, so we change it to e.g. 0x20, and the game
immediately shows us we have 32 bullets. This confirms that we know where the ammo count for this gun is stored
in memory.
Now, we set a 'write memory' breakpoint on this memory location, and trigger the cheat code again. This should break in the function that handles cheat code stuff, if we're lucky.
As indicated by the little arrow to the right of the instruction, this is the position where the debugger break happened. From this point on, we can start to analyze the code and see where the cheat code is being handled. The same approach was used for all three games, manipulate a value you have easy control over, in our case the ammo count, trigger the cheat code and break on that value being changed.
Grand Theft Auto III
If we take another look at the screenshot above, showing where the debugger breakpoint was triggered, we see
something very interesting. A couple lines above the breakpoint we see
CWeaponInfo::GetWeaponInfo(...). It looks like there are debugging symbols available for GTA III !
This will make our life a lot easier of course, as we don't have to guess what functions exactly do, we just
have to look at the function names provided.
We'll move over to Ghidra now, so we can analyze the code a little bit easier. We're dealing with a MIPS III variant here, so we'll make use of Ghidra's decompilation window and easy browsing of functions and their references to make sense of things.
Let's start by looking up the same memory address that triggered the breakpoint in PCSX2, namely
0x0019BB98, in Ghidra.
The function signature of the function where we ended up, as defined in the symbols, appears to be
Reload__7CWeaponFv(). When we look at the XREF table in the top right, we can see that
this function is referenced, or rather called from the GiveWeapon... function. This of course all
make sense, because the cheat code is literally giving us multiple weapons (and the corresponding ammo).
We simply need to move up in the call hierarchy to see if we encounter the function that handles the cheat code.
It won't be the GiveWeapon function obviously, as we can presume that that function is being
called for *every* picking up of a weapon, not just via a cheat code. Most likely its parent function will be
the cheat handler.
We double-click on the XREF entry for the GiveWeapon function in Ghidra, and see a
whole list of references again, with one standing out completely.
The game couldn't have made it easier on us, there's literally a function called AddToCheatString.
Let's double-click on the first XREF of the AddToCheatString function and see where we
end up.
Looks like we end up somewhere around the beginning of the AddToCheatString function. If we only
look at the magenta text, we see there are multiple references to the GiveWeapon function, a
similar amount of calls to FindPlayerPed, to locate the player pedestrian entity. The 'All weapons'
cheat code of course gives you a whole set of weapons, so multiple calls to GiveWeapon seem
warranted. There's also a call to SetHelpMessage, which displays the 'Weapon Cheat' message, or
whatever is appropriate for the respective cheat code.
More interestingly though, there is a call to strncmp at the top, with a branch instruction after
that. The target of the branch function skips the entire region of GiveWeapon calls. This is a good
candidate for a cheat code verification! Let's look in detail.
The strncmp function, which simply compares two pieces of text, expects three arguments: the two
strings to compare and the maximum length. These arguments are passed to the function in the a0,
a1 and a2 registers, in this architecture. We do see all three arguments being
assigned in the above screenshot.
The first argument, in register a0 is assigned at address 0x00282D78, with the
addiu a0,s0,0x5F instruction. Basically, we are assigning whatever is in s0 + 0x5F to
the a0 register. That means that s0 probably refers to some struct, with the value we
are interested in, in position at offset 0x5F within that struct. In a moment we'll take a look in
the PCSX2 debugger what that memory location contains, as Ghidra of course can't give us that information.
The second argument to strncmp, in register a1 is assigned in the next instruction:
addiu a1=>@2690,v0,-0x3370. Ghidra helps us out here because it knows exactly where this string is
located. The position of v0 - 0x3370 can be deduced, because v0 is assigned a
hard-coded value just above this section of code. That's why it inserts the string reference @2690
in the instruction. On the right side, it even shows us the exact contents of that string, which is
'URDLURDL4144'.
The third argument is slightly weird, because of a convention in the architecture we are dealing with. Below the
jal strncmp instruction, we see that a2 is being loaded with the value 12
(0x0C). You'd expect that the call to the strncmp function has already occurred and
that setting of the a2 register is simply too late.
This is however not the case. The jump and link (jal) instruction actually executes its subsequent
instruction first, before branching to the target, and then puts its return position two instructions after
itself, in this case at the bne instruction. It definitely takes some getting used to reading
assembly like this.
Now we return to PCSX2 to find out more about the first argument, the string we are comparing to
'URDLURDL4144'. We'll need to add a breakpoint at address 0x282D78, where
a0 is assigned, so we can see what register s0 contains. Then we'll add
0x5F to that, and inspect that memory address.
The breakpoint immediately triggers whenever we make an input on the controller.
The right arrow points to our breakpoint indicated by the little green square, where execution is currently
halted. We're interested in the value of the s0 register, which we can see by looking at the
leftmost arrow. The s0 register contains at this point in time the value 0x0050A440.
We of course have to add 0x5F to that, to find the address of the string we are comparing.
That addition leaves us at address 0x0050A49F, to which the bottom arrow points. The ASCII
representation on the right shows the string currently as 'X41RR42D4312'.
That string is of course quite similar to 'URDLURDL4144', and seeing as we are dealing with cheat
codes here, these must represent the inputs we make. When the string comparison succeeds, the cheat code is
routine is activated, otherwise that piece of code is jumped over. If we deactivate the breakpoint and continue
the game, we do see our last 12 inputs appear in reverse order at this memory location.
The cheat code we are currently dealing with, the 'All weapons' cheat code, is: 'R2, R2, L1, R2, LEFT,
DOWN, RIGHT, UP, LEFT, DOWN, RIGHT, UP', and the internal representation of this cheat code is
apparently 'URDLURDL4144'. It is simply reversed, with these letter representing the inputs:
U= UpD= DownL= LeftR= Right1= L12= L23= R14= R2C= CircleT= TriangleS= SquareX= X
To replace the default cheat code input combinations in GTA III we simply need to do two things. First, we need
to overwrite the hardcoded string with the combination we want. We can for example patch the first three
characters of the above mentioned string with 'TCC'. This would result in the cheat code
'Circle Circle Triangle'. Remember, we need to reverse the code.
Leaving it like this will not work! The second thing we need to do is adjust the third parameter to the
strncmp function, from 12 characters to only three, because our new cheat code is only three
characters long. It's as easy as that.
As for all the other cheat codes, they are simply located in the AddToCheatString function.
Grand Theft Auto - Vice City
For Vice City we employ the same initial technique. Figure out where the ammo count is stored, put a write breakpoint on that address and enter the cheat code. The code for activating the weapons 1 cheat is easily found.
We can see several identical calls, with different parameters (weapon identifier and ammo) in this code. Unfortunately, as you can tell, there are no symbols available, so we have to figure things out for ourselves.
This function is only referenced in one parent function, which of course makes sense as this is the code that handles the cheat for weapons 1, so that parent function must handle all the cheats.
The function that performs the weapons 1 cheat is highlighted (FUN_00280350). Even though we don't
see an obvious strncmp being used, we do see a similar pattern in this function.
FUN_0027F210 is being called many times, presumably for each cheat code, and its output is checked
for zero. If it is, the cheat code handler (e.g. our weapons cheat) gets executed.
We need to figure out how FUN_0027F210, which determines whether the cheat code gets executed,
works. Let's start by looking at its parameters. To check what param_1 contains, we have to use the
PCSX2 debugger, break at this point in the code and inspect the correct register. The equivalent assembly for
the call to this function is this:
We can see that the a0 register, which will be the first parameter to the function call, is formed
by adding 0x79 to the s0 register. When we look in the debugger at the s0
register at that point in time, and add the hardcoded offset, we see this:
Exactly the same as in GTA III, this contains the reversed inputs we made on the controller.
If we're validating whether the cheat code is correct, and the first parameter is the inputs we made, surely the
second parameter must be the cheat code itself. In the example of the Weapons 1 cheat, the second parameter is
the address 0x4A2300. Let's look at this value in Ghidra.
Instead of seeing a certain sequence of input codes, as we did in GTA III, we see a garbled string of
characters, seemingly having nothing to do with inputs. The value is "WWNM\XNW;:7;" (Ghidra
escapes the backslash).
The cheat code itself for Weapons 1 is R1, R2, L1, R2, Left, Down, Right, Up, Left, Down, Right, Up. This seems
to resemble in no way to this garbled string, other than the length being 12. We'll have to decode the actual
function FUN_0027F210 to see what is going on.
Clearly we're not dealing with a simple string compare anymore. I've given the function a name, and also named
the function parameters. To avoid confusion with double negatives, I didn't put the return type as a boolean,
because the callers of this function expect the result to be zero (false) if the entered inputs match a cheat
code. If we see a return 1, we know the function failed, and we're not dealing with a valid cheat
code.
Basically, we have an infinite while loop, with one success exit condition and 13 fail exit
conditions. The while loop iterates over the individual characters of the cheat code, by way of the
ix variable, incremented by one at the bottom of the loop.
In line 16 we break if our counter ix reaches above 11, implying a maximum cheat code length of 12.
In line 14, we return zero, a success state for a matching cheat code when the null character is reached in the
cheat code string.
The remaining failure exit conditions happen in the big switch statement, which apparently switches on
ix, the position of the current character in the cheat code (not the character itself).
The algorithm becomes pretty clear now: for each position in the cheat code string, the entered input at that same position must have the same value, with some arbitrary number subtracted from it.
For position 0, we subtract 2, for position 1 we subtract 5, etc... If we therefore add these offsets to the
garbled cheat code string, we should end up with the expected inputs for that cheat code. Let's try this with
the Weapons 1 code, which had "WWNM\XNW;:7;" as its garbled cheat code string.
| Garbled code | ASCII hex value | Offset | Adjusted character |
|---|---|---|---|
W |
0x87 |
-2 |
U |
W |
0x87 |
-5 |
R |
N |
0x78 |
-10 |
D |
M |
0x77 |
-1 |
L |
\ |
0x92 |
-7 |
U |
X |
0x88 |
-6 |
R |
N |
0x78 |
-10 |
D |
W |
0x87 |
-11 |
L |
; |
0x59 |
-7 |
4 |
: |
0x58 |
-9 |
1 |
7 |
0x55 |
-3 |
4 |
; |
0x59 |
-8 |
3 |
There we go, 'URDLURDL4143' is the result, which matches with the inverse of the inputs: R1, R2,
L1, R2, Left, Down, Right, Up, Left, Down, Right, Up.
Replacing cheat codes is now as easy as locating the garbled string, putting in our own, by applying the offsets to the correct positions. Looks like they made a conscious effort to obfuscate how the cheat codes are stored in the binary. Finding cheat codes wouldn't be as simple as searching through all the strings in the executable. In GTA III they would have stood out more, considering 'URDL' is easily recognizable. A simple obfuscation prevents this.
We also don't have to worry about patching the length of a cheat code, as we had to do for the
strncmp function in GTA III. The code in Vice City will just loop through all the cheat code
characters until it finds a match. Of course, this means you can make cheat codes 1 input long, but that would
of course render the game pretty much unplayable.
Grand Theft Auto - San Andreas
For San Andreas we employ the exact same procedure, figure out where the ammo is stored, look higher up in the hierarchy to find the function that handles the weapons cheat, and look one function above that to find the function that handles the cheat codes.
This time, we see something completely different.
The highlighted line 51 is where Ghidra tells us we were calling the Weapons 1 cheat from. Except, we don't see
a straight call to this function, but rather an array lookup (indexed by iVar6), cast to a
code/function pointer, which is then executed.
Let's start by looking at this array called PTR_FUN_0064AD60.
Inspecting the first three pointers in this array indeed confirms that these are function pointers to the cheat handlers. This is easy to conclude because we know how the three weapons cheats sort of look like, basically a set of calls to the same function with different weapon type and ammo counts.
Strangely enough, we see null pointers in this list as well. That is something we'll have to investigate later.
We'll clean this array up a little bit in Ghidra by telling it what exactly it contains, as it seems to be slightly confused. First, we need to determine how many entries are in the list, in other words, how many cheats are in the game.
If we look at the main function again, we see two nested do-while loops around the middle/end of the function.
The while condition for the inner loop (line 58) is iVar6 being less than 0x5C, or 92
in decimal.
Considering iVar6 is actually used to index into the array of pointers (line 51), we can assume
that there are 92 cheat codes in the game (0-91). Of course, the list of cheats in San Andreas is known, and
there do seem to be somewhat 90 codes floating around online, in several lists.
Knowing this information, we tell Ghidra to make an array at address 0x0064AD60 of 92 pointers, and
we'll call it CHEAT_HANDLER_PTRS.
Let's look at our nested do-while loops again.
We know what triggers the cheat code (line 51), so let's see what the condition is to get to that part of the
code. This condition can be found in line 36: iVar2 needs to equal whatever piVar5 is
pointing to.
iVar2 is assigned the result of a function call in line 32, and piVar5 is set to a fixed hard-coded
address. Higher up, piVar5 is defined as a pointer to an integer, and we see on line 57 that within
the inner loop piVar5 gets incremented by 1 as well.
Of course, this doesn't mean that this pointer progresses by 1 byte, but rather by an entire int, as if it were
an array of ints. piVar5 keeps in sync with iVar6, our cheat code function indexing
counter, so we can assume that piVar5 is an array of 92 integers. Let's look at this in Ghidra, and
create a similar array. We can find the address of the first int on line 34, where piVar5 is
initialized.
Isn't this a coincidence! The array of integers is positioned right after the array of cheat handler function pointers.
Unfortunately, we have no clue what this array of integers means, only that there are also 92 of them, and they presumably have a one-to-one link to the respective cheat code at that index.
The answer to what these integers mean must lie in the call to the function that assigns iVar2, as
that result is being compared to our ints, and if they match, the cheat code is executed. Let's take a look at
this function:
Ghidra is letting us down unfortunately. This can't possibly be the decompiled equivalent of what the assembly is doing. There's no return value (we're expecting an int of course), and the code itself doesn't seem to make sense, we're just looping over a character array until we reach null. Let's compare this to the assembly.
Before we delve into this function though, we need to know what the first parameter to this function contains.
For
that, we'll put a breakpoint on this function in the debugger, and inspect register a0.
At the point in time where the breakpoint triggers (by pressing an input on the controller), register
a0 contains the address 0x01FFECC0. To the surprise of nobody, this again contains the
string of last made inputs, similar to the two other games. Back to the assembly of the function we are looking
at.
First thing we do in this function is load the first byte of our input parameter (the string containing the last
made inputs) into register a2, and checking whether a2 is zero/null. If so, we bail
out of this function.
We're loading -1 into register v0, or 0xFFFFFFFF if you will. Then, we're
building up the value 0x00626BB0 in register a1.
Notice how the next line has received a label, namely LAB_004D6798. This label is referenced below
in the conditional branch instruction bne. This would make this little section the body of a loop,
which would make sense as we presumably have to loop all the inputs currently made.
The bne instruction checks whether a2, containing our input is the null
character. a2 is assigned the next character through the addiu and
lbu instructions in the middle of the function. Basically, we're indeed looping every
character/input.
Next, we're doing a whole lot of complex stuff, that's not easy to keep track of in our brain. Stuff is being
AND-ed, shifted right, XOR-ed, shifted left, ... Things become interesting however
when register a1 becomes involved again. In a1 we had built up the hardcoded value of
0x00626BB0. v0 is now being added to this by way of the addu v0,a1,v0
instruction, and then immediately after v0 is dereferenced by the lw instruction. This
dereferencing implies that again, we are indexing in some sort of array of bytes. Let's see what Ghidra shows us
at this address.
I've included the string that precedes this array of bytes, containing "Used in script". We don't know how many bytes in our array we are dealing with. When we ask Ghidra to convert this to an array, it gives a suggestion of the size.
It suggests 1023 bytes, which is awfully close to the round number 1024. Let's look at the end of these 1023 bytes.
Ghidra thinks that the dash (0x2D) is part of the next string, "Ped2Pl_Conversation", which is a
reasonable assumption. However, the label it assigns to this next string, without the s_ prefix is
Ped2Pl_Conversation_00626fb0. Also, the address in that label ends with fb0, which is
of course one more than faf, where the dash resides.
It's safe to say that this array contains 1024 bytes, not 1023.
It's becoming clear what this little function does, with the string of inputs we give it. It computes a hash in the form of a 32-bit integer, based on a 1024 byte hash table. This hash value is then compared to all 92 hash values in the table. If they match, then the corresponding cheat function pointer is fetched from the function pointer array, and the cheat is executed!
This makes sense when we look at the progression of hiding cheat codes in the three games. GTA III was unobfuscated, cheat codes hiding in plain sight. Vice City had a simple obfuscation mechanism, but easily reversible, so cheat codes could be decoded.
San Andreas however uses a hashing algorithm, which is of course one-way. You can't simply recreate the inputs that went into the hash function. Pretty clever when it comes to hiding stuff!
Substituting the cheat codes with our own now simply comes down to reimplementing the little hashing routine,
running our own inputs through that routine, and replacing the hash value in the big table of 92 hashes. The
algorithm is quite simple, as you can read from the assembly, it's just a bunch of AND-ing,
bitshifting left and right, XOR-ing, etc...
One last thing, remember the big cheat function pointer array containing null pointers? Well, if we look at the cheats handler function again, we can see that this is explicitly handled in line 47.
If the cheat handler function pointer equals null, then in line 48 the array at 0x88E380 gets
indexed (by the current cheat code index), and the boolean value is inverted. Some cheat codes need a function
to execute, if it needs to do more complex stuff. Other cheat codes simply need to toggle a boolean.
An online tool to remap the cheat codes
After figuring out how all the cheat codes were stored in the three games, I created a tool to easily patch in your own custom cheat code. This is done by way of PCSX2 pnach files.
Simply choose the game and region to start, then pick a cheat code you want to alter from the drop down. You will be presented with the original cheat code in the game. The green arrow underneath indicates the current position you will be altering.
Click on different inputs just below and you will see the cheat code change. The green 'blank' box allows you to erase the current position (and everything to the right of that). Press the 'Save to output file' button to store that new cheat configuration in the table at the bottom of the page, and continue with other cheats you want to alter.
Once you have finished reconfiguring your cheats, click the 'Download pnach file' button at the bottom of the page, and load the file into the cheats folder in PCSX2.
I've also added a way of changing which vehicle gets spawned for the 'Spawn Caddy' (GTA VC and SA) or 'Spawn Rhino' (GTA III) cheats. Combine this with remapping the inputs for the actual Spawn Caddy/Rhino cheat and you can easily spawn whichever vehicle you want.
You can access the cheat remapping tool here.
Brute-forcing the San Andreas hashes
The consequence of using hashing to hide the cheat codes is that hash collisions might exist. Two sets of inputs might generate the same hash value, therefore be functionally identical cheat codes. When I was looking through the lists of cheat codes for San Andreas online, I noticed that the 'Country vehicles' cheat was sometimes listed as "Triangle, Left, Square, R2, Up, L2, Down, L1, X, L1, L1, L1" and sometimes as "L1, L1, R1, R1, L2, L1, R2, Down, Left, Up". Both work, and result in the same cheat code being triggered, an example of a hash collision!
Considering the hashing algorithm was quite small, I figured I'd give it a go at brute-forcing the entire set of possible inputs. Considering there's a minimum length of 7 inputs, I started with all possible combinations of 7 inputs. That pretty much immediately yielded 4 hash collisions. Three of those were the official cheat codes for their respective cheats, but one was shorter than the official one. The official 'Spawn Jetpack' cheat code has a length of 10, yet "Down, Circle, L1, Circle, Up, Down, Right" only has seven and does the exact same thing!
I then hashed all combinations of 8, 9, 10 and 11 length codes as well, each taking progressively longer to complete. All the inputs of length 11 took about 17 hours, with my -granted- relatively unoptimized hashing code. Hashing the 12 input length codes would take about a week of continuous work, but would not give me any codes shorter than the official ones, seeing as the max length of those is 12 anyway.
Three (perhaps) undiscovered cheat codes were found. There are two 'Country style' cheat codes available. One changes all vehicles to 'country vehicles', and one that gives you a country outfit, spawns country vehicles and pedestrians. The cheat codes I could find only claimed to be the latter, but didn't give you the country outfit. They were mislabeled 'Country vehicles' cheat codes.
By brute-forcing, I did of course find lots of collisions for the Country outfit, vehicles and pedestrians cheat, but couldn't find any references to these inputs online. So, this might be 'undiscovered', or I just coulnd't find it in any of the online lists of cheats.
There also seems to be a Weather: rain/storm cheat code that I couldn't find any reference to online, but it is pretty much equivalent to the Weather: rainy cheat.
There's also an undiscovered cheat code, but it literally does nothing at all.
Finally, the two last hashes in the array of 92, at index 90 and 91, represent two unused/blank cheat codes. I deem them unused, not because they don't have an associated function pointer to handle the cheat (it would still toggle the boolean that might have an effect somewhere), but because the hash value is literally zero.
Of course, many input combinations exist that do have zero as an end result, so you can find those inputs in the list as well.
Have a look at the definitive list of San Andreas cheats. Shorter than official codes are indicated in green, equal length and longer in red. Not all cheat codes have a shorter equivalent than the official one, but some inputs might be easier to perform!
If you want to watch me do this type of reverse engineering live, follow me on twitch.tv/zappatic, or check out my YouTube channel. You can also contact me through BlueSky, or X/Twitter.