Unlocking Android games

My phone runs LineageOS without any Google components, which prevents Google from tracking me - but also means that the standard Google Play Store libraries are not available. Buying applications or installing applications bought with the account on another phone is not possible.

There were two games I'd like to play in the full version but could not because of that, and here is how I solved my problems:

com.vendor.game.v1

I began by pulling the .apk from the phone with adb pull after finding the file with pm, the Android package manager:

$ adb shell
$ pm list package
$ pm path com.vendor.game.v1
package:/data/app/com.vendor.game.v1-1/base.apk
$ exit
$ adb pull /data/app/com.vendor.game.v1-1/base.apk

Then I downloaded the latest version of jadx, a decompiler for Android applications and ran jadx-gui base.apk.

For a full hour, I tried to understand the class structure of that game. It turned out to be written in Lua, and utilized the Corona platform. This means that the classes were mostly platform boilerplate code and huge in numbers.

I failed to locate the actual Lua code, and the only code related to paying/unlocking I found was MyLicenseCheckerCallback::translateResponse() which returns Licensed if the user has bought the game.

I was lost, turned back to the ADB shell and simply listed all the files related to that package:

$ find / -type d -name com.vendor.game.v1
[...]
/mnt/expand/.../user/0/com.vendor.game.v1
[...]

And in this directory, I found app_data/game_options.dat, which was an INI-style file with key-value pairs. One of the lines was paid=0. After changing the value to 1 with nano on the phone's shell and starting the game, the "Buy full version" button was gone. Advertisements also do not show up anymore.

Great success!

com.vendor.game.v2

Version 2 of the game is the one I am actually interested in. It runs on my OUYA gaming console, whose servers (and store) have been bought by some chinese company 3 years ago, and which I expect to be shut off this or next year.

Buying the game in the OUYA store thus was not something that I would do, given that the money would be gone - and the game unplayable, or at least the purchase not recoverable after reinstallation - when the servers are offline.

I even e-mailed the game's creator and asked if I could get a non-DRMed version in exchange for money, but he had re-installed his work computer and the Android development environment was not available now. I was on my own.

Configuration

After the successful unlocking of v1, I followed the same path and inspected the settings file that I found at

/data/data/com.vendor.game.v2/shared_prefs/MonkeyGame.xml

That XML file consisted of a single tag <string name=".monkeystate"> whose contents were again INI-style. The keys were no full words, but only one or two letters long. One of them was P=f.

f as in "false", because other values were simply t. So I changed the f to t (this time with sed, because the OUYA did not have nano installed), started the game and .. it was still locked. The t in the config file had mysteriously turned back to f.

Code

So something in the app detected that I did not actually buy the game, and I had to find it. Again I fired up jadx-gui and looked for looked for keywords like "buy", "full", "pay", "paid" and so.

The first match confirmed that the "P" setting really means "paid":

class c_GameNameData {
    public final boolean p_IsFullVersion() {
        return p_GetBool("P");
    }
}

Now that I knew this I could look for places in which this configuration key is set, and found

class c_RestorePurchaseCompleteCallback {
    ...
    p_Set5("P", false);
}

That "restore purchases" process was started in the main menu:

class c_MenuState {
    public final void p_Activate() {
        //...
        if (!this.m_hasCheckedForRestore) {
            this.m_hasCheckedForRestore = true;
            p_RestorePurchases();
        }
        //...
    }
 
    public final void p_RestorePurchases() {
        bb_icemonkey.g_eng.p_SetRenderLayerActiveAndVisible("25", false, true);
        bb_icemonkey.g_eng.m_store.p_GetProductsAsync(this.m_restorePurchaseComplete);
    }
}

When the main menu is activated (game started), it tries to fetch the purchases to be able to "restore" them. Every. Single. Start. And when the list of purchases did not include the app ID, then it would set the "paid" flag to false.

It took me some time until I realized that the exception handler in the c_RestorePurchaseCompleteCallback did not change P at all.

So I quit the game, set P=t in the config file, pulled the network cable (luckily I don't use WiFi!), started the game and .. could play all the worlds.

Even greater success!

Fin

I did contact the developer and asked for his bank account number, so that I can pay for the games - I don't want to steal, just be sure that no server shutdown can take the games away once I paid.

Written by Christian Weiske.

Comments? Please send an e-mail.