About the game
This is a simple game (that I won’t be naming just in case, I don’t want to get in any trouble) that consists in popping balloons. Each balloon popped adds into a currency that can be used to unlock stuff (more balloons, wallpapers and stuff like that). As seen in the video, the balloons appear very slowly, so we’ll find a quicker way to obtain currency.
Shared Preferences
Before doing any work on an app it is usually worth it to check which information is stored on the Shared Preferences. From the Android documentation:
A SharedPreferences object points to a file containing key-value pairs and provides simple methods to read and write them. Each SharedPreferences file is managed by the framework and can be private or shared.
Ultimately it is a simple clear text XML file stored in the Android File system, that is easily accessible using adb
.
OnePlus5T:/data/data/com.turner.jerrysgame/shared_prefs # ls -la
total 40
drwxrwx--x 2 u0_a226 u0_a226 4096 2021-01-04 10:39 .
drwx------ 5 u0_a226 u0_a226 4096 2021-01-04 10:35 ..
-rw-rw---- 1 u0_a226 u0_a226 1694 2021-01-04 10:39 APP_MEASUREMENT_CACHE.xml
-rw-rw---- 1 u0_a226 u0_a226 2284 2021-01-04 10:39 com.turner.jerrysgame.v2.playerprefs.xml
-rw-rw---- 1 u0_a226 u0_a226 354 2021-01-04 10:35 embryo.xml
Here is a dump of the Shared preferences file of this game:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<int name="GAME_SAVE_DATA_balloonCurrencySpentKey" value="0" />
<int name="Screenmanager%20Resolution%20Height" value="2160" />
<string name="unity.player_sessionid">XXXXXXXXXXXXXXXXXXXX</string>
<int name="GAME_SAVE_DATA_numGamesPlayedKey" value="3" />
<float name="GAME_SAVE_DATA_midSaveGameTimeKey" value="0.0" />
<string name="GAME_SAVE_DATA_gameItemPopCountKey">ARUAAAASAAAACgAAABIAAAAS
AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAQAAAA%3D%3D</string>
<int name="Screenmanager%20Resolution%20Width" value="1080" />
<int name="GAME_SAVE_DATA_numAppSessionsKey" value="2" />
<int name="GAME_SAVE_DATA_mainMenuPlaySelectedKey" value="0" />
<int name="GAME_SAVE_DATA_storeIntroDisplayedKey" value="1" />
<int name="GAME_SAVE_DATA_midSaveGameStateKey" value="0" />
<int name="GAME_SAVE_DATA_scoreYetToBeSubmittedKey" value="43" />
<int name="GAME_SAVE_DATA_midSaveScoreAchievementIndex" value="0" />
<int name="GAME_SAVE_DATA_balloonCurrencyKey" value="51" />
<int name="GAME_SAVE_DATA_didSaveMidGameKey" value="0" />
<int name="GAME_SAVE_DATA_totalBalloonsEarnedKey" value="56" />
<string name="GAME_SAVE_DATA_unlockedStoreItemsKey">AhYAAAAAAAE%3D</string>
<int name="__UNITY_PLAYERPREFS_VERSION__" value="1" />
<int name="GAME_SAVE_DATA_midSaveBalloonsPoppedKey" value="0" />
<int name="GAME_SAVE_DATA_firstRunCompleteKey" value="1" />
<int name="GAME_SAVE_DATA_boostsFeatureUnlockedKey" value="1" />
<int name="Screenmanager%20Fullscreen%20mode" value="-1" />
<int name="GAME_SAVE_DATA_unlockAllPurchasedKey" value="0" />
<int name="GAME_SAVE_DATA_boostIntroDisplayedKey" value="0" />
<string name="GAME_SAVE_DATA_loadedBoostsKey">AhYAAAAAAAA%3D</string>
<int name="GAME_SAVE_DATA_isSoundtrackEnabledKey" value="1" />
<string name="GAME_SAVE_DATA_equippedStoreItemsKey">AhYAAAAAAAA%3D</string>
<int name="GAME_SAVE_DATA_isSoundFXEnabledKey" value="1" />
<float name="GAME_SAVE_DATA_midSaveTimeLeftKey" value="0.0" />
<string name="GAME_SAVE_DATA_startingBoostsKey">AhYAAAAAAAA%3D</string>
<int name="GAME_SAVE_DATA_saveDataVersionNumberKey" value="0" />
</map>
As we can see there are several entries that seem interesting for our purposes, specifically
<int name="GAME_SAVE_DATA_balloonCurrencyKey" value="51" />
<int name="GAME_SAVE_DATA_totalBalloonsEarnedKey" value="56" />
The are values that seem to contain the in-game items that we have unlocked stored using a base64 encoded byte array, but we’ll look into this another day.
Let’s modify this value, relaunch the app and see what happens
<int name="GAME_SAVE_DATA_balloonCurrencyKey" value="995" />
<int name="GAME_SAVE_DATA_totalBalloonsEarnedKey" value="1004" />
Note how the in-game menu has an in-app purchase option to get 100k ballons for some real world money
Mission successful! But this was a quite easy hack and not really a challenge… We’ll try to modify the game to obtain 9999 points for each balloon popped instead of one.
Reversing Unity Games
As most Android Games out there this game is programmed using the Unity Game Engine. This means that when reversing the game we won’t find the game logic in smali
files since it is programed inside Unity and not java/kotlin.
As we can see when decompiling, there are very few java classes, and it seems mostly boilerplate code.
Extracting the DLL from the APK file
To extract all the contents of the apk file we can use apktool
jserrats@glados:~/reversing-android/com.turner.jerrysgame$ apktool d base.apk
I: Using Apktool 2.4.0-dirty on base.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/jserrats/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
Unity stores the logic in dll
files stored under /base/assets/bin/Data/Managed
. In particular, we are interested in Assembly-CSharp.dll
jserrats@glados:~/reversing-android/com.turner.jerrysgame/base/assets/bin/Data/Managed$ lsl
total 17M
-rw-rw-r-- 1 jserrats jserrats 591K Jan 4 11:28 Assembly-CSharp.dll
Modifying the DLL file
In order to work with the DLL file I’ll be using dnSpy, which it is an awesome tool (but only works on windows :/ ). Also keep in mind that you’ll need to open all the DLL files in the project, or we won’t be able to recompile the DLL since there will be missing dependencies.
We can see that inside Assembly-CSharp.dll
there are all the classes that contain the game logic.
After browsing for some time all the classes, we seem to find the class that adds the score for each balloon popped.
Doing right click and selecting Edit C# Method
we can open a window where we can edit code. Doing this we are decompiling the DLL into C#, modifying the logic and then recompiling again into a DLL. Another option is to directly edit the DLL at CLI level.
After modifying the line and hitting Compile
we get several compilation errors.. It seems that the debugging attributes are not valid here. Rather than trying to fix this, I have opted for the quickest way, which is to remove all the conflictive debugging attributes (we are not debugging this application anyway) and proceed.
Then we can save the DLL using the option Save Module
Repackaging the APK
With the modified DLL in our hands, we can proceed into repackaging the APK so it can be installed. To do so we place the modified DLL into the original folder generated by Apktool.
jserrats@glados:~/reversing-android/com.turner.jerrysgame/base2$ cp ~/vmshared/Assembly-CSharp.dll assets/bin/Data/Managed/Assembly-CSharp.dll
Then we can repackage it again using the build
option:
jserrats@glados:~/reversing-android/com.turner.jerrysgame$ apktool b base2
I: Using Apktool 2.4.0-dirty
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
W: aapt: brut.common.BrutException: brut.common.BrutException: Could not extract resource: /prebuilt/linux/aapt_64 (defaulting to $PATH binary)
I: Copying libs... (/lib)
I: Building apk file...
I: Copying unknown files/dir...
I: Built apk...
Keep in mind that when building the application the original signature from the developer is no longer valid (that is why it exists in the first place), and to install it into our device we’ll have to sign it. To do so whe can use our own self signed key.
jserrats@glados:~/reversing-android$ jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -storepass 123456 -keystore android.keystore com.turner.jerrysgame/base2/dist/base.apk android jar signed.
Warning:
The signer's certificate is self-signed.
The SHA1 algorithm specified for the -digestalg option is considered a security risk. This algorithm will be disabled in a future update.
The SHA1withRSA algorithm specified for the -sigalg option is considered a security risk. This algorithm will be disabled in a future update.
The only step left is to install the apk again using adb
. Remember to delete the original one first, since Android won’t allow the installation of a package with the same name as one already installed with a different signature.
jserrats@glados:~/reversing-android$ adb install com.turner.jerrysgame/base2/dist/base.apk
We can now beat get thousands of balloons easily in seconds.
Conclusions
When I started this experiment I was not expecting to dig into a DLL file while doing an Android reversing! It was really interesting to learn a bit about how Unity integrates with all the platforms and the possibilities of Android development. The security conclusions are obvious:
- Do not store sensitive information on SharedPreferences
- Apply some kind of obfuscation on your core game logic
This is specially important if your revenue model is based in an in-app purchase that is useless now that we have unlimited currency!