Skip to content

Pointers

Zorg edited this page Jul 5, 2024 · 15 revisions

Introduction

Sometimes you may find a variable but find out its memory address is no longer pointing to what you want after taking an action in the game or restarting the game.

In these cases you may find there is another variable that contains a pointer (a memory address) to the variable you found. If you find this pointer variable (a variable whose value is a memory address), you may be able to read the memory address it stores to get to the variable you're looking for. This is an example of using one level of indirection. You can also have a pointer variable that points to another pointer variable that finally points to the variable you're looking for, which is an example of using two levels of indirection. The number of levels of indirection can be even greater and more numerous; in some cases you may have to chase up to 10 levels of indirection to find the variable you're looking for!

Moreover, pointer variables don't have to hold addresses that point exactly to the specific piece of data you're searching for. At each level, you may have to add an additional offset. For example, you may have found a pointer variable located at 0x1009A2D00 that stores a pointer to a player's structure which is located at 0x1009A2D88 but the player's health (the variable you're looking for) is located at 0x1009A2D88 + 0x10. In this case an offset of 0x10 needs to be applied after reading the pointer stored from 0x1009A2D00. In Bit Slicer, this dynamic address can be represented as [0x1009A2D00] + 0x10. Here, 0x1009A2D00 is the base address used to compute your variable's final address.

And then for this example you may find that there is another pointer variable to this first base pointer. For example, [0x1008A1A00] + 0x20 may compute to 0x1009A2D00 which is the address of the first level pointer variable found above. In Bit Slicer, this dynamic two-level address can then be represented as [[0x1008A1A00] + 0x20] + 0x10. In this example, 0x1008A1A00 is the new base address. You want to keep recursing with the number of pointer levels until the base address you find doesn't change, for example when you start up a new game or relaunch your game.

If the memory address is static to the program, Bit Slicer will use a base(...) + SomeOffset expression as the base address which indicates the address is relative to one of the mapped binaries in the running target. In most cases, you will want to find a static base address. Ultimately, the goal is to find a stable dynamic variable address so that you do not have to perform a search every time you want to manipulate a variable after you relaunch the game.

Finding pointers to variables

This guide assumes you are using Bit Slicer 1.8 or later.

I wrote a small game, DodgeDanger (source code) to help understand the process of finding pointers to variables. This game requires macOS 11.0 or later.

I will discuss three different approaches and guides to finding pointers to variables. Following each approach will widen your understanding. These approaches are meant to be followed in-order.

Searching one level at a time

  1. Start the game and find your player's score (a 32-bit integer) in Bit Slicer. Advance in the game to increase your player's score. In the process of narrowing down the results, do not let the player hit an obstacle. If you find two results, find out which variable is the real one by trying to overwrite them and see which one updates the score. Delete the other false positive variable results.
  2. Optional: if you let the player lose by hitting an obstacle and Play Again, you will notice this variable you found is no longer valid and not representing the player's score. If you try this, repeat step 1 and find the player's score again.
  3. Change the search type from value to address in the toolbar. Bit Slicer should populate the search field automatically with the variable address that you found. In cases that the search address is not populated, select the variable you found and click Variable -> Search Pointers to Variable from the menubar. This is also an available option if you right click on the variable.
  4. Search for the memory address. Bit Slicer should return a few dozen or so results. If you hover over the newly found variable addresses and let the tooltip show (or edit the variable addresses), you will see that they are all one-level pointers with an expression matching [base_address] + offset. None of these variables contains a static base address however.
  5. Optional: if you let the player lose by hitting an obstacle and Play Again, you will notice some of the variables you found are still valid and represent the player's score. Switch the search type from address to value and try narrowing the results as much as you can by searching for your player's new score. You might still have multiple results which is okay. Then switch the search type from value to address. Ensure the search field is populated with the most recent variable address you found.
  6. Optional: if you use the game's menu to Exit the game, and click Play again, you will notice the variables you found are no longer valid and not representing the player's score. If you try this, you will need to repeat steps 1, 3, and 4.
  7. Bump the number of max levels from 1 to 2. Search for the memory address again. If you inspect the newly found variable addresses, you will find out they are all either one level pointers found in step 4 or new two level pointers. At the top of the results should be a two level pointer that has a static base address. Bit Slicer also annotates this variable as static in its description. This static variable looks very promising.
  8. Optional: use the game's menu to Exit the game. Click Play to start a new game, advance your score, and go back to Bit Slicer. Change the search type to value and narrow down the list of results by searching for the player's current score. You may find that there are still two-level pointer variables that point to your player's score. You might still have multiple results which is okay.
  9. Quit the game and launch the game again. Click play to start a game, advance your score, and go back to Bit Slicer. Change the search type to value and narrow down the list of results by searching for the player's current score. This should only leave you with one result which is a two-level pointer containing a static base address.

The optional steps are for showing how different levels of pointers may correspond to different levels of states in the game. In this game the destructive actions that lead to different states are:

  • Hitting an obstacle and playing again
  • Using the game menu to Exit the current game and hitting Play to start another round of games
  • Quitting the game and launching the app again

In this game, no use of pointers may result in finding a memory address that is stable until you hit an obstacle. One-level pointers may stay stable until you use the Exit option from the game menu. Two-level pointers may stay stable in this case, however relaunching the game may result in many of the two-level pointer results being invalid.

The optional steps (steps 5 and 8) also show that you can further narrow down results before proceeding to re-search those results using a deeper max level. Eliminating results earlier on may help for speeding up searches in larger games. Searching for pointers multiple levels of deep can sometimes take a considerable amount of time.

Searching with a bigger beginning max level

The first one level at a time approach started with a Max Level of 1 but this second approach will start with a more opportunistic deeper Max Level.

  1. Start the game and find your player's score (a 32-bit integer) in Bit Slicer. Advance in the game to increase your player's score. In the process of narrowing down the results, do not let the player hit an obstacle. If you find more than one result, find out which variable is the real one by trying to overwrite one of them and see which one updates the score. Delete the other false positive variable results.
  2. Change the search type from value to address in the toolbar. Bit Slicer should populate the search field automatically with the variable address that you found. In cases that the search address is not populated, select the variable you found and click Variable -> Search Pointers to Variable from the menubar. This is also an available option if you right click on the variable.
  3. Bump the Max Levels from 1 to 3.
  4. Search for the memory address. Bit Slicer may return a thousands of results back. If you hover over the newly found variable addresses and let the tooltip show (or edit the variable addresses), you will see that they are all one-level, two-level, or three-level pointers and a handful of them even contain static base addresses.
  5. Quit the game and launch the app again. Start a game and advance your score, then go back to Bit Slicer.
  6. Change the search type to value and narrow down the list of results by searching for the player's current score. This should leave you with one or a few valid results.

This approach starts with a larger opportunistic Max Level as opposed to the one level at a time approach above. We guessed to pick 3 as our Max Levels hoping that we can find a stable address that uses up to 3 levels of pointers. In the end, the address we wanted to find only uses 2 levels of pointers. A drawback of this approach for starting with a large max level is that you may be searching for larger amount of results than is actually necessary which may increase the searching time. If we used a Max Level of 5 for this game, you may find that you will retrieve a lot of static results that will be hard to narrow down to the most ideal/stable one even after restarting the game.

However sometimes if you do not search for a deep enough Max Level, you may not find a stable variable result that you are looking for. This approach is a valid one especially if you know from the beginning that multiple levels of pointers may be involved. Note though you can always bump the Max Levels later and perform a subsequent search on those previously found results.

Searching one level at a time using exact offsets

Until now we have been searching for pointers to variables with offsets that are ≤ 2048 (2048 is the default Max Offset). This means that for every pointer expression we found like [base_address] + offset1 or [[base_address] + offset1] + offset2, each offset1, offset2, offsetN match 0 ≤ offsetX ≤ 2048. For this game, 2048 is large enough to find the pointers we want to find. 2048 is a reasonably large default but it is possible some games may require even larger offsets. However also note that larger offsets may increase the searching time.

To better eliminate false results, we can also search for exact offsets if we are able to find them. Note this approach assumes you are familiar with watching variable accesses.

  1. Start the game and find your player's score (a 32-bit integer) in Bit Slicer. Advance in the game to increase your player's score. In the process of narrowing down the results, do not let the player hit an obstacle. If you find more than one result, find out which variable is the real one by trying to overwrite one of them and see which one updates the score. Delete the other false positive variable results.
  2. Select the variable you found and click Variable > Watch Variable > Watch Write Accesses…. This option is also available if you right click on the variable.
  3. Go back to the game and advance your score but do not let the player hit an obstacle.
  4. Go back to Bit Slicer. You will see Bit Slicer found an instruction that stores a value to the player's score (like str w22, [x7, #0x10]). Add this instruction to the table. Note that this instruction stores a result to x7 + 0x10 and the offset is 0x10.
  5. Change the search type from value to address in the toolbar. Bit Slicer should populate the search field automatically with the variable address that you found. In cases that the search address is not populated, select the variable you found and click Variable -> Search Pointers to Variable from the menubar. This is also an available option if you right click on the variable.
  6. Change the Offset operator from Offset ≤ to Offset = and type in 0x10 as the offset.
  7. Search for the memory address. Bit Slicer should only return a couple one-level results back. Try to reason about which one is valid if possible (likely it is the one annotated with Malloc). Because the variable found does not use a static base address, we will recurse deeper again.
  8. Select the one-level pointer found and click Variable > Watch Variable > Watch Write Accesses to Base Address… in the menubar. This option is also available when right clicking on the variable.
  9. Go back to the game. Run into an obstacle to make the player lose and Play Again.
  10. Go back to Bit Slicer. You will see Bit Slicer found an instruction that stores a new value to the first pointer variable we found (like str x0, [x3, #8]). Add this instruction to the table. Note that this instruction stores a result to x3 + 8 and the offset is 8.
  11. Bump the Max Levels from 1 to 2 and set the Offset = 8. Note you will need to change the operator from Offset ≤ to Offset = again because by default Bit Slicer assumes you may not know the next exact offset to search for.
  12. Because the memory address of the player's score changed when the player lost, we will need to update the address to search. Select the one-level pointer variable that is still pointing to the player's score. Click Variable > Search Pointers to Variable from the menubar to update the address to search for in the search field.
  13. Search for the memory address. You will find that only one or very few two-level pointer results are returned back.
  14. Quit the game and launch the app again. Play a game, advance your score, and go back to Bit Slicer.
  15. Change the search type from address to value and narrow down the current results by searching for the player's new score.

With this approach, we were able to find pointers using exact offsets and eliminate more results earlier on. The drawbacks of this approach is that finding the correct offsets to use may not always be straightforward and using an exact offset may not be convenient when you already have multiple results that you want to narrow down further. However this approach is useful if you know what offset is used at the current level you're searching pointers in.

Conclusion

Overall, finding pointers to variables is useful for obtaining stable variable addresses that can be re-used or shared with others.

The three approaches and examples here for finding pointers should better help understand how to find them and how a game may use pointers. These different approaches can also all be combined at different levels for a pointer search. Sometimes you may want to quickly use a high max level, or other times you want to carefully build your way up one level at a time.

The final variable address you want to find will usually contain a static base address. Bit Slicer will annotate static variables in their description and will return static results at the top of the search results when searching for addresses. On the other hand, variables that are annotated with Malloc indicate they are in a dynamically allocated region and are not static.

Sometimes searching pointers can still be difficult so you may also want to consider other approaches like searching for a unique byte array pattern that is close to the data you're looking for, or watching variable accesses and debugging how the addresses are used in the debugger, or scripting any combination of this.

In other cases you may also find games that do not use pointers and uses static addresses, which requires no further work.

Clone this wiki locally