Mordhau: From reversing Unreal Engine 4 to breaking animations

Index | Up


Table of Contents

1. Introduction

Mordhau is a multiplayer video game made in Unreal Engine 4. I have been playing this game for a long time, and I made a lot of friends along the way. The best part of this game has not been its fighting mechanics, but how funny the people were.

1.1. The golden era of the game

There was a time where you could do a lot of funny glitches, like flying around. This was by far when I had the most fun with people. The game was so broken that you could fly anywhere, clip into stuff, and do much more.

mordhau1.jpg

You might think that people were using it to take advantage of people, but that was not the case. Instead, everyone was teaching glitches to each other, and goofing around. If someone was abusing a glitch, someone else could just use another glitch and kill them, or simply vote kick them.

However, they ended up patching most of the funny glitches, and even removing vote-kicks entirely. You could still clip into stuff and do other glitches, since the developers did such a bad job, but it was not the same.

mordhau2.jpg

The game is only available for Windows (and consoles), but you can still run it on GNU/Linux through Proton. A friend I met while playing told me about this, and it’s in fact what made me definitely switch from Windows to GNU/Linux on my main computer.

This means that any memory inspecting (more on this below) has to be done within the same wine prefix. I personally use this tool to accomplish that.

1.2. The amazing developer team

The developers of this game are terrible. In plenty of ways. They are bad at programming, they are bad at promoting their game, they are bad at basic decision making, and they are bad at moderating. Basically, they are not mature enough to handle a big game like this.

The game has no anti-cheat at all. You can read and write any values in memory, and change whatever you want as if this was Half-Life 1. I heard they had an anti-cheat at some point, but they removed it for some reason.

Not only do they lack an anti-cheat, but they look like they want people to cheat in their game. Years ago I noticed that they published an update with a PDB file, which contains very useful debugging information. I remember copying it quickly after noticing, because I obviously thought it was a mistake. Well, let’s have a look at the binaries folder now.

mordhau3.png

There it is, the third file on the list. They have been shipping the debugging information along the game for years. This is a big mistake, that allows anyone (with some reversing knowledge) to basically browse the source code of the game. I am assuming they don’t know what the file is, so they just leave it in the output folder because they assume that the game needs it.

You might also notice the suspicious .i64 files. In case the reader is not familiar with reversing, these are 64-bit IDA Pro databases, and they will be covered below.

As a side note, I think it’s very funny that this could happen to the developers to begin with. I don’t know what they used to compile this program, but I think the compiler/IDE should only generate this .pdb file when compiling in debug mode, and not in release mode. I am not sure if this means that they are shipping the debug version or not.

In any case, this is very funny, and it reminded me of how I didn’t know what object files were when I first started with C/C++, so I was not sure about including them on my git projects or not.

This article was been written in May of 2024, so keep in mind that they might have actually removed the .pdb file after I spoon-fed this information to them. If it’s still there, you can import it from IDA Pro, although the rest of this article doesn’t asume you have imported it.

1.3. My motives

I like reverse engineering, and I enjoy a few video-games. That’s why, if I really enjoy playing a game, I might try to reverse engineer it, to see how it was done. Since I use GNU/Linux, I mainly enjoy reversing native Linux programs and games. Even if I really enjoy a Windows video-game, the reversing interest is usually not big enough to make me switch to Windows.

However, since I had been playing Mordhau for such a long time, I decided to give it a try and, through Proton, see if I could find something interesting. I did find a lot of interesting stuff. I already knew how bad the game was, but noticing how many things were client-sided definitely made me realize that the developers were clueless.

I knew I could get banned, because I found things that I had never seen anyone do before, but I decided that I had played the game long enough, and that I didn’t care if I got banned. I rather have fun my way, than just play normally to avoid getting banned. I even took notes on writing this article once I got banned.

I don’t particularly enjoy cheating, as in, getting an unfair advantage over other players. I like reversing and making cheats because I enjoy the low-level aspect of understanding a game enough to do something that the developers are actively trying to prevent you from doing. That was a long sentence, by the way.

And indeed, I started reversing the game on January 2024, and I didn’t get banned until May of 2024. I think I saw a moderator in-game once… and it was before starting reversing the game. In the end, it was a jealous kid who reported my account because I wouldn’t tell him how to T-pose in game. They immediately banned me exclusively based on my screenshots.

mordhau4.png

Anyhow, this is good news, since now I am able to explain the reverse engineering process without being worried about bans, which was something I have wanted since I started reversing the game, just like I did with Devil Daggers or TF2.

2. Finding the necessary offsets with IDA Pro

As I said, the game is made in Unreal Engine 4. There is an amazing tool called UEDumper, which allows you to explore the game’s structures, even live. However, this is a “universal” dumper, and it needs some offsets. We are going to get them using IDA Pro.

This is meant to be a quick guide for getting the needed offsets by UEDumper, so I won’t be diving into too much detail. The general method for finding them is to look at the Unreal Engine 4 source code, find where these symbols are used, and look for cross references you can use. Try looking for close strings, and things you can easily find from IDA.

