Unpacking and Repairing the TERA Executable
Creating an analysis-friendly executable that is also runnable.
I’ve been a fan of action-oriented MMORPGs for many years. One such game is TERA, which I’ve played for many years since its release and leading up to its closure in 2022. Another hobby of mine is poking around in the internals of games that I play. That used to mean reverse engineering World of Warcraft for server emulation purposes way back in the day, but more recently, it meant reverse engineering TERA in order to develop third-party tools, such as Alkahest (discontinued) and Novadrop.
I spend most of my time working on Celerity these days, but sometimes, it’s good to take a break to avoid burnout. So I decided to spend a week on some reverse engineering that would hopefully yield useful results - and it did!
Background and History
TERA.exe
, the game executable for TERA, is protected with Themida. This hasn’t quite hindered the development of third-party tools for the game, but it has certainly made it much harder. It’s pretty much impossible to statically analyze the executable as-is; you have to create a dump of the executable from memory after Themida unpacks the original contents. But such dumps are flawed in a variety of ways. Additionally, if you wanted to actually modify the executable, your only option was to inject a DLL at run time to perform them, which comes with a whole bunch of other problems.
These problems have only gotten more relevant as the game officially shut down. As the community tries to keep the game going by way of private servers, being able to evolve the game — including its executable — is crucial.
The Unlicense Project
Fortunately, sometime around 2021-2022, Erwan Grelet unveiled the Unlicense project. This is an automatic unpacker for Themida versions 2.x and 3.x - the latter being used by TERA. I won’t go into the details of how it works here, but long story short, it can successfully unpack TERA.exe
such that it’s feasible to statically analyze it without all the issues you would traditionally run into with an executable dumped from memory.
Unfortunately, the unpacked executable is not runnable. In other words, DLL injection still remains the best option for modifying the executable. So, not quite sunshine and rainbows all the way.
But, as I’ve been alluding to, that can be fixed.
A Stroke of Luck
It just so happens that an old build of the Korean version of TERA was released to a test server in 2020 without Themida protection properly applied. The calls to Themida’s APIs were there, but the post-processing of the executable hadn’t been done. This was during the upgrade to the latest version of Unreal Engine 3 and the transition to 64-bit, so I would guess that something went wrong with their build system that they only noticed and corrected for subsequent builds.
This was an incredibly lucky incident, all things considered. Despite its age, the executable from this build helped me save a lot of time. Without it, I would have had to spend weeks/months debugging and fixing a nasty crash (more on this below).
Repairing the Executable
With that little history lesson out of the way, let’s get to the gory details of fixing up TERA.exe
.
For this article, I’ll be using revision 377345 of the client — mainly because active private servers for this revision actually exist — but the same principles apply to later revisions as well. For example, I did the same for revision 387486, which is the final revision that was released before the game shut down.
If you’d like to follow along at home, you can grab both original and unpacked executables on my tera-re repository.
Unpacking Themida
First, we’ll unpack the PE with Unlicense:
$ unlicense TERA.packed.exe
INFO - Detected packer version: 3.x
frida-agent: Setting up OEP tracing for "TERA.packed.exe"
frida-agent: Exception handler registered
frida-agent: TLS callback #0 detected (at 0x7ff666b9c080), skipping ...
frida-agent: TLS callback #1 detected (at 0x7ff666b9c480), skipping ...
frida-agent: OEP found (thread #28320): 0x7ff666b9c86c
INFO - OEP reached: OEP=0x7ff666b9c86c BASE=0x7ff664de0000 DOTNET=False
INFO - IAT found: 0x7ff666cb9000INFO - Resolving imports ...
INFO - Imports resolved: 739
INFO - Fixed IAT at 0x7ff666cb9000, size=0x18a0
INFO - Dumping PE with OEP=0x7ff666b9c86c ...
INFO - Fixing dump ...
$ mv unpacked_TERA.packed.exe TERA.exe
Cool! If you load up the resulting executable in e.g. Binary Ninja or IDA, you’ll see that static analysis basically Just Works™️.
Image Base Woes
If we immediately try to run the unpacked executable under WinDbg Preview, we’ll be faced with this:
... snip ...
ModLoad: 00007ff7`cbe40000 00007ff7`cffcc000 TERA.exe
... snip ...
(71a0.782c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
00007ff6`66b9c080 ?? ???
Not good. It seems to be jumping into random memory.
The stack trace looks like this:
So the executable loader is clearly trying to run the thread-local storage initialization callbacks specified in the TLS directory and failing because the callback addresses are somehow wrong.
Let’s switch over to Binary Ninja and see what’s what:
All of these addresses look correct. They all point into the .tls
section at the spots you’d expect. So let’s take a look at the list of callback function pointers:
The first address is where we crashed!
Binary Ninja is usually pretty good about detecting function pointers in data sections, but not here. After some investigation by the developers, it turns out that this is because Binary Ninja currently relies on relocation data to detect function pointers, and there are no relocations for these pointers.
We can verify the lack of relocations by checking the base relocation directory in PE Tools:
To correlate these RVAs with the absolute addresses we see in Binary Ninja, we need to check the PE headers to find the executable’s preferred image base:
Now we simply subtract an absolute address from the image base and compare the resulting RVA to the RVAs in the relocations:
>>> hex(0x7ff667ff02d8 - 0x7ff664de0000)
'0x32102d8'
>>> hex(0x7ff667ff02e0 - 0x7ff664de0000)
'0x32102e0'
As you can see, none of the relocations affect the callback list above.
Going back, let’s define the callback list properly:
If we click _TLS_Entry_0
, we’ll be taken to the function as expected:
(I won’t go into the details of how this function works as it’s uninteresting for our purposes.)
OK, so why is this actually problematic at run time? Well, you might have heard of a security technique called ASLR. When ASLR is enabled, the loader is free to ignore the PE’s preferred image base, and instead load it at an arbitrary address. That’s where relocations come in: They fix up pointers like the ones in the callback list. On the other hand, if the PE is loaded at its preferred image base, relocations become no-ops.
Now, combine that knowledge with the fact that the callback addresses we just looked at have no relocations, and you have a recipe for disaster: Looking at the WinDbg output above, TERA.exe
was loaded at 0x7ff7cbe40000
(a random address) rather than the preferred 0x7ff664de0000
, so none of the callback function pointers are valid!
A good question at this point would be why there are no relocations for these function pointers. But we’ll get to that soon.
For now, we can work around this by disabling ASLR for TERA.exe
:
$ editbin /dynamicbase:no TERA.exe
Microsoft (R) COFF/PE Editor Version 14.37.32705.0
Copyright (C) Microsoft Corporation. All rights reserved.
If we run the unpacked executable again, we now get to this error:
This is good progress as it indicates we made it past all the static initialization in the C runtime, all the way to the game’s launcher check. From now on, we’ll run the executable through the launcher and attach the debugger.
A Virtual Nightmare
We now get to a much more ominous-looking crash:
(73a8.8d94): C++ EH exception - code e06d7363 (first chance)
(73a8.8d94): C++ EH exception - code e06d7363 (first chance)
(73a8.8d94): C++ EH exception - code e06d7363 (first chance)
(73a8.8d94): C++ EH exception - code e06d7363 (first chance)
(73a8.8d94): C++ EH exception - code e06d7363 (first chance)
(73a8.8d94): C++ EH exception - code e06d7363 (first chance)
(73a8.8d94): C++ EH exception - code e06d7363 (first chance)
(73a8.8d94): C++ EH exception - code e06d7363 (first chance)
EUR(73a8.8d94): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
TERA+0x3521b1e:
00007ff6`68301b1e 458b09 mov r9d,dword ptr [r9] ds:000001f8`335d7210=????????
Note that the C++ exceptions are expected and handled by the program. The access violation is the real issue.
The stack trace looks broken:
The disassembly for the top frame also looks rather unlike anything a C/C++ compiler would produce:
Well, if you’ve done any reverse engineering of Themida-protected executables, you’ll immediately recognize the above as being virtualized code. This is also easy to tell by the fact that this code is in the .winlice
section. Reverse engineering virtualized code is essentially a whole field in its own right, and can easily take months…
Fortunately for us, though, IDA is able to give us a somewhat more useful stack trace than WinDbg:
This doesn’t quite tell the whole story, but, it’s a start. The second frame points to 0x7ff6654606c6
:
This is a huge function (beginning at 0x7ff66545e190
) related to game initialization. A cursory glance at the code makes it fairly obvious that this is part of Unreal Engine code. The virtual call here indicates that we’re likely calling into a function overridden by TERA.
Curiously, though, this function has no references in Binary Ninja. We would expect it to at least be referenced from a virtual function table.
Well, remember how earlier the lack of relocations caused Binary Ninja to fail to detect the function pointers in the callback list? It turns out that this issue is pervasive throughout the executable. Recall also how the base relocation directory only had a handful of relocations in the .tls
section and nothing else.
If you take a look at the base relocation directory of any regular program or library, you’ll typically see hundreds or thousands of relocations. So something is clearly up here. We’ll need to take a slight detour to fix this.
Repairing the Section Headers
Before fixing the base relocation directory, we need to address a small oddity in the section headers to make our lives easier. We’ll use PE Tools for this:
Some inspection of the unnamed sections makes it fairly obvious that these are in fact the original sections of the PE that Themida unpacked when we ran Unlicense earlier. The named sections, then, were synthesized by Themida when the PE was packed. (.SCY
is an exception - that one was synthesized by Scylla while running Unlicense.) .winlice
contains virtualized code areas, while .boot
contains the (also virtualized) boot loader that unpacks the original sections.
I’ll skip over how I determined which of the unnamed sections contain what; this isn’t too hard to do if you compare to any regular program and look for patterns in the section contents and layout. Let’s give names to the unnamed sections and rename Themida’s sections to use an exclamation mark instead of a dot:
If we open the section characteristics for .rdata
, we’ll see that it’s set as being writeable:
.rdata
is, by convention, read-only data, so we’ll just go ahead and untick that flag. There is a reason why this is set, but we’ll get to that later.
Finally, we can save the changes we’ve made.
Now would be a good time to make sure the section header changes are reflected in Binary Ninja and IDA. The easiest way to do this is just to analyze the executable anew, but if you don’t want to wait for that, you can also manually patch the section headers in Binary Ninja, and use IDA’s segment editing UI.
Fixing Relocations
In fixing the section headers, we uncovered the fact that there’s an original .reloc
section, and it’s fairly big too:
This is where our relocations are hiding!
Let’s take a look at the data directories in the PE headers:
If we follow this RVA, we’ll see that it points into the section that we’ve renamed to !reloc
. So let’s go ahead and fix it to point to the beginning of the .reloc
section, i.e. 0x3155000
.
But how big should the directory be? 0x14
is clearly not the answer. But naïvely using the section size isn’t the answer either, as the section has a bunch of zeros at the end:
The PE format documentation gives us a hint:
The base relocation table contains entries for all base relocations in the image. The Base Relocation Table field in the optional header data directories gives the number of bytes in the base relocation table. […] The base relocation table is divided into blocks. Each block represents the base relocations for a 4K page. Each block must start on a 32-bit boundary.
Additionally, the base relocation block documentation enables us to spot that a block is starting at 0x7ff667fe2514
:
The size of the block is 0x48
bytes, so it ends at 0x7ff667fe255c
, which is on a 32-bit boundary:
We’ve found our size: 0x7ff667fe255c - 0x7ff667f35000 = 0xad55c
.
Fixing Unwind Data
Before we return to analyzing the crash, there’s another data directory we should fix to make analysis work better.
When loading the executable in IDA earlier, you might have noticed these messages:
Reading exception directory (.pdata)...
7FF66885B4E8: function entry has invalid unwind data, ignoring.
7FF66885B4F4: function entry has invalid unwind data, ignoring.
7FF66885B500: function entry has invalid unwind data, ignoring.
7FF66885B50C: function entry has invalid unwind data, ignoring.
7FF66885B518: function entry has invalid unwind data, ignoring.
7FF66885B524: function entry has invalid unwind data, ignoring.
7FF66885B530: function entry has invalid unwind data, ignoring.
7FF66885B53C: function entry has invalid unwind data, ignoring.
7FF66885B548: function entry has invalid unwind data, ignoring.
too many function entries with invalid unwind data, skipping the rest.
360 out of 117882 function entries had invalid unwind data and were skipped.
Parsing .pdata and creating 117882 functions...
This is not good. The .pdata
section — containing unwind data for use by exception handling infrastructure — is a super useful resource for identifying function locations in an executable. We want parsing of this section to work properly.
Let’s take a look at the exception directory in the PE headers:
This actually points to the !winlice
section. So, once again, Themida has synthesized a directory that contains at least partially bogus data. This also indicates that IDA, reasonably, follows the PE headers to find unwind data, rather than assuming that the section named .pdata
(procedure data) actually contains it - which is only a convention, not a requirement.
This one is also easy to fix. We just need to change the RVA to point to the actual .pdata
section, i.e. 0x2fe2000
.
The function table entry documentation tells us that each entry is 12 bytes on x64, and that the last field is an RVA pointing to the unwind data for the function. The end of the section looks like this:
The very last RVA clearly points to an UNWIND_INFO
structure in the .rdata
section.
So now we can calculate our directory size: 0x7ff667f1a4c0 - 0x7ff667dc2000 = 0x1584c0
. This is divisible by 12, as expected.
With the base relocation and exception directories fixed, we once again need to make sure that the changes are reflected in Binary Ninja and IDA. Once done, both will be fully able to detect pointers in the data sections — such as those in virtual function tables — and will find all function ranges covered by unwind data.
Unbreaking Import Binding
After making our changes, if we debug the executable with WinDbg again, we’ll be faced with a new loader crash:
(5308.7748): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
ntdll!LdrpSnapModule+0x39e:
00007ffa`b322278e 49897d00 mov qword ptr [r13],rdi ds:00007ff6`66cb9000={advapi32!AdjustTokenPrivilegesStub (00007ffa`b2c63b90)}
The stack trace:
The loader seems to be trying to write the address of a resolved import (AdjustTokenPrivileges
) while loading TERA.exe
. But the address it’s trying to write to (0x7ff666cb9000
) lies within .rdata
, a read-only section, so an access violation occurs.
At that address, we find the import address table (deobfuscated by Unlicense):
Remember that we removed the writeable flag from .rdata
? That’s why we’re now crashing here. But overwriting the addresses in the IAT is a normal part of resolving imports while loading a PE. So this really should work.
Well, while each entry in the import directory contains an RVA to the relevant part of the IAT, it turns out that the full IAT area is also described in a distinct IAT directory. Where is that pointing?
So, nowhere. But we can fix that easily. The RVA just needs to be set to the beginning of the .rdata
section, i.e. 0x1ed9000
. The size can be found by locating the terminating null pointer in the last part of the IAT:
So the size is: 0x7ff666cba8a0 - 0x7ff666cb9000 = 0x18a0
.
With the IAT directory fully described in the PE headers, the loader is now able to make it temporarily writeable during import binding, so this crash is resolved.
Again, we need to make sure that the changes are reflected in Binary Ninja and IDA. This is important because we’re going to do some code patching in Binary Ninja soon, so we need to be working on the canonical version of the executable.
We can now finally turn our attention back to the crash in virtualized code from earlier.
Investigating the Crash
To make it easier to figure out virtual functions, we’re going to use the ClassyPP plugin for Binary Ninja to find virtual function tables based on RTTI. After running it, we can see that our function at 0x7ff66545e190
is a method of UGameEngine
and is referenced from its virtual function table:
Let’s return to the crashing location:
A bit of analysis and guesstimation will reveal that the pointer at offset 0x7e4
in UGameEngine
is the UGameViewportClient
instance. TERA has a derived class, US1GameViewportClient
. So let’s see what we can find at offset 0x2a0
in its virtual function table:
The interesting bits are at the end of this function:
From past experience reverse engineering TERA, I know what these functions are. Let’s name them and add some types:
There are two problematic calls here. The vast majority of the code in the S1Context
constructor is virtualized, while about half of the code in S1Context::PostInitialize
is virtualized. S1Context::Initialize
is normal code, however.
We need to step through in IDA and see where we get before the crash.
We make it past the S1Context
constructor:
We make it past S1Context::Initializ
e:
Finally, we get to S1Context::PostInitialize
:
But before returning, we crash as before:
This is where things would ordinarily get really bad. We’d have to actually reverse engineer Themida’s virtual machine to make sense of this. But, as I mentioned in the history lesson earlier, we have access to an unprotected executable from a Korean build of the game. There, S1Context::PostInitialize
looks as follows:
SECheckProtection
is a Themida API that detects whether the executable has been unpacked by an attacker - which is certainly the case for us. I would hypothesize that the body of the if
statement executing is the cause of the crash.
To test that hypothesis, we can manually assemble the surrounding code and skip over the rest of the virtualized code. Specifically, we just want to assemble the 5 calls and then a jump to 0x7ff666161c6c
(0x1412cbcdc
in the Korean build):
For the sake of not making this article twice as long, I’ll skip over finding the correct offsets into S1Context
and locating the right functions. This is fairly straightforward to do by comparing to the Korean build. Here’s the code we want:
mov rcx, qword [rbx + 0x50]
call 0x7ff6666f7560
mov rcx, qword [rbx + 0x1e0]
call 0x7ff6665c6410
mov rcx, qword [rbx + 0x240]
call 0x7ff666695f90
mov rcx, qword [rbx + 0x220]
call 0x7ff666296ec0
mov rcx, qword [rbx + 0x248]
call 0x7ff6662bb800
mov rcx, qword [rbx + 0x2a8]
call 0x7ff666237100
jmp 0x7ff666161c6c
After assembling the patch with Binary Ninja, S1Context::PostInitialize
now looks like this:
We can now export the executable from Binary Ninja:
Running the game again, we are now able to log in and play:
So, indeed, the protection logic in that virtualized code was the issue.
Conclusion
This little adventure shows that it is possible to restore an unpacked Themida-protected executable to a runnable state. Some of the PE surgery here can maybe even be automated, but I don’t think there’ll ever be a practical way to deal with virtualized protection logic automatically.
Also, it bears repeating that having the unprotected Korean build of the executable available was ridiculously lucky.
In the next article, I’ll go over a bunch of cleanups that can still be done on the executable to get it as close to the original, unprotected executable as possible, as well as reduce its size.
Excellent article Alex. I have done a little IDA work, but never attempted to get past Themida...well done :)