Neutralizing Protection in the TERA Executable
Patching sneaky induced crashes that prevent modification and debugging.
Having repaired and trimmed the TERA executable, it’s time to deal with the anti-tampering mechanisms within the game code. Getting rid of these will leave us with an unpacked, runnable, analyzable, and moddable
TERA.exe. For all practical purposes, this is the holy grail for community members working on server emulation and/or evolving the game post-shutdown.
If you’ve been following along at home, you may have noticed that if you try to play the game with the executable you have at this stage, you’ll get some seemingly random crashes after a while. This is the game’s anti-tampering mechanisms kicking in.
Generally speaking, you’ll see this manifest in two ways. Let’s investigate them.
Crash in Virtualized Code
This crash is what you’ll likely see first. It looks like this:
IDA fails to give us a stack trace for this one because we’re deep in virtualized code, so we’ll have to manually look at the stack contents and see if we can work out where we are.
This looks promising:
So a virtual call.
The stack also contains a pointer to this C++ lambda type’s virtual function table:
Jumping to the 3rd virtual method, we see:
This is clearly virtualized. Given that the virtual call earlier is calling the 3rd method, it’s fairly safe to say that this is how we got to the crashing virtualized code.
Let’s switch over to Binary Ninja for further analysis. We’ll find the virtual function table for that C++ lambda type and see where it’s referenced:
S1CheckCodePatchJob, you say? 🤔
Further analysis will reveal that
sub_7ff665ff7f30 is scheduling the lambda to be run asynchronously, but for the sake of brevity, I won’t delve deeper into those details.
So, this probably means that we’re tripping the game’s check for code patches. But how? Well, recall that in the first article, we did patch
S1Context::PostInitialize. The protection provided by Themida knew what the
.text (code) section looked like at build time, and it looks different now, thanks to our mucking about.
To confirm our suspicions, let’s try to find the relevant code in the Korean build:
Looks like the crash is likely in the body of the
if statement, just as was the case for
As an aside, in the Korean build, this function is scheduled to be run by calling into the Unreal Engine 3 I/O subsystem. As far as I can tell, it runs regularly as part of I/O requests. In that build,
S1CheckCodePatchJob just schedules a dummy I/O request to trigger it periodically. Kinda crazy; no wonder they changed it.
Crash in Random Code
The other crash you’re liable to hit is related to timing - most likely as a result of setting a breakpoint and resuming the game after a while. To trigger this one, we’ll set a breakpoint in
S1Context::Tick (an arbitrarily chosen function):
When the breakpoint is hit, we’ll just let the debugger sit there for a few minutes. After resuming the game, we should hit the crash shortly. Note that this crash is non-deterministic so you may or may not hit it.
If you do hit it, it looks like this:
It can happen basically anywhere, in either normal or virtualized code, but generally speaking, it manifests as a memory access to a very low address - usually a strong indicator that a null pointer snuck in somewhere.
Let’s look at the crashing code in this case:
qword_7FF667D785C0? Let’s hop over to Binary Ninja:
Ah. Well, how did the game’s god object variable end up pointing to nonsense like
This variable has well over 20,000 references, so I’ll spare you the boring investigation. If you do decide to check them, you’ll eventually arrive here:
So this method is trashing the
S1Context pointer - right after it trashes that object’s
player field containing the
S1Player pointer, just for good measure.
This method is actually shared among a series of types —
S1SecurityCrashJob2, etc — 16 in all:
(There are also 16 other jobs using a different crashing mechanism, starting from
So what’s scheduling these jobs? There seems to be a constructor function in the references:
This function itself has no references. At this point, you probably realize that this is an indicator that virtualized code is calling it.
We could figure this out the hard way, but we have the Korean build available, so let’s just save some time:
S1SecurityCheckJob periodically checks the
S1SecurityTokens values and schedules a bunch of crashing jobs if they look wrong. Those values are modified in various areas of the game code. Where is this job scheduled, then?
A bunch of interesting things going on here! This function schedules
S1CheckCodePatchJob too, and
SECheckVirtualPC function is a Themida API that can detect whether the game is running inside a virtual machine (in the sense of VirtualBox, VMware, etc). Looking at references to this function, it’s called near the end of
So, that’s that mystery solved. The nice thing here is that we can kill a whole flock of birds — including
S1CheckCodePatch, the cause of the previous crash — with one stone by just getting rid of the call to
With the two crashes figured out, we can begin the process of neutralizing all this protection code.
Note that for this section, I’ll skip over some of the routine analysis work and focus on the patching.
First of all, we’ll need a complete list of virtualized functions. Fortunately, the trampoline functions for Themida-virtualized functions always contain a
pushfq instruction. You’ll almost never see that instruction in regular compiled code, so we can just use a simple Python script to find all virtualized functions:
>>> for func in bv.functions:
... for (toks, addr) in func.instructions:
... if toks.text == "pushfq":
Let’s go through them.
0x7ff668661cc7 is called from the
S1SecurityTokens static initializer:
We’ll leave this one alone as there’s nothing interesting going on.
0x7ff6686942e4 is called from
We’ll leave this alone too since there’s nothing interesting going on. (We could patch it to a
ret instruction if we wanted to get rid of the virtualized code, though.)
Looking at a few addresses together:
0x7ff6686d2b78is called from
0x7ff6686e8b66is called from
0x7ff6686edf42is called from
0x7ff6686f6c8ais called from the C++ lambda originating from
As mentioned earlier, we can kill most of these together by removing the call to
0x7ff6686fcd5c is called from the
This virtualized function contains a ton of essential code. We can’t do anything to it. But while we’re here, let’s take a peek at the constructor in the Korean build:
Oh. So neutralizing
S1CheckSecurityJob will take a bit more work. The easiest fix here is just to patch the
0x7ff6686f9b8b is called from
Like the S1Context constructor, this contains a bunch of essential code. There’s also nothing interesting here anyway, so we’ll leave it alone.
0x7ff6686f0a17 is called from
This job is a bit similar to
S1SecurityCrashJob0 and friends in that it’s meant to induce a confusing crash when the game detects suspicious activity.
data_143345988 is reset to zero at the end of
Since we’ve thoroughly neutralized
S1CheckSecurityJob, this variable will never be reset to zero. That’s going to be a problem, so we’ll need to deal with this job as well.
The job is scheduled in
We’ll just patch the function to skip over the allocation and scheduling of this job:
And that’s it. All the relevant protection mechanisms have been neutralized.
If we really wanted to, we could go through and patch out any (non-virtualized) code that modifies
S1SecurityTokens::Singleton in order to eke out an extremely tiny bit of speed. But I don’t really think it’s worth the effort - at least not until someone manages to devirtualize all code in
TERA.exe so we can get all the code that modifies it.
We can now go ahead and save the patched executable. We should also make sure to use PE Tools to update the checksum in the PE headers.
At this point, you can run the game to confirm that the changes took.
We’re at the end of this little adventure in reverse engineering. It was a pretty fruitful endeavor as we now have a fully analyzable, runnable, and moddable
TERA.exe that can just be dropped into a revision 377345 client.
Future work here would involve devirtualizing all the virtualized functions in the executable. That’s a massive undertaking, however. I have no idea if I’ll ever personally find the time for that. But if someone does manage to do it, I think the only patching that would be strictly necessary is to remove the
SECheckVirtualPC calls. Also, it would make it possible to turn on ASLR for the executable, and completely remove the
.winlice section. Maybe one day.
Finally, just a reminder that unpacked executables for revisions 377345 and 387486 of the client are provided at my tera-re repository, as is a slightly newer revision 367239 of the unprotected Korean build.
👋 for now.