Note: I can no longer recreate the approach used to identify the value 4294967295. I will update the tutorial in the future with a more consistent method.
Target
Our target in this lesson will be Wesnoth 1.14.9.
Identify
Our goal in this lesson is to create a map hack, a type of hack that displays the entire map to the player and removes elements like fog-of-war.
Understand
In strategy games like Wesnoth, tiles on the map can either be visible or hidden by fog-of-war:
We know that the game must store whether the tiles are visible or not somewhere in memory. These locations are most likely in one large block of memory. One way a game might choose to represent the map is through the use of an array. In this array, each element would represent one map tile’s visibility status:
int map[map_size] = {0, 0, 1, 0, 0, 1, 1, 1, 0, ...}
The game could then iterate over each tile in the array to determine whether fog should be drawn over the tile.
We also know that the game must calculate the values for each tile every time the player moves a unit. If the player moves a unit in range of a tile, the game needs to set the tile’s visibility to true. If the player moves a unit out of range of a tile, the game needs to set the tile’s visibility to false. To make this calculation easier, games will often first set all tiles to an invisible state:
for(tile in map) {
map[tile] = 0
}
Then the game can go through each unit that the player controls and set all the surrounding tiles to visible.
While every game will have its own way of handling map data, they all must follow a similar set of steps to calculate visible tiles. We can use the following approach to create a map hack for any strategy game:
- Search for an unknown value.
- Move a unit to reveal part of the map.
- Filter for changed values.
- Move a unit to hide the revealed part.
- Filter for changed values.
- Repeat this process until you have a reasonable amount of results (~50).
- Look for patterns in the results and edit each one until you figure out which ones represent tile data.
Once you have found the tile data, a breakpoint can be set on one of the tiles. Then, a unit can be moved and the breakpoint will pop in the function responsible for writing values to the map data.
Locating Map Data
Locating the map data is the most time-consuming part of creating a map hack. First, create a local game in Wesnoth with a single player-controlled opponent. Since we do not know what values we are searching for, start a new scan for an Unknown value type. After the scan completes, select a unit and move it to a new location to reveal additional tiles. Make sure you remember this location to use on all future requests.
After moving the unit, change the scan type to Changed value and filter the results. When the filtering has completed, move the unit back to the start location and end your turn to hide the terrain again. Quickly end the next player’s turn and then scan again for Changed value when you regain control of the first player. Continue this process until you filter the results down to a manageable amount.
To quickly reduce the amount of results, you can also change the scan types to eliminate values that may change constantly but not in a manner related to the map tiles. For example, if you recruit a unit that does not reveal additional squares and then search for Unchanged value, many results will be filtered out. This approach can be used with different conditions (for example, pausing and resuming the game and searching for unchanged values) to quickly reduce the search size.
Eventually, you will get your set of results down to a reasonable level that will allow you to observe game behavior manually. In this lesson, we have managed to narrow down the result set to 10 possible addresses. Due to DMA, these addresses will be different each time we start a new game:
Initially, these values do not appear correct, as the values seem almost random. To determine if we have the correct addresses, check the box next to each address in the Active column. Checking this box will disable modifications to the addresses’ values. With the addresses inactivated, move your unit away from the tiles you have been testing on. You should notice that the tiles no longer display fog-of-war when moving away. This test confirms that we have found the correct addresses.
Locating Map Code
Now we need to determine how the game handles map tile data. When reversing an unknown game, we start by making educated guesses. In this case, we guessed that the game stored individual tiles with a simple visible/invisible scheme to determine visibility. We used this model to help track down the data we are interested in, but we now realize that this model is incomplete. Before we can continue, we have to update our model to reflect our findings.
Observing Cheat Engine, move a unit to reveal and hide tiles. You should notice that the values we found change consistently when doing this. When a tile is hidden, it appears to be several large values. However, when a tile is visible, it appears to always be set to 4294967295:
This is a distinct value, so let’s try setting another address we identified to this value. When this is done, a whole column of tiles should appear visible:
By scrolling up the map, you can see that the whole column from the top of the map down is controlled by this one value:
With this information, we can now alter our model. Instead of an array of tiles, Wesnoth appears to use an array of tile columns. These columns are then set to a value between 0 and 4294967295, depending on the number of tiles in the column that are visible:
int map[column_size] = {0, 0, 4294967295, ...}
The value of 4294967295 converted into hexadecimal is
0xFFFFFFFF
.
Now we can begin tracking down the code responsible for setting these values. Attach x64dbg and set a breakpoint on write on one of the map column addresses. Move your unit to reveal that tile, and the breakpoint should pop:
Looking at the registers, we can identify that esi holds our map column data. Scrolling up, we immediately see the code responsible for setting this column’s value:
If we examine this code, we can see that a value is loaded into the register eax, modified, and then used to set our column’s value.
Changing Map Code
Since we always want the column to appear visible, we can modify this code
to set the column’s value to 0xFFFFFFFF
. We will do this
through the use of an or operation. An
or operation takes two sets of bits and creates a new set
in which the value of each bit is 0 if both source sets are 0, and 1 if
either source set is 1. Since 0xFFFFFFFF
translates to all
1’s, or’ing a value with this value will always produce
0xFFFFFFFF
. We will conduct this operation on our tile column
and nop out the other instructions:
We could also use a mov instruction here to accomplish the same goal. However, one drawback to the mov operation is its size, or the amount of opcodes we would require. We simply do not have enough room and would require a code cave. To avoid this extra complexity, we use the or instruction instead since it is shorter.
With this modification made, go back into Wesnoth and observe that the entire map is now visible:
We can use a similar approach covered in the Code Caves & DLL’s lesson to create a DLL to accomplish this behavior. First, note down and copy the opcodes generated by x64dbg when making our alteration. We will place these values into an array so we can iterate over them:
unsigned char new_bytes[8] = { 0x90, 0x90, 0x90, 0x83, 0x0E, 0xFF, 0x90, 0x90 };
Next, just like we did in other lessons, we will unprotect the memory at the hooking location. Then, we will iterate through each opcode in our new_bytes variable and write it into the game’s memory:
unsigned char* hook_location = (unsigned char*)0x6CD519;
if (fdwReason == DLL_PROCESS_ATTACH) {
VirtualProtect((void*)hook_location, 8, PAGE_EXECUTE_READWRITE, &old_protect);
for (int i = 0; i < sizeof(new_bytes); i++) {
*(hook_location + i) = new_bytes[i];
}
}
This DLL can then be injected like we did in all the previous lessons. When injected, our map hack will reveal the tiles for every map in the game.
The full code for this lesson is available on github.