2.1. Important IDA notes

Remember to open the Mordhau-Win64-Shipping.exe file, not Mordhau.exe files. We will also use this executable with UEDumper below.

Also, you will need some settings in order to find the required offsets.

First, open the Strings subview with View > Open subviews > Strings, or by pressing Shift+F12. From there, right click on the string list and press Setup. Make sure all C-style string types are allowed.

mordhau5.png

You can then press Ctrl+F in the Strings view to search for strings.

2.2. Finding GNames

Search for the string MulticastDelegateProperty, and choose the one with C type, not 16-bits.

mordhau6.png

Double-click the string, then open the cross-references window by right-clicking the aMulticastdeleg symbol, or by pressing the x key. Jump to its only xref.

mordhau7.png

You are now in a big subroutine that handles a lot of strings. I opened the decompiler window so it’s easier to tell what’s going on, but it’s not necessary. From the assembly view, scroll to the top of the subroutine and list its cross references.

mordhau8.png

Jump to the first one, the Direction of the xref should be Up. The function where the MulticastDelegateProperty string was located is being called with the GNames structure as its argument, so we should be looking for something like this.

mordhau9.png

As you can see, the address of stru_57BEE80 is being loaded into rcx, which according to the Windows x64 calling convention, is used for the first parameter when calling a function. Note that the function call appears highlighted because I marked the location from IDA.

However, 0x57BEE80 is not the real offset of GNames. This is the “absolute” offset that IDA shows us, which would be accurate if we were loaded at 0x0. However, we want to get the relative offset to where the executable was mapped in memory. To get this relative offset, we will need to subtract the ImageBase, which we can obtain by typing get_imagebase() in the IDA command prompt.

mordhau10.png

2.3. Finding GObjects

Search for the string CloseDisregardForGC, and jump to its only xref. In the assembly, you will see that the string is being used as a function parameter.

mordhau11.png

Right before that function call, you can see that it’s checking for a byte_57D7AEC value. This is the assembly generated when compiling the following Unreal Engine code.

if (GUObjectArray.OpenForDisregardForGC())
{
    SCOPED_BOOT_TIMING("CloseDisregardForGC");
    GUObjectArray.CloseDisregardForGC();
}

The OpenForDisregardForGC value being checked is a 4-byte padded boolean. In the GUObjectArray structure, it’s located right before the ObjObjects array. Therefore, we can simply add 4 bytes to that byte_57D7AEC value, and we will get the offset of GObjects. Again, remember to also subtract the ImageBase.

2.4. Finding UWorld

Search for the string Failed to load package '%s' into a new game world., and jump to its only xref. Once you are in the subroutine, press F5 to decompile it.

mordhau12.png

In this case, we were lucky and the function call with our string has no labels near it, and the the decompiled subroutine looks pretty clean.

We are looking for that qword_590DC40 line at the bottom. That’s the UWorld global variable, but remember that we still need to subtract the ImageBase.

Although this was the ideal scenario, the decompiler might not always interpret the branches this way, and might show a bunch of jumps across labels. The function might look something like this:

  // ...
  // Label mess, qword gets assigned somewhere over here

LABEL_123:
  sub_1822B30(&PerformanceCount, L"Failed to load package '%s' into a new game world.", v124);
  if ( v6 == &PerformanceCount )
  {
      v7 = PerformanceCount;
  }
  else
  {
      // ...
  }

  // ...
  // Subroutine returns, qword assigment is not here

  return 0;
}

In that case, we would have to jump to the xref of that LABEL_123 and look for the same qword assignment below the jump.

3. Dumping and exploring the game with UEDumper

As I mentioned, you will first need to edit some files, and compile the UEDumper project itself.

You will have to edit UEDumper-1.8/UEDumper/Engine/Userdefined/Offsets.h, and replace the offsets you got from IDA in the setOffsets() function. The format should be the following.

inline std::vector<Offset> setOffsets()
{
    std::vector<Offset> offsets;

    // Mordhau-Win64-Shipping.exe
    offsets.push_back({ OFFSET_ADDRESS | OFFSET_DS, "OFFSET_GNAMES", 0x57ABCD });
    offsets.push_back({ OFFSET_ADDRESS | OFFSET_DS, "OFFSET_GOBJECTS", 0x57CDEF0 });
    offsets.push_back({ OFFSET_ADDRESS | OFFSET_DS | OFFSET_LIVE_EDITOR, "OFFSET_UWORLD", 0x58FEDCB });

    return offsets;
}

The first offset should be GNames, the second GObjects and the third UWorld. Again, make sure you have subtracted the ImageBase!

Then, you can save the file and compile the project using the “Release” version in the top bar of Visual Studio. Unfortunately I can’t insert a screenshot because I compiled the project once, back when I had Windows installed. Just imagine how often the developers update their game.

