TL;DR? Watch the video demo:
About one week ago, Gameloft released a My Little Pony game on iOS and Android devices, and if my Twitter feed is any indication, it has been wildly popular. According to the Google Play page, it already has hundreds of thousands of downloads.
Wanting to see what all the fuss is about, I downloaded the game as well, but only poked around at it for about 15 minutes before deciding it wasn't quite my cup of tea. But not wanting to feel completely left out, I decided it would be a good exercise to see if I could keep up with my friends by way of cheating. 0:)
There are quite a few types of resources in the game, in particular coins and gems. Like many mobile games today, it seems like the game producer's revenue stream (since the game is free to download) is in offering players to option of purchasing more of these resources using real money. So it will come as no surprise that the game has applied some safeguards to naive methods of manipulating the game state. However, since the game can be played completely offline, the entire game state is saved on our device, so we know going into it that this may prove difficult, but not impossible.
Initial ObservationsThe primary goal was to find a way to manipulate the resources available, i.e. give myself more coins and gems. So my first step was to try running a memory trainer on the process. Here's a quick description of a memory trainer for those unfamiliar:
In general, variables relevant to a video game (heath, money, experience) are stored in the process's memory. A memory trainer attempts to isolate the addresses of these variables in memory, and the manipulate them as desired.So put another way, I wanted to figure out where the amount of coins I have is kept in memory, and then change it to a significantly larger value. So I dug up a generic memory trainer I'd written previously, and fired it up. I told it to find places in memory that contained a value equal to my current number of coins, but to my surprise, there were no locations in memory containing that value!
Very strange. It looks like the developers over at Gameloft have applied some obfuscation of data in memory to avoid such naive attacks. Kudos for making this a challenge. :D
Having failed at that, the next attack surface I analyzed was the game's save file. It wasn't hard to find (the file is named mlp_save.dat in the game's data directory), but on opening it up in a hex editor, I didn't see any obvious layout to it. But this isn't too surprising since many games just employ a binary data format. So the usual trick is to spot the difference in save files with only minor changes.
So I exported a save file from when I had 3000 coins, and then a second save file from when I had 3300 coins, hoping to spot only a few bytes of difference so that I could identify where this value was being saved. However, to my surprise (once again), the entire file had changed. There were literally only a few dozen (out of ten thousand) bytes that had remained the same. Such a small difference leading to a high variation suggests encryption is in play. Although once again, this game can be played entirely offline, so the encryption and decryption is done locally, which means that the cipher is likely symmetric with a key saved somewhere nearby.
I should say that it's possible that the encryption of the save files could be incidental to some other purpose such as simply to preventing sharing of save files online, but regardless it's certainly a roadblock.
Disassembling and DecompilingGiven that both memory and save files are obfuscated, breaking into this game necessarily requires reverse engineering of the code. Expanding the APK for the game, we can see that it comes with the usual Dalvik classes (in the classes.dex file), but also utilizes a very large JNI library (libPonyWorld.so, which is a surprising 10MB of compiled code). So I grabbed JD-GUI and IDA and went to town on it.
Before even looking at the code, I noticed that the string for the save file name "mlp_save.dat" only appears in the native binary libPonyWorld.so and not in the Java classes, suggesting that the interesting things are going to be happening there. Indeed, after opening up a bunch of the Java classes (which have useful names like aa and ba; I suppose their app was obfuscated with ProGuard or something similar) I found out that the Java code is mostly just graphics/hardware handling. All the interesting game logic looks like it's in the native binary.
I spent about two days coming up with everything in this post, so I won't go through the analysis step-by-step, but here are some of the things I figured out just casually walking around the disassembly.
- The native code is mostly written in C++.
- The save files, before they're written to disk, are in an XML format, so it is easy to tell if one has successfully decrypted the save files. Once decrypted, they'll be easy to manipulate as well.
- Parsing of the XML appears to be done with the open source TinyXML library (which has been embedded in libPonyWorld.so).
- The player data I originally wanted to manipulate is contained in the PlayerData class, which has GetCoins(void) and GetGems(void) methods: this is probably where I want to look to understand how the player data is obfuscated in memory.
Creating a New Memory TrainerSince manipulating the save file requires a lot of steps (figuring out the encryption algorithm, figuring out the encryption key, reverse engineering the file structure, etc.), I decided to first see if I could understand why my memory trainer was failing. As mentioned above, the place to look seemed to be the PlayerData::GetGems(void) method. Here's what it looks like in IDA.
An ordinary field getter would just LDR a memory address into R0 and return, so obviously there's something much more interesting going on here. Here's a quick MS-PAINT style mock-up of how I interpret this code.
A and C and XOR'd together, as are B and D. Those two values then have a ROR4 (rotate right by 4 bits) operation applied to them. If those two values aren't equal, exit. (And I don't just mean exit the function, I mean exit the whole program; apparently these guys are serious about not wanting you to poke around in their memory). If they are equal, then return that value.
So in other words, the game keeps two copies of the variable in memory so that if you try to manipulate one, the whole game will crash on you. Further, each copy is obfuscated with a ROR4 operation and then XOR'd with whatever is saved as integers C and D. I immediately suspected that these were integers were random values just there to hide data. And indeed if you find your way to the PlayerData constructor, these memory addresses are given a random value from the lrand48 function.
Given this knowledge, I made a new memory trainer specifically for this game (based on the old memory trainer I dug up) that searches for memory, taking into account the above obfuscation techniques. After a wee bit of trial and error, I met with success!
Reverse Engineering Save FilesWith that success under my belt, I wanted to go back to manipulating save files. Based on my observations, the game has an entire SaveManager class dedicated to this task. Again, going through all the steps would take awhile, so here's what I was able to figure out.
- As mentioned above, the raw save files are in an XML format.
- The XML is compressed using the zlib library. (version 1.2.3 based on strings I found in the binary).
- This output is then encrypted using the XXTEA algorithm, and using your GLUID as the encryption key.
- The GLUID (Gameloft Unique/User Identifier, I think), is unique per device, so no sharing of save files between devices. (Well, at least not until I'm done).
- The GLUID isn't saved on disk, but it is printed in the logs if you just want to use ADB logcat.
- Alternatively, the GLUID derivation is actually done in the Java code which is much more readable, so I was able to pretty quickly discern that the GLUID is just an MD5 hash of the phone's IMEI concatenated with the string "com.gameloft.android.ANMP.GloftPOHM". If the device doesn't have an IMEI (i.e. if it's a tablet), then the device's ANDROID_ID is used instead.
- The output of this MD5 hash is passed around as 4 4-byte signed integers, but there's a bit of funny business that goes on with it. First of all, the bytes from the MD5 hash are turned into integers under big-endian encoding, despite the fact that everything else in our world is little-endian. Why did they code it that way? Because they could.
- Further, there's some funny mangling that goes on if those integers are negative. More on that below.
Once all that was determined (which was the majority of the time spent on this), I spent a little while staring at the code which actually reads in the raw save file, and determined it has the following format.
+ 4 Bytes + 4 Bytes + 4 Bytes + +
+ Uncomp. Size + Compr. Size + Encry. Size + +
+ (Arbitrary Size) +
+ XXTEA Encrypted Data +
+ + 4 Bytes +
+ + Reserved +
The first 4-byte integer is the size in bytes of the uncompressed save file (i.e. the XML). The next 4-byte integer is the size in bytes of the decrypted data, and the final 4-byte integer in the header is the size in bytes of the encrypted data.
The final 4 bytes appended after the encrypted data (which I labeled as reserved) always have the value 1 (as a four-byte little-endian integer); I suspect this is meant to be a save file format version, but as far as I could see the save file loader doesn't even bother reading those four bytes in.
As a side-note, the code which reads in save files appears to blindly trust the file sizes in the header and then reads in the file to heap space malloc'd accordingly. As such, this seems like an ideal target for a classical buffer overflow attack. I could easily see a crafted save file that triggers arbitrary code execution. But I'll leave that to someone else as a separate project.
As I mentioned, the encrypted data is decrypted with the XXTEA algorithm using the GLUID as the key. The only particular issue I ran in to is the way the game mangles integers in the GLUID that are negative. It took a bit a trial-and-error, but I finally figured out that if an integer X is negative, the game replaces X by 0x7FFFFFFF - X. It's in no way apparent to me why they do this. *shrug*
Furthermore, whether it is a bug or by design, the last of the four integers is mangled in an even more confusing manner. The above operation is only applied to it if the first integer was negative. Why would they do this?! It doesn't even feel like effective obfuscation.
Once the data portion in the middle is decrypted, the output has the following format:
+ (Arbitrary Size) +
+ zlib Compressed Data +
+ + 4 Bytes +
+ + Checksum +
The checksum is a CRC32 hash of the uncompressed data. Incidentally, I discovered that there are quite a few algorithms that call themselves CRC32, so it was some more trial-and-error before I found an open-source implementation that matched the checksums I was looking for.
Finally, after passing the compressed data through zlib, we have the raw XML of the save file. I've uploaded an example here.
There's lots of things you can toy with in the save file. One thing in particular that jumped out at me though:
<Timers TrashTimer="86400" DerpyTimer="25"/>There's a "DerpyTimer" in the game. Apparently the Gameloft developers have no compunction about calling her by name. ;)
After using an ordinary text editor to change the save file, I just inverted the process to create a new save file and put it back on my phone. Success!
ConclusionsGames that are played offline are inherently hackable. All the game state is local and there's no opportunity for asymmetric encryption since all the keys have to be kept locally. Gameloft has implemented several blockades which made it a non-trivial matter of getting to the save files and applying memory hacking. However, as an amateur reverse engineer it only took me two days to break through all those barriers, so I would hardly describe the game as impenetrable.
As I mentioned in the beginning, I wasn't really that into the game to begin with, but I did really enjoy the challenge to breaking into it. Intentional or not, they provided me with many hours of entertainment. So kudos to Gameloft for supplying me with a challenge that was neither trivial nor intractable.
I'm releasing both the memory trainer and scripts to pack/unpack save files. Note that the former requires using ADB on a rooted Android device while the game is running. The latter are perl scripts run on your computer, so you don't strictly need a rooted phone (in fact, it will probably work for iPhone saves as well, but I haven't tried it), but you still need a method for getting the save files in and out of your phone (the save files are saved to internal flash, not the SD card). Personally, I use ADB, although there may be other solutions.
Usage: Start the game, upload the memory trainer your device, and then run it in a shell. It will prompt you for your current coin value, and then it will scan memory (this make take up to 5 minutes).
./adb push mlp_mem_trainer /data/local/
Enter current coin quantity: 3795
Scanning range: a000-376e000
Player data address found: 1a24f7c
unpack_savefile.pl / pack_savefile.pl (You'll also need this modified XXTEA.pm library)
Usage: ./unpack_savefile.pl mlp_save.dat #IMEI# > save.xml
./pack_savefile.pl save.xml #IMEI# >mlp_save.dat
./adb pull /data/data/com.gameloft.android.ANMP.GloftPOHM/files/mlp_save.dat ./mlp_save.dat
./unpack_savefile.pl mlp_save.dat 001122334455667 > save.xml
xmllint --format save.xml > new_save.xml
./pack_savefile.pl new_save.xml 001122334455667 > mlp_save.dat
./adb push mlp_save.dat /data/data/com.gameloft.android.ANMP.GloftPOHM/files/mlp_save.dat
Android AppHere's a working proof-of-concept contained in an easy-to-use Android app. It works on my two devices (both of which run Cyanogenmod) without needing root access, but your mileage may vary.
* App now attempts to use root access if save-file is not read/writable. See my follow-up post for more details.
* Added ability to edit number of hearts.
* Updated to allow user-editable values for coins/gems/level.
* Added ability to edit number of shards.
* Initial release