Introduction

Age of Mythology is a real-time strategy game in which a player hopes to build up their civilization and ultimately conquer his or her enemies. The standard mode of play has the player starting off in a map that is covered in black, signifying unknown and unexplored territory.





As the game progresses, the player explores the map and explored regions show signs of terrain, resources, enemy buildings, and are overlaid with a “fog of war” that signifies explored territory that the player does not have vision of anymore.





The goal of this post will be to develop a hack that provides a significant advantage to the player by revealing the entire map. This allows the player to see what enemies on the map are doing as well as where and when it is best to attack. This hack will be developed for the original version and then later parts of this article series will show how it applies to the newer extended edition which is currently found on Steam.

Here are the hashes for the main executable that will be reversed in this set of articles:

CRC32: 7F1AF498

MD5: 09876F130D02AE760A6B06CE6A9C92DB

SHA-1: AAAC9CD38B51BEB3D29930D13A87C191ABF9CAD4

Starting off

The goal is to develop something that reveals everything on the map to the player, giving the player full knowledge of what is going on in the game at all times. The good news is revealing and hiding the map is built-in functionality of the game. The game supports playing back recorded games, and the option to reveal and hide the map is part of this UI.

Clicking the “Fog of War” button toggles fully revealing the map and setting it back to the normal state, which only shows what the player has vision of or has explored. The plan is to find out where the handler for this button is and trace it through to where the logic for revealing the map is. Once that is found, it will simply be a matter of injecting a DLL into the game process to call the function for revealing the map. The tool most appropriate for something like this is Cheat Engine, which is a useful tool for browsing and manipulating memory, debugging, disassembling, and much more, with a specific focus on game hacking. This post will not go over usage of the tool — there are hundreds of resources out there that can cover usage targeted at various skill levels.

After starting Cheat Engine and attaching, it becomes a matter of finding out where the code that interacts with the button is located. The easiest way to find this is to assume some normal programming practices, specifically that an active button will have a value of 1 in memory somewhere and unactive will be 0. Then it becomes a matter of testing and patience. Searching in the process memory for a value of “1” (when the button was active) returned 337,597 results for me. If you’re following along at home then don’t expect identical numbers.

This is far too many to enumerate. Clicking the button again to set it in an unactive state and searching for 0 then returned 376 results — still too many.

Repeating this process a few more times eventually narrows it down to a much more manageable 21 addresses. 20 of these 21 were very close in range. 0x08FC71A4 seemed the odd address of the bunch. Inspecting it closer and forcing its value to 0 did in fact toggle successfully toggle the button. At this point the correct address was found and the other 20 could safely be ignored. The next step is to see what is writing to it.

At this point Cheat Engine will attach a debugger and monitor all writes to 0x08FC71A4. Clicking the button a few times gets the following instructions to pop up. These were the instructions that were found to have written to 0x08FC71A4.

The next step is to inspect these and begin setting breakpoints in order to better understand what is going on around these writes. Setting a breakpoint on the write instructions

and messing around with the game a bit reveals that this function is called for every button. Here, ECX is the pointer to the button, and presumably +0x1A4 holds an IsToggled property that gets set accordingly. This set happens on the second write instruction, where EDX will either be 0 for disabled or 1 for active. The code might look a bit complicated, but it is basically ensuring that the toggled state is a valid state and then setting the IsToggled property before calling a function and returning.

The destination address +0x14B670 is still code that is specific to all buttons. The goal is to begin slowly stepping through everything and finding areas of code that might be specific to the “Fog of War” button. There are a lot of different approaches here, but what I usually look for is:

Call destinations that are calculated via a register. This can signify a callback to be invoked after the state of the button has changed, something like an OnChanged/OnEnabled/OnDisabled or similar function.

Function parameters that are pointers to functions.

Function calls that take in arguments of 1 or 0.

Stepping into +0x14B670 gives the following (partial) assembly listing listed below. The assembly listings will have an absolute address instead of module base + offset address since they were a lot easier to copy from IDA than from Cheat Engine.

.text:0054B670 mov eax, large fs:0 .text:0054B676 push 0FFFFFFFFh .text:0054B678 push offset SEH_54B670 .text:0054B67D push eax .text:0054B67E mov large fs:0, esp .text:0054B685 sub esp, 8 .text:0054B688 push esi .text:0054B689 mov esi, ecx .text:0054B68B mov eax, [esi+148h] .text:0054B691 push edi .text:0054B692 mov edi, [esi] .text:0054B694 push eax .text:0054B695 push esi .text:0054B696 lea ecx, [esp+24h+var_10] .text:0054B69A call sub_4D7470 .text:0054B69F mov ecx, [eax] .text:0054B6A1 push ecx .text:0054B6A2 push 1 .text:0054B6A4 mov ecx, esi .text:0054B6A6 call dword ptr [edi+54h] .text:0054B6A9 cmp [esp+1Ch+arg_0], 0Dh .text:0054B6AE jnz loc_54B769 .text:0054B6B4 lea edi, [esi+154h] ...

The call to 0x004D7470 (red) was stepped into and found to return relatively quickly so it will not be shown here. The next call (blue) at +0x14B6A6 makes a call via a register. This is a good candidate to step into and observe closely. This function has two possible destinations that it can call:

... .text:0054BF98 push 0Ch .text:0054BF9A call dword ptr [eax+0CCh] .text:0054BFA0 .text:0054BFA0 loc_54BFA0: ; CODE XREF: sub_54BF80+Fj .text:0054BFA0 ; sub_54BF80+14j .text:0054BFA0 mov ecx, [esp+0Ch+arg_8] .text:0054BFA4 push ecx .text:0054BFA5 push edi .text:0054BFA6 push ebx .text:0054BFA7 mov ecx, esi .text:0054BFA9 call sub_4D4EF0 .text:0054BFAE pop edi ...

The instruction at +0x14BF9A (red) never gets invoked when debugging and stepping through, so there’s no point in looking at it. That just leaves the next call at +0x14BFA9 (blue) to step into and look at. This functions turn out to be very large in size and has a lot of branching and potential call sites. With the aid of debugging, a lot of this logic can be skipped. After only tracing the code that is executed when the “Fog of War” button is toggled to reveal the map, there are only three call sites left.

... .text:004D504C cmp esi, dword_A9D068 .text:004D5052 jz short loc_4D5087 .text:004D5054 push esi .text:004D5055 call sub_424750 .text:004D505A mov edi, eax .text:004D505C add esp, 4 .text:004D505F test edi, edi .text:004D5061 jz short loc_4D5070 .text:004D5063 push esi .text:004D5064 call sub_4D58B0 .text:004D5069 add esp, 4 .text:004D506C test edi, edi .text:004D506E jnz short loc_4D5079 .text:004D5070 .text:004D5070 loc_4D5070: ; CODE XREF: sub_4D4EF0+171j .text:004D5070 pop edi .text:004D5071 pop esi .text:004D5072 pop ebp .text:004D5073 xor al, al .text:004D5075 pop ebx .text:004D5076 retn 0Ch .text:004D5079 ; --------------------------------------------------------------------------- .text:004D5079 .text:004D5079 loc_4D5079: ; CODE XREF: sub_4D4EF0+17Ej .text:004D5079 mov eax, [esp+10h+arg_4] .text:004D507D mov edx, [edi] .text:004D507F push ebp .text:004D5080 push eax .text:004D5081 push ebx .text:004D5082 mov ecx, edi .text:004D5084 call dword ptr [edx+54h] .text:004D5087 .text:004D5087 loc_4D5087: ; CODE XREF: sub_4D4EF0+157j .text:004D5087 ; sub_4D4EF0+162j .text:004D5087 pop edi ...

The call at +0xD5055 (red) leads down a dead-end path after a bit of tracing. The same goes for +0xD5064 (orange). If you step into them with the debugger and begin tracing through the code paths, these two functions have very similar behavior. However, there is nothing to indicate that they have anything to do with specific functionality of the “Fog of War” button in terms of interacting with the map. Setting a breakpoint on these two instructions will show that they are constantly being invoked from somewhere and that they only perform logic on the caller object. At this point, we are still in common code related to UI and button clicks, so it’s reasonably safe to rule out these two functions as having anything to do with revealing the map.

