β οΈ 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.

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.

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.

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.

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.
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.
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

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.

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.
Looking even further in the sources, we see that cdState is being referenced in a switch case at 0x4ee4cd

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:

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.

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

Yay! π The game launches without the error message for the missing CD and we get access to all singleplayer options.
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
First, we must look for the gold, depending on the faction, we have different start. here we had 20,000

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


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

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.

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:

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)
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.



We can then active our cheat which will stop the decrease of the movement.
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, β¦)
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:

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
}


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.
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.

We find this FUN_00558f20 function.

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.
First we dumped the memory at the address to verify it was the right place:

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);
}
});

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.

2025 Β© Philippe Cheype
Base theme by Digital Garden