Preview
← BACK

Part 1 - Game Hacking

< Go back

⚠️ This article is provided for educational purposes only. It describes reverse engineering techniques and related methods and is not intended to encourage any illegal or unethical activities, including software piracy. The information is offered "as is" without any warranties, express or implied.

Project done with the amazing help of M0rb1us and naashs.

Step 1 - Crack the game to start it with no CD-ROM

  • Reverse the binary to understand how it verifies that the proper CD is inserted
  • Patch it so that the main campaign can be launched without the CD
  • The game must behave normally, and all features of the game must be available !

Game

Tools

  • Ghidra ~ A reverse engineering tool
    • Cartographer ~ A plugin for Ghidra to visualize code coverage
  • DXWnd ~ A tool to run old games in windowed mode
  • x32dbg ~ A 32-bit debugger
  • DynamoRIO ~ A dynamic binary instrumentation tool (used to grab coverage reports of the game running, useful with Ghidra Cartographer)

1. Static analysis

With Ghidra we find among the imports, from KERNEL32.DLL, the function GetDriveTypeA. This function returns a value depending on the type of drive found. We assume this function is used to identify the drives installed on the computer to then locate the CD-ROM.

We use this function call as the entry point of our attack vector, let's rename it handleCD.

handleCD function

2. Dynamic analysis

From the HandleCD memory address i.e. 0x50c1c0 we can now attempt to find this function dynamically in the running program.

To do so we will start the .exe using DXWnd to run the app in windowed mode, and then attach it to the x32dbg debugger.

Here is our setup.

x32dbg setup

Let's attempt to reach our handleCD function inside x32dbg, grab the memory address and do [Ctrl] + G, then paste 50c1c0 inside and Ok.

We reach a new memory address space, unlike the default one that was somewhere in the 0x77______ range.

We can now play around inside our function and try to identify something of interest. For example, we can see a call to the KERNEL32.DLL function GetLogicalDrives which returns a bitmask of the available drives.

GetLogicalDrives

3. First patch and testing

1. R&D patching

This `GetLogicalDrives` function is very interesting, as just after it's called, an if statement is called that we don't usually go into, inside this if statement there's one of two calls to `GetDriveTypeA` that we can see in the disassembly. Let's try to patch the binary so that the if statement is always true, and we never go into the `else` statement. Looking at the assembly of the default binary just before that `je` (x86 `jump equals` instruction) je default Just before that `je` we have a `dec ecx` instruction, `ecx` is at `1` before the `dec` so let's attempt to patch (with Ghidra) the `dec` into a `inc`, `je` should be happy and let's us pass trough without jumping. je patched Let's save that with `File` > `Export Program (O)` and as the Format select `Original File`. I'm using Win10 inside a VM so I had to launch both DXWnd and x32dbg as administrator to be able to run and attach to the patched binary (permission issues). After running the patched binary, we can see that the game is now running without the CD and we skip the `je` instruction. Game

handleCD is called at 0x4ed9ab. Using DynamoRIO and it's drrun tool we can get a coverage report of the program that ran:

/path/to/DynamoRIO/bin32/drrun.exe -t drcov -- ./<REDACTED_GAME_NAME>.EXE

Next we can open the gcovr report using the Ghidra Cartographer plugin, Tools > Code Coverage > Load Code Coverage File(s)....

Select our drcov report, and then select the <REDACTED_GAME_NAME>.EXE binary (ignore .dll's we don't care about the report for libraries).

Now in handleCD we can see that the only return statement used is return 2

handleCD coverage

We see one return statement being return 0 that is not used, let's just replace the entire call to handleCD at 0x4ed9ab, (i.e. call 0x50c1c0) with axor eax, eax instruction, because the xor is smaller than the call we need to pad the rest of the bytes with nop instructions.

handleCD patch

For reference we will call this new function POI (Point Of Interest), at 0x4ed650.

And the actual address that stores the result of handleCD we will call cdState as it will come later.

This alone doesn't seem to be enough to bypass the CD check, let's continue our analysis.

4. Second patch and testing

Looking even further in the sources, we see that cdState is being referenced in a switch case at 0x4ee4cd

cdState switch

We see that we explore 2, and subsequently 5 and 6, which might not be that interesting, 3 and 4 seem to be error cases, with a message box being displayed. We are then left with 1 and 0 being default.

default is the only one that skips a value being set to 1 after the switch case:

cdState after switch

So we'd guess that we want to go into default currently, we are on our way to do so but our previous patch is being overwritten by a following instruction.

There is a if statement that checks if cdState is set to 0, if it's the case we enter it and in our current case we end up with a cdState = 5 state, as we've seen previously these all seem to be error states.

cdState 0

Let's patch 0x4eda45: mov dword ptr [cdState],0x5 to a 0x0, re-build the app and try to run it.

game launches without the error message

Yay! πŸŽ‰ The game launches without the error message for the missing CD and we get access to all singleplayer options.

5. Recap

We successfully patched the game to run without the CD, here is a summary of the actions taken.

At address 0x4ed9ab,call 0x50c1c0 into:

xor eax, eax
nop
nop
nop

And at address 0x4eda45, mov dword ptr [cdState],0x5 into:

mov dword ptr [cdState],0x0

Step 2 - Develop cheat engine cheats for the game

  • Cheat the game memory
  • You must produce 3 different cheats (at least one with unknown initial value)
  • Your cheats must be persistent after game reboot
  • Example of valid cheat with known initial value : Override the gold amount
  • Example of valid cheat with unknown initial value : Get infinite amount of moves in a day

Tools

  • Cheat Engine ~ A memory scanner and editor
  • Frida ~ A dynamic instrumentation toolkit

1. Known initial value (same steps done for every ressources)

