Skip to content

Instantly share code, notes, and snippets.

@unixpickle
Created November 14, 2019 20:58
Show Gist options
  • Save unixpickle/eb3898f2b72b6c1589c1be0d64ea1003 to your computer and use it in GitHub Desktop.
Save unixpickle/eb3898f2b72b6c1589c1be0d64ea1003 to your computer and use it in GitHub Desktop.
Hacking Lily's Garden

Overview

In this document, I will describe how I managed to get unlimited stars and coins in the Android game "Lily's Garden". In short, here are the steps:

  • Use the adb backup feature to extract all of the game's data
  • Extract the Android backup into a tar file
  • Modify the file which stores the number of coins and stars
  • Re-sign the coins/stars field using a reverse-engineered HMAC key
  • Convert the tar file back to an Android backup
  • Use the adb restore feature to put the new game data on the phone

Steps

To backup the game's data, run the following command with your phone plugged in and in developer mode:

$ adb backup dk.tactile.lilysgarden

This creates a file called backup.ab. To extract a tarbal from this, run this command (source):

$ dd if=backup.ab bs=1 skip=24 | python2 -c "import zlib,sys;sys.stdout.write(zlib.decompress(sys.stdin.read()))" >backup.tar

Now you want to edit the file called apps/dk.tactile.lilysgarden/sp/dk.tactile.lilysgarden.v2.playerprefs.xml inside of backup.tar. You can do this with vim, or with some other tool. I found that untarring->editing->tarring did not work, probably because of permissions.

Inside of dk.tactile.lilysgarden.v2.playerprefs.xml, look for a line starting with:

<string name="4b79e50f33d58cde3869c24d8dd0102c">

This contains most of the game's state, as far as I can tell. The tag contains a bunch of URL-encoded JSON, followed by a 32-byte HMACSHA256 checksum. Near the end of the JSON should be something like this:

"in":{"it":{"BoosterBomb":0,"BoosterMagic":0,"BoosterRocket":0,"BoosterClearSingle":1,"Life":0,"Coin":590,"star":0,"BoosterClearCross":0,"UnlimitedLives":0}}

Here, you can change Coin and star as much as you like. However, you must re-generate the checksum as follows:

checksum = hmac_sha256(key="9e30c2ab-e2d6-47bf-9999-3371f14bdac0", data=<json>+"UserSettingsManagerLocalPrivateUserSettings")

Where <json> is the new JSON data (not URL-encoded) and + is string concatenation.

Now that you have modified the backup tarbal, you can create a new backup, backup_new.ab file as follows:

$ dd if=backup.ab bs=1 count=24 >backup_new.ab
$ cat backup.tar | python2 -c "import zlib,sys;sys.stdout.write(zlib.compress(sys.stdin.read()))" >>backup_new.ab

Finally, upload this backup file to your phone like so:

$ adb restore backup_new.ab

How I did it

First, I extracted the apk from my phone using adb, and then I extracted it with apktool. I quickly noticed that the game uses Unity, and that most of the source code was in a file called armeabi-v7a/libil2cpp.so. Upon discovering that libil2cpp.so had no symbols, I tried using adb backup to inspect the app's data directly.

Using the data extracted from adb backup, I grep'd for keywords like "coin" and "star", and eventually found where the game stored these quantities. However, I could not modify these fields as they were signed. Modifying any values resulted in the game being effectively reset to the starting point.

Since the backup data was signed, I knew I needed to look deeper at the code. I found Il2CppInspector, and used it to get symbols. With symbols, I found a few things of note:

public class TactilePlayerPrefs // TypeDefIndex: 5728
{
  ...
	public static void SetSignedString(string name, string value); // 0x00B2060C
	public static void SetSecuredString(string name, string value); // 0x00B209F0
	public static string GetString(string name); // 0x00B20A98
	public static string GetString(string name, string defaultValue); // 0x00B20B48
	public static string GetSignedString(string name, string defaultValue); // 0x00B20C00
	public static string GetSecuredString(string name, string defaultValue); // 0x00B20EC8
  ...
}

public static class TactilePrefsExtensions // TypeDefIndex: 5730
{
	// Extension methods
	public static string TactilePlayerPrefsHash(this string value, string salt); // 0x00B20724
	public static byte[] TactilePlayerPrefsCryptKeyHash(this string value); // 0x00B219DC
}

I used objdump to dump the assembly for libil2cpp.so, and found the corresponding blocks of instructions for these methods. It turned out that SetSecuredString() simply called SetSignedString, which md5-hashes the key name and signs the value using a hash from TactilePlayerPrefsHash. Looking at the disassembly for TactilePlayerPrefsHash() (which I include below), I inferred the following original code:

key = <something from a static variable after a lot of indirection>
x = HMACSHA256(Encoding.UTF8.GetBytes(key))
y = x.ComputeHash(Encoding.UTF8.GetBytes(value + salt))
z = StringBuilder()
foreach (byte b in y) {
    z.append(b.ToString("x2"))
}
return z.ToString()

The only missing piece was key. It seemed that the key was stored somewhere in the .bss section (for static variables). This looked bad, since it could be generated at runtime in some complex way. However, I was hopeful that the key was just sitting inside of the binary somewhere, or in global-metadata.dat (where libil2cpp.so stores many of its strings and symbols). Thus, I wrote a little program (keysearch.cs below) that tried every short substring in the entire binary as the key. The program worked, and I found the key of 9e30c2ab-e2d6-47bf-9999-3371f14bdac0!

// Run with a grep command like this:
// mono-csc keysearch.cs
// mono keysearch.exe | grep -i 8a-fc-37-ef-22-65-bb-57-3e-8b-11
using System;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.IO;
public class Program {
public static void Main() {
Encoding ascii = Encoding.UTF8;
Byte[] data = File.ReadAllBytes("assets/bin/Data/Managed/Metadata/global-metadata.dat");
String salt = "UserSettingsManagerLocalPublicUserSettings";
String body = "{\"_settingsVersion\":0}";
String combined = body + salt;
for (int keyLen = 1; keyLen <= 0x60; keyLen++) {
Console.Error.WriteLine("working on keys of length " + keyLen);
Byte[] arrCopy = new Byte[keyLen];
for (int i = 0; i < data.Length-keyLen; i++) {
Array.Copy(data, i, arrCopy, 0, keyLen);
HMACSHA256 hmac = new HMACSHA256(arrCopy);
Console.WriteLine(i + ": " + BitConverter.ToString(hmac.ComputeHash(ascii.GetBytes(combined))) + " (key: " + BitConverter.ToString(arrCopy) + ")" );
}
}
}
}
; Start of TactilePlayerPrefsHash
b20728: e28db018 add fp, sp, #24
b2072c: e24dd008 sub sp, sp, #8
b20730: e59f027c ldr r0, [pc, #636] ; b209b4 <dladdr@plt+0x90b9d8>, value: 0x00a6722c
b20734: e1a08002 mov r8, r2
b20738: e59f4278 ldr r4, [pc, #632] ; b209b8 <dladdr@plt+0x90b9dc>, value: 0x000936fc
b2073c: e1a05001 mov r5, r1
b20740: e08f0000 add r0, pc, r0 ; value is now 0x1587974
b20744: e0840000 add r0, r4, r0 ; value is now 0x161b070
b20748: e5d00145 ldrb r0, [r0, #325] ; 0x145
b2074c: e3500000 cmp r0, #0
b20750: 1a000008 bne b20778 <dladdr@plt+0x90b79c>
b20754: e59f0260 ldr r0, [pc, #608] ; b209bc <dladdr@plt+0x90b9e0>
b20758: e59f1260 ldr r1, [pc, #608] ; b209c0 <dladdr@plt+0x90b9e4>
b2075c: e08f6000 add r6, pc, r0
b20760: e7910006 ldr r0, [r1, r6]
b20764: e5900000 ldr r0, [r0]
b20768: eb18eb33 bl 115b43c <_ZNSt6vectorISsSaISsEE19_M_emplace_back_auxIJSsEEEvDpOT_+0x7da4>
b2076c: e0840006 add r0, r4, r6
b20770: e3a01001 mov r1, #1
b20774: e5c01145 strb r1, [r0, #325] ; 0x145
b20778: e59f0244 ldr r0, [pc, #580] ; b209c4 <dladdr@plt+0x90b9e8>
b2077c: e59f1244 ldr r1, [pc, #580] ; b209c8 <dladdr@plt+0x90b9ec>
b20780: e08f0000 add r0, pc, r0
b20784: e7910000 ldr r0, [r1, r0]
b20788: e3a01000 mov r1, #0
b2078c: e5cd1007 strb r1, [sp, #7]
b20790: e5900000 ldr r0, [r0]
b20794: e5d010b2 ldrb r1, [r0, #178] ; 0xb2
b20798: e3110001 tst r1, #1
b2079c: 0a000003 beq b207b0 <dladdr@plt+0x90b7d4>
b207a0: e5901060 ldr r1, [r0, #96] ; 0x60
b207a4: e3510000 cmp r1, #0
b207a8: 1a000000 bne b207b0 <dladdr@plt+0x90b7d4>
b207ac: eb193173 bl 116cd80 <_ZNSt6vectorISsSaISsEE19_M_emplace_back_auxIJSsEEEvDpOT_+0x196e8>
b207b0: e59f0214 ldr r0, [pc, #532] ; b209cc <dladdr@plt+0x90b9f0>, 0x00a671b0
b207b4: e3a01000 mov r1, #0
b207b8: e59f7210 ldr r7, [pc, #528] ; b209d0 <dladdr@plt+0x90b9f4>, 0x00002a98
b207bc: e08f0000 add r0, pc, r0
b207c0: e7974000 ldr r4, [r7, r0] ; read from 158a40c, 0x01603b2c
b207c4: e3a00000 mov r0, #0
b207c8: ebf32e00 bl 7ebfd0 <dladdr@plt+0x5d6ff4> ; UTF-8
b207cc: e1a06000 mov r6, r0
b207d0: e5940000 ldr r0, [r4]
b207d4: e5d010b2 ldrb r1, [r0, #178] ; 0xb2
b207d8: e3110001 tst r1, #1
b207dc: 0a000003 beq b207f0 <dladdr@plt+0x90b814>
b207e0: e5901060 ldr r1, [r0, #96] ; 0x60
b207e4: e3510000 cmp r1, #0
b207e8: 1a000000 bne b207f0 <dladdr@plt+0x90b814>
b207ec: eb193163 bl 116cd80 <_ZNSt6vectorISsSaISsEE19_M_emplace_back_auxIJSsEEEvDpOT_+0x196e8>
b207f0: e3560000 cmp r6, #0
b207f4: 0a00006b beq b209a8 <dladdr@plt+0x90b9cc>
b207f8: e59f01d4 ldr r0, [pc, #468] ; b209d4 <dladdr@plt+0x90b9f8>, value 00a6716c
b207fc: e5961000 ldr r1, [r6]
b20800: e08f4000 add r4, pc, r0
b20804: e7970004 ldr r0, [r7, r4] ; 2a98 + b20808 + a6716c = 158a40c -> 01603b2c
b20808: e5913104 ldr r3, [r1, #260] ; 0x104
b2080c: e5912108 ldr r2, [r1, #264] ; 0x108
b20810: e5900000 ldr r0, [r0] ; -> 0a0000ea
b20814: e5900050 ldr r0, [r0, #80] ; 0x50
b20818: e5901000 ldr r1, [r0]
b2081c: e1a00006 mov r0, r6
b20820: e12fff33 blx r3
b20824: e1a07000 mov r7, r0
b20828: e59f01a8 ldr r0, [pc, #424] ; b209d8 <dladdr@plt+0x90b9fc>
b2082c: e7900004 ldr r0, [r0, r4]
b20830: e5900000 ldr r0, [r0]
b20834: eb19eba2 bl 119b6c4 <_ZNSt8_Rb_treeISsSsSt9_IdentityISsESt4lessISsESaISsEE8_M_eraseEPSt13_Rb_tree_nodeISsE+0xafac>
b20838: e1a06000 mov r6, r0
b2083c: e1a01007 mov r1, r7
b20840: e3a02000 mov r2, #0
b20844: ebe94c98 bl 573aac <dladdr@plt+0x35ead0> ; HMACSHA256
b20848: e59f018c ldr r0, [pc, #396] ; b209dc <dladdr@plt+0x90ba00>
b2084c: e3a01000 mov r1, #0
b20850: e7904004 ldr r4, [r0, r4]
b20854: e3a00000 mov r0, #0
b20858: ebf32ddc bl 7ebfd0 <dladdr@plt+0x5d6ff4> ; UTF-8
b2085c: e1a07000 mov r7, r0
b20860: e5940000 ldr r0, [r4]
b20864: e5d010b2 ldrb r1, [r0, #178] ; 0xb2
b20868: e3110001 tst r1, #1
b2086c: 0a000003 beq b20880 <dladdr@plt+0x90b8a4>
b20870: e5901060 ldr r1, [r0, #96] ; 0x60
b20874: e3510000 cmp r1, #0
b20878: 1a000000 bne b20880 <dladdr@plt+0x90b8a4>
b2087c: eb19313f bl 116cd80 <_ZNSt6vectorISsSaISsEE19_M_emplace_back_auxIJSsEEEvDpOT_+0x196e8>
b20880: e3a00000 mov r0, #0
b20884: e1a01005 mov r1, r5
b20888: e1a02008 mov r2, r8
b2088c: e3a03000 mov r3, #0
b20890: ebf2c70d bl 7d24cc <dladdr@plt+0x5bd4f0> ; concat
b20894: e1a01000 mov r1, r0
b20898: e3570000 cmp r7, #0
b2089c: 0a000041 beq b209a8 <dladdr@plt+0x90b9cc>
b208a0: e5970000 ldr r0, [r7]
b208a4: e5903104 ldr r3, [r0, #260] ; 0x104
b208a8: e5902108 ldr r2, [r0, #264] ; 0x108
b208ac: e1a00007 mov r0, r7
b208b0: e12fff33 blx r3
b208b4: e1a01000 mov r1, r0
b208b8: e3560000 cmp r6, #0
b208bc: 0a000039 beq b209a8 <dladdr@plt+0x90b9cc>
b208c0: e1a00006 mov r0, r6
b208c4: e3a02000 mov r2, #0
b208c8: ebe93a8d bl 56f304 <dladdr@plt+0x35a328> ; HashAlgorithm::ComputeHash
b208cc: e1a09000 mov r9, r0
b208d0: e3590000 cmp r9, #0
b208d4: 0a000033 beq b209a8 <dladdr@plt+0x90b9cc>
b208d8: e59f0100 ldr r0, [pc, #256] ; b209e0 <dladdr@plt+0x90ba04>
b208dc: e59f1100 ldr r1, [pc, #256] ; b209e4 <dladdr@plt+0x90ba08>
b208e0: e08f0000 add r0, pc, r0
b208e4: e7910000 ldr r0, [r1, r0]
b208e8: e5900000 ldr r0, [r0]
b208ec: eb19eb74 bl 119b6c4 <_ZNSt8_Rb_treeISsSsSt9_IdentityISsESt4lessISsESaISsEE8_M_eraseEPSt13_Rb_tree_nodeISsE+0xafac>
b208f0: e1a05000 mov r5, r0
b208f4: e599000c ldr r0, [r9, #12]
b208f8: e3a02000 mov r2, #0
b208fc: e3a07000 mov r7, #0
b20900: e1a01080 lsl r1, r0, #1
b20904: e1a00005 mov r0, r5
b20908: ebf2fbb1 bl 7df7d4 <dladdr@plt+0x5ca7f8> ; StringBuilder()
b2090c: e599000c ldr r0, [r9, #12]
b20910: e3500001 cmp r0, #1
b20914: ba00001a blt b20984 <dladdr@plt+0x90b9a8>
b20918: e59f10c8 ldr r1, [pc, #200] ; b209e8 <dladdr@plt+0x90ba0c>
b2091c: e2896010 add r6, r9, #16
b20920: e59f20c4 ldr r2, [pc, #196] ; b209ec <dladdr@plt+0x90ba10>
b20924: e28d8007 add r8, sp, #7
b20928: e08f1001 add r1, pc, r1
b2092c: e7924001 ldr r4, [r2, r1]
b20930: e1500007 cmp r0, r7
b20934: 8a000001 bhi b20940 <dladdr@plt+0x90b964>
b20938: eb19afed bl 118c8f4 <_ZNSs12_S_constructIN9__gnu_cxx17__normal_iteratorIPcSt6vectorIcSaIcEEEEEES2_T_S7_RKS4_St20forward_iterator_tag+0x12a8>
b2093c: eb19ac25 bl 118b9d8 <_ZNSs12_S_constructIN9__gnu_cxx17__normal_iteratorIPcSt6vectorIcSaIcEEEEEES2_T_S7_RKS4_St20forward_iterator_tag+0x38c>
b20940: e5941000 ldr r1, [r4]
b20944: e3a02000 mov r2, #0
b20948: e7d60007 ldrb r0, [r6, r7]
b2094c: e5cd0007 strb r0, [sp, #7]
b20950: e1a00008 mov r0, r8
b20954: ebec2be6 bl 62b8f4 <dladdr@plt+0x416918> ; Byte::ToString() ?
b20958: e1a01000 mov r1, r0
b2095c: e3550000 cmp r5, #0
b20960: 0a000010 beq b209a8 <dladdr@plt+0x90b9cc>
b20964: e1a00005 mov r0, r5
b20968: e3a02000 mov r2, #0
b2096c: ebf2c92c bl 7d2e24 <dladdr@plt+0x5bde48> ; Append()
b20970: e599000c ldr r0, [r9, #12]
b20974: e2877001 add r7, r7, #1
b20978: e1570000 cmp r7, r0
b2097c: baffffeb blt b20930 <dladdr@plt+0x90b954>
b20980: ea000001 b b2098c <dladdr@plt+0x90b9b0>
b20984: e3550000 cmp r5, #0
b20988: 0a000006 beq b209a8 <dladdr@plt+0x90b9cc>
b2098c: e5950000 ldr r0, [r5]
b20990: e59020cc ldr r2, [r0, #204] ; 0xcc
b20994: e59010d0 ldr r1, [r0, #208] ; 0xd0
b20998: e1a00005 mov r0, r5
b2099c: e12fff32 blx r2
b209a0: e24bd018 sub sp, fp, #24
b209a4: e8bd8bf0 pop {r4, r5, r6, r7, r8, r9, fp, pc}
@Kskjshare
Copy link

hello,I have been studying how to modify this game for a long time . but i have any progress
there is several buttons in the top of main view such as facebook button and setting button so on .
now i want to remove one of them or both of them, how to do ? can you give me hints?
i use ida open so but i don't find any symbols of them . and how to remove a components such as label or button in U3d game
thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment