Wargame: Red Dragon Destruction Points Display

In this Real-Time Strategy game there is a gamemode called Conquest where the player captures sectors and accumulates victory points. Whoever reaches the limit first or has the most at the end of the game wins.

The thing with this gamemode is that it does not show players how many points worth of enemy units they have killed. Maybe that's fair during a game, but it would be interesting to see during a replay.

This post will be about how I created this tool, for those who may be interested. Experts in reverse engineering will probably be bored or have a lot of things to nitpick about, so this is more geared towards those who are vaguely familiar with the internal workings of software and want to see how something like this is made. Or something.

The general gist of it is to find the location in memory where the variable that tallies up kills is stored, then make a program that reads the relevant data from that place in memory. This is a fairly simple task, but on a real piece of software, so some may find it interesting.

We know the data is there-- we just have to find it. Even if the variable is dynamically allocated, pretty much everything can be traced back to a statically allocated piece of data. Eventually.

Some vocabulary for those with less familiarity with software:

Cheat Engine

The first order of business is to find the location in memory of the relevant variables. Personally, I've always avoided using Cheat Engine because it has a reputation for being used by script kiddies. However, it's perfectly suited to finding the location in memory of variables, unsurprisingly. As far as I'm concerned, there's no point to reinventing the wheel when the wheel is perfectly suitable.

Locating the variable

So we've got the game running, a replay loaded, and Cheat Engine (henceforth referred to as CE) or whatever running and attached to the game.

We don't exactly know what type of data the "kills" variable is. However, since it always shows up as a positive integer at the end of the game, it can be assumed that it's an integer. It's also unlikely that the developers would try to save 3 bytes worth of memory, so it's likely that it's a 32-bit integer. Also, this is also a 32-bit game, so the pointers should also be 32-bit.

It is likely that the kills variable starts at 0, because nobody has gotten a kill yet. However, scanning for 0's takes forever since there is probably a ton of 0's in memory. So we wait until the first kill.

https://i.imgur.com/IsricSx.png

First blood is drawn by the second player when a 5-point APC gets blown up by a tank. Because of this, we'll find player 2's kills first.


https://i.imgur.com/FyhcmYA.png

Now we start a scan for a 4 byte value of 5. There's a lot, but a lot less than there are 0's. If it was something like 85, there might be less, but oh well. We'll have to keep scanning.


https://i.imgur.com/9HiJzGz.png

An expensive 115-point M1A1 Abrams gets destroyed instantly from infantry anti-armor weapons. Now we should have 120 points, so we scan for that value. CE should now show everything that was 5 last scan, and is now 120 this time around. There should be far less than before, but the number itself tends to vary greatly. Let's try to narrow it down further.


https://i.imgur.com/aKIxdxx.png

A Panzerfaust 3 from the Korean Marines finishes off the last M1A1 Abrams. Now we should have 235 points in terms of player 2 kills.


https://i.imgur.com/cavkhcR.png

Scan for 235 and we now have 2 variables. One is player 2 kills and the other is player 1 deaths. We can find out exactly which one is which if there's a case of friendly fire, but honestly no matter which one is chosen, it'll probably be good enough. Let's just choose the one that ends with a 0. Since we found the variable we're interested in, we can stop closely watching for kills.


Locating the pointer to the variable

The problem is that the variable is not statically allocated. That is, it may appear anywhere in the block of memory that our game is allocated. So next replay or game, it can and will be at an entirely different memory location. As I said before, all things can be traced back to a static offset from the process' base address so we will have to do just that.

https://i.imgur.com/8kjbiCZ.png

We now want to monitor what instructions write to that address. CE can do this. Attach the debugger and wait until player 2 gets more kills.


https://i.imgur.com/jdXMuoU.png

There we go. There's an instruction that adds stuff to the variable located at esi + 0x30, which is our variable. The address of our variable is 0x38F9D210, which is equal to esi + 0x30. In other words, +0x30 is our offset and esi is our base address. Through either basic math or just clicking on the instruction to find what esi was at the moment of time the instruction was executed, we will find that the base address of the struct that contains our kills variable is 0x38F9D1E0.


Locating the pointer to the pointer to the variable

The address we found still isn't statically allocated. It will change the next replay or the next time the game is launched. We have to go deeper.

https://i.imgur.com/kTOTPnC.png

