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.

Revisions

  1. unixpickle created this gist Nov 14, 2019.
    106 changes: 106 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,106 @@
    # 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](https://myridia.com/dev_posts/view/1772)):

    $ 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](https://ibotpeaches.github.io/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](https://github.com/djkaty/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`!
    161 changes: 161 additions & 0 deletions TactilePlayerPrefsHash.s
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,161 @@
    ; 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}
    30 changes: 30 additions & 0 deletions keysearch.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,30 @@
    // 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) + ")" );
    }
    }
    }
    }