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.
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”
Comment by Matteo — March 4, 2017 @ 10:52 AM
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
Comment by Matteo — March 4, 2017 @ 11:29 AM
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!
Comment by admin — March 12, 2017 @ 2:56 PM
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?
Comment by John — March 12, 2017 @ 6:05 PM
@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.
Comment by admin — March 12, 2017 @ 10:22 PM
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.
Comment by Matteo — March 13, 2017 @ 2:29 PM
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.
Comment by John — March 14, 2017 @ 12:04 PM
Superb tutorials you have here!
Comment by Rake — March 14, 2017 @ 3:36 PM
@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, …
Comment by Matteo — March 23, 2017 @ 11:47 AM