Scan for that base address and we will find a single result. Nice. This is the variable that holds the base address to our struct and now we want to find the base address of the struct of this variable.


https://i.imgur.com/YuCSK18.png

Once again, we monitor that memory location to see what instruction writes to it.


https://i.imgur.com/tSLHGfH.png

This one only gets written to after the replay ends. We only care about the first instruction because that memory address can be used for pretty much anything after that. The thing is, we actually don't care about the base address of this struct. The replay has ended, so now it's meaningless. However, now we know its offset, which is +0x134.

Let's start another replay.


https://i.imgur.com/w6x82Of.png

Do the same stuff as before to find the new memory address for the kills variable. We chose the variable that had an address ending in 0 last time, so choose the one ending in 0 again. We can speed things up a bit because now we know the offsets. The base address of this struct is the address of the variable minus 0x30, or 0x354EB1B0.


https://i.imgur.com/y20XmnN.png

Scan for the base address we just found and we will find a single variable that contains that memory address. From the first part of this subsection, we figured out that this variable has an offset of +0x134, so a simple subtraction and we have 0x58147B60.


Locating the pointer to the pointer to the pointer to the variable

It might feel like a cruel joke, but the truth is we still have to go deeper. The address we just found will still change the next replay, not to mention the next time the game is launched.

https://i.imgur.com/zgkAEYw.png

Scan for that base address we just found. There is a very large amount of entries. We have to narrow it down somehow. This is where things get a little tricky. The goal is to try to find a variable that reliably points to the struct that contains the struct that contains the struct that has the variable we want.

Open up a new scan tab (very important) and a new replay (also very important).


https://i.imgur.com/S4AROB3.png

Find the kills variable once again. Again, we use the one ending with a zero because that's what we used in the first place. It has an offset of +0x30, so the base address of its struct is 0x5F184B40 this time.


https://i.imgur.com/g2mkoKq.png

Once again, we scan for that base address. As we have found earlier, the variable containing the base address has an offset of +0x134 from its struct, so we subtract it to get the base address of its struct.


https://i.imgur.com/jK4q4cY.png

Now we return to the original scan tab with an unreasonably large amount of possible results. We can narrow things down by finding which one of those variables now has our latest base address.


https://i.imgur.com/beZnBAx.png

The nature of reverse engineering is that while there aren't truly any unknowns, there sure can be a lot of uncertainties. I have done this same exact thing multiple times and sometimes I get one variable, and sometimes I get two. This is one of those times that I get two variables.


https://i.imgur.com/NvqkQBf.png https://i.imgur.com/gwZO8Px.png

We monitor for instructions writing to either variable.


https://i.imgur.com/ItqOpBU.png https://i.imgur.com/xxmKKDb.png

Once we leave the replay, we get these. Let's just stick with the first one, and if that doesn't work, we can try the second.


https://i.imgur.com/qAYD2z8.png

We have a base address of eax (0x356F9E40) and an offset of ecx*0x4, or 0x14, as this is hexadecimal.

Also, that list on the bottom is what I meant earlier by "clicking on the instruction" and just copying the value of esi.


Locating the pointer to the pointer to the pointer to the pointer to the variable

Is it finally over? Yes, almost.

https://i.imgur.com/JdMLOhk.png

We search for the base address we found just now and get this.

This time the address is green. If you were wondering, "how do we know when to stop?", well then, this is how. If you tried to stop prior to this, you would only experience disappointment or frustration as you find that the addresses of everything you found changed and you must find them again.

It's finally over. If we copy the address by right-clicking on it, we will actually get:

wargame3.exe+1D1FE0C

That is the base address of our game and the offset of this pointer relative to that address. Any time the game starts, no matter where it starts, this variable will always be located at the same offset.


https://i.imgur.com/7jRMhnd.png

In CE, we can trace our path. It looks like this. This is yet another replay, so the addresses have changed, but we are able to end up at the kills variable.


https://i.imgur.com/dUH1tFx.png

Another replay might look like this.


Even if we close the game and start it again, because we found everything relative to the process base address, everything will still function.

The last step is to write a program that reads from process memory. Code for such can be found here. It is a fairly straightforward program. Access the process memory, go through the memory locations we found, and then read and display our kills variable.

This goes to show that even a fairly simple task can be pretty tedious when applied to a "real" piece of software.