BattleTech (CE Mono vs. JustDecompile)

RCE Fanatics

Thought I'd give this a shot, since there's a ton of people out there not mastering (or at least understanding) the concept of matching whatever they see in Unity decompiled outputs versus the decompiled code Cheat Engine's MonoDataCollector returns. For this explanation I will use the below:

Fire up the game, get in a map. Open Cheat Engine, load table, activate [ Enable ] script, activate Cheat Handler script. Go back in-game and hit Numpad 0 to execute DEBUG_PlayerOneGodMode. Wait, wait.. what did I just do? Rewind, please

Let's take them one by one, from JustDecompile perspective, so you understand how I got to what the script just did

Fire-up Telerik JustDecompile. Hit Ctrl+O and choose Assembly-CSharp.dll from your <SteamLibFolder>\BATTLETECH\BattleTech_Data\Managed folder. Once opened, you will see a Search button up-top on the toolbar. Click it and type godmode in the Find What: field. You will see this:

See DEBUG_PlayerOneGodMode reference on second line? Double-click it. You'll be taken here:

What can you see in that picture? Simple:

Left tree shows the function, as well as the Namespace and Type Name. Why are these important? Because we will make use of them to get to this function in Cheat Engine.

Based on the above, we can concatenate these to a string with ":" as separator - - BattleTech.UI:CombatDebugHUD - - this denotes hierarchy in C++ (a member of).

Our function is called DEBUG_PlayerOneGodMode, so it will be appended to the above with a ":" as well (member of).

Now that you've seen what's going on, next logical question would be: "OK, but how do I run this in Cheat Engine? I want to activate the function!". The logical answer would be create a thread to call in this function - DEBUG_PlayerOneGodMode - and execute it. All would be nice and dandy, but once you manage doing that and are able to call it, you'll notice game crashes. Why? Because you also need an instance pointer to be able to call it. Even though you see a void type of function, this only means it doesn't take-in parameters. However, the instance pointer - also referenced as this - is required.

See this.SetGodMode? See this.combatHUD.Combat.Teams.Find? That's how you know

2) To get it you would need to break on a function you know - from testing - is executed in the Namespace:Type Name (just so I use the references from JustDecompile). Most Unity functions come - conveniently - with an Update function You'll see in a bit.

Time to move to Cheat Engine; remember you've loaded the table and executed Numpad 0. If you've not done it, do it now; I'll explain why it is important for this tutorial: although Cheat Engine is able to decompile Unity code, it will not also execute functions to retrieve symbols. So, all code in the two functions - DEBUG_PlayerOneGodMode, SetGodMode - will show no actual symbolicals.

OK, OK.. but what about the instance pointer? We'll get it from debugging BattleTech.UI:CombatDebugHUD:Update. So head there in Cheat Engine's Memory Viewer via Ctrl+G and set a breakpoint at the function's prologue:

Note that once you set breakpoint there, Mono gets un-linked. You'll know this has happened when you don't see symbols anymore Do your stuff, then activate it again in main CE window (Mono > Activate mono features). Reason why this happens is the mono thread makes use of your hardware breakpoints.

Your instance pointer is in RCX. In my case, 0x0000000142C77380.

What you can do at this point - - as I did - - is to hook the Update function and store this pointer to a static location. Then make use of it in your thread code. Another thing to note is your CreateThread needs to be attached/detached to/from the mono thread. How can this be done? See the code in the table

So up to this point you have the tools you need to understand a void type (no parameters) Unity function and what it needs to be run from a thread in Cheat Engine. Now that you've also executed the DEBUG_PlayerOneGodMode function, thus implicitly the SetGodMode function, let's take a look at how to interpret it from JustDecompile to the ASM code you see in Memory Viewer.

Another reason for creating this tutorial was the various people nagging me to alter some parameters SetGodMode changed that they didn't like, making the game too easy or dull (e.g.: DamagePerShot too high).

Let's see; I'll take the first parameter for a spin, the rest should be easy once you get this one:

But, but.. how do I know which is which? Well.. it's called debugging + sense of observation. It's something you'll have to learn in your own free time.

Let's take them one by one, for the first piece of code (as I was saying), in the order of how x64 registers are used for the parameters of a function (rcx, rdx, r8, r9, etc.). Just keep in mind rcx with always be the this pointer - - the instance pointer - - so start from rdx onward:

So Float_Multiply is 0xC in that list, right? Good. Let's continue; next-up is a float value - 0.1 - followed by a -1 and a true. The true is translated as 1. Just so you know. So what we need to find in the ASM code is the right sequence for these 4:

Cheat Engine actually shows us where the 0.1 float reference is. It is first converted into a double-precision float (see CVTSS2SD instruction here), then to a single-precision float (see CVTSD2SS instruction here). Then comes the push 1, push -1, storing the MMX conversion to stack and the push C (remember Float_Multiply?).

Noobzor

You can't tutorial years of dev experience. There are more moving pieces in these things then you can think of at one time. And this is stuff even most devs will find challenging. If you really want to learn CE you need to learn software development.

RCE Fanatics

There are times in life when you take a step back and see the bigger picture. I was going to update the table last night (which I'm gonna do anyway), then I thought to myself "why not extend the tutorial?" So here goes.

If you check my table here, you will notice I use hooks to get the instance pointers needed for some of the functions in the Cheat Handler. Considering the code runs in a thread, therefore requires no per-se interaction from the user - just using the designed hotkey to execute it - what if we don't rely on hooks at all? The reason I am mentioning this is game updates have the tendency to break these hooks, as the prologue of the functions often changes (or are JIT-ed differently). To explain it even better, see this practical example in my 1.2 table, which, as you reported, crashes on latest version. Let's see why:

Keep in mind CE will most likely create a 14-bytes jump when allocation is not done close to game module. In this case, the Assembly-CSharp.dll is dynamically allocated, so there are 99% chances you won't be able to create 5-bytes jumps. What I mean with this is when you write "jmp Hook" to your allocated memory space, CE can do this:

Code:

E8 xx xx xx xx - JMP Hook

or this:

Code:

FF 25 xx xx xx xx xx xx xx xx xx xx xx xx - jmp Hook

We rule out the possibility of 5-bytes JMPs; let's focus on 14-bytes.

Back to my explanation. We're at BattleTech.UI:CombatDebugHUD:Update with displayed ASM:

So code from 543BA8D0 (prologue) down to 543BA8DE was replaced with a long JMP. All nice and dandy, but.. from our cave, we want to return to "push rdi" instruction, so game resumes execution after having passed through our cave. If you count 14 bytes from top going down, you got:

Code:

55 48 8B EC 53 56 57 41 54 41 55 41 56 41

As you can see the 0x41 byte is part of a 2-bytes instruction. So CE will break it with the JMP:

Code:

543BA8DD - 41 57 - push r15

So it wouldn't be correct the return JMP to point to 543BA8DE, because it would execute an invalid instruction. If you check my code, I am indicating exactly that:

By assigning the CombatDebugHUD_Update_b label right under jmp CombatDebugHUD_Update_h, I am telling the JMP in the cave to land right below it to the invalid instruction: jmp CombatDebugHUD_Update_b. That's why 1.2 table crashes with latest version of the game. And that's why you should avoid methods like these unless you intend to update the table with each freakin' game update (because the Mono code is JIT-ed differently, having different sizes or containing a different order of the instructions). In short, your AOBs (if you use them) will certainly break as well.

So.. to avoid this, I thought of not actually using a hook. In some occasions, you can fetch a pointer without the need to use hooks. Which is what I'm going to show you in the next episode