Home > Game Hacking, Programming, Reverse Engineering > Game Hacking (2/3): The Easy Way

Game Hacking (2/3): The Easy Way

February 25th, 2017 Leave a comment Go to comments

The previous post detailed how to develop a map hack by taking advantage of existing functionality in the game. The technique relied on being able to toggle the map to a hidden/revealed state, and then using this functionality to methodically step through the assembly code. This eventually led to logic that was specific to hiding and revealing the map, where it was then possible to write a hack that invokes this functionality at will. The technique presented in this post is much easier than that, and is only made possible due to useful strings that were found in the binary.

The tool used in this post will be x64dbg, which is a really great debugger and disassembler, and what I consider to be the successor to the now-ancient OllyDbg. Unfortunately, it won’t really be used too much in this post since there won’t be much need of live analysis (this post is titled “the easy way” after all). Assembly snippets will be pasted from IDA Pro, since I find their copy+paste format to be the most readable.

Starting off by attaching to the process and doing a string dump (Right click -> Search for -> Current Module -> String references) for the main executable yielded 25817 strings for me — plenty to search through.

Filtering on the string “map” yields a much more manageable set. Looking through, there are a few strings that seem like they might lead somewhere interesting:

"trSetFogAndBlackmap(<true/false> <true/false>): turn fog and black map on/off."
"trRevealEntireMap -- shows whole map, similar to how revealed mode works"
"trPlayerResetBlackMap(: Resets the black map for a given HUMAN player."
"map visibility"
"blackmap([integerState]) : toggles or sets unexplored black map rendering."

The two most promising places seem to be the ones highlighted in orange. The strings give a clear description of what the function does and even provides parameter arguments. The “trX” functions appear to be related to the triggers system that is available in the game and allows map makers to add effects and conditions to their custom maps. Looking at references to the first string goes to the following:

...
.text:008B2B76 loc_8B2B76:                             ; CODE XREF: sub_8AE4A0+46CDj
.text:008B2B76                 mov     ecx, esi
.text:008B2B78                 call    sub_59C270
.text:008B2B7D                 push    1
.text:008B2B7F                 push    offset loc_8AAEE0
.text:008B2B84                 push    offset aTrsetfogandbla ; "trSetFogAndBlackmap"
.text:008B2B89                 mov     ecx, esi
.text:008B2B8B                 call    sub_59BE80
.text:008B2B90                 test    al, al
.text:008B2B92                 jnz     short loc_8B2BAE
.text:008B2B94                 push    offset aTrsetfogandbla ; "trSetFogAndBlackmap"
.text:008B2B99                 push    offset aSyscallConfigE ; "Syscall config error - Unable to add th"...
.text:008B2B9E                 push    esi             ; int
.text:008B2B9F                 call    sub_59DBC0
...

The code here begins by passing in the string, a pointer to a function, and a constant (1) as arguments to another function (teal). The return value of this call is checked for 0, which is an error condition (blue). From looking at the what is happening in a disassembler, this pattern is found throughout everywhere. This code, and all of the surrounding code, is attempting to register triggers and is providing the trigger name, a callback to where the trigger code lives, and a yet unknown constant of 1. Given that, the real place to look would be in the callback.

Following through to the callback leads to the following section of code:

.text:008AAEE0 loc_8AAEE0:                             ; DATA XREF: sub_8AE4A0+46DFo
.text:008AAEE0                 mov     eax, dword_A9D244
.text:008AAEE5                 mov     ecx, [eax+140h]
.text:008AAEEB                 test    ecx, ecx
.text:008AAEED                 jz      short locret_8AAF13
.text:008AAEEF                 mov     edx, [esp+4]
.text:008AAEF3                 push    0
.text:008AAEF5                 push    edx
.text:008AAEF6                 call    sub_5316B0
.text:008AAEFB                 mov     eax, [esp+8]
.text:008AAEFF                 mov     ecx, dword_A9D244
.text:008AAF05                 mov     ecx, [ecx+140h]
.text:008AAF0B                 push    0
.text:008AAF0D                 push    eax
.text:008AAF0E                 call    sub_5316D0
.text:008AAF13
.text:008AAF13 locret_8AAF13:                          ; CODE XREF: .text:008AAEEDj
.text:008AAF13                 retn

The two calls here (green) should be familiar if you have read the first part of this series recently. These are the two functions that were eventually found to control revealing and hiding the map to the player. Each function takes in a “this” pointer, which we can see here is loaded from a constant address and is likely the class for the main player, along with a true/false value which describes what should happen to the map. There’s also a third constant parameter of 0 here, which is different from the constant parameter of 1 at the other call site from the previous post, possibly indicating whether the map state is being changed via player interaction or a trigger.

Knowing this, the hack from the previous post can be made a bit better. With the old hack, there was an issue of having to provide a fake “this” pointer which needed to have a field written into, and there was only a true/false toggle option. Going from the documentation provided by the string dump, this function takes in two booleans — presumably to control the black overlap and the fog of war which obscures areas that the player has already explored but does not have vision of anymore.

The new (and still hacky) code is below:

#include <Windows.h>
 
using pToggleMapFnc = void (__cdecl *)(bool bEnableBlackOverlay, bool bEnableFogOfWar);
 
int APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
{
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
    {
        (void)DisableThreadLibraryCalls(hModule);
 
        pToggleMapFnc ToggleMap = (pToggleMapFnc)0x008AAEE0;
 
        while (!GetAsyncKeyState('0'))
        {
            if (GetAsyncKeyState('6'))
            {
                ToggleMap(true,  true);
            }
            else if (GetAsyncKeyState('7'))
            {
                ToggleMap(true, false);
            }
            else if (GetAsyncKeyState('8'))
            {
                ToggleMap(false, true);
            }
            else if (GetAsyncKeyState('9'))
            {
                ToggleMap(false, false);
            }
 
            Sleep(10);
        }
 
        break;
    }
 
    case DLL_PROCESS_DETACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
 
    return TRUE;
}

Calling the function with the various parameter combinations reveals the following behavior:
True/True – Black map overlay with fog of war
True/False – No black map overlay, fog of war is still present. Features are missing from the map.
False/True – Black map overlay without fog of war. Areas that were explored will always have line of sight
False/False – No black map overlay, no fog of war. Everything is visible.

Screenshots of the mini-map is shown for these four states below:

The hack becomes a bit cleaner since now it is just making a direct call to a function and doesn’t require passing anything unknown. Hopefully it is obvious why this is considered the “easy way” versus the previous post, which required a large amount of debugging and tracing.

The next, and last, part of this series will cover how to clean this hack up a bit more and make it more professional. Additionally, it will cover what is involved in porting this hack to the newer Extended Edition version of the game.

Thanks for reading and follow on Twitter for more updates.

  1. Matteo
    March 4th, 2017 at 10:52 | #1

    I have done with aoe3; but the “hard way” was quite easy:
    -ce while watching replay: search for fog on=1 fog off=0
    -find what write
    -break point
    -check return address (to find who call the map toggle function)
    -end here: .text:0061CED9 call 008C9877
    -create small code that set ecx to a good value (ecx+0x12 will be written in the first instructions of the call)
    -push 0/1 to set the map mode
    -call that function
    -done 🙂

    now looking for the easy way; found something promising: “trRevealEntireMap”

  2. Matteo
    March 4th, 2017 at 11:29 | #2

    ok, you are right 🙂 if the ahrd way was easy the easy way was even easier!
    aoe3 uses the same callback style code here is the final hack:
    push 0 ;map 0=visible
    push 0 ;fog war 0=off
    call 00937B89
    add esp,8
    ret
    and here is where is it called:
    .text:00945440 push offset toggleMapTRIGGER <—————
    .text:00945445 push offset aTrsetfogandbla ; "trSetFogAndBlackmap"
    .text:0094544A mov ecx, esi
    .text:0094544C call sub_52837A
    ——
    .text:00937B89 toggleMapTRIGGER

  3. admin
    March 12th, 2017 at 14:56 | #3

    That seems to make sense that it would be so closely related; it’s the same game engine under the hood. Glad to see that you’ve found some good use from the posts!

  4. John
    March 12th, 2017 at 18:05 | #4

    I always find these posts useful too. I remembered I also have a age of empires game (dunno how old this one is), so I tried it out as well, but it doesn’t seem to be the same engine as age of mythology or age of empires 3.

    Game is Age of Empires II HD

    http://prntscr.com/ej6xmg

    I traced to the very top, and it seems if you set eax to 1 fog of war is enabled. If you set eax to 0 fog of war is disabled. I think I am correct, am I admin?

  5. admin
    March 12th, 2017 at 22:22 | #5

    @John

    AoE 2 is older than Age of Mythology and uses a different game engine. I haven’t looked into developing a map hack for it yet. If you think you’ve found it, try forcing the different values in EAX to 0 and 1 and observe what happens. Step through and see where that value is eventually written into the players game state structure.

  6. Matteo
    March 13th, 2017 at 14:29 | #6

    But aoe2 HD should be newer. the original aoe2 is quite old.
    [base]
    [+84]
    [eax+5ACA] <—this looks promising

    try add a pointer from ce using the offsets from the code and change the value so when it is compared and pushed it gets different value.
    but i think its better to edit the value at that address instead of editing eax in debugger.
    otherwise it will probably work for one frame only.

  7. John
    March 14th, 2017 at 12:04 | #7

    After further looking into it, I can say its almost like Age of Mythology but kind of different. When you view a recorded game and play it back, once you click the fog of war button it makes use of the cheat commands “marco, polo” and executes them at the same time (but separately) to reveal map and remove fog.

    Lets look at marco command:

    http://prnt.sc/ejwm2s

    It lets us reveal map only but leaves the fog.

    It’s a thiscall (class object) and takes a int (I am relying on Hex Rays, probably shouldn’t do that).

    I try calling it almost like yours in the hard way (passing dummy object to this), but it just crashes.

  8. March 14th, 2017 at 15:36 | #8

    Superb tutorials you have here!

  9. Matteo
    March 23rd, 2017 at 11:47 | #9

    @john:
    how big is the dummy object? pass an array from C of even simpler use CE alloc memory function to get a page of memory (=4kb) it should be enough.
    call the function and then by hand or with ce search for everything != 0 in that region.
    the highest non zero byte its the needed array size to be passed.
    this supposing that the function want to write somewhere; if it wants to read something you might try memory breakopoint for example.

    to call the function ce has “create thread” (+ parameter in ebx). you might want to alloc new memory (again) to store a quick asm snippet that do everything is needed (for example in case of 2 parameters pushed, fix ecx, …

  1. No trackbacks yet.