Turning the frustration of a mobile game into a reverse engineering training by Guillaume Lesniak


I consider myself a grey hat. I’m far from being the best hacker or cracker in this world, and I tend to notify most of the people I “hack” about their systems weaknesses. Almost all the “hacks” I am doing involve using some software other people built: Slowloris test tools, SQLmap, and just randomness/luck (like finding out where my ex-university stores everyone’s cookies, or just trying random stuff on APIs).

Yes, I am closer to a script-kiddie than a hacker. However, for the first time ever, today, I felt like I leveled-up my skills.

A little bit of back story. I am a huge fan of a mobile tower defense game. It is the typical “play 10 minutes a day” game that you enjoy playing while eating your breakfast, and I’ve been playing this game for 622 days now. That’s almost two years of dedication, every day, playing the daily quests, tournaments, and everything this game has to offer.

But since August 2018, the developers of this game have pulled an awful lot of errors, bugs and mistakes. Every event had an issue where early birds could get like 10x the normal reward, every new hero had a huge amount of bugs, and every update brought as many fixes as new game mechanics bugs and flaws. Fast-foward 6 months, every event, every gift, every single thing the developers of this game did, allowed a few lucky ones to grab more than the other could have, and the developers did nothing to smooth things out. The community is upset, but they don’t care: each event give them money, since many people will buy the “special offer bundle” with real money. They have enough popularity to just ignore the complaints, and the game has become a cash-cow for them.

For me, this was too much. I love this game, I really do. But their behavior regarding the repeated (similar) problems drove my frustration above my tolerance level. I had to do something. I decided that I had to find a way to compensate for the bugs by giving myself some in-game credits (gems). It’s wrong at many levels, but you all know that feeling of “the fun goes beyond morality” — and it looks fair from a certain point of view.

So I got started.

Photo by Braden Collum on Unsplash

On to the actual reverse engineering

The game has a feature where it can do a cloud save and cloud restore of the player’s progress. This include level progession, elixir, gems, tokens, unlocked heroes, heroes levels, etc. So, if we can forge our own save data, we can change our amount of gems.

It is time to fire up a lovely mitmproxy and take a look at what’s going on.

Looking at the first few HTTP queries that unveil on my screen, I notice that every single query, except for static data, is a POST request with a JSON-formatted body. There doesn’t seem to be any kind of encryption or player authentication of some kind. However, something caught my eyes, an interesting “Hash” header:

Header for a typical request, with an empty JSON body 

Let’s fire up Postman and see what happens when we change the hash:

That was… kind of expected 

With a valid hash, we get the expected response. However, as soon as the hash is invalid, we’re greeted with a 400 status code with a simple “SPAM” body.

I start looking for another HTTP POST request where the response body is the same as the previous request I inspected, and soon enough I can find one:

Headers on a response, with an empty JSON body 

Notice how the hash is the same? That’s a good news: the algorithm that calculates this hash is the same for both the request and the response, and it doesn’t seem to include any player-specific data (like using the player ID as a salt, or device IMEI, etc). This means that if we can calculate the hash, we can both send forged queries to the server, and forged response to the client (by intercepting and modifying the server responses, or even building our own game server if we wanted to), and it would work for everything and everyone.

We’ve got our goal.

A test on the hash length reveals that this is very likely an MD5 hash. Let’s see if it is simply an MD5SUM of the data:

That would’ve been too easy 

Obviously, the hash is salted. I would’ve lost all faith in the developers if they didn’t even salt the hash.

MD5 is relatively fast to brute-force. However, even with a very powerful computer, cracking the entire hash would be quite lengthy. We don’t know the length of the salt, if it is before or after the actual body, if there’s XOR-ing or any other kind of funny thing going on. Brute-forcing at that point could work, but would be a waste of time (and power, think about the planet).

It’s time to reverse engineer the thing. I fire up my Android SDK, and start pulling the game’s data from my phone. I hadn’t really paid attention so far, but the game is actually built using Unity. In Unity, if you use the default settings, the C# assembly will be packaged as-is in the APK, and a quick Resharpr can reverse almost the entire code base in a fairly human-readable way, not unlike using Dex2Jar and JD-GUI on a classic, unobfuscated Java Android app for example:

Example of decompiled Java app, screenshot courtesy of https://futurestud.io

However, after a quick unpack of the game’s APK, we can immediately notice the devs took some precautions to prevent people like me from doing what I’m trying to do. The game’s LUA files are encrypted, and the C# logic went through Unity’s IL2CPP engine, which converts most of the game’s C# assembly logic to native code, leaving a single “libil2cpp.so” binary, much harder to reverse. A quick look in a disassembler make it clear: everything that could be stripped was stripped, there’s no function name that looks like the one of a game (like “health”, “energy”, “map”, “level”, etc), so loading this file into IDA would just be a hell to go through.

After some Googling, I found a nice tool by GitHub’s user nevermoe, called unity_metadata_loader. This tool scans the global-metadata.dat file generated by Unity during the Il2Cpp process, which contains a mapping of the C# Assembly names and their location inside the libil2cpp.so generated file. The result can then be loaded into IDA for a cleaner reverse engineering. The Linux fan I am is hurt by the Windows-exclusive availability of those tools, but I guess we can make an exception for today.

I copied both libil2cpp.so and global-metadata.dat files from the game into a separate folder, and ran the tool on them. Yay, it worked! We obtained two output files, method_name.txt which contains close to 28,000 method names, and string_literal.txt which contains about 9,000 string constants. The contents of the first file looks very promising:

This looks like methods a game could use 

This seems to confirm that we’re headed in the right direction. I can see many other interesting class names, like “HttpClient”, “HttpWrap” and “LibHttp”, which we’ll hear more about in a minute.

I fire up IDA, load the (huge) libil2cpp.so ARM binary, wait for all functions to be disassembled, and load the unity_decoder.py script. It loads the two text files we’ve generated earlier, map it to the library’s entry point, and boom, it looks like something we can work on:

The functions, originally called “sub_XXXXX” (where XXXXX is their address in the binary), now have proper labels. We can start digging. 

Remember those interesting class names we’ve had previously? Time to take a look at them! Earlier in the mitmproxy dump, we could see that every request was an HTTP post. My first match to load will be “HttpWrap$$PostAsync”. A quick tap on the F5 key lets us see C-like pseudocode. I’m quite experienced with C, however I’m terrible at reading assembler code, even less at ARM assembler code, so this approximate translation will help quite a bit in the process.

This is what we obtain:

Ehhh… What’s all this? 

So we’re half-lucky here. A lot of labels are missing, but there’s a great deal of ToLua calls, which seems to indicate that we’re in a function that is mapped in Lua. As a reminder, Unity allows you to write C# code, but also have some gameplay logic written in Lua for the sake of simplicity. It seems like the HttpWrap class is simply a wrapper around another class performing the actual HTTP POST request, bridging the call to LUA. The HttpWrap$$Register method seems to confirm this, as it contains multiple calls to LuaState__RegFunction, with a pointer starting off the HttpWrap class definition pointer every time.

At line 34 and 49 of this pseudocode, you can notice that there are calls to LibHttp__PostAsync_0 and LibHttp__PostAsync, so let’s look at them. The last one is simply a shortcut call to the first, more complete one:

Inside this function’s definition, we can see a call this time to another class method, HttpClient__PostAsync. We look inside this method’s pseudocode, and we’re going to stop there. From the looks of it, it seems like this is where the request is built, and the headers populated, most likely thanks to the call of a method named “Crypto__ComputeHash”. A-ha! There you are!

However, we now have one big issue: what are all the function parameters? Since the Il2Cpp wraps many C# structures, most of the parameters passed are just integers, or rather pointer to C#-mapped structures. They could be actual integers, but they can also be strings, byte arrays, etc. IDA all map them in order, calling them a1, a2, a3, … with int type.

During my Googling earlier, I stumbled upon another cool tool on GitHub called Il2CppDumper. This tool also takes libil2cpp.so and global-metadata.datfiles as inputs, and builds a dummy C# assembly DLL with the method names. Resharpr freaks out on this DLL though, but .NET Reflector seems to be able to read the definitions just fine without trying to decompile the code (which doesn’t exist in the DLL since it’s just a dummy shell with only the class and methods declarations).

Opening the assembly gives us a nice overview of the classes, most noticeably our HttpClient and Crypto classes:

We now have all the methods signatures and types 

A few interesting information are given here. On each method, the Offset address is given. This is the function offset inside the libil2cpp.so binary, and matches what IDA gave us. If we didn’t run unity_decoder earlier, we’d have a function inside IDA called “sub_3ADBA0”. In fact, it would be the Crypto.ComputeChecksum function, since its address is 0x3ADBA0. We can cross-check this with unity_decoder’s mapping:

The method unity_decoder labeled “Crypto__ComputeChecksum” is indeed located at 0x3ADBA0 

Another interesting information given by Il2CppDumper’s assembly is the offset of each static field. On many occasions, you’ll end up looking at a line like this in IDA’s pseudocode:

result = *(_DWORD *)(dword_13FE7D8 + 80);
*(_DWORD *)(result + 4) = v4;

This can actually be found in Crypto__cctor function, the constructor of the Crypto class. We can find a very similar line of code in the same function:

**(_DWORD **)(dword_13FE7D8 + 80) = v2

If you’ve looked at the class definition screenshot above from .NET Reflector, you noticed two interesting static fields, _secret and _salt. Noticed also how _secret had no FieldOffset annotation, while _salt had a 0x4 offset? Yup. 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 results 

Another 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 words

That’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.

EDIT: Part 2 is now available, with memory dump of encryption keys

If you’re interested in learning how to do this: practice like I did. It’s the best way to learn.


Originally posted: https://medium.com/@xplodwild/turning-the-frustration-of-a-mobile-game-into-a-reverse-engineering-training-a9887043efdf

February 6, 2019
Notify of

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest Most Voted
Inline Feedbacks
View all comments
© HAKIN9 MEDIA SP. Z O.O. SP. K. 2023