State of things

First things first, I fired up again my mitmproxy, and checked if the API calls changed. They did update the endpoint, and now the data looks encrypted, both in the request and the response:

What used to be pure JSON is now unreadable bytes

However, we can notice that they haven’t done anything about packet replay: performing the same actions on the game to produce another tournament query led to the same POST body and Hash being generated.

Next, as previously, we need to dump the new libil2cpp.so and global-metadata.dat files, and run the tools on them, then load it into IDA. Since we already know where the hash was loaded (Crypto class constructor), and where the packets were built (HttpClient), we can kickstart a bit these parts.

Digging into the code

After giving IDA some time to load the few thousands functions, let’s have a look at the new and updated HttpPost method. Since I started off a clean disassembly project, I relabeled a few field based on the Il2CppDumper DLL assembly output, reversed with .NET Reflector, to get a cleaner pseudocode:

We can find again our friendly Crypto__ComputeHash which computes the Hash value based on the unencrypted bytes, then the same bytes are fed into the Crypto.Encrypt method. So, they are indeed encrypting the body before sending it. Inspecting that method reveals Rijndael-based (AES) encryption:

The cool thing about this is that they are using standard .NET crypto methods, which means that we’ll have all the fields description and details directly in the .NET Framework documentation, as well as example code to replicate their encryption and decryption process right from MSDN.

Finally, the keys and salts moved from the Crypto class to a separate CryptoConstants class, but that won’t change much our work. Our lovely game developers even helped us with size in the C# fields:

We know the offsets at which the computed key and IV are stored, so no need to manually calculate them! Also spoiler: I couldn’t manage to calculate them properly from the base Salt and Password, for some reason, so directly using Key and IV was good enough.

So what we need to hack this new game revision is the AES key, and the new salt. With that, we should be able to encrypt and decrypt the messages and forge some to the server, as we could in the previous article.

In my previous article, I was stuck at retrieving the secret bytes from the app code directly, which made me brute-force it originally, but I mentioned an alternative method: debugging on device. This time, we have no other choice: we can’t access byte arrays directly from static code (at least not that I know of), so our best and only way is to dump our device’s (or emulator) memory while the game is running, to get those bytes live. Let’s do it.

Debugging a live process on device

Our goal here is to dump the memory of the game, for example at a time where it is hashing a server message, to dump the hash-salt bytes, and the AES IV and key.

To do that, we install IDA’s debugging server on our device, run it, and use ADB to forward the port to our host machine through USB. Once that’s done, we can attach our IDA debugger to any process on the device:

We can attach to any process on our device

But before actually attaching to a process, we need to set a few breakpoints in the pseudo-code. This will make the process pause, and IDA show the corresponding pseudo-code when the line is about to be executed by our phone’s CPU.

First, as we want to dump the bytes that are appended to the JSON body before it is MD5-hashed, we just need to set a breakpoint where the final “salt” array is appended:

Let’s break immediately after the salt bytes are loaded in ComputeHash

Then, we want to break when encryption keys are used, so that we can dump the AES key and IV memory blocks:

Note that if you want to break in a portion of code that is executed very early only once, you need to attach your debugger before those lines are executed. I haven’t found any trick to make IDA run the app from scratch with the debugger immediately attached, but luckily the Unity assembly takes some time to load, so we can tap the app icon, then immediately press Home to pause the game’s execution, giving us time to attach the IDA debugger, then resume the game. Here, we’re breaking at any call to the Decrypt method, so attaching afterwards is fine.

A quick note on Dalvik and IDA: the debugger will catch a few signals sent by the Dalvik VM, caused by GC. Signals such as SIGPWR and SIGXCPU are then expected, so we need to set those as “ignore and pass to application” in the debugger setup, otherwise IDA will break the execution every time.

Then, after attaching to the running game process, once we press a button that needs Decrypt and CalculateHash (any button that performs something online, like the Tournament button), the debugger will pause at the breakpoints we’ve set earlier:

If I double-click the value I tagged “vSalt”, it will reveal the memory value. We skip the first 16 bytes (0x10), as those are Mono’s headers and metadata for byte arrays, and then we can see the salt value before our eyes:

However, the AES IV and Key are put in a raw memory address that IDA didn’t wrap as a variable, so we’ll need some manual brain math from the pseudocode:

**(_DWORD **)(dwCryptoClass + 80)

*(_DWORD *)(*(_DWORD *)(dwCryptoClass + 80) + 4)

We know, from the previous article, that the dwCryptoClass + 80 value is a pointer to the value of the first field of the class, since they start at 80 (0x50) offset. Our second field (the IV) is the next pointer (+ 4) at the value pointed by dwCryptoClass + 80.

So here, to get the actual values for these two fields, we first need to take dwCryptoClass pointer (0xCCA71300 in my case), then add 0x50. This gives us our first pointer address (pointers are 4 bytes, since this is an ARMv7 library, so 32-bits memory addressing):

The ARM instruction set is also little-endian, so our address has to be read from bottom to top: 0xE6BD6D60. Then, we have to jump there to get the actual pointers to each field byte array:

Here, we have two pointers next to each other. If you get the operator priority right from the code extract above, our answers are hidden behind those two addresses: 0xE699F4D0 for the first field (Crypto._aesKey), and 0xCB2EBDE8 for the second field (Crypto._ivKey). Again, at each of those addresses, we add 0x10 to get the actual array bytes:

Our pointer starts at 0xE699F4D0, and data starts 0x10 bytes later, at 0xE699F4E0.

We know that the IV is 16 bytes long and the key is 32 bytes long, as that’s fairly standard, and also noted in the Rfc2898DeriveBytes .NET documentation (since we noted from the pseudocode that they used this class), so we just dump enough bytes from those two memory addresses. We can also infer that from the pseudocode.

From there, we could write a small encryption/decryption using glorious Golang, but for the sake of not having to waste time fiddling with the algorithm settings, let’s reuse C# and decrypt a message we dumped earlier in mitmproxy:

It works! On the left, the encrypted query. On the right, the decrypted JSON. Behind, our small C# decryption program running.

We can use the same methodology then to dump the ComputeHash key. Since we have a direct “vSaltBytes” variable that we were able to map in IDA, double-clicking reveals the value. Updating our previous hash-calculation, and feeding it our decrypted query, leads us to the same hash as we had in the mitm’d request.