First, we must look for the gold, depending on the faction, we have different start. here we had 20,000

In game

We then search with Cheat Engine for the 20000 value, then, buy something and search again for the new value

scan for value in CE

fixed value for gold

We end up with a single value, we can change it and put whatever we need.

unlimited gold

But if we stop the game, we will have to search the value again. To make it persistent, we need to find the pointer. We click on the address that we added to the cheat table and press F6 to show what writes to this adress.

CE dynamic address

We find that the pointer is 0x03450BCC that is stored to ecx, to that value we then add edx * 4 + 0x9c, edx being 0x6 we can finally search on CE for the value at the address 0x03450BCC with the pointer offset of 6*4+9c:

CE dynamic address

Now we can change the gold even after restarting the game.

The same can be done easily with other ressources by changing the offset (modify edx value from 0 to 6)

2. unknown initial value

Here we change the scan type to unknow value, move inside the game, scan for a decreased value till we have between 2 and 4 value, from there after moving again, we can identify the value that matches our actions or play with the active mode in the cheat table to see if the value bar change.

When the good one has been found, we first do the same thing, find what writes this adress, show disassemble CTRL + a to open the auto assemble and CTRL + SHIFT + a to create the injection template. then we replace the mov by a nop and assign it to the cheat table.

CE Identify user movement

CE who writes user movement

CE scripting infinite user movement

We can then active our cheat which will stop the decrease of the movement.

Step 3 - Spy the memory

  • Spy the memory of Heroes III with Frida
  • Example : Display in real time the amount of all resources in game

Episode I – The Phantom Premises

In step 2, we used CheatEngine. with it, we learned the pointer value to the resources in the game: ("<REDACTED_GAME_NAME>-noCD-crack.EXE" + 29CCB0 + 9c + eax * 4) where eax is an offset from 0 to 6 to represent different resources (gold, wood, ore, …)

Episode II – Attack of Frida

We want to be able to hook into that address and visualize our in game resources from frida in real time. So if we get more gold for example it needs to update in frida automatically.

Let's first launch it:

frida -f .\<REDACTED_GAME_NAME>-noCD-crack.EXE -l .\inject.js

Our first idea was to have inject.js dump the memory at the address calculated by CE for the resources:

Addresses of resources in memory

Now obviously we can't just use 0x3e30bc0 because as we said previously to find the address of our resources, we are calculating based on "<REDACTED_GAME_NAME>-noCD-crack.EXE" A.K.A. the base pointer for the .exe.

So if we reload the game this base pointer will change. In Frida we need to calcualte the base pointer. This is done pretty easily:

var moduleBase = Module.findBaseAddress("<REDACTED_GAME_NAME>-noCD-crack.EXE");

This is the same as for CE. This gives us 0x400000, we can directly determine this with the following:

[Local::<REDACTED_GAME_NAME>-noCD-crack.EXE ]-> Process.enumerateModulesSync()[0]
{
    "base": "0x400000",
    "name": "<REDACTED_GAME_NAME>-noCD-crack.EXE",
    "path": "C:\\Users\\Win\\Desktop\\Heroes 3\\<REDACTED_GAME_NAME>-noCD-crack.EXE",
    "size": 2842624
}

Episode III – Revenge of CheatEngine

Specific address of resources

Base address of program

We tried to understand why the base pointer in CE was this big, and how the maths above make zero sense… 0x905a4d + 0x29ccb0 is equals to 0xba26fd AND NOT 0x3e30b18 we are an entire order of magnitude off.

We first tried reverse engineering the source code of CE, to try and understand where this discrepancy comes from, the entire code base is written in Pascal, so that's already hard enough, using a bit ChatGPT to speed up the process, we find the exact function that deals with getting the address from a object name, this happens at symbolhandler.pas:5122 the function is a painful 1000 lines long, but essentially is checks for different types of objects and if the object is a .exe it return the base address by querying the system.

In our case Frida might actually be mis-alligned and considers 0x400000 to be the base pointer due to it being the standard without actually querying the system or trying to determine it, this could be due to how DxWnd is wrapping the process, this is just speculations.

We found no one having such issues online.

Episode IV – Ghidra: A New Hope

We got tired of doing it the CE way and tried to change our approach.

Instead of looking for the exact address the resources are at, let's instead work with what writes to them.

We opened Ghidra and searched for 8b 8c 83 9c 00 00 00 this is the instruction we can see in CE that "writes to" the in-game resources.

Resources function decompiled in ghidra

We find this FUN_00558f20 function.

Literal offset value of resources in memory

We now have something to look for "<REDACTED_GAME_NAME>-noCD-crack.EXE" + 0x158f20 + 0x37 (0x158f20 being the offset of the function and 0x37 the instruction offset inside the function), from here we will be able to read the registers and get the right values to find the resource addresses.

Episode V – Frida Strikes Back

First we dumped the memory at the address to verify it was the right place:

Hexdump of found resources in frida

We can see 8b 8c 83 9c 00 00 00, we're good!

Then we tried to print ebx + eax * [OFFSET] + 9c by hooking at that moment in code and replacing OFFSET with the offset of any specific resource, bingo, we had every value printing:

var targetInstruction = moduleBase.add(0x158f20).add(0x37);

Interceptor.attach(targetInstruction, {
    onEnter: function (args) {
        var ebx = this.context.ebx;
        pointer = ebx.add(0x9C);
    }
});

Frida printing addresses of resources and values

Episode VI – Conclusion of Frida

We have the pointer and know the offsets for every ressource. Let's make a quick function that every 5 seconds print every ressource.

First we hook, take ebx + 9c and put it in a global variable. then we loop over each offset with their respectives names (see inject.js) and print the value.

Hooked game with resources