A new update came out, and as I was dumping stuff around for a little fun side-project with the game’s API, I realized they had a new API endpoint. They’ve switched from v3 (the encrypted endpoint) to v4, which still seems encrypted, although somewhat similar to the previous API endpoint. Unfortunately, the encryption keys changed, so we need to go fetch them again.

Using our usual tools, we decompile the libil2cpp.so file, and we see now that in the “firstpass” assembly generated by Il2CppDumper, there’s a new class that wasn’t there previously:

Beebyte.Obfuscator? Seems like we’re in for a ride! Were those $60 worth it?

That Obfuscator class is there for an obvious reason: they’ve now obfuscated the non-public parts of their assembly, as we can quickly see once we open the Athena namespace (the main namespace of the game’s code):

These likely aren’t there by mistake. They’ve taken precautions to specifically obfuscate the classes I was using, such as the HttpClient and Crypto classes. Nicely played!

There are two paths we can follow here. First, the easy way, since we know already the layout of the classes we’re looking for (the Crypto/CryptoConstants class that we used in the previous article), would be to simply look for classes with the same signature, something that we can do in mere seconds:

On the left, the obfuscated class (fun fact: it was the very first in the namespace). On the right, the unobfuscated one from a previous APK version.

From there, we can simply redo exactly the same thing we did in the previous article, dumping memory from that class at the specified offsets, and we’d be done.

But, the second path I was talking about is, what if A) we don’t have a previous, insecure APK to start from, and B) we only have that obfuscated code? Well, we need to backpedal a bit and do something akin to what we did in the very first article.

We had quickly found out that the hash was a MD5 hash, which we could infer without even decompiling anything from the game, just looking at the network headers. So, by simply looking at any class with a “MD5” call, we could find a strong starting point.

But there’s yet another way. If we dig a little bit in the game data, we could see that the “luaroot” file, which seemignly (from the filename) contains the game’s LUA files, is encrypted, unlike every other files in the same folder, which are regular Unity assets bundles. In the Loader class, one of the non-obfuscated method is called “LoadScriptBundle”. If you put 2 and 2 together, we should be safe to assume that this method, at some point, decrypts the data, and thus needs access to the encryption keys. A quick look at the pseudocode in IDA seems to confirm this:

They’re reading from a file, then they call some “magic obfuscated method”, then they tell Unity to load that bundle from memory.

If you’ve paid attention to the assembly screenshots above, you’ll realize that the “APAEBAJDOBE” namespace is indeed our previously called Crypto class. Opening that “CDFIJNMMDIG” method in IDA seems to confirm our initial thought, even if we didn’t have any unobfuscated class to start with, as the calls are quite obvious:

Seems familiar? Rfc2898 key generation, loading two byte arrays (key and IV? Wink wink) into that class, then creating the RijndaelManaged class with a MemoryStream… Sounds an awful lot like some decryption going on!

Bingo! Here’s where we can put our breakpoints to load the key and IV. Even if we don’t know which is which, a general crypto rule with AES is that the key is always twice the size of the IV, so we’ll quickly know which is which.

Assuming we didn’t have any “LoadScriptBundle” method to start with, we could’ve searched for calls to known C# cryptography classes in the assembly namespaces, until we’d have found one, or try to start from the very first entry point we could find. That’s longer, but not impossible. Looking for keywords (like MD5, SHA, Rijndael, …) generally quickly gets you on track, as you can skip all the .NET Framework classes fairly easily to focus on the assembly’s specific namespaces.

Anyway, we’ve found the decryption method. But how can we find the encryption one, and find again our “Hash” header computation method? Simply backpedal again. We now know the Crypto class location (dword_13FE8DC), so all we need to do is use IDA’s Xref function to locate uses of that location (class’ fields):

There’s a great similarity between our encryption’s “CDFIJMMDIG” method, and another “CBKLBODDPDI” method on the same class, with a similar call count to our location.

Opening that “CBKLBODDPDI” method reveals, indeed, yet another call to Rfc2898DeriveBytes, MemoryStream and RijndaelManaged. The other methods doing something completely different, we can safely assume that this is indeed the Encrypt method we were looking for. Let’s go up a bit again, and see what parts of the code use that method:

Great! There’s not a lot of locations that are calling it, and from two obfuscated assembly classes nonetheless. If we have a look at the second one (ECAOIANHBJE$$ENMDOMJAGJC), we see a very simple class that write bytes to a file. That’s not what we’re looking for right now, but from parallel research, I’ve found out that the game save data is also encrypted. This is very likely the method that takes care of creating that save file, since all it does is calling the encrypt method and storing the result in a file.

Let’s have a look at the first one then, nicely and wonderfully called OGKDSCANGHQSTUR$$MHDSWQOD:

We can find calls to “WWW__ctor”, that’s a good hint we’re dealing with Web stuff.

There is a call to our “CBKLBODDPDI” encryption method.

There also is a call to a “LJNBHEBALPF” method, in our same class containing our encryption and decryption methods, which seems to take an array of bytes.

These look like pretty strong hints that we’re reaching our goal. Once again, we can start relabeling the methods to be a bit more clear in what’s happening. Digging into “LJNBHEBALPF”, we can see calls to ComputeHash methods from the .NET HashAlgorithm classes, so this is likely our MD5 calculation method.

After relabeling with what we know, the behavior becomes clear. Just like in the previous article, we can see the incoming text decoded using Encoding.UTF8, the hash being computed on it, and then the same bytes being encrypted.

Compared to our previous APK, some fields have moved. Offsets have changed, for example what used to be a “kPassword” string constant, at offset 0xC, becomes a byte array, and the string is now at offset 0x4. We can’t trust the offsets from the previous APK, but it’s not a big deal, as following the code calls will be more than enough to determine what each field is:

Although the class layout is similar, we can’t trust the first field to be kNaiveSecret anymore, since 0x4 is now a string, compared to the byte[] it used to be. Same goes for the kPassword at, previously, 0xC.

However, after reversing the logic in what we determined to be the ComputeHash method, we could see that the bytes copied at the end of the body before it is hashed, is the field at 0xC (12). It is then our new kSecret array.

Finally, now that we have a clear(ish) layout of how it all works, it’s time to look for the key, IV, and hash salt again. Just like in the previous article, we’re firing up the IDA debugging server on a rooted device (or the Android emulator), and dump the memory at strategic breakpoints:

We’ll break at the Buffer__BlockCopy call above for the Hash bytes, and at some point after the IV and key are loaded (variables at 0x0 and 0x4 after dwCryptoClass + 80)

And once again, once the game is running and our breakpoint is triggered, the data unfolds before us:

Here’s the memory address of our byte array being appended to our body (the hash salt). Double-click, add 0x10 to go to the start of the data, and dump the bytes!

The hash salt and encryption keys are now in our hands, again.