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.
Mysterious Crashes
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 S1Context::PostInitialize
previously.
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:
So what’s 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 0x574
?
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 — S1SecurityCrashJob0
, S1SecurityCrashJob1
, S1SecurityCrashJob2
, etc — 16 in all:
(There are also 16 other jobs using a different crashing mechanism, starting from S1SecurityCrashJob2_16
.)
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:
So 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 S1CheckMsgProcJob
. The 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 S1Context::Initialize
:
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 S1Context::RunSecurityJobs
.
Bird Hunting
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[0].text == "pushfq":
... print(hex(addr))
...
0x7ff668661cc7
0x7ff6686942e4
0x7ff6686d2b78
0x7ff6686e8b66
0x7ff6686edf42
0x7ff6686f0a17
0x7ff6686f6c8a
0x7ff6686f9b8b
0x7ff6686fcd5c
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 S1AutoLogoutJob::Run
:
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:
0x7ff6686d2b78
is called fromS1CheckMsgProcJob::Run
.0x7ff6686e8b66
is called fromS1CheckSecurityJob::Run
.0x7ff6686edf42
is called fromS1Context::RunSecurityJobs
.0x7ff6686f6c8a
is called from the C++ lambda originating fromS1CheckCodePatchJob
.
As mentioned earlier, we can kill most of these together by removing the call to S1Context::RunSecurityJobs
:
0x7ff6686fcd5c
is called from the S1Context
constructor:
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 Run
method:
0x7ff6686f9b8b
is called from S1LauncherProxy::Initialize
:
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 S1MemoryAllocJob::Run
:
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 S1CheckSecurityJob::Run
:
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 S1DataCenter::Initialize
:
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.
Conclusion
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 SECheckProtection
, SECheckCodeIntegrity
, and 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.