├── LICENSE ├── ToDoList ├── encryptor.py ├── decryptor.py ├── FindEncryptionMethod ├── README.md ├── decrypt.cs └── Wallet.json comparison /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 @Noobgamer0111 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /ToDoList: -------------------------------------------------------------------------------- 1 | This is just a list of things that I aim to do with this project. 2 | 3 | 1) Edit the manifest file to set the apk into a debuggable state. 4 | 2) Use Android Studio or equivalent Android debugging application to scan which files and their locations are being called. 5 | 3) Extract encryption keys. 6 | 4) Develop CLI application that can automate conversion of a user's JSON's data to plain/human-readable text. (I have no coding skills, so someone else can do this lol) 7 | 8 | Potential issues with this method: 9 | 10 | 1 - Currently, the game can operate completely normally in an offline state, as it stores the encryption key on the user's internal disk drive. 11 | 12 | However, if the encryption key was made dynamic in any form or even worse, online-only, this project will be made ineffective. 13 | 14 | Example: If the game used a rolling-code system, where the client will use a key and a piece that increases by one, while the system stores the current and future keys. 15 | 16 | Let's say the current key is "N#8e&N&9rP$15C" with a SHA-1 hash of 7fce7dfaf4d31700b6cac431ca93a5c03fcb1b35, then the new key is N#8e&N&9rP$15C + D = N#8e&N&9rP$15D, hence the SHA-1 hash would be: 4c73a7808839eef2ee6e993d09014c242c603354. 17 | 18 | This would take a long time to figure out the original key, and to generate possible combinations. 19 | 20 | 2 - Since the game runs in Unity, the game's code is difficult to examine. -------------------------------------------------------------------------------- /encryptor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from base64 import b64encode 4 | from Crypto.Cipher import AES 5 | 6 | 7 | KEY = bytes( 8 | [ 9 | 57, 114, 107, 120, 67, 80, 108, 106, 83, 10 | 77, 49, 71, 86, 81, 104, 87, 119, 49, 114, 11 | 49, 114, 75, 79, 72, 71, 99, 99, 98, 50, 105, 74, 53 12 | ] 13 | ) 14 | 15 | SKIPPED_FILES = [ 16 | "profile_flag.json", 17 | ] 18 | 19 | 20 | def encrypt(opentext: str) -> str: 21 | aes = AES.new(KEY, AES.MODE_CBC, bytes(16)) 22 | ciphertext = aes.encrypt(opentext.encode()) 23 | buffer = b64encode(ciphertext) 24 | return buffer.decode() 25 | 26 | 27 | def validate_json(json_data): 28 | if json_data.get("encrypted", False): 29 | return False 30 | if "data" not in json_data: 31 | return False 32 | return True 33 | 34 | 35 | def main(): 36 | for dirpath, dirnames, filenames in os.walk("./profile"): 37 | for filename in filenames: 38 | with open(os.path.join(dirpath, filename), "r") as file: 39 | if filename in SKIPPED_FILES: 40 | continue 41 | json_data = json.loads(file.read()) 42 | if not validate_json(json_data): 43 | continue 44 | json_data["data"] = encrypt(json_data["data"]) 45 | 46 | with open(os.path.join(dirpath, filename), "w") as file: 47 | if json_data.get("encrypted"): 48 | json_data["encrypted"] = True 49 | file.write(json.dumps(json_data)) 50 | 51 | 52 | if __name__ == '__main__': 53 | main() 54 | -------------------------------------------------------------------------------- /decryptor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from base64 import b64decode 4 | from Crypto.Cipher import AES 5 | 6 | 7 | KEY = bytes( 8 | [ 9 | 57, 114, 107, 120, 67, 80, 108, 106, 83, 10 | 77, 49, 71, 86, 81, 104, 87, 119, 49, 114, 11 | 49, 114, 75, 79, 72, 71, 99, 99, 98, 50, 105, 74, 53 12 | ] 13 | ) 14 | 15 | SKIPPED_FILES = [ 16 | "profile_flag.json", 17 | ] 18 | 19 | 20 | def decrypt(ciphertext: str) -> str: 21 | aes = AES.new(KEY, AES.MODE_CBC, bytes(16)) 22 | buffer = b64decode(ciphertext) 23 | decrypted_data = aes.decrypt(buffer) 24 | return decrypted_data.decode() 25 | 26 | 27 | def validate_json(json_data): 28 | if not json_data.get("encrypted", True): 29 | return False 30 | if "data" not in json_data: 31 | return False 32 | if not json_data["data"].startswith("IegY21"): 33 | return False 34 | return True 35 | 36 | 37 | def main(): 38 | for dirpath, dirnames, filenames in os.walk("./profile"): 39 | for filename in filenames: 40 | with open(os.path.join(dirpath, filename), "r") as file: 41 | if filename in SKIPPED_FILES: 42 | continue 43 | json_data = json.loads(file.read()) 44 | if not validate_json(json_data): 45 | continue 46 | json_data["data"] = decrypt(json_data["data"]) 47 | 48 | with open(os.path.join(dirpath, filename), "w") as file: 49 | if json_data.get("encrypted"): 50 | json_data["encrypted"] = False 51 | file.write(json.dumps(json_data)) 52 | 53 | 54 | if __name__ == '__main__': 55 | main() 56 | -------------------------------------------------------------------------------- /FindEncryptionMethod: -------------------------------------------------------------------------------- 1 | In this file, we will attempt to figure out which encryption method is used. 2 | 3 | In the actual SS application, the content is decrypted once the application is running, and rendering on the phone/tablet screen. 4 | This likely means that the program has to have the decryption key on disk, and is also available even if the device is offline (i.e. no Internet connection). 5 | 6 | In Line 4, I meant to say that the decryption key is loaded from storage into memory, and readily decrypts the relevant *.json files when the user makes a call to that file e.g. user buys a bundle for 20 keys, and the wallet.json is updated to reduce the keys value by 20. When the app is closed, the jsons seem to be encrypted again. 7 | 8 | It is imperative that the encryption method is identified and can be reliably proven to decrypt the relevant jsons. 9 | 10 | List of possible locations (v3.1.1 on an unrooted Samsung Galaxy S9): 11 | Chili 12 | Auth 13 | 14 | Other plans: 15 | Create debuggable version of app 16 | Run in emulator 17 | Find memory dumper app 18 | Export to find key. 19 | Build and release decryption software. 20 | 21 | Analysis so far: 22 | Looking at supplied Unity documentation on encryption of game data strings, there are a few hard-to-crack-in-reasonable-time methods used. 23 | 24 | The most common is Base64(closer to obfuscation, but is trivial to decode), XOR (not used here, it is easy to crack), AES-128/192/256 bit, Blowfish, or RSA. 25 | 26 | Encryption methods we can ignore: 27 | - Base64 (cipher text was not decoded into human-readable) 28 | - XOR (ciphertext would have been in binary) 29 | - RSA (a public key and private key makes no sense for this simple data) 30 | - AES (AES performance decreases exponentially as data size gets bigger) 31 | 32 | It is most likely symmetric encryption. 33 | Blowfish is the fastest decryption method out there, yet AES is hardware-accelerated, and most modern mobile CPUs do support AES encryption. 34 | 35 | Most likely: AES encryption. 36 | Based on previous discussions, we've found that the key is, "C38FB23A402222A0C17D34A92F971D1F", however, the IV has not been found. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SubwaySurfersDecrypted 2 | Figuring out the new encryption used for the Subway Surfers profile *.jsons. 3 | This is basically an attempt to decrypt/decipher the new Version 3 of the profile *.json files stored on the Android version of Subway Surfers. 4 | 5 | Why? Previously, the *.json files' contents used to store the user's game data in plaintext (i.e. what you see here), yet with the September 2022 update (v3.1.0), this content was encrypted, making it temporarily impossible for users with JSON editors to change out the values. 6 | 7 | This encryption of previously-accessible content was seemingly caused by the explosion of Tiktoks and Youtube videos during July-September 2022 that provided live examples of editing JSON files to exploit in-game currency. However, this method of editing JSON files predictably angered Kiloo (publisher), given that the sale of Coins and Keys via microtransactions are the sole two sources of income for this game. 8 | 9 | AIM: To successfully decrypt the json contents, and to be able to release tools to do so. 10 | 11 | It is not expected that this project will be successful in any form, and I am expecting a Cease & Desist notice if Kiloo/SYBO took action against me. 12 | 13 | Update: (29/01/2023) I have not forgotten this project. This project is tedious as my machines are both old, and weak. I'm planning to get an Android emulator running on my Ubuntu laptop, so it can done more easily. 14 | 15 | The current plan is to locate the initialisation vector (IV) and confirm if any keys found are actually correct. 16 | 17 | If only kids on TikTok could actually STFU about game modification, and piracy, this project would not have to exist. 18 | 19 | Update (26/05/2023): 20 | 21 | Nothing much has changed with this project. Currently, the most effective tactic available (the HerrErde method) is to generate a JSON in the original format, and add the intended values. After that, the new JSON is copied into the target folder to be replaced. 22 | 23 | When this new JSON is inserted, it will be re-formatted to the V3 style and encrypted upon first load of the SS application. 24 | 25 | Additionally, we need more people searching for the key location. I'm suspecting it's hidden to the user, but may be visible using a rooted Android device/emulator. Some people have claimed that they've cracked it, but there is no major breakthrough so far. 26 | 27 | Discord Server: https://discord.gg/NAXbNwKK2V -------------------------------------------------------------------------------- /decrypt.cs: -------------------------------------------------------------------------------- 1 | // https://dotnetfiddle.net/b5Da7A 2 | using System; 3 | using System.IO; 4 | using System.Security.Cryptography; 5 | 6 | for (;;) 7 | { 8 | var input = Console.ReadLine() ?? throw new ArgumentNullException(); 9 | //"IegY21/091yYehMIQUAncP9X1ZysdCXyeguJZXrk4dtBKa/lCTaC2RzTS5ZmyV7qghKo/BE3mSVFM2EMKXx+QFYSFGFdJIqci0ClgJlMT/rCuT4oKcm9LcwVeyXji8fJNHUrudK/eWUDwqPGDLYzkWbGFebxmW/sMpi5nHHQB9h/Kyn4LM74FDv4CcyG/fi/JVWQ0Iad3qmR0nr6Rh1uUwyhzzOQ3V3Uww8TjbHF/CEzK5Jg4ucC5t2NnVxuVf4jF3lyX7ZC5yArZuOob1qwTMHavdo30jtoxeWFiLwnHrFhII4laUuKj750tAq0lUQcFTKEfqfEIcbopZPnbXXBlZfga211H+uQ/IL+DRXt2Webr06DK90NvnsroPoyAWE+57v0hjcWQaoIDAPaUOvTWDQ29tuxK9G9WeW+xNiUEAaKrQI+HL0OyPwK7qwm37evIuU9CMyvCFZ11xILE/h1LwZs9RRPrn2VvmAhwwZdAvaMeUy80tF3set8nxKNjY2SCfe0tI2GUO9c12Ky1BGA+54Qphybd8hyTx7vOyvan1zCD0/cG9YS1AhqSBA/IXISPYsmTozWc60cbVAAJz8HCAlg1ELALd8K4NpDemnmKBin4GtAITTQR47f42xV7JCGe0/iIsJiGm53OzknttgT4ZD/7RmsNIwY1a4JExNDfuFeOmPT2hwoMqJmCMUcUKFyK2D+FEcvU/d0bAKm9NgjQLEjKZ3NQr8KX553GH3dbRzQUaz4tCCtvhZEglEuHsxmvMCkC8AewwwfQ6CO936k9dLLPeVM+AL9pRLWzkW6q6Z0i0FBufwqOhs9oicjFQPcn5/C9V4HgT3XR2Sa98E3wBT9ESOAKuj1Cu0vvG/rslzwdv51YHyQxeavlTsPO1LPvbSMfWnqpLXiUSAndFNaIgX/fYYxOZI+pB3Fq6GBH1za9D5yXN5e/JE1s1n01+qUW+bhw6tJcpGFHpPescQ0GhaTrIcoloZVqKAxerEd+QVsOhjclB8URn41T85Ilmmqd8qku/GB/3iCanKWIwK7iB0kOgQGDedVLOzMqjegiNFcPtjbGY8EUuh8Hcjz9sMmkNuflXJtasJU5JTYgYnK19i/2o/OOdvaj7UNG5byNu/IFRbUlXxtlyhuERwcy0L1wSEr/UgLNIE/kBgea9Ni2CIJcT7sRtwbPvSGkE7bw/ziNFj4ltsydsSg7wwUylqDE5Gr/IPKxZvScRUkjpQvT6q/xBmc/OapoL9itCf5BTw9kgvIHlGnOOYXTpWErwv4b7+IYcjoBIsQFsh1bMkxRRFMhWtXwZHm5QQCihMo7AF0d4BWUsruTRjCe2fQbCMCOfH2+DFE+gvVG4jnY/xxxx5qXb7FGWUuTX4sgiaGpPAOUc+cUsWgBtgBg3gIWQfPUhAk0d4u8SYdCuePDYVvB+SNPS9erwKPiBkCI8XbVlKDsWWvAHjw1/sFndlAmCpqf3f51RDjppzW/V3X6Wtfaw8zWrrOEyx5aUsxdFQeuXeZNdnyuxhVayUNCCKeHdZZ/5mLBNRDS9/mJxtHink9yGhf0TYPXgh97K4qGUTDPXfFui6gx1trsP/WlYNmzoM7vUKVALeGayooNSVmgidYIGPgiu70BQ/MwP4adSvirzUtgDPK5Y6cJ69MRLAYHNaMvSDPu7Tk9rR5xRwVADw/KwDBmXOFIgCYB5sMmbIy1lBFuv2TxZ9ufSIyrNG0xm4C"; 10 | var aes = Aes.Create(); 11 | aes.Key = new byte[] {57,114,107,120,67,80,108,106,83,77,49,71,86,81,104,87,119,49,114,49,114,75,79,72,71,99,99,98,50,105,74,53}; 12 | aes.IV = new byte[16]; 13 | 14 | try 15 | { 16 | byte[] buffer = Convert.FromBase64String(input); 17 | MemoryStream memoryStream = new MemoryStream(buffer); 18 | CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Read, false); 19 | StreamReader streamReader = new StreamReader(cryptoStream); 20 | 21 | Console.ForegroundColor = ConsoleColor.Green; 22 | Console.WriteLine(streamReader.ReadToEnd()); 23 | Console.ResetColor(); 24 | } 25 | catch (Exception ex) 26 | { 27 | Console.ForegroundColor = ConsoleColor.Red; 28 | Console.WriteLine(ex.Message); 29 | Console.ResetColor(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Wallet.json comparison: -------------------------------------------------------------------------------- 1 | # This is a sample of a plaintext and encrypted Wallet.json file. It is normally stored here: [Your Phone Name]\Phone\Android\data\com.kiloo.subwaysurf\files\profile\wallet.json. The plaintext and the encrypted are NOT the same. 2 | 3 | # Plaintext: (from: https://github.com/HerrErde/SubwayBooster/blob/main/Android/data/com.kiloo.subwaysurf/files/profile/wallet.json) 4 | { 5 | "version": 2, 6 | "data": "{\"lastSaved\":\"2022-04-28T11:37:59.056234Z\",\"patchVersion\":0,\"currencies\":{\"3\":{\"value\":100000038,\"expirationType\":0},\"2\":{\"value\":99999666,\"expirationType\":0},\"1\":{\"value\":99846374,\"expirationType\":0},\"5\":{\"value\":100000009,\"expirationType\":0},\"4\":{\"value\":100000017,\"expirationType\":0},\"20\":{\"value\":100000000,\"expirationType\":0},\"23\":{\"value\":0,\"expirationType\":0},\"22\":{\"value\":0,\"expirationType\":0},\"24\":{\"value\":0,\"expirationType\":0},\"21\":{\"value\":0,\"expirationType\":0},\"6\":{\"value\":100001700,\"expirationValue\":1652651999999,\"expirationType\":1}},\"lootboxQueue\":{\"unopenedLootboxes\":{\"0\":[],\"2\":[]}},\"currencyAllowedInRun\":{\"5\":true,\"4\":true},\"lootBoxesOpened\":{\"mini_mystery_box\":3,\"mystery_box\":7,\"token_box\":16,\"super_mystery_box\":5},\"ownedOnlyBuyOnceProducts\":[\"free_token_box\",\"multiplier_pack\",\"speed_crew\"]}" 7 | } 8 | Encrypted: (from my own copy) 9 | {"version":3,"data":"IegY21/091yYehMIQUAncP9X1ZysdCXyeguJZXrk4dtBKa/lCTaC2RzTS5ZmyV7qghKo/BE3mSVFM2EMKXx+QFYSFGFdJIqci0ClgJlMT/rCuT4oKcm9LcwVeyXji8fJNHUrudK/eWUDwqPGDLYzkWbGFebxmW/sMpi5nHHQB9h/Kyn4LM74FDv4CcyG/fi/JVWQ0Iad3qmR0nr6Rh1uUwyhzzOQ3V3Uww8TjbHF/CEzK5Jg4ucC5t2NnVxuVf4jF3lyX7ZC5yArZuOob1qwTMHavdo30jtoxeWFiLwnHrFhII4laUuKj750tAq0lUQcFTKEfqfEIcbopZPnbXXBlZfga211H+uQ/IL+DRXt2Webr06DK90NvnsroPoyAWE+57v0hjcWQaoIDAPaUOvTWDQ29tuxK9G9WeW+xNiUEAaKrQI+HL0OyPwK7qwm37evIuU9CMyvCFZ11xILE/h1LwZs9RRPrn2VvmAhwwZdAvaMeUy80tF3set8nxKNjY2SCfe0tI2GUO9c12Ky1BGA+54Qphybd8hyTx7vOyvan1zCD0/cG9YS1AhqSBA/IXISPYsmTozWc60cbVAAJz8HCAlg1ELALd8K4NpDemnmKBin4GtAITTQR47f42xV7JCGe0/iIsJiGm53OzknttgT4ZD/7RmsNIwY1a4JExNDfuFeOmPT2hwoMqJmCMUcUKFyK2D+FEcvU/d0bAKm9NgjQLEjKZ3NQr8KX553GH3dbRzQUaz4tCCtvhZEglEuHsxmvMCkC8AewwwfQ6CO936k9dLLPeVM+AL9pRLWzkW6q6Z0i0FBufwqOhs9oicjFQPcn5/C9V4HgT3XR2Sa98E3wBT9ESOAKuj1Cu0vvG/rslzwdv51YHyQxeavlTsPO1LPvbSMfWnqpLXiUSAndFNaIgX/fYYxOZI+pB3Fq6GBH1za9D5yXN5e/JE1s1n01+qUW+bhw6tJcpGFHpPescQ0GhaTrIcoloZVqKAxerEd+QVsOhjclB8URn41T85Ilmmqd8qku/GB/3iCanKWIwK7iB0kOgQGDedVLOzMqjegiNFcPtjbGY8EUuh8Hcjz9sMmkNuflXJtasJU5JTYgYnK19i/2o/OOdvaj7UNG5byNu/IFRbUlXxtlyhuERwcy0L1wSEr/UgLNIE/kBgea9Ni2CIJcT7sRtwbPvSGkE7bw/ziNFj4ltsydsSg7wwUylqDE5Gr/IPKxZvScRUkjpQvT6q/xBmc/OapoL9itCf5BTw9kgvIHlGnOOYXTpWErwv4b7+IYcjoBIsQFsh1bMkxRRFMhWtXwZHm5QQCihMo7AF0d4BWUsruTRjCe2fQbCMCOfH2+DFE+gvVG4jnY/xxxx5qXb7FGWUuTX4sgiaGpPAOUc+cUsWgBtgBg3gIWQfPUhAk0d4u8SYdCuePDYVvB+SNPS9erwKPiBkCI8XbVlKDsWWvAHjw1/sFndlAmCpqf3f51RDjppzW/V3X6Wtfaw8zWrrOEyx5aUsxdFQeuXeZNdnyuxhVayUNCCKeHdZZ/5mLBNRDS9/mJxtHink9yGhf0TYPXgh97K4qGUTDPXfFui6gx1trsP/WlYNmzoM7vUKVALeGayooNSVmgidYIGPgiu70BQ/MwP4adSvirzUtgDPK5Y6cJ69MRLAYHNaMvSDPu7Tk9rR5xRwVADw/KwDBmXOFIgCYB5sMmbIy1lBFuv2TxZ9ufSIyrNG0xm4C","encrypted":true} 10 | 11 | # Things that I've noticed: 12 | # - The encrypted string always starts with "IegY21/091yYehMIQUAnc" 13 | # - Potentially means that the same encryption key is used all of the json's stored in profile. 14 | # - Find encryption key + know what data is supposed to look like (human readable) = operation.success 15 | --------------------------------------------------------------------------------