Now you can open the UEDumper.exe file on the build folder. As I mentioned on the introduction, if you are running the game in GNU/Linux through Proton, you will need to use some kind of tool to run the dumper under the same wine prefix as Mordhau.

mordhau13.png

Enter any project name and the name of the Mordhau executable. Again, remember this is not Mordhau.exe but Mordhau-Win64-Shipping.exe. When you have done this, press “Dump Game”. If your offsets were correct, you should see something like this.

mordhau14.png

Once it’s done loading, you can start inspecting the game live. Press “Live editor” on the top bar, then the “Add Inspector” button, then switch to the “Add Offset” tab, select UWorld, and enter any display name.

mordhau15.png

After pressing “Add”, you should be able to see the structures in a nice format. Expand “UWorld”, and you will be able to navigate the game’s structures by name. Each +0030 value in red represents an offset relative to the parent. If you expand a structure, you are “dereferencing” it, and its offsets will be relative to that parent. In other words, you are traversing a chain (or tree) of pointers.

4. Magic hexadecimal tree

These are the most interesting values I have found. If you have been following until now, you might realize what these numbers mean, and what to do with them.

<UWorld>
 |
 |- 0x120
 |  |
 |  |- 0x689  // Try 0
 |  |- 0x697  // Try 1
 |
 |- 0x180, 0x38, 0x0, 0x30
    |
    |- 0x2B8, 0x23C
    |- 0x28C  // Not useful
    |- 0xA28  // Try fixing
    |- 0xDB1  // Try 1
    |- 0x260
    |  |
    |  |- 0x8A0  // Next 2 are client-sided, unfortunately
    |  |- 0x8A4
    |  |- 0x8A8  // Vehicle. Others can use, carefuly
    |  |- 0x8C4
    |  |- 0x8CC  // Vehicle too
    |  |- 0xDE8
    |  |- 0x6A0
    |     |- 0xC8  // Try fixing, with next one
    |     |- 0xEC  // Try 9999, not client-sided
    |
    |- 0x2A0
       |
       |- 0xB80, 0x128  // Not useful
       |- 0xF09         // Try 2, new gameplay
       |- 0xB78, 0x830  // Careful
       |- 0x6A0, 0xF8   // Client-sided, unfortunately
       |- 0xB08
       |  |
       |  |- 0x61  // Try kick, etc.
       |  |- 0x6C
       |  |- 0x6D
       |  |- 0x6E
       |
       |- 0x288
       |  |
       |  |- 0xC49  // Not useful
       |  |- 0xD1C  // Try 1, very OP
       |
       |- 0xE66   // Try 1
       |- 0xEA4   // Try increasing
       |- 0xEA8   // Try -1
       |- 0xE94   // Not sure if it does anything
       |- 0x1029  // Needs another option to be useful
       |- 0x8C4   // Try -1
       |- 0x8CC   // Try -1
       |
       |- 0x1200
       |  |
       |  |- 0xCEC   // Next 3 will feel weird
       |  |- 0xCF4   // Try... non ranged?
       |  |- 0xD00
       |  |- 0xD1F   // Not useful
       |  |- 0xCC8   // Try ranged
       |  |- 0xEF8   // Useful in very few weapons
       |  |- 0xCBC   // Try throw
       |  |- 0xD23   // Extremelly goofy one, personal favorite
       |  |- 0x1A0C  // Needed for a previous option
       |  |- 0xCB9   // Try pavise
       |  |- 0x1C98  // Very goofy, pavise again
       |
       |- 0x11F8
          |
          |- 0xCEC   // Most are shared with 0x1200
          |- 0xCF4
          |- 0xD00
          |- 0xD1F
          |- 0xD40   // Try heal, unfortunately not shared
          |- 0xCC8
          |- 0xEF8
          |- 0xCBC
          |- 0xD23
          |- 0x1A0C
          |- 0xCB9
          |- 0x1C98
          |- 0xF40   // Try polehammer, for example
          |- 0xF41   // Should be always 1
          |- 0xF54   // Try 0
          |- 0xF58   // Try 0
          |- 0x13E0  // Try maul, for example
          |- 0x13E1  // Should be always 1
          |- 0x13F0  // Try lowering a bit, combine with next 2
          |- 0x13F4  // Try 0
          |- 0x13F8  // Try 0
          |- 0xD40, 0xB0
             |
             |- 0x14  // Be creative
             |- 0x18
             |- 0x1C
             |- 0x7C  // Try 0 for next 3
             |- 0x80
             |- 0x84
             |- ...   // Groups of 6 repeat

I am sorry if someone was expecting a quick guide on how to break X animation. You can do everything I did with the numbers above. I intentionally wanted to make this misleading to inexperienced programmers, so the game developers couldn’t figure out how I did my magic.

When the game updates, you just need to find and change the UWorld offset, and you will be good to go most of the times. If something bigger changes, you can just update the values easily.

And more importantly, get creative. I found all this stuff by trial and error.