Turning the frustration of a mobile game into a reverse engineering training


dword_13FE7D8 is nothing more than the base pointer for the Crypto class instance in memory, 80 is the offset at which the class static fields start, so we can deduct our “v2” variable is what initializes the _secret, and v4 is what initializes the _salt value, since it performs the “+4” offset on it.

I went on, and labeled as many fields and classes as I could.

I ended up with the conclusion that the PostAsync just feeds the body bytes to the ComputeHash method, which initializes an array of a certain size, copies a bunch of stuff in there and uses the _secret field from the Crypto class (not the _salt field, suprisingly) as we see another call to the dword_13FE7D8 pointer without offset, do some magic with all this, and then perform the MD5 hash of all those bytes.

The result is a string containing the hexadecimal representation of the hash bytes.

That’s our Hash header.

We’re getting close!However, this is the first time I’m disassembling a binary like this.

And I’m hitting a wall: I can’t figure out where the _secret bytes are initialized from.

All I can figure out is that the _secret is a byte array of a certain size, and the _salt field is a byte array of another size.

Two solutions:Fire up gdbserver on device, and gdb on my host machine, put a breakpoint in the Crypto ctor, and dump the memory at the address of the _secret variable.

This can also be done interactively in IDA, but it’s quite heavy to do.

Since we now know the layout of the hashed string, and since the values we need to find are actually small-ish, we can just use Hashcat and brute-force the MD5 hash with a proper mask.

There is likely a way to get the bytes arrays values directly from the libil2cpp.

so binary, but I couldn’t find anything online about where to start or how il2cpp stores the static bytes array in the final binary.

If anyone has info on that, I’ll be glad to learn and add it here.

A quick test in Hashcat reveals that it will take only a handful of hours to brute-force the hash (on a GTX1080), so I’ll use that method.

If the value was larger, the time would have exponentially increased, and the first solution would’ve likely been required.

Note that any obfuscation method on the hash data (XOR, reverse, …) can be deducted from the pseudocode, so it doesn’t have much impact on the reverse engineering complexity.

An hour and 47 minutes later, the good news appeared on the screen:I have successfully cracked the missing bytes to reach the target hash.

This hash is what we had earlier on the simple “{}” JSON message, and I now have all I need to compute my own, valid hashes.

A quick shell script later, I now have a one-liner to generate a valid hash.

I replay a simple “time” query to get the server time, and I compare the response header against my script.

It works!The computed hash with the same body is identical, we did it!Wait, there’s no authentication?This is where the game’s protocol becomes really shady.

Each player is assigned an user ID, which is a classic UUIDv4.

Since the POST requests are all stateless — there isn’t any authentication involved, besides the hash header — it means that we can get and modify the data of any player with just his UUID.

We can have it from other requests (like the comments window, the tournament list, …), or simply from the game’s Facebook page: for most events, they ask people to post their user ID to get rewards.

We could even match player’s in-game nicknames with their real-world profiles.

I tried to forge a request using a random person’s user ID, and I could list his in-game messages:This lucky guy won 2,000 gems thanks to the Facebook event.

I didn’t :(To reach our final goal, which is to add myself a few gems, all we need to do is to dump one of the “cloud save” JSON message, figure out where the gems value is (and how it is… “obfuscated”, simple math operation), change it, compute the hash, and send it to the server as my new save.

Then, on my phone, I simply pressed “Restore” and voilà, my amount of gems is changed.

My first attempt led to unexpected resultsAnother way to do this would’ve been to change the cached data in the game’s data directory, setting a new hash there, then simply restarting the game.

The server wouldn’t even know it, until the next automated cloud save the game would generate itself.

Some final wordsThat’s it!.The funny thing about all this is that even if the devs choose to ban my in-game account for cheating, I could simply just create another new account, and apply it my current “cloud save” data to get back to where I was in the game.

I could also ruin the entire game’s database, since having an user ID is enough to change its data.

Hell, I could even make thousands of fake accounts, having the best performance in tournaments with level 1 basic heroes.

But that’s not what I’m here for.

I’m not here to wreak havoc on the game.

I’ll be transparent here: I gave myself a few in-game gems to catch up on what others have been getting through game bugs.

I don’t plan to ruin the game, cheat in tournament, or anything like that.

I just gave myself a small bump, compensating for all the bugs that have ruined my gaming experience, while at the same time enhancing my reverse engineering knowledge.

Some of you will find that unethical, and I agree at some point, but one can only handle so much frustration.

I hope the game developers will understand that, and hopefully improve the quality and security of their game (and not ban me).

I won’t give out the hash calculation algorithm, and I voluntarily was vague around those bits to avoid ruining the fun for everyone.

Overall, it was a really fun exercise, and if you want to cheat, you should go through it too.

This was the first time ever I actually successfully RE’d native code in IDA, making sense of the pseudocode, and figuring out the original code’s logic.

I learned a lot about the way Unity’s il2cpp system works, and how it’s all matched against C# code, not unlike JNI code.

While it looked pretty straightforward in the article, it took me in reality two entire days to reach my goal.

There were lots of red herrings, I spent a lot of time grepping through the library’s strings for what looked like potential salts, looked for flaws in the actual HTTP API, before actually using tools like unity_decoder (which didn’t work on Linux using Mono, but worked on an actual Windows machine) and finally feeding all this into IDA and getting my hands dirty.

If you want to push it further, you can try to figure out what decrypts the Lua bytecode file, and see if we can peek into the game’s logic.

We’re not given any exact stat on any hero by the developers, so perhaps it would give some nice insights and help building strategies.

I might give this a go in the near future, and write an article about it if there’s interest in that.

If you’re interested in learning how to do this: practice like I did.

It’s the best way to learn.


. More details

Leave a Reply