The last call site is at +0xD5084 (blue). Stepping into it leads to +0xD4EF0, which is again another large function.

.text:004D4EF0 push ebx .text:004D4EF1 mov ebx, [esp+4+arg_0] .text:004D4EF5 push ebp .text:004D4EF6 mov ebp, [esp+8+arg_8] .text:004D4EFA push esi .text:004D4EFB mov esi, ecx .text:004D4EFD mov ecx, [esi+0B8h] ...

Toggling a breakpoint on it still shows it getting hit all of the time, so it still remains common handling code. If you step through it, you will actually see that it goes back to code found in the previous listing. The same two calls to 0x00424750 and 0x004D58B0 will be made. Then there will be a call to [EDX+0x54], except this time EDX will have a different value. On this second call, it will lead to the following function at +0xD0C70:



.text:004D0C70 mov ecx, [ecx+14Ch] .text:004D0C76 test ecx, ecx .text:004D0C78 jz short loc_4D0C91 .text:004D0C7A mov edx, [esp+arg_8] .text:004D0C7E mov eax, [ecx] .text:004D0C80 push edx .text:004D0C81 mov edx, [esp+4+arg_4] .text:004D0C85 push edx .text:004D0C86 mov edx, [esp+8+arg_0] .text:004D0C8A push edx .text:004D0C8B call dword ptr [eax+30h] .text:004D0C8E retn 0Ch .text:004D0C91 ; --------------------------------------------------------------------------- .text:004D0C91 .text:004D0C91 loc_4D0C91: ; CODE XREF: sub_4D0C70+8j .text:004D0C91 xor al, al .text:004D0C93 retn 0Ch .text:004D0C93 sub_4D0C70 endp

There’s only one real call site here so this function is pretty easy to analyze. Setting a breakpoint on it still reveals that it is getting hit from everywhere, so this is still common code. The call to [EAX+0x30] leads to +0x680D0. Repeating the breakpoint process yet again reveals that it is still getting hit from everywhere, so still nothing useful yet.

.text:004680D0 push 0FFFFFFFFh .text:004680D2 push offset SEH_4680D0 .text:004680D7 mov eax, large fs:0 .text:004680DD push eax .text:004680DE mov large fs:0, esp .text:004680E5 sub esp, 0F8h .text:004680EB mov eax, [esp+104h+arg_8] .text:004680F2 push ebx .text:004680F3 push ebp .text:004680F4 push esi .text:004680F5 mov esi, [esp+110h+arg_0] .text:004680FC push edi .text:004680FD mov ebp, ecx .text:004680FF mov ecx, [esp+114h+arg_4] .text:00468106 push eax .text:00468107 push ecx .text:00468108 push esi .text:00468109 mov ecx, ebp .text:0046810B mov [esp+120h+var_F0], ebp .text:0046810F call sub_4718B0 .text:00468114 test al, al ...

Finding the specific code

Stepping into the first call site at +0x6810F takes you to a function that contains a gigantic jump table (screenshot below). This could be a promising sign of hitting an area that is responsible for dispatching events or invoking callbacks.

Stepping through the code leads down to the following case:

.text:00471DB4 loc_471DB4: ; CODE XREF: sub_4718B0+4FDj .text:00471DB4 ; DATA XREF: .text:off_471FA0o .text:00471DB4 push edi ; jumptable 00471DAD case 4 .text:00471DB5 call sub_54E7D0 .text:00471DBA mov esi, eax .text:00471DBC add esp, 4 .text:00471DBF test esi, esi .text:00471DC1 jz loc_471F5F ; jumptable 00471DAD case 3 .text:00471DC7 push edi .text:00471DC8 call sub_4D58B0 .text:00471DCD add esp, 4 .text:00471DD0 test esi, esi .text:00471DD2 jz loc_471F5F ; jumptable 00471DAD case 3 .text:00471DD8 mov edx, [esi+1A4h] .text:00471DDE mov ecx, [esp+50h+var_40] .text:00471DE2 cmp edx, ebx .text:00471DE4 setz al .text:00471DE7 push eax .text:00471DE8 call sub_58EA10 .text:00471DED mov al, 1 .text:00471DEF jmp loc_471F65 ...

Setting a breakpoint on +0x71DB4 (pink) and continuing shows that nothing is constantly getting hit anymore. Clicking the “Fog of War” button shows +0x71DB4 getting hit. Finally, after tracing for a long time, there is a sign of being inside of code that is specific to the “Fog of War” button. The first call instruction is at+0x71DB5 (red). This function takes one parameter via EDI, and was always a constant value. Carefully stepping through it and observing the values of all of the parameters or referenced/loaded addresses, there was nothing that indicated a toggle value. Specifically, clicking to reveal and hide the map in game and then tracing through this function did not show anything different, so it was ruled out. The instruction at +0x71DC8 (orange) calls address 0x004D58B0, which is one that was already investigated before. The same case happened with this function. It always took in the same value as the previous function and did not show anything that indicated writing in a toggle value or handling code based on a toggle value.

The next call is at +0x71DE8. This function also takes in one parameter and is also the last function called before the jump-table handling function exits. There is some really interesting logic in the teal block. A value is loaded from [ESI+0x1A4] then compared against EBX. The result of this comparison sets a byte in AL to 0 or 1 depending on the case. EAX, which will be 0 or 1, is then passed as an argument to the function at 0x0058EA10. Toggling the button in the game and stepping through shows that EBX always contains the value of 1 and EDX contains a 0 or 1 depending on whether the map is being hidden or revealed. An assumption can begin to be made that this is the function used to reveal and hide the map. The assembly listing for 0x0058EA10 is shown below:

.text:0058EA10 sub_58EA10 proc near ; CODE XREF: sub_4718B0+538p .text:0058EA10 ; sub_58DF30+919p ... .text:0058EA10 .text:0058EA10 arg_0 = dword ptr 4 .text:0058EA10 .text:0058EA10 push ebx .text:0058EA11 mov ebx, [esp+4+arg_0] .text:0058EA15 mov [ecx+53h], bl .text:0058EA18 mov eax, dword_A9D244 .text:0058EA1D mov ecx, [eax+140h] .text:0058EA23 test ecx, ecx .text:0058EA25 jz short loc_58EA43 .text:0058EA27 push 1 .text:0058EA29 push ebx .text:0058EA2A call sub_5316B0 .text:0058EA2F mov ecx, dword_A9D244 .text:0058EA35 mov ecx, [ecx+140h] .text:0058EA3B push 1 .text:0058EA3D push ebx .text:0058EA3E call sub_5316D0 .text:0058EA43 .text:0058EA43 loc_58EA43: ; CODE XREF: sub_58EA10+15j .text:0058EA43 pop ebx .text:0058EA44 retn 4 .text:0058EA44 sub_58EA10 endp

It forwards the value of 0 or 1 to two more functions, which both take two parameters. The first parameter is the 0 or 1 toggle and the second one is always a hard-coded 1 value. Looking at these two functions shows that they write the 0 or 1 value to an object and then call a function

.text:005316B0 ; =============== S U B R O U T I N E ======================================= .text:005316B0 .text:005316B0 .text:005316B0 public sub_5316B0 .text:005316B0 sub_5316B0 proc near ; CODE XREF: sub_442070+1684p .text:005316B0 ; sub_4C91E0+14Cp ... .text:005316B0 .text:005316B0 arg_0 = byte ptr 4 .text:005316B0 arg_4 = dword ptr 8 .text:005316B0 .text:005316B0 mov edx, [esp+arg_4] .text:005316B4 mov al, [esp+arg_0] .text:005316B8 push edx .text:005316B9 push 1 .text:005316BB mov [ecx+40Eh], al .text:005316C1 call sub_5316F0 .text:005316C6 retn 8 .text:005316C6 sub_5316B0 endp .text:005316C6 .text:005316C6 ; --------------------------------------------------------------------------- .text:005316C9 align 10h .text:005316D0 .text:005316D0 ; =============== S U B R O U T I N E ======================================= .text:005316D0 .text:005316D0 .text:005316D0 sub_5316D0 proc near ; CODE XREF: sub_442070+1698p .text:005316D0 ; sub_4C91E0+137p ... .text:005316D0 .text:005316D0 arg_0 = byte ptr 4 .text:005316D0 arg_4 = dword ptr 8 .text:005316D0 .text:005316D0 mov edx, [esp+arg_4] .text:005316D4 mov al, [esp+arg_0] .text:005316D8 push edx .text:005316D9 push 1 .text:005316DB mov [ecx+40Fh], al .text:005316E1 call sub_5316F0 .text:005316E6 retn 8 .text:005316E6 sub_5316D0 endp

Patching

mov al, [esp+arg_0]

to

mov al, 0

nop

nop

now leaves the mini-map revealed regardless of whether the “Fog of War” button is set to be enabled or disabled. The code to reveal and hide the map has now been found.

Developing the hack

At this point, it is possible to develop a hack to reveal the map — it should simply be a matter of invoking 0x0058EA10 with true/false depending on how we want to the map to look. There is one slight problem however: there is a write to [ECX+0x53] at 0x0058EA15. This means that we would need to pass in an object with a writable field at +0x53, which would serve as the “this” parameter that is typically passed in via ECX with the __thiscall calling convention. ECX is re-written further along in the function after being loaded from a constant address, so this seems to be a safe approach. The hacky code for this is found below:

#include <Windows.h> struct DummyObj { char Junk [ 0x53 ] ; } ; DummyObj dummy = { 0 } ; using pToggleMapFnc = void ( __thiscall * ) ( void * pDummyObj, bool bHideAll ) ; int APIENTRY DllMain ( HMODULE hModule, DWORD dwReason, LPVOID lpReserved ) { switch ( dwReason ) { case DLL_PROCESS_ATTACH : { ( void ) DisableThreadLibraryCalls ( hModule ) ; pToggleMapFnc ToggleMap = ( pToggleMapFnc ) 0x0058EA10 ; while ( ! GetAsyncKeyState ( '0' ) ) { if ( GetAsyncKeyState ( '7' ) ) { ToggleMap ( & dummy, true ) ; } else if ( GetAsyncKeyState ( '8' ) ) { ToggleMap ( & dummy, false ) ; } Sleep ( 10 ) ; } break ; } case DLL_PROCESS_DETACH : case DLL_THREAD_ATTACH : case DLL_THREAD_DETACH : break ; } return TRUE ; } #include <Windows.h> struct DummyObj { char Junk[0x53]; }; DummyObj dummy = { 0 }; using pToggleMapFnc = void (__thiscall *)(void *pDummyObj, bool bHideAll); int APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: { (void)DisableThreadLibraryCalls(hModule); pToggleMapFnc ToggleMap = (pToggleMapFnc)0x0058EA10; while (!GetAsyncKeyState('0')) { if (GetAsyncKeyState('7')) { ToggleMap(&dummy, true); } else if (GetAsyncKeyState('8')) { ToggleMap(&dummy, false); } Sleep(10); } break; } case DLL_PROCESS_DETACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break; } return TRUE; }

After injecting the DLL into the game process, the map can be toggled to a fully revealed state or a hidden state via the ‘7’ and ‘8’ keys.

Conclusion

This concludes developing a map hack for the game. This approach was very involved and complicated and the next post will show how to greatly simplify everything by taking advantage of useful information that the developers left in the executable. Reading through this might give the impression that everything followed a linear path from start to finish, but there was a lot omitted for the sake of brevity in terms of dead-end code paths. Including those and their explanations would have likely made this post the length of a typical dissertation. While initially developing the hack, I stepped through these various code paths many dozens of times while scribbling notes and as to what might be relevant. The end result of this post is all of the useful information aggregated together into a coherent and semi-linear guide.

Thanks for reading and follow on Twitter for more updates