├── .gitignore ├── .gitmodules ├── Dockerfile ├── LICENSE ├── README.md ├── abstract_dump.py ├── apk_analyzer.py ├── apktool.jar ├── config.example.yml ├── config.py ├── console_dump.py ├── jsonlines_dump.py ├── learnset_create_tool.py ├── lib_blacklist.txt ├── log_config.json ├── main.py ├── mongodb_dump.py ├── my_model ├── __init__.py ├── lib_string.py ├── my_string.py ├── resource_string.py └── smali_string.py ├── my_tools ├── apktool_yml_parser.py ├── getch.py ├── manifest_parser.py ├── memoized.py ├── smali_parser.py ├── strings_tool.py └── strings_xml_parser.py ├── requirements.txt └── test ├── AndroidManifest.xml ├── AndroidManifest2.xml ├── AndroidManifest3.xml ├── AndroidManifest4.xml ├── JavaKey.smali ├── __init__.py ├── test_manifest_parser.py └── test_smali_parser.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom additions 2 | .idea/ 3 | config.yml 4 | apks/* 5 | apks_decoded/* 6 | apks_analyzed/* 7 | !apks/README.md 8 | !apks_analyzed/README.md 9 | !apks_decoded/README.md 10 | /string_classification/test_results/ 11 | key_dump.json 12 | apikey.jsonl 13 | all_strings.jsonl 14 | *.lock 15 | **/__pycache__/ 16 | 17 | # Byte-compiled / optimized / DLL files 18 | __pycache__/ 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | env/ 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | .hypothesis/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # dotenv 99 | .env 100 | 101 | # virtualenv 102 | .venv 103 | venv/ 104 | ENV/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "api-key-detector"] 2 | path = api-key-detector 3 | url = https://github.com/alessandrodd/api-key-detector.git 4 | [submodule "api_key_detector"] 5 | path = api_key_detector 6 | url = https://github.com/alessandrodd/api_key_detector.git 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Pull base image. 2 | FROM ubuntu:18.04 3 | 4 | # Install. 5 | RUN \ 6 | apt-get update && \ 7 | apt-get install -y python3 && \ 8 | apt-get install -y git python3-pip default-jdk && \ 9 | git clone --recursive https://github.com/alessandrodd/apk_api_key_extractor.git && \ 10 | cd apk_api_key_extractor && \ 11 | cp config.example.yml config.yml && \ 12 | pip3 install -r requirements.txt 13 | 14 | # Add files. 15 | COPY apks /apk_api_key_extractor/apks/ 16 | 17 | # Set environment variables. 18 | ENV HOME / 19 | ENV PATH /usr/bin:$PATH 20 | 21 | # Set workdir 22 | WORKDIR /apk_api_key_extractor 23 | 24 | # Define default command. 25 | CMD python3 /apk_api_key_extractor/main.py --monitor-apks-folder 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017-2018 Alessandro Di Diego 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apk_api_key_extractor 2 | Automatically extracts API Keys from APK (Android Package) files 3 | 4 | For technical details, [check out my thesis (_Automatic extraction of API Keys from Android applications_) and, in particular, **Chapter 3 and 4** of the work.](https://goo.gl/uryZeA) 5 | 6 | The library responsible for identifying the API Keys is [a standalone project](https://github.com/alessandrodd/api_key_detector). 7 | 8 | Searches for API Keys embedded in Android String Resources, Manifest metadata, Java code (included Gradle's BuildConfig), Native code. 9 | 10 | ## Requirements 11 | 12 | - Java 7+ 13 | - Python 3.5+ 14 | - Modules in requirements.txt (use pip3 to install) 15 | ``` 16 | pip3 install -r requirements.txt 17 | ``` 18 | 19 | ## Installation 20 | 21 | ```bash 22 | $ git clone --recursive https://github.com/alessandrodd/apk_api_key_extractor.git 23 | $ cd apk_api_key_extractor 24 | $ cp config.example.yml config.yml 25 | $ pip3 install -r requirements.txt 26 | $ python3 main.py 27 | ``` 28 | 29 | ## Test 30 | ```bash 31 | $ git clone https://github.com/alessandrodd/ApiKeyTestApp.git 32 | $ cd apk_api_key_extractor 33 | $ python3 main.py --analyze-apk ../ApiKeyTestApp/apk/apikeytestapp_obfuscated.apk 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```bash 39 | usage: main.py [-h] [--debug] [--analyze-apk APK_PATH] [--monitor-apks-folder] 40 | 41 | A python program that finds API-KEYS and secrets hidden inside strings 42 | 43 | optional arguments: 44 | -h, --help show this help message and exit 45 | --debug Print debug information 46 | --analyze-apk APK_PATH 47 | Analyze a single apk to find hidden API Keys 48 | --monitor-apks-folder 49 | Monitors the configured apks folder for new apks. When 50 | a new apk is detected, the file is locked and analysis 51 | starts. 52 | ``` 53 | 54 | Let's say you want to find any API Key hidden inside a set of Android apps. 55 | 56 | Using the default settings: 57 | - Copy all the desired .apk files to the [_apks_](apks) folder 58 | - Start apk_api_key_extractor with the _monitor-apks-folder_ argument, i.e. : ```python3 main.py --monitor-apks-folder``` 59 | - Check the _apikeys.json_ file for detected API Keys 60 | - As soon as an apk is analyzed, it gets moved from the [_apks_](apks) to the [_apks_analyzed_](apks_analyzed) folder 61 | 62 | Note that this software is process-safe, meaning that you can start multiple instances of the same script without conflicts. You can also configure the _apks_ folder as a remote folder in a Network File System (NFS) to parallelize the analysis on multiple hosts. 63 | 64 | ## Run in a docker container 65 | ```bash 66 | NOTE: make sure your .APK file is in the 'apks' folder. 67 | 68 | $ git clone --recursive https://github.com/alessandrodd/apk_api_key_extractor.git 69 | $ cd apk_api_key_extractor 70 | $ docker build -t apk_key_extractor:latest . 71 | $ docker run -it apk_key_extractor:latest 72 | 73 | Rebuild your image when you've added other .apk's in your 'apks' folder. 74 | ``` 75 | 76 | ## Config File Explained 77 | ### config.yml 78 | **apks_dir** => The folder containing the .apk files to be analyzed 79 | 80 | **apks_decoded_dir** => The folder that will temporarily contain the decompiled apk files 81 | 82 | **apks_analyzed_dir** => The folder that will contain the already analyzed .apk files. Each time an APK is analyzed, it gets moved from the apks_folder to this folder 83 | 84 | **save_analyzed_apks** => If false, the .apk files gets removed instead of being moved to the apks_analyzed folder 85 | 86 | **apktool** => Path of the main [apktool](https://ibotpeaches.github.io/Apktool/) jar file, used to decode the apks 87 | 88 | **lib_blacklists** => Txt files containing names of the native libraries (one for each line) that should be ignored during the analysis 89 | 90 | **shared_object_sections** => When analyzing native libraries, all the ELF sections listed here will be searched for API Keys 91 | 92 | **dump_all_strings** => If true, the script dumps not only API Keys but also every other string found in the APK. Useful to make a dataset of common not-api-key strings that can be used to train the model itself. 93 | 94 | **dump_location** => Where to dump the API Keys found (as well as every other strin, if dump_all_strings is set to true). Possible values are console (stdout), jsonlines (text files in the [jsonlines](http://jsonlines.org/) format), mongodb. 95 | 96 | **jsonlines.dump_file** => Path of the jsonlines file where the API Keys will be dumped 97 | 98 | **jsonlines.strings_file** => Path of the jsonlines file where the every string will be dumped, if dump_all_strings is true 99 | 100 | **mongodb.name** => Used if key_dump is set to mongodb; name of the MongoDB database 101 | 102 | **mongodb.address** => Address of the MongoDB database 103 | 104 | **mongodb.port** => Port to which to contact the MongoDB database (default 27017) 105 | 106 | **mongodb.user** => MongoDB database credentials 107 | 108 | **mongodb.password** => MongoDB database credentials 109 | 110 | # Notes 111 | I'm a curious guy; if you make something cool with this and you want to tell me, drop me an email at didiego.alessandro+keyextract (domain is gmail.com) 112 | -------------------------------------------------------------------------------- /abstract_dump.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class AbstractDump(ABC): 4 | 5 | @abstractmethod 6 | def dump_apikeys(self, entries, package, version_code, version_name): 7 | pass 8 | 9 | @abstractmethod 10 | def dump_strings(self, entries): 11 | pass -------------------------------------------------------------------------------- /apk_analyzer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import glob 3 | import itertools 4 | import logging 5 | import os 6 | import subprocess 7 | 8 | import numpy as np 9 | from api_key_detector import string_classifier 10 | from api_key_detector.classifier_singleton import classifier 11 | from elftools.common.exceptions import ELFError 12 | from flufl.lock import Lock, AlreadyLockedError, TimeOutError 13 | from api_key_detector.strings_filter_singleton import s_filter 14 | 15 | import config 16 | from my_model.lib_string import LibString 17 | from my_model.resource_string import ResourceString 18 | from my_tools.apktool_yml_parser import ApktoolYmlParser 19 | from my_tools.manifest_parser import AndroidManifestXmlParser 20 | from my_tools.smali_parser import SmaliParser 21 | from my_tools.strings_tool import strings 22 | from my_tools.strings_xml_parser import AndroidStringsXmlParser 23 | 24 | LOCK_PREFIX = ".lock" 25 | logging.getLogger("flufl.lock").setLevel(logging.CRITICAL) # disable logging for lock module 26 | 27 | lib_blacklist = None 28 | if config.lib_blacklists: 29 | lib_blacklist = set() 30 | for txt in config.lib_blacklists: 31 | for line in open(txt, "r"): 32 | lib_blacklist.add(line.replace('\n', '').replace('\r', '')) 33 | 34 | 35 | class ApkAnalysisError(Exception): 36 | def __init__(self, value): 37 | self.value = value 38 | 39 | def __str__(self): 40 | return repr(self.value) 41 | 42 | 43 | def get_next_apk(apks_dir): 44 | """ 45 | Gets the first available (not-locked) apk files and locks it 46 | 47 | :param apks_dir: directory to scan 48 | :return: a tuple containing the apk's path and the locked lock 49 | :rtype: (str, lockfile.LockFile) 50 | """ 51 | try: 52 | files = os.listdir(apks_dir) 53 | except FileNotFoundError: 54 | # folder doesn't exist 55 | return None, None 56 | for f in files: 57 | if not f.endswith(".apk"): 58 | continue 59 | f = os.path.join(apks_dir, f) 60 | try: 61 | # lock file should not exist 62 | filename = f + LOCK_PREFIX 63 | 64 | lock = Lock(filename, lifetime=datetime.timedelta(seconds=6000)) # expires in 10 minutes 65 | if not lock.is_locked: 66 | lock.lock(timeout=datetime.timedelta(milliseconds=350)) 67 | if os.path.isfile(f): # the original file could be deleted in the meantime 68 | return f, lock 69 | if lock.is_locked: 70 | lock.unlock() 71 | except (AlreadyLockedError, TimeOutError): 72 | # some other process is analyzing the file; go ahead and look for another file 73 | pass 74 | return None, None 75 | 76 | 77 | def analyze_strings(mystrings): 78 | """ 79 | A list of mystrings gets classified and only the predicted API keys are returned 80 | 81 | :param mystrings: a list of mystrings to be analyzed 82 | :return: a list of valid api keys 83 | :rtype: list 84 | """ 85 | # for performance it's better to create a new list instead of removing elements from list 86 | smali_strings_filtered = [] 87 | strings_features = [] 88 | for string in mystrings: 89 | features = string_classifier.calculate_all_features(string.value) 90 | if features: 91 | features_list = list(features) 92 | smali_strings_filtered.append(string) 93 | strings_features.append(features_list) 94 | if len(strings_features) > 0: 95 | prediction = classifier.predict(np.array(strings_features)) 96 | api_keys_strings = itertools.compress(smali_strings_filtered, prediction) # basically a bitmask 97 | return api_keys_strings 98 | return [] 99 | 100 | 101 | def extract_metadata_resource(manifest_parser): 102 | metadata = manifest_parser.get_metadata() 103 | metadata_resources = [] 104 | for m in metadata: 105 | if s_filter.pre_filter(m[1]): 106 | metadata_resources.append(ResourceString(ResourceString.TYPE_METADATA, m[0], m[1])) 107 | return metadata_resources 108 | 109 | 110 | def extract_strings_resource(decoded_apk_folder): 111 | """ 112 | Extracts all Android's string resources from a decoded apk 113 | 114 | :param decoded_apk_folder: folder that contains the decoded apk 115 | :return: a list of resource strings 116 | :rtype: list of ResourceString 117 | """ 118 | strings_path = os.path.join(decoded_apk_folder, "res", "values", "strings.xml") 119 | if not os.path.exists(strings_path): 120 | logging.error("Strings resource file not found in {0}".format(decoded_apk_folder)) 121 | return [] 122 | strings_parser = AndroidStringsXmlParser(strings_path) 123 | resource_strings = strings_parser.get_string_resources(s_filter.pre_filter_mystring) 124 | resources_filtered = [] 125 | for resource in resource_strings: 126 | if s_filter.pre_filter_mystring(resource): 127 | resources_filtered.append(resource) 128 | return resources_filtered 129 | 130 | 131 | def extract_smali_strings(decoded_apk_folder, package, manifest_parser): 132 | """ 133 | Extracts the strings contained in the smali files from a decoded apk 134 | 135 | :param decoded_apk_folder: folder that contains the decoded apk 136 | :param package: name of the app (e.g. com.example.myapp) 137 | :param manifest_parser: initialized instance of AndroidManifestXmlParser containing the application's Manifest 138 | :return: a list of strings 139 | :rtype: list of SmaliString 140 | """ 141 | main_activity = manifest_parser.get_main_activity_name() 142 | if not main_activity: 143 | logging.error("Unable to find Main Activity. Using package name to find smali source") 144 | main_activity = package 145 | package_pieces = main_activity.split(".")[:-1] 146 | package_pieces = package_pieces[:2] 147 | # if, for example, we have a package named 'com.team.example', to avoid looking in countless library files, 148 | # we must search for smali files in paths like: 149 | # 150 | # smali/com/team/example/randomfolder/a.smali 151 | # smali_classes6/com/team/anotherparent/childfolder/test.smali 152 | # smali_classes2/com/team/X/0eb.smali 153 | # 154 | # and so on. So we use a path with wildcards like this: 155 | # 156 | # smali*/com/team/**/*.smali 157 | # 158 | # Note that we don't consider the last part of the package, because 159 | # a lot of developers use this folders structure: 160 | # app name: com.team.example 161 | # com.team.example.MainActivity.class 162 | # com.team.myutilities.Tool.class 163 | # com.team.api.server.Model.class 164 | # ... 165 | source_path = os.path.join(decoded_apk_folder, "smali*") 166 | for piece in package_pieces: 167 | source_path = os.path.join(source_path, piece) 168 | source_path = os.path.join(source_path, '**') 169 | source_path = os.path.join(source_path, '*.smali') 170 | 171 | smali_strings = [] 172 | source_identified = False 173 | for filename in glob.iglob(source_path, recursive=True): 174 | source_identified = True 175 | parser = SmaliParser(filename) 176 | smalis = parser.get_strings() 177 | smalis_filtered = [] 178 | for smali_string in smalis: 179 | if s_filter.pre_filter_mystring(smali_string): 180 | smalis_filtered.append(smali_string) 181 | smali_strings += smalis_filtered 182 | if not source_identified: 183 | logging.error("Unable to determine source folder in {0}".format(package)) 184 | return [] 185 | return smali_strings 186 | 187 | 188 | def extract_native_strings(decoded_apk_folder): 189 | """ 190 | Extract the strings contained in the native libraries found in a decoded apk 191 | 192 | :param decoded_apk_folder: folder that contains the decoded apk 193 | :return: a list of strings 194 | :rtype: list of LibString 195 | """ 196 | lib_strings = set() 197 | lib_path = os.path.join(decoded_apk_folder, "lib") 198 | if os.path.exists(lib_path): 199 | path_to_be_inspected = None 200 | arc_priority_list = ["armeabi", "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"] 201 | for arc in arc_priority_list: 202 | if os.path.exists(os.path.join(lib_path, arc)): 203 | path_to_be_inspected = os.path.join(lib_path, arc) 204 | break 205 | if path_to_be_inspected: 206 | for filename in glob.iglob(path_to_be_inspected + '/**/*.so', recursive=True): 207 | logging.debug("Found shared object lib: {0}".format(filename)) 208 | base_filename = os.path.basename(filename) 209 | if base_filename in lib_blacklist: 210 | # if the library is a generic one, we can safely ignore it 211 | # since it would probably not contain any interesting information 212 | continue 213 | try: 214 | for string in strings(filename, config.shared_object_sections, 4): 215 | lib_string = LibString(base_filename, string) 216 | if s_filter.pre_filter_mystring(lib_string): 217 | lib_strings.add(lib_string) 218 | except (ELFError, ValueError) as e: 219 | logging.error(str(e)) 220 | return lib_strings 221 | 222 | 223 | def analyze_decoded_apk(decoded_apk_folder): 224 | """ 225 | Given a decoded apk (e.g. decoded using apktool), analyzes its content to extract API keys 226 | 227 | :param decoded_apk_folder: folder that contains the decoded apk 228 | :return: a list of api keys found, the name of the package, the version code and the version name 229 | """ 230 | manifest = os.path.join(decoded_apk_folder, "AndroidManifest.xml") 231 | if not os.path.exists(manifest): 232 | logging.error("Unable to find manifest file for {0}".format(decoded_apk_folder)) 233 | return 234 | manifest_parser = AndroidManifestXmlParser(manifest) 235 | package = manifest_parser.get_package() 236 | if not package: 237 | logging.error("Unable to determine package name for {0}".format(decoded_apk_folder)) 238 | return 239 | version_code = 0 240 | version_name = "0" 241 | yml_file = os.path.join(decoded_apk_folder, "apktool.yml") 242 | if not os.path.exists(yml_file): 243 | logging.warning("Unable to find apktool.yml file for {0}".format(decoded_apk_folder)) 244 | else: 245 | try: 246 | yml_parser = ApktoolYmlParser(yml_file) 247 | version_code = int(yml_parser.get_version_code()) 248 | version_name = yml_parser.get_version_name() 249 | except (IOError, ValueError) as e: 250 | logging.error("Unable to parse apktool.yml; {0}".format(e)) 251 | extracted = extract_metadata_resource(manifest_parser) 252 | extracted += extract_strings_resource(decoded_apk_folder) 253 | extracted += extract_smali_strings(decoded_apk_folder, package, manifest_parser) 254 | extracted += extract_native_strings(decoded_apk_folder) 255 | apikey_strings = analyze_strings(extracted) 256 | apikey_postfiltered = [] 257 | for mystring in apikey_strings: 258 | if s_filter.post_filter_mystring(mystring): 259 | apikey_postfiltered.append(mystring) 260 | return apikey_postfiltered, extracted, package, version_code, version_name 261 | 262 | 263 | def decode_apk(apk_path, output_path, apktool_path): 264 | """ 265 | Decodes an apk using the external apktool tool 266 | 267 | :param apk_path: path of the apk to be decoded 268 | :param output_path: decoded apk folder 269 | :param apktool_path: where apktool.jar resides 270 | """ 271 | completed_process = subprocess.run(["java", "-jar", apktool_path, "d", apk_path, "-o", output_path, "-f"], 272 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 273 | if completed_process.stdout: 274 | logging.info('Apktool: \n{0}'.format(completed_process.stdout)) 275 | if completed_process.stderr: 276 | logging.error('Apktool: \n{0}'.format(completed_process.stderr)) 277 | completed_process.check_returncode() 278 | 279 | 280 | def analyze_apk(apk_path, decoded_apk_output_path, apktool_path): 281 | """ 282 | Given an apk, decodes it and analyzes its content to extract API keys 283 | 284 | :param apk_path: path of the apk to be analyzed 285 | :param decoded_apk_output_path: where the decoded apk should be placed 286 | :param apktool_path: where apktool.jar resides, used for apk decode 287 | """ 288 | try: 289 | decode_apk(apk_path, decoded_apk_output_path, apktool_path) 290 | if os.path.exists(decoded_apk_output_path): 291 | return analyze_decoded_apk(decoded_apk_output_path) 292 | else: 293 | raise ApkAnalysisError("Unable to find decoded folder for {0}".format(apk_path)) 294 | except subprocess.CalledProcessError as err: 295 | logging.error("Error in " + str(err.cmd)) 296 | raise ApkAnalysisError(err.stderr) 297 | -------------------------------------------------------------------------------- /apktool.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbhunter/apk_api_key_extractor/cbc2a8b02a5758886ac6aa08ed4b1b193e2b02aa/apktool.jar -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | apks_dir: apks 2 | apks_decoded_dir: apks_decoded 3 | apks_analyzed_dir: apks_analyzed 4 | save_analyzed_apks: true 5 | apktool: apktool.jar 6 | lib_blacklists: 7 | - lib_blacklist.txt 8 | shared_object_sections: 9 | - .rodata 10 | - .text 11 | dump_all_strings: false 12 | # possible values: console, jsonlines, mongodb 13 | dump_location: console 14 | # which of the following configuration will be used depends on the key_dump value 15 | jsonlines: 16 | dump_file: apikey.jsonl 17 | strings_file: all_strings.jsonl 18 | mongodb: 19 | name: mytestdb 20 | address: 127.0.0.1 21 | port: 27017 22 | user: api_key_extractor 23 | password: alessandrodd -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Singleton implementation for config objects 3 | """ 4 | import os 5 | import yaml 6 | 7 | __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) 8 | 9 | CONFIG_PATH = "config.yml" 10 | 11 | with open(os.path.join(__location__, CONFIG_PATH), 'r') as ymlfile: 12 | cfg = yaml.safe_load(ymlfile) 13 | # Load the yaml content into this module global variables 14 | globals().update(cfg) 15 | -------------------------------------------------------------------------------- /console_dump.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import os 5 | 6 | from abstract_dump import AbstractDump 7 | 8 | 9 | class ConsoleDump(AbstractDump): 10 | 11 | def __init__(self): 12 | pass 13 | 14 | def dump_apikeys(self, entries, package, version_code, version_name): 15 | for entry in entries: 16 | entry_dict = entry.__dict__ 17 | entry_dict['package'] = package 18 | entry_dict['versionCode'] = version_code 19 | entry_dict['versionName'] = version_name 20 | print(entry_dict) 21 | 22 | def dump_strings(self, entries): 23 | for entry in entries: 24 | entry_dict = entry.__dict__ 25 | print(entry_dict) 26 | -------------------------------------------------------------------------------- /jsonlines_dump.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import os 5 | 6 | from flufl.lock import Lock, AlreadyLockedError, TimeOutError 7 | 8 | from abstract_dump import AbstractDump 9 | 10 | import config 11 | 12 | LOCK_PREFIX = ".lock" 13 | 14 | 15 | class JsonlinesDump(AbstractDump): 16 | 17 | def __init__(self): 18 | self.dest = os.path.abspath(config.jsonlines["dump_file"]) 19 | self.strings_dest = os.path.abspath(config.jsonlines["strings_file"]) 20 | 21 | def dump_apikeys(self, entries, package, version_code, version_name): 22 | lock_acquired = False 23 | while not lock_acquired: 24 | try: 25 | filename = self.dest + LOCK_PREFIX 26 | lock = Lock(filename, lifetime=datetime.timedelta(seconds=6000)) # expires in 10 minutes 27 | if not lock.is_locked: 28 | lock.lock(timeout=datetime.timedelta(milliseconds=350)) 29 | lock_acquired = True 30 | with open(self.dest, 'a') as f: 31 | first = True 32 | if os.path.exists(self.dest) and os.path.getsize(self.dest) > 0: 33 | first = False 34 | for entry in entries: 35 | entry_dict = entry.__dict__ 36 | entry_dict['package'] = package 37 | entry_dict['versionCode'] = version_code 38 | entry_dict['versionName'] = version_name 39 | if first: 40 | first = False 41 | else: 42 | f.write("\n") 43 | json.dump(entry_dict, f) 44 | if lock.is_locked: 45 | lock.unlock() 46 | except (AlreadyLockedError, TimeOutError): 47 | # some other process is analyzing the file; go ahead and look for another file 48 | pass 49 | 50 | def dump_strings(self, entries): 51 | lock_acquired = False 52 | while not lock_acquired: 53 | try: 54 | filename = self.strings_dest + LOCK_PREFIX 55 | lock = Lock(filename, lifetime=datetime.timedelta(seconds=6000)) # expires in 10 minutes 56 | if not lock.is_locked: 57 | lock.lock(timeout=datetime.timedelta(milliseconds=350)) 58 | lock_acquired = True 59 | with open(self.strings_dest, 'a') as f: 60 | first = True 61 | if os.path.exists(self.strings_dest) and os.path.getsize(self.strings_dest) > 0: 62 | first = False 63 | for entry in entries: 64 | entry_dict = entry.__dict__ 65 | if first: 66 | first = False 67 | else: 68 | f.write("\n") 69 | json.dump(entry_dict, f) 70 | if lock.is_locked: 71 | lock.unlock() 72 | except (AlreadyLockedError, TimeOutError): 73 | # some other process is analyzing the file; go ahead and look for another file 74 | pass 75 | -------------------------------------------------------------------------------- /learnset_create_tool.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility tool used for supervised learnset creation, blacklist population and database cleaning 3 | """ 4 | import os 5 | import sys 6 | 7 | from words_finder_singleton import finder 8 | 9 | from mongodb_dump import MongoDBDump 10 | from my_tools.getch import getch 11 | 12 | 13 | def main(argv): 14 | if len(argv) != 4: 15 | print("Usage: python {0} text_learnset_output_path api_learnset_output_path blacklist_output_path".format( 16 | argv[0])) 17 | return 18 | if os.path.exists(argv[1]): 19 | newline_text = '\n' 20 | else: 21 | newline_text = '' 22 | if os.path.exists(argv[2]): 23 | newline_api = '\n' 24 | else: 25 | newline_api = '' 26 | if os.path.exists(argv[3]): 27 | newline_blk = '\n' 28 | else: 29 | newline_blk = '' 30 | 31 | text_dest = open(argv[1], "a", 1) 32 | api_dest = open(argv[2], "a", 1) 33 | blk_dest = open(argv[3], "a", 1) 34 | print("Press 0 if the string is NOT an API Key, 1 otherwise\n" 35 | "Press b to blacklist the entry\n" 36 | "Press r to remove the record from database\n" 37 | "Press q to quit") 38 | try: 39 | 40 | dump = MongoDBDump() 41 | 42 | while True: 43 | api_doc = dump.get_apikey_unverified() 44 | if not api_doc: 45 | print("No more keys!") 46 | break 47 | print("{0} {1} {2} {3}".format(api_doc.get("package"), api_doc.get("source"), api_doc.get("name"), 48 | api_doc.get("value"))) 49 | x = '-1' 50 | if finder.get_words_percentage(api_doc.get("value")) >= 0.4: 51 | dump.remove_apikey(api_doc.get("_id")) 52 | else: 53 | while x != '0' and x != '1' and x != 'r' and x != 'b' and x != 'q': 54 | x = getch() 55 | if x == '0': 56 | dump.remove_apikey(api_doc.get("_id")) 57 | text_dest.write(newline_text + api_doc.get("value")) 58 | newline_text = '\n' 59 | elif x == '1': 60 | dump.set_apikey_verified(api_doc.get("_id")) 61 | api_dest.write(newline_api + api_doc.get("value")) 62 | newline_api = '\n' 63 | elif x == 'r': 64 | dump.remove_apikey(api_doc.get("_id")) 65 | elif x == 'b': 66 | dump.remove_apikey(api_doc.get("_id")) 67 | blk_dest.write(newline_blk + api_doc.get("value")) 68 | newline_blk = '\n' 69 | elif x == 'q': 70 | text_dest.close() 71 | api_dest.close() 72 | exit() 73 | 74 | except KeyboardInterrupt: 75 | print('\nInterrupted!') 76 | 77 | 78 | if __name__ == '__main__': 79 | main(sys.argv) 80 | -------------------------------------------------------------------------------- /lib_blacklist.txt: -------------------------------------------------------------------------------- 1 | libunity.so 2 | libmonosgen-2.0.so 3 | libxwalkcore.so 4 | libjs.so 5 | libCore.so 6 | libfolly_json.so 7 | libysshared.so 8 | libcocos2dcpp.so 9 | libplayer.so 10 | libiconv.so 11 | libopenal.so 12 | libVuforia.so 13 | libyoyo.so 14 | libmodpdfium.so 15 | libcorona.so 16 | libgdx-freetype.so 17 | libengine.so 18 | libgpg.so 19 | libgame.so 20 | libngnative.so 21 | libavformat.so 22 | libffmpeg.so 23 | libOpenAL_android.so 24 | libnobexjni.so 25 | libmupdf.so 26 | libgsengine.so 27 | libil2cpp.so 28 | libmagzter.so 29 | libjingle_peerconnection_so.so 30 | libc++_shared.so 31 | libaviary_native.so 32 | libweibosdkcore.so 33 | libavcodec.so 34 | libvlcjni.so 35 | libCocoonJSLib.so 36 | libopencv_java3.so 37 | libopencv_imgproc.so 38 | libmapbox-gl.so 39 | libopencv_core.so 40 | libucrop.so 41 | libijkffmpeg.so 42 | libkroll-v8.so 43 | libimagepipeline.so 44 | libCanvasPlus.so 45 | libQt5Core.so 46 | libgvrunity.so 47 | libcocos2djs.so 48 | libeveryplay.so 49 | libQt5Gui.so 50 | libQt5Qml.so 51 | libQCAR.so 52 | libvlc.so 53 | libJavaVoipCommonCodebaseItf.so 54 | libplugins_platforms_android_libqtforandroid.so 55 | libvrunity.so 56 | libRSSupport.so 57 | libQt5Network.so 58 | libopentok.so 59 | libcocos2dlua.so 60 | libpjsipjni.so 61 | libplugins_imageformats_libqdds.so 62 | libmsc.so 63 | libgvr.so 64 | libtmessages.22.so 65 | libtmessages.8.so 66 | libBugly.so 67 | libUE4.so 68 | libbass.so 69 | libffmpeg_codec.so 70 | libmonodroid.so 71 | librealmreact.so 72 | libmetaiosdk.so 73 | libijksdl.so 74 | libvideokit.so 75 | libqml_QtQuick_Controls_Styles_Flat_libqtquickextrasflatplugin.so 76 | libnme.so 77 | libavformat-56.so 78 | libplugpdf.so 79 | libQt5Widgets.so 80 | libaot-System.Net.Http.dll.so 81 | libaot-System.Xml.dll.so 82 | libaot-System.Runtime.Serialization.dll.so 83 | libaot-mscorlib.dll.so 84 | libPDFNetC.so 85 | libcxx.so 86 | libaot-Mono.Android.dll.so 87 | libaot-System.dll.so 88 | libaot-System.Core.dll.so 89 | libaot-Xamarin.Android.Support.Animated.Vector.Drawable.dll.so 90 | libulsTracker_native.so 91 | libaot-Java.Interop.dll.so 92 | libaot-Xamarin.Android.Support.Design.dll.so 93 | libaot-Xamarin.Android.Support.v7.AppCompat.dll.so 94 | libaot-Newtonsoft.Json.dll.so 95 | libicu.so 96 | libaot-Xamarin.Android.Support.v7.RecyclerView.dll.so 97 | libSDL2.so 98 | libaot-Xamarin.Android.Support.v7.CardView.dll.so 99 | libaot-Xamarin.Android.Support.Vector.Drawable.dll.so 100 | libavcodec-56.so 101 | libaot-Xamarin.GooglePlayServices.Basement.dll.so 102 | libaot-Xamarin.Android.Support.v4.dll.so 103 | libfilter.so 104 | libCommonCrypto.so 105 | libCoreGraphics.so 106 | libaot-System.Xml.Linq.dll.so 107 | libxmp.so 108 | libaot-Microsoft.CSharp.dll.so 109 | libpango.so 110 | libaot-Xamarin.GooglePlayServices.Base.dll.so 111 | libarchitect.so 112 | libaot-Mono.Security.dll.so 113 | libappMobiCanvasGL.so 114 | libPDFNetC-v7a.so 115 | libaot-Mono.Android.Export.dll.so 116 | liblocSDK3.so 117 | libopencv_java.so 118 | libplugins_platforms_libqoffscreen.so 119 | libFFmpeg.so 120 | libandroid_player.so 121 | libgetuiext2.so 122 | libtess.so 123 | libaudiopluginvrunity.so 124 | libs3e_android.so 125 | libaot-Xamarin.GooglePlayServices.Tasks.dll.so 126 | libaot-Plugin.Settings.dll.so 127 | libaot-Plugin.Connectivity.dll.so 128 | libaot-Xamarin.Android.Support.Core.UI.dll.so 129 | libaot-Xamarin.Android.Support.Compat.dll.so 130 | libaot-Xamarin.Android.Support.Transition.dll.so 131 | libpdfview2.so 132 | libaot-Xamarin.Android.Support.Fragment.dll.so 133 | libaot-Xamarin.Android.Support.Media.Compat.dll.so 134 | libaot-Xamarin.Android.Support.Core.Utils.dll.so 135 | libpano_video_renderer.so 136 | libaot-System.Numerics.dll.so 137 | libaot-Xamarin.Android.Support.CustomTabs.dll.so 138 | libpldroidplayer.so 139 | libxguardian.so 140 | libaot-Plugin.Connectivity.Abstractions.dll.so 141 | libNativeScript.so 142 | libaot-Xamarin.Firebase.Iid.dll.so 143 | libtpnsWatchdog.so 144 | libaot-Xamarin.Firebase.Common.dll.so 145 | libaot-Xamarin.GooglePlayServices.Analytics.dll.so 146 | libfolly_json_abi15_0_0.so 147 | libaot-Xamarin.Firebase.Analytics.dll.so 148 | libaot-Xamarin.GooglePlayServices.TagManager.V4.Impl.dll.so 149 | libaot-Xamarin.GooglePlayServices.Analytics.Impl.dll.so 150 | libaot-Xamarin.Firebase.Crash.dll.so 151 | libpanorenderer.so 152 | libfolly_json_abi14_0_0.so 153 | libmobclickcpp.so 154 | libaot-Xamarin.Firebase.Analytics.Impl.dll.so 155 | libRongIMLib.so 156 | libaot-Xamarin.Android.Support.Annotations.dll.so 157 | libaot-Plugin.CurrentActivity.dll.so 158 | libfolly_json_abi16_0_0.so 159 | libaot-Square.OkIO.dll.so 160 | liblept.so 161 | libaot-Microsoft.AspNet.SignalR.Client.dll.so 162 | libfolly_json_abi18_0_0.so 163 | libaot-Xamarin.Facebook.dll.so 164 | libaot-Bolts.Tasks.dll.so 165 | libaot-Xamarin.GooglePlayServices.Ads.dll.so 166 | libfolly_json_abi17_0_0.so 167 | libaot-Xamarin.GooglePlayServices.Ads.Lite.dll.so 168 | libmupdf_java.so 169 | libmono.so 170 | libaot-Bolts.AppLinks.dll.so 171 | libaot-Square.OkHttp.dll.so 172 | libbctoolbox-armeabi.so 173 | libaot-RoundedImageView.dll.so 174 | libmediastreamer_voip-armeabi.so 175 | libaot-SQLitePCL.raw.dll.so 176 | libaot-Square.Picasso.dll.so 177 | libaot-SQLitePCL.batteries.dll.so 178 | libaot-Xamarin.Facebook.AudienceNetwork.dll.so 179 | libaot-Base.Droid.dll.so 180 | libaot-ExoPlayer.SmoothStreaming.dll.so 181 | libaot-Xamarin.GooglePlayServices.SafetyNet.dll.so 182 | libaot-SQLitePCLPlugin_esqlite3.dll.so 183 | libchilkat.so 184 | libaot-I18N.dll.so 185 | libaot-I18N.CJK.dll.so 186 | libaot-Xamarin.GooglePlayServices.Auth.Base.dll.so 187 | libaot-Xamarin.Android.Support.Percent.dll.so 188 | libaot-ExoPlayer.Core.dll.so 189 | libaot-ExoPlayer.Dash.dll.so 190 | libwd220uni.so 191 | libaot-MoPub.Droid.Bindings.dll.so 192 | libaot-Xamarin.GooglePlayServices.Auth.dll.so 193 | libaot-I18N.MidEast.dll.so 194 | libaot-ExoPlayer.Hls.dll.so 195 | libaot-Yandex.Metrica.Droid.Bindings.dll.so 196 | libaot-Xamarin.Android.Support.v14.Preference.dll.so 197 | libaot-I18N.Other.dll.so 198 | libaot-ExoPlayer.UI.dll.so 199 | libaot-Xamarin.Android.Support.v7.Preference.dll.so 200 | libaot-I18N.Rare.dll.so 201 | libaot-I18N.West.dll.so 202 | libgdinamapv4sdk752.so 203 | libaot-ExoPlayer.dll.so 204 | libaot-TagView.Droid.Bindings.dll.so 205 | libpspdfkit.so 206 | libwd200uni.so 207 | libfolly_json_abi19_0_0.so 208 | libmodplug-1.0.so 209 | liblinphone-armeabi.so 210 | libsdkbox.so 211 | libsinch-android-rtc.so 212 | libtmessages.7.so 213 | libtpnsSecurity.so 214 | librealm-jni.so 215 | libsqlcipher_android.so 216 | libplugins_platforms_libqminimalegl.so 217 | libClawApp.so 218 | libfolly_json_abi20_0_0.so 219 | libAVEAndroid.so 220 | libtracker.so 221 | libgideros.so 222 | libgdx-bullet.so 223 | libavformat-55.so 224 | liblocSDK7a.so 225 | libagora-rtc-sdk-jni.so 226 | libfpdfembedsdk.so 227 | libpldroid_streaming_h264_encoder.so 228 | libfotobeautyengine.so 229 | libMyBubble.so 230 | libqutils.so 231 | libtracker_neon.so 232 | libwd220com.so 233 | libpldroid_mmprocessing.so 234 | libaurasma_cpufeatures.so 235 | libcronet.61.0.3163.27.so 236 | libdodo.so 237 | libst_mobile.so 238 | liblocSDK4.so 239 | libbctoolbox-armeabi-v7a.so 240 | libV8Simple.so 241 | libGraphicsServices.so 242 | libmitek.so 243 | libtdm-4.0-90-jni.so 244 | libebookdroid.so 245 | libavcodec-55.so 246 | libgvrbase.so 247 | libs3eAndroidGooglePlayBilling.so 248 | libpngt.so 249 | libaot-Xamarin.Forms.Xaml.dll.so 250 | libEasyAR.so 251 | libbd_etts.so 252 | libvrapi.so 253 | libaot-Xamarin.Forms.Core.dll.so 254 | libj2v8.so 255 | libaot-Xamarin.Forms.Platform.Android.dll.so 256 | libxigncode.so 257 | libaot-System.ServiceModel.Internals.dll.so 258 | libwd210uni.so 259 | libmediastreamer_voip-armeabi-v7a.so 260 | libopenal32.so 261 | libmonodroid_bundle_app.so 262 | libsqlcipher.so 263 | libPlayCtrl.so 264 | libentryexpro.so 265 | libgdndk.so 266 | libaot-FormsViewGroup.dll.so 267 | libWasabiJni.so 268 | libSkiaSharp.so 269 | libaot-Xamarin.Android.Support.v7.MediaRouter.dll.so 270 | liblime.so 271 | libpldroid_rtc_streaming.so 272 | libBaiduMapSDK_base_v3_7_3.so 273 | libphotomusicvideomaker.so 274 | libARWrapper.so 275 | libcproxy.so 276 | libBaiduMapSDK_v2_3_1.so 277 | libImmEmulatorJ.so 278 | libBaiduMapSDK_base_v4_0_0.so 279 | libopencvforunity.so 280 | libeasemob_jni.so 281 | libgstreamer_android.so 282 | libruntimecore_java.so 283 | libHCCore.so 284 | libpng16.so 285 | libkeys.so 286 | libPlayCtrl_v7.so 287 | libverde.so 288 | libjavascriptcoregtk-4.0.so 289 | libhyphenate.so 290 | libPlayCtrl_v5.so 291 | libSDL2_ttf.so 292 | libMixpanelSDK.so 293 | libpeople_det.so 294 | libS3DClient.so 295 | libopencv_imgcodecs.so 296 | libnama.so 297 | libhwrword.so 298 | libpzspeed.so 299 | libMAME4all.so 300 | libopencv_video.so 301 | libaot-Xamarin.Forms.Platform.dll.so 302 | libJunJiAdpcmDec.so 303 | libprocessing.so 304 | libSPenEngine.so 305 | libfolly_json_abi13_0_0.so 306 | libyuv_shared-armeabi.so 307 | libfmod.so 308 | liblove.so 309 | librtspplr-armeabi.so 310 | libcrypto.so 311 | libspotify_embedded_shared.so 312 | libtealeaf.so 313 | libSPenBase.so 314 | libpicsel.so 315 | libadcolony.so 316 | libwebrtc_shared.so 317 | libVidyoClientApp.so 318 | libopencv_ml.so 319 | libvoH264Dec_v7_OSMP.so 320 | libvoTsParser_OSMP.so 321 | libpjsip.so 322 | libvoAudioFR_OSMP.so 323 | libplugins_platforms_libqeglfs.so 324 | libd2d1.so 325 | libmaascrypto.so 326 | libfmodstudio.so 327 | libwd190uni.so 328 | libvoAdaptiveStreamHLS_OSMP.so 329 | libSPenModel.so 330 | libvoMP4FR_OSMP.so 331 | libaot-Validation.dll.so 332 | libopencv_dnn.so 333 | libmp4.so 334 | libaot-Splat.dll.so 335 | libsyscore.so 336 | libBaiduMapSDK_map_v4_3_0.so 337 | libImgFun.so 338 | libvompEngn_OSMP.so 339 | libACRCloudEngine.so 340 | libvoAMediaCodec_OSMP.so 341 | libvoOSSource_OSMP.so 342 | libvoAACDec_OSMP.so 343 | libaot-Acr.UserDialogs.dll.so 344 | libtensorflow_inference.so 345 | libportsip_core.so 346 | libmp3lame.so 347 | libcom.yandex.runtime.so 348 | libjavafx_font_freetype.so 349 | libaot-ModernHttpClient.dll.so 350 | libaot-Acr.UserDialogs.Interface.dll.so 351 | libaot-Xamarin.GooglePlayServices.Gcm.dll.so 352 | libwd180uni.so 353 | libcom.yandex.mapkit.so 354 | libaot-Plugin.Permissions.dll.so 355 | libcommon.so 356 | libplay.so 357 | libRTSPPlayerCodec.so 358 | libvoH264Dec_OSMP.so 359 | libOculusMediaSurface.so 360 | libaot-AndHUD.dll.so 361 | libana-sdk.so 362 | libAura.so 363 | libvoOSEng_OSMP.so 364 | libvoAudioMCDec_OSMP.so 365 | libCertResourcesPkg.so 366 | libvoStreamingDownloader_OSMP.so 367 | libmlabsegment.so 368 | libcronet.61.0.3142.0.so 369 | libapp_BaiduVIlib.so 370 | libvoSourceIO_OSMP.so 371 | libposclient.so 372 | libcrypto_here.so 373 | libTextureDecoder.so 374 | libpolarisoffice7.so 375 | libvudroid.so 376 | libSkyFilter.so 377 | libopencv_contrib.so 378 | libflutter.so 379 | libHDACEngine.so 380 | libepub3.so 381 | libnexalfactory.so 382 | libMozzoMZV.so 383 | libfolly_json_abi12_0_0.so 384 | libvoAudioSpeed_OSMP.so 385 | libSDL2_image.so 386 | libjsqlite.so 387 | libSdkResourcePkg.so 388 | libabtovoipjni.so 389 | libpng-decoder.so 390 | libavla.so 391 | libSecShell.so 392 | libuptsmaddon.so 393 | libacr.so 394 | libMAPSJNI.so 395 | libabto_video_android.so 396 | libaot-ExifLib.dll.so 397 | libcore.so 398 | libg.so 399 | libentryexstd.so 400 | libvoSrcPD_OSMP.so 401 | libtdm-3.2-100-jni.so 402 | libpjsua2.so 403 | libYvImSdk.so 404 | libffmpeg_neon.so 405 | libtmessages.so 406 | libfolly_json_abi11_0_0.so 407 | libRCSSDK.so 408 | libcocosworkAndroid.so 409 | libqalcodecwrapper.so 410 | libaot-PCLCrypto.dll.so 411 | libTBAudioEngineUnity.so 412 | libAdobeReader.so 413 | libBaiduMapSDK_base_v4_1_1.so 414 | libKudan.so 415 | libnh.so 416 | libsoftphone.so 417 | libmtmakeup.so 418 | libRtmMediaManagerDyn.so 419 | libaot-Plugin.Media.dll.so 420 | libMaply.so 421 | libMobileUtility.so 422 | libaot-Xamarin.Android.Support.v7.Palette.dll.so 423 | libezjoygame.so 424 | libPositioningResourcePkg.so 425 | libmortargame.so 426 | libmediaplayer.so 427 | libmlabmakeup.so 428 | libapp_BaiduPanoramaAppLib.so 429 | lib_imcore_jni_gyp.so 430 | libgvr_keyboard_shim_unity.so 431 | libogrekit.so 432 | libyuv.so 433 | libffmpegencoder.so 434 | libicu_common.so 435 | libfullsslsdk.so 436 | librdpdf.so 437 | libmonochrome.so 438 | libnexplayerengine.so 439 | libvoLogSys.so 440 | libmega.so 441 | libGNaviUtils.so 442 | lib0x000005-KSL-0x01.so 443 | libmtmakeup3.so 444 | libsoftphone-neon.so 445 | libeffect_core.so 446 | libhydra.so 447 | lib1cem.so 448 | libclmf_jni.so 449 | libde.so 450 | libmcbpcore-jni.so 451 | libbctoolbox.so 452 | libmediastreamer_voip.so 453 | libirrXML.so 454 | libvideochat_jni.so 455 | libnetsdk.so 456 | libgwallet.so 457 | libaot-Xamarin.GooglePlayServices.Iid.dll.so 458 | libcronet.so 459 | libProject1.so 460 | libaot-XLabs.Serialization.dll.so 461 | libaot-Xamarin.Auth.dll.so 462 | libjsc.so 463 | libBEARGL.so 464 | libqupai-media-jni.so 465 | libec.so 466 | libscanditsdk-android-4.16.4.so 467 | libtinyWRAP.so 468 | libnmsssa.so 469 | libaot-Plugin.DeviceInfo.dll.so 470 | libutility.so 471 | libmm_jni.so 472 | libvoAdaptiveStreamISS_OSMP.so 473 | libRTSP.so 474 | libwhamcitylights.so 475 | libaot-Acr.Support.Android.dll.so 476 | libentryex.so 477 | libh264tomp4.so 478 | libSystemTransform.so 479 | libjnnat.so 480 | libhevcdec.so 481 | libaot-PInvoke.Windows.Core.dll.so 482 | libBaiduMapSDK_base_v4_2_0.so 483 | libav.so 484 | libNewAllStreamParser.so 485 | libs3eAndroidGooglePlayBilling_ext.so 486 | libmcbpcryptoservice-jni.so 487 | libCouchbaseLiteJavaForestDB.so 488 | libopencv_xfeatures2d.so 489 | libsadp.so 490 | libmtbana-rsa-sdk.so 491 | libscanditsdk-android-5.1.1.so 492 | libavfilter.so 493 | libgfxandroid.so 494 | libcocos2d.so 495 | liblinphone-armeabi-v7a.so 496 | libBmobStat.so 497 | libdingtone.so 498 | libhdmmapcore.so 499 | libBlinkID.so 500 | libfisheye.so 501 | libmffacedetector.so 502 | libscanditsdk-android-4.14.1.so 503 | libtensorflow_demo.so 504 | libaot-XLabs.Platform.dll.so 505 | libimostream.so 506 | libaot-OkHttp.dll.so 507 | libAppGuard.so 508 | libBaiduMapSDK_base_v3_6_1.so 509 | libHQRenderLib.so 510 | libAudioDecoder.so 511 | libpdfium.so 512 | libaot-ImageCircle.Forms.Plugin.Abstractions.dll.so 513 | libcompress.so 514 | libjniInterface.so 515 | libaot-PInvoke.Kernel32.dll.so 516 | libmMap.so 517 | libaot-PInvoke.BCrypt.dll.so 518 | libbacaarab.so 519 | libarsa.so 520 | libperftest.so 521 | librevandroid.so 522 | libedex.so 523 | libomaplugin.so 524 | libaot-PInvoke.NCrypt.dll.so 525 | libsbml.so 526 | librelease_sig.so 527 | libmain - 2.8.12.so 528 | libaot-XLabs.Platform.Droid.dll.so 529 | libvoSmthParser_OSMP.so 530 | libacs_sdk.so 531 | libCoreText.so 532 | libvoAdaptiveStreamDASH_OSMP.so 533 | libA9VSMobile.so 534 | libaot-ImageCircle.Forms.Plugin.Android.dll.so 535 | libmdxlib.so 536 | libStreamPackage.so 537 | libAndroid.so 538 | libsrtp.so 539 | libBaiduMapSDK_map_v4_2_0.so 540 | libacs.so 541 | libassimp.so 542 | libmsxml.so 543 | libipcamera.so 544 | libVisageVision.so 545 | libscanditsdk-android-4.2.2.so 546 | libcityguide.so 547 | libchrome.so 548 | libNavigationSystem.so 549 | libffmpeg_jni.so 550 | libfoxit.so 551 | libapp_BaiduNaviApplib.so 552 | libelips_porting.so 553 | libopencv_highgui.so 554 | libebook.so 555 | libdecoder_audio.so 556 | libminecraftpe.so 557 | libacomo.so 558 | libaot-XLabs.Core.dll.so 559 | libmtcrypt.so 560 | libhdmgeoprojection.so 561 | libcork.so 562 | librender.so 563 | libscenegraph.so 564 | libHomeDesign3D.so 565 | libnode.so -------------------------------------------------------------------------------- /log_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "logging": { 3 | "version": 1, 4 | "formatters": { 5 | "f": { 6 | "format": "%(asctime)s %(name)-4s %(levelname)-4s %(message)s", 7 | "datefmt": "%m-%d %H:%M" 8 | } 9 | }, 10 | "handlers": { 11 | "log_file_handler": { 12 | "class": "logging.FileHandler", 13 | "formatter": "f", 14 | "filename": "apk_api_key_extractor.log", 15 | "mode": "a", 16 | "encoding": "utf-8" 17 | }, 18 | "console_handler": { 19 | "class": "logging.StreamHandler", 20 | "formatter": "f" 21 | } 22 | }, 23 | "root": { 24 | "handlers": [ 25 | "log_file_handler", 26 | "console_handler" 27 | ], 28 | "level": "INFO" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import logging 4 | import logging.config 5 | import os 6 | import shutil 7 | import sys 8 | import time 9 | import json 10 | 11 | from console_dump import ConsoleDump 12 | from mongodb_dump import MongoDBDump 13 | from jsonlines_dump import JsonlinesDump 14 | 15 | import config 16 | 17 | LOG_CONFIG_PATH = "log_config.json" 18 | __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) 19 | 20 | # Configure logging 21 | with open(os.path.join(__location__, LOG_CONFIG_PATH), "r", encoding="utf-8") as fd: 22 | log_config = json.load(fd) 23 | logging.config.dictConfig(log_config["logging"]) 24 | 25 | import apk_analyzer 26 | 27 | 28 | def clean_resources(apk_path, lock, decoded_apk_output_path, apks_analyzed_dir, remove_apk=False): 29 | """ 30 | Clean the resources allocated to analyze a string 31 | 32 | :param apk_path: path of the apk that has been 33 | :param lock: a lock that is locking the aforementioned apk 34 | :param decoded_apk_output_path: where the decoded apk where placed 35 | :param apks_analyzed_dir: where the apk should be moved after analysis; None if it should not be moved 36 | :param remove_apk: True if the apk should be deleted; ignored if apks_analyzed_dir is not None 37 | """ 38 | apk = os.path.basename(apk_path) 39 | try: 40 | if os.path.exists(decoded_apk_output_path): 41 | shutil.rmtree(decoded_apk_output_path) 42 | else: 43 | logging.error("Unable to find decoded folder for {0}".format(apk_path)) 44 | if apks_analyzed_dir: 45 | if not os.path.exists(apks_analyzed_dir): 46 | os.mkdir(apks_analyzed_dir) 47 | # either way, move the apk out of apk dir 48 | shutil.move(apk_path, os.path.join(apks_analyzed_dir, apk)) 49 | elif remove_apk: 50 | os.remove(apk_path) 51 | finally: 52 | if lock: 53 | # unlock and clean temp files; cleaning should not be necessary 54 | # noinspection PyProtectedMember 55 | lockfile = lock._lockfile 56 | if lock.is_locked: 57 | lock.unlock() 58 | if os.path.exists(lockfile): 59 | os.remove(lockfile) 60 | 61 | 62 | def analyze_apk(apk_path, apks_decoded_dir, apks_analyzed_dir, apktool_path, lock=None): 63 | apk = os.path.basename(apk_path) 64 | decoded_output_path = os.path.join(apks_decoded_dir, apk) 65 | try: 66 | apikeys, all_strings, package, version_code, version_name = apk_analyzer.analyze_apk(apk_path, 67 | decoded_output_path, 68 | apktool_path) 69 | if apikeys: 70 | dump = None 71 | if config.dump_location == "console": 72 | dump = ConsoleDump() 73 | elif config.dump_location == "jsonlines": 74 | dump = JsonlinesDump() 75 | elif config.dump_location == "mongodb": 76 | dump = MongoDBDump() 77 | else: 78 | print("Unrecognized dump location: {0}".format(config.dump_location)) 79 | exit(1) 80 | dump.dump_apikeys(apikeys, package, version_code, version_name) 81 | if config.dump_all_strings: 82 | dump.dump_strings(all_strings) 83 | except apk_analyzer.ApkAnalysisError as e: 84 | logging.error(str(e)) 85 | clean_resources(apk_path, lock, decoded_output_path, apks_analyzed_dir, not config.save_analyzed_apks) 86 | 87 | 88 | def monitor_apks_folder(apks_dir, apks_decoded_dir, apks_analyzed_dir, apktool_path): 89 | logging.info("Monitoring {0} for apks...".format(apks_dir)) 90 | try: 91 | while True: 92 | apk_path, lock = apk_analyzer.get_next_apk(apks_dir) 93 | if lock is not None: 94 | logging.info("Detected {0}".format(apk_path)) 95 | analyze_apk(apk_path, apks_decoded_dir, apks_analyzed_dir, apktool_path, lock) 96 | logging.info("{0} analyzed".format(apk_path)) 97 | else: 98 | time.sleep(1) 99 | 100 | except KeyboardInterrupt: 101 | print('\nInterrupted!') 102 | 103 | 104 | def main(): 105 | parser = argparse.ArgumentParser( 106 | description='A python program that finds API-KEYS and secrets hidden inside strings', add_help=True 107 | ) 108 | parser.add_argument('--debug', action="store_true", dest='boolean_debug', 109 | default=False, help='Print debug information') 110 | parser.add_argument('--analyze-apk', action='store', dest='apk_path', 111 | help='Analyze a single apk to find hidden API Keys') 112 | parser.add_argument('--monitor-apks-folder', action="store_true", dest='boolean_monitor', 113 | default=False, help='Monitors the configured apks folder for new apks. ' 114 | 'When a new apk is detected, the file is locked and analysis starts.') 115 | 116 | results = parser.parse_args() 117 | 118 | # functions that don't need gibberish detector 119 | 120 | if results.boolean_debug: 121 | logging.basicConfig(level=logging.DEBUG) 122 | elif results.apk_path: 123 | analyze_apk(results.apk_path, os.path.abspath(config.apks_decoded_dir), 124 | None, os.path.abspath(config.apktool)) 125 | return 126 | elif results.boolean_monitor: 127 | apks_analyzed_dir = None 128 | if config.save_analyzed_apks: 129 | apks_analyzed_dir = os.path.abspath(config.apks_analyzed_dir) 130 | monitor_apks_folder(os.path.abspath(config.apks_dir), os.path.abspath(config.apks_decoded_dir), 131 | apks_analyzed_dir, os.path.abspath(config.apktool)) 132 | return 133 | parser.print_help() 134 | 135 | 136 | if __name__ == '__main__': 137 | main() 138 | -------------------------------------------------------------------------------- /mongodb_dump.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import os 5 | 6 | import pymongo 7 | from pymongo.errors import AutoReconnect 8 | from retry_decorator import retry 9 | 10 | from abstract_dump import AbstractDump 11 | 12 | import config 13 | 14 | COLLECTION_NAME = "key_dump" 15 | STRINGS_COLLECTION_NAME = "all_strings" 16 | 17 | 18 | class MongoDBDump(AbstractDump): 19 | 20 | def __init__(self): 21 | self.client = pymongo.MongoClient(config.mongodb["address"], int(config.mongodb["port"]), 22 | username=config.mongodb["user"], password=config.mongodb["password"]) 23 | self.db = self.client[config.mongodb["name"]] 24 | self.collection = self.db[COLLECTION_NAME] 25 | self.strings_collection = self.db[STRINGS_COLLECTION_NAME] 26 | 27 | @retry(pymongo.errors.AutoReconnect, tries=5, timeout_secs=1) 28 | def dump_apikeys(self, entries, package, version_code, version_name): 29 | # noinspection PyUnusedLocal 30 | entries_dicts = [] 31 | for entry in entries: 32 | entry_dict = entry.__dict__ 33 | entry_dict['package'] = package 34 | entry_dict['versionCode'] = version_code 35 | entry_dict['versionName'] = version_name 36 | entries_dicts.append(entry_dict) 37 | if entries_dicts: 38 | self.collection.insert_many(entries_dicts, False) 39 | 40 | @retry(pymongo.errors.AutoReconnect, tries=5, timeout_secs=1) 41 | def dump_strings(self, entries): 42 | operations = [] 43 | for entry in entries: 44 | operations.append(pymongo.UpdateOne({'_id': entry.value}, {'$inc': {'count': 1}}, upsert=True)) 45 | if len(operations) > 0: 46 | try: 47 | self.strings_collection.bulk_write(operations, ordered=False) 48 | except pymongo.errors.BulkWriteError as bwe: 49 | print(bwe.details) 50 | # filter out "key too large to index" exceptions, which have error code 17280 51 | # we don't care about them 52 | filtered_errors = filter(lambda x: x['code'] != 17280, bwe.details['writeErrors']) 53 | if len(list(filtered_errors)) > 0: 54 | raise 55 | 56 | @retry(pymongo.errors.AutoReconnect, tries=5, timeout_secs=1) 57 | def get_apikey_unverified(self): 58 | document = self.collection.find_one({"verified": None}) 59 | return document 60 | 61 | @retry(pymongo.errors.AutoReconnect, tries=5, timeout_secs=1) 62 | def set_apikey_verified(self, api_id): 63 | document_before = self.collection.find_one_and_update({'_id': api_id}, {"$set": {"verified": True}}) 64 | if not document_before: 65 | logging.error("Unable to set api {0} as verified! Id not found.".format(api_id)) 66 | 67 | @retry(pymongo.errors.AutoReconnect, tries=5, timeout_secs=1) 68 | def remove_apikey(self, api_id): 69 | self.collection.remove(api_id) 70 | -------------------------------------------------------------------------------- /my_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbhunter/apk_api_key_extractor/cbc2a8b02a5758886ac6aa08ed4b1b193e2b02aa/my_model/__init__.py -------------------------------------------------------------------------------- /my_model/lib_string.py: -------------------------------------------------------------------------------- 1 | from my_model.my_string import MyString 2 | 3 | 4 | class LibString(MyString): 5 | """ 6 | String found with Unix's strings utility 7 | """ 8 | TYPE_LIB = "TYPE_LIB_STRING" 9 | 10 | def __init__(self, name, value): 11 | super().__init__(name, value, LibString.TYPE_LIB) 12 | 13 | def __str__(self): 14 | return super(LibString, self).__str__() 15 | 16 | def __repr__(self): 17 | return self.__str__() 18 | -------------------------------------------------------------------------------- /my_model/my_string.py: -------------------------------------------------------------------------------- 1 | class MyString(object): 2 | """ 3 | An object representing an interesting string 4 | """ 5 | TYPE_UNKNOWN = "TYPE_UNKNOWN" 6 | 7 | def __init__(self, name, value, source=TYPE_UNKNOWN): 8 | self.name = name 9 | self.value = value 10 | self.source = source 11 | 12 | def __str__(self): 13 | return "{0} {1} {2}".format(self.name, self.value, self.source) 14 | 15 | def __repr__(self): 16 | return self.__str__() 17 | -------------------------------------------------------------------------------- /my_model/resource_string.py: -------------------------------------------------------------------------------- 1 | from my_model.my_string import MyString 2 | 3 | 4 | class ResourceString(MyString): 5 | """ 6 | An interesting string that has been found in an Android's resource file 7 | """ 8 | TYPE_STRING = "TYPE_RESOURCE_STRING" 9 | TYPE_ARRAY_ELEMENT = "TYPE_RESOURCE_ARRAY_ELEMENT" 10 | TYPE_METADATA = "TYPE_RESOURCE_METADATA" 11 | 12 | def __init__(self, row_type, name, value): 13 | super().__init__(name, value, row_type) 14 | 15 | def __str__(self): 16 | return super(ResourceString, self).__str__() 17 | 18 | def __repr__(self): 19 | return self.__str__() 20 | -------------------------------------------------------------------------------- /my_model/smali_string.py: -------------------------------------------------------------------------------- 1 | from my_model.my_string import MyString 2 | 3 | 4 | class SmaliString(MyString): 5 | """ 6 | An interesting string that has been found inside a smali code 7 | """ 8 | TYPE_LOCAL_VAR = "TYPE_LOCAL_VAR" 9 | TYPE_INSTANCE_VAR = "TYPE_INSTANCE_VAR" 10 | TYPE_STATIC_VAR = "TYPE_STATIC_VAR" 11 | TYPE_METHOD_PARAMETER = "TYPE_METHOD_PARAMETER" 12 | 13 | def __init__(self, string_type, class_name, method_name, var_name, value, in_array=False, parameter_of=None): 14 | super().__init__(var_name, value, string_type) 15 | self.class_name = class_name 16 | self.method_name = method_name 17 | self.in_array = in_array 18 | self.parameter_of = parameter_of 19 | 20 | def __str__(self): 21 | s = super(SmaliString, self).__str__() 22 | s += " {0} {1}".format(self.class_name, self.method_name) 23 | if self.in_array: 24 | s += " In Array: {0}".format(self.in_array) 25 | if self.parameter_of: 26 | s += " Parameter of: {0}".format(self.parameter_of) 27 | return s 28 | 29 | def __repr__(self): 30 | return self.__str__() 31 | -------------------------------------------------------------------------------- /my_tools/apktool_yml_parser.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | class ApktoolYmlParser(object): 5 | """ 6 | Parser utility for apktool's apktool.yml file 7 | """ 8 | 9 | def __init__(self, yml_file): 10 | """ 11 | constructor for ApktoolYmlParser object 12 | 13 | :param yml_file: path of the apktool's apktool.yml file to be parsed 14 | """ 15 | with open(yml_file, 'r') as f: 16 | # ignore yaml class tag 17 | # e.g. !!brut.androlib.meta.MetaInfo 18 | line = f.readline() 19 | if not line.startswith("!!"): 20 | f.seek(0) 21 | self.doc = yaml.safe_load(f) 22 | 23 | def get_version_code(self): 24 | return self.doc['versionInfo']['versionCode'] 25 | 26 | def get_version_name(self): 27 | return self.doc['versionInfo']['versionName'] 28 | -------------------------------------------------------------------------------- /my_tools/getch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2013-2015 Joe Esposito 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | """ 22 | 23 | 24 | try: 25 | from msvcrt import getch 26 | except ImportError: 27 | def getch(): 28 | """ 29 | Gets a single character from STDIO. 30 | """ 31 | import sys 32 | import tty 33 | import termios 34 | fd = sys.stdin.fileno() 35 | old = termios.tcgetattr(fd) 36 | try: 37 | tty.setraw(fd) 38 | return sys.stdin.read(1) 39 | finally: 40 | termios.tcsetattr(fd, termios.TCSADRAIN, old) -------------------------------------------------------------------------------- /my_tools/manifest_parser.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | 3 | 4 | class AndroidManifestXmlParser(object): 5 | """ 6 | Parser utility for Android's AndroidManifest.xml file 7 | """ 8 | 9 | def __init__(self, xml_file): 10 | """ 11 | constructor for AndroidManifestXmlParser object 12 | 13 | :param xml_file: path of the Android's manifest (AndroidManifest.xml) file to be parsed 14 | """ 15 | path = xml_file 16 | self.root = etree.parse(path, etree.XMLParser(encoding='utf-8', recover=True)).getroot() 17 | 18 | def get_package(self): 19 | return self.root.get("package") 20 | 21 | def get_metadata(self): 22 | metadata_rows = self.root.findall('.//meta-data') 23 | metadata = [] 24 | for row in metadata_rows: 25 | name = row.get("{" + str(self.root.nsmap.get("android")) + "}name") 26 | value = row.get("{" + str(self.root.nsmap.get("android")) + "}value") 27 | if name and value: 28 | metadata.append((name, value)) 29 | return metadata 30 | 31 | def get_activities(self): 32 | activities_rows = self.root.findall('.//activity') 33 | activities = [] 34 | for row in activities_rows: 35 | activities.append(row.get("{" + str(self.root.nsmap.get("android")) + "}name")) 36 | return activities 37 | 38 | def get_activities_aliases(self): 39 | aliases_rows = self.root.findall('.//activity-alias') 40 | aliases = [] 41 | for row in aliases_rows: 42 | aliases.append((row.get("{" + str(self.root.nsmap.get("android")) + "}name"), 43 | row.get("{" + str(self.root.nsmap.get("android")) + "}targetActivity"))) 44 | return aliases 45 | 46 | def get_main_activity_name(self): 47 | activities_rows = self.root.findall('.//activity') 48 | for row in activities_rows: 49 | action_rows = row.findall('.//action') 50 | for action_row in action_rows: 51 | if action_row.get("{" + str(self.root.nsmap.get("android")) + "}name") == "android.intent.action.MAIN": 52 | activity_name = row.get("{" + str(self.root.nsmap.get("android")) + "}name") 53 | if activity_name is not None and activity_name.startswith("."): 54 | return self.get_package() + activity_name 55 | elif activity_name is not None and "." not in activity_name: 56 | return self.get_package() + "." + activity_name 57 | return activity_name 58 | aliases_rows = self.root.findall('.//activity-alias') 59 | for row in aliases_rows: 60 | action_rows = row.findall('.//action') 61 | for action_row in action_rows: 62 | if action_row.get("{" + str(self.root.nsmap.get("android")) + "}name") == "android.intent.action.MAIN": 63 | activity_name = row.get("{" + str(self.root.nsmap.get("android")) + "}targetActivity") 64 | if activity_name.startswith("."): 65 | return self.get_package() + activity_name 66 | elif "." not in activity_name: 67 | return self.get_package() + "." + activity_name 68 | return activity_name 69 | return None 70 | -------------------------------------------------------------------------------- /my_tools/memoized.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import functools 3 | 4 | 5 | class Memoized(object): 6 | """ 7 | Thanks to https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize 8 | 9 | Decorator. Caches a function's return value each time it is called. 10 | If called later with the same arguments, the cached value is returned 11 | (not reevaluated). 12 | """ 13 | 14 | def __init__(self, func): 15 | self.func = func 16 | self.cache = {} 17 | 18 | def __call__(self, *args): 19 | if not isinstance(args, collections.Hashable): 20 | # uncacheable. a list, for instance. 21 | # better to not cache than blow up. 22 | return self.func(*args) 23 | if args in self.cache: 24 | return self.cache[args] 25 | else: 26 | value = self.func(*args) 27 | self.cache[args] = value 28 | return value 29 | 30 | def __repr__(self): 31 | """Return the function's docstring.""" 32 | return self.func.__doc__ 33 | 34 | def __get__(self, obj, objtype): 35 | """Support instance methods.""" 36 | return functools.partial(self.__call__, obj) 37 | -------------------------------------------------------------------------------- /my_tools/smali_parser.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import logging 3 | import re 4 | import sys 5 | 6 | from my_model.smali_string import SmaliString 7 | 8 | 9 | class SmaliParser(object): 10 | """ 11 | A quick and dirty, partial parser for Smali code. 12 | Made for the apk-apikey-grabber project and probably not suitable for other tasks. 13 | The objective of this code is to retrieve as much information as possible about 14 | String variables while avoiding implementing an entire Smali parser. The complete set of smali 15 | operations is quite big, here just a subset of these operations was considered, so 16 | it will NOT be 100% accurate, especially when trying to parse arrays of strings or method parameters. 17 | """ 18 | 19 | def __init__(self, smali_file): 20 | """ 21 | constructor for SmaliParser object 22 | 23 | For information about dalvik opcodes see here: 24 | http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html 25 | 26 | :param smali_file: path of the smali file to be parsed 27 | """ 28 | self._smali_file = smali_file 29 | 30 | def get_strings(self): 31 | strings = [] 32 | with codecs.open(self._smali_file, 'r', encoding='utf8') as f: 33 | current_class = None 34 | current_method = None 35 | current_const_string = None 36 | # indicates how many elements (starting from the last element f the list) 37 | # are part of the array currently being parsed 38 | # e.g. 3 means that the latest 3 elements in the 'strings' list are part of the same array 39 | # that is still being parsed 40 | current_array_reverse_index = 0 41 | current_call_index = 0 42 | 43 | for l in f.readlines(): 44 | l = l.lstrip() 45 | 46 | if not l: 47 | continue 48 | 49 | if not l.startswith(('.line', '.local', 'iput-object', 'sput-object', 'invoke')): 50 | current_const_string = None 51 | if not l.startswith(('const/4', 'const-string', 'aput-object', 'new-array', 'fill-array-data')): 52 | current_array_reverse_index = 0 53 | 54 | if l.startswith('.class'): 55 | match_class = is_class(l) 56 | if match_class: 57 | current_class = extract_class(match_class) 58 | 59 | elif l.startswith('.field'): 60 | match_class_property = is_class_property(l) 61 | if match_class_property: 62 | field = extract_class_property(match_class_property) 63 | if not field: 64 | logging.warning("Unable to extract class property from " + l) 65 | elif field['value'] and field['value'] != 'null': # we don't want empty strings or variables 66 | if not current_class: 67 | logging.warning("Cannot retrieve class information for local var! String: {0}".format( 68 | current_const_string)) 69 | cls = "" 70 | else: 71 | cls = current_class['name'] 72 | smali_string = SmaliString(SmaliString.TYPE_STATIC_VAR, cls, "", field['property_name'], 73 | field['value']) 74 | strings.append(smali_string) 75 | current_const_string = None 76 | current_array_reverse_index = 0 77 | 78 | elif l.startswith('const-string'): 79 | match_const_string = is_const_string_jumbo(l) 80 | if not match_const_string: 81 | match_const_string = is_const_string(l) 82 | if match_const_string: 83 | current_const_string = extract_const_string(match_const_string) 84 | if type(current_const_string) == list: 85 | for c in current_const_string: 86 | current_array_reverse_index += 1 87 | push_smali_string(strings, current_class, current_method, c) 88 | strings[-1].in_array = True 89 | current_const_string = None 90 | elif not current_const_string: 91 | logging.warning("Unable to extract current const string from " + l) 92 | else: 93 | push_smali_string(strings, current_class, current_method, current_const_string) 94 | else: 95 | logging.warning("unmatchable const-string: {0}".format(l)) 96 | 97 | elif l.startswith('.method'): 98 | match_class_method = is_class_method(l) 99 | if match_class_method: 100 | m = extract_class_method(match_class_method) 101 | if not m: 102 | logging.warning("Unable to extract class method from " + l) 103 | else: 104 | current_method = m 105 | current_call_index = 0 106 | 107 | elif l.startswith('invoke'): 108 | match_method_call = is_method_call(l) 109 | if match_method_call: 110 | m = extract_method_call(match_method_call) 111 | if not m: 112 | logging.warning("Unable to extract method call from " + l) 113 | else: 114 | # Add calling method (src) 115 | m['src'] = current_method['name'] 116 | 117 | # Add call index 118 | m['index'] = current_call_index 119 | current_call_index += 1 120 | 121 | if current_const_string and not current_array_reverse_index: 122 | strings[-1].parameter_of = m['to_class'] + "." + m['to_method'] 123 | strings[-1].string_type = SmaliString.TYPE_METHOD_PARAMETER 124 | elif not current_const_string and current_array_reverse_index and len(strings) > 0: 125 | end_index = max(-1, len(strings) - current_array_reverse_index - 1) 126 | for i in range(len(strings) - 1, end_index, -1): 127 | strings[i].parameter_of = m['to_class'] + "." + m['to_method'] 128 | strings[i].string_type = SmaliString.TYPE_METHOD_PARAMETER 129 | current_const_string = None 130 | current_array_reverse_index = 0 131 | 132 | elif l.startswith('aput-object'): 133 | match_aput_object = is_aput_object(l) 134 | if match_aput_object: 135 | aput_info = extract_aput_object(match_aput_object) 136 | if not aput_info: 137 | logging.warning("Unable to extract aput object from " + l) 138 | elif strings and strings[-1].name == aput_info['reference']: 139 | strings[-1].in_array = True 140 | current_array_reverse_index += 1 141 | current_const_string = None 142 | else: 143 | current_array_reverse_index = 0 144 | 145 | elif l.startswith('iput-object'): 146 | match_iput_object = is_iput_object(l) 147 | if match_iput_object: 148 | iput_info = extract_iput_object(match_iput_object) 149 | if not iput_info: 150 | logging.warning("Unable to extract iput object from " + l) 151 | elif current_const_string and not current_array_reverse_index: 152 | strings[-1].name = iput_info['variable_name'] 153 | strings[-1].string_type = SmaliString.TYPE_INSTANCE_VAR 154 | elif not current_const_string and current_array_reverse_index and len(strings) > 0: 155 | end_index = max(-1, len(strings) - current_array_reverse_index - 1) 156 | for i in range(len(strings) - 1, end_index, -1): 157 | strings[i].name = iput_info['variable_name'] 158 | strings[i].string_type = SmaliString.TYPE_INSTANCE_VAR 159 | else: 160 | pass 161 | # logging.warning("Encountered iput-object in an unexpected position in line " + l) 162 | current_const_string = None 163 | current_array_reverse_index = 0 164 | 165 | elif l.startswith('sput-object'): 166 | match_sput_object = is_sput_object(l) 167 | if match_sput_object: 168 | sput_info = extract_sput_object(match_sput_object) 169 | if not sput_info: 170 | logging.warning("Unable to extract spunt object from " + l) 171 | elif current_const_string and not current_array_reverse_index: 172 | strings[-1].name = sput_info['variable_name'] 173 | strings[-1].string_type = SmaliString.TYPE_STATIC_VAR 174 | elif not current_const_string and current_array_reverse_index and len(strings) > 0: 175 | end_index = max(-1, len(strings) - current_array_reverse_index - 1) 176 | for i in range(len(strings) - 1, end_index, -1): 177 | strings[i].name = sput_info['variable_name'] 178 | strings[i].string_type = SmaliString.TYPE_STATIC_VAR 179 | else: 180 | pass 181 | # logging.warning("Encountered sput-object in an unexpected position in line " + l) 182 | current_const_string = None 183 | current_array_reverse_index = 0 184 | 185 | elif l.startswith('.local'): 186 | match_local = is_local_debug_info(l) 187 | if match_local: 188 | local_debug_info = extract_local_debug_info(match_local) 189 | if not local_debug_info: 190 | logging.warning("Unable to extract local debug info from " + l) 191 | elif current_const_string and not current_array_reverse_index: 192 | strings[-1].name = local_debug_info['variable_name'] 193 | elif not current_const_string and current_array_reverse_index and len(strings) > 0: 194 | end_index = max(-1, len(strings) - current_array_reverse_index - 1) 195 | for i in range(len(strings) - 1, end_index, -1): 196 | strings[i].name = local_debug_info['variable_name'] 197 | else: 198 | pass 199 | # logging.warning("Encountered .local in an unexpected position in line " + l) 200 | current_const_string = None 201 | current_array_reverse_index = 0 202 | 203 | f.close() 204 | return strings 205 | 206 | 207 | def push_smali_string(strings, current_class, current_method, current_const_string): 208 | """ 209 | Adds a SmaliString resource to the strings list with as much information as possible 210 | 211 | :param strings: SmaliString list 212 | :param current_class: the class that is being parsed or None 213 | :param current_method: the method that is being parsed or None 214 | :param current_const_string: the string that is being parsed 215 | :return:True if an element was added, False otherwise 216 | :rtype: bool 217 | """ 218 | if not current_const_string: 219 | logging.debug("false") 220 | return False 221 | 222 | if not current_class: 223 | logging.warning("Cannot retrieve class information for local var! String: {0}".format(current_const_string)) 224 | cls = "" 225 | else: 226 | cls = current_class['name'] 227 | 228 | if not current_method: 229 | logging.warning( 230 | "Cannot retrieve method information for local var! String: {0}".format(current_const_string)) 231 | mthd = "" 232 | else: 233 | mthd = current_method['name'] 234 | 235 | var_name = current_const_string['name'] 236 | 237 | smali_string = SmaliString(SmaliString.TYPE_LOCAL_VAR, cls, mthd, var_name, current_const_string['value']) 238 | strings.append(smali_string) 239 | return True 240 | 241 | 242 | # thanks to 0rka for regex patterns 243 | # https://0rka.blog/2017/07/04/dalviks-smali-static-code-analysis-with-python/ 244 | regex_class_name = re.compile(r"^\.class.*\ (.+(?=\;))", re.MULTILINE) 245 | regex_method_data = re.compile(r'^\.method.+?\ (.+?(?=\())\((.*?)\)(.*?$)(.*?(?=\.end\ method))', 246 | re.MULTILINE | re.DOTALL) 247 | regex_called_methods = re.compile( 248 | r'invoke-.*?\ {(.*?)}, (.+?(?=;))\;\-\>(.+?(?=\())\((.*?)\)(.*?)(?=$|;)', re.MULTILINE | re.DOTALL) 249 | regex_move_result = re.compile(r'move-result.+?(.*?)$', re.MULTILINE | re.DOTALL) 250 | regex_class = re.compile("\.class\s+(?P.*);") 251 | regex_property = re.compile("\.field\s+(?P.*)") 252 | regex_const_string = re.compile("const-string\s+(?P.*)") 253 | regex_const_string_jumbo = re.compile("const-string/jumbo\s+(?P.*)") 254 | regex_method = re.compile("\.method\s+(?P.*)$") 255 | regex_invoke = re.compile("invoke-\w+(?P.*)") 256 | regex_aput_object = re.compile("aput-object\s+(?P.*)") 257 | regex_ipub_object = re.compile("iput-object\s+(?P.*)") 258 | regex_sput_object = re.compile("sput-object\s+(?P.*)") 259 | regex_local = re.compile(".local\s+(?P.*)") 260 | regex_extract_class = re.compile('(?P[^:]*):(?P[^(;|\s)]*)(;|\s)*(?P.*)') 261 | regex_value = re.compile('\s*=\s+(?P.*)') 262 | regex_var = re.compile('(?P.*),\s+"(?P.*)"') 263 | regex_class_method = re.compile("(?P.*)\((?P.*)\)(?P.*)") 264 | regex_method_call = re.compile('(?P\{.*\}),\s+(?P.*);->' 265 | '(?P.*)\((?P.*)\)(?P.*)') 266 | regex_extract_aput_object = re.compile('(?P.*),\s+(?P.*),\s+(?P.*)') 267 | regex_extract_iput_object = re.compile('(?P.*),\s+(?P.*),\s+(?P[^;]*);*->' 268 | '(?P[^:]*):(?P[^;]*);*') 269 | regex_extract_sput_object = re.compile('(?P.*),\s+(?P[^;]*);*->(?P[^:]*):(?P[^;]*);*') 270 | regex_extract_local = re.compile('(?P.*),\s+"(?P.*)":(?P[^;]*);*(?P.*)') 271 | 272 | 273 | def is_class(line): 274 | """Check if line contains a class definition 275 | 276 | :line: Text line to be checked 277 | 278 | :return: A one-element Tuple (basically a string) that contains class informations or None 279 | :rtype: str 280 | """ 281 | match = regex_class.search(line) 282 | if match: 283 | logging.debug("Found class: %s" % match.group('class')) 284 | return match.group('class') 285 | else: 286 | return None 287 | 288 | 289 | def is_class_property(line): 290 | """Check if line contains a field definition 291 | 292 | :line: Text line to be checked 293 | 294 | :return: A one-element Tuple (basically a string) that contains class property information or None 295 | :rtype: str 296 | """ 297 | match = regex_property.search(line) 298 | if match: 299 | logging.debug("Found property: %s" % match.group('property')) 300 | return match.group('property') 301 | else: 302 | return None 303 | 304 | 305 | def is_const_string(line): 306 | """Check if line contains a const-string 307 | 308 | :line: Text line to be checked 309 | 310 | :return: A one-element Tuple (basically a string) that contains const-string information or None 311 | :rtype: str 312 | """ 313 | match = regex_const_string.search(line) 314 | if match: 315 | logging.debug("Found const-string: %s" % match.group('const')) 316 | return match.group('const') 317 | else: 318 | return None 319 | 320 | 321 | def is_const_string_jumbo(line): 322 | """Check if line contains a const-string/jumbo 323 | 324 | :line: Text line to be checked 325 | 326 | :return: A one-element Tuple (basically a string) that contains const-string information or None 327 | :rtype: str 328 | """ 329 | match = regex_const_string_jumbo.search(line) 330 | if match: 331 | logging.debug("Found const-string/jumbo: %s" % match.group('const')) 332 | return match.group('const') 333 | else: 334 | return None 335 | 336 | 337 | def is_class_method(line): 338 | """Check if line contains a method definition 339 | 340 | :line: Text line to be checked 341 | 342 | :return: A one-element Tuple (basically a string) that contains method information or None 343 | :rtype: str 344 | """ 345 | match = regex_method.search(line) 346 | if match: 347 | logging.debug("Found method: %s" % match.group('method')) 348 | return match.group('method') 349 | else: 350 | return None 351 | 352 | 353 | def is_method_call(line): 354 | """Check [if the line contains a method call (invoke-*) 355 | 356 | :line: Text line to be checked 357 | 358 | :return: A one-element Tuple (basically a string) that contains call information or None 359 | :rtype: str 360 | """ 361 | match = regex_invoke.search(line) 362 | if match: 363 | logging.debug("Found invoke: %s" % match.group('invoke')) 364 | return match.group('invoke') 365 | else: 366 | return None 367 | 368 | 369 | def is_aput_object(line): 370 | """Check if line contains an aput-object command 371 | 372 | :line: Text line to be checked 373 | 374 | :return: A one-element Tuple (basically a string) that contains aput-object or None 375 | :rtype: str 376 | """ 377 | match = regex_aput_object.search(line) 378 | if match: 379 | logging.debug("Found aput-object: %s" % match.group('aput')) 380 | return match.group('aput') 381 | else: 382 | return None 383 | 384 | 385 | def is_iput_object(line): 386 | """Check if line contains an iput-object command 387 | 388 | :line: Text line to be checked 389 | 390 | :return: A one-element Tuple (basically a string) that contains iput-object or None 391 | :rtype: str 392 | """ 393 | match = regex_ipub_object.search(line) 394 | if match: 395 | logging.debug("Found iput-object: %s" % match.group('iput')) 396 | return match.group('iput') 397 | else: 398 | return None 399 | 400 | 401 | def is_sput_object(line): 402 | """Check if line contains an sput-object command 403 | 404 | :line: Text line to be checked 405 | 406 | :return: A one-element Tuple (basically a string) that contains sput-object or None 407 | :rtype: str 408 | """ 409 | match = regex_sput_object.search(line) 410 | if match: 411 | logging.debug("Found sput-object: %s" % match.group('sput')) 412 | return match.group('sput') 413 | else: 414 | return None 415 | 416 | 417 | def is_local_debug_info(line): 418 | """Check if line contains a local debug information 419 | 420 | :line: Text line to be checked 421 | 422 | :return: A one-element Tuple (basically a string) that contains debug information or None 423 | :rtype: str 424 | """ 425 | match = regex_local.search(line) 426 | if match: 427 | logging.debug("Found local debug info: %s" % match.group('local')) 428 | return match.group('local') 429 | else: 430 | return None 431 | 432 | 433 | def extract_class(data): 434 | """Extract class information from a string 435 | 436 | :data: Data would be sth like: public static Lcom/a/b/c 437 | 438 | :return: Returns a class object, otherwise None 439 | :rtype: dict 440 | """ 441 | class_info = data.split(" ") 442 | logging.debug("class_info: %s" % class_info[-1].split('/')[:-1]) 443 | c = { 444 | # Last element is the class name 445 | 'name': class_info[-1], 446 | 447 | # Package name 448 | 'package': ".".join(class_info[-1].split('/')[:-1]), 449 | 450 | # Class deepth 451 | 'depth': len(class_info[-1].split("/")), 452 | 453 | # All elements refer to the type of class 454 | 'type': " ".join(class_info[:-1]), 455 | 456 | # Properties 457 | 'properties': [], 458 | 459 | # Const strings 460 | 'const-strings': [], 461 | 462 | # Methods 463 | 'methods': [] 464 | } 465 | 466 | return c 467 | 468 | 469 | def extract_class_property(data): 470 | """Extract class property information from a string 471 | 472 | :data: Data would be sth like: private cacheSize:I 473 | 474 | :return: Returns a property object, otherwise None 475 | :rtype: dict 476 | """ 477 | 478 | match = regex_extract_class.search(data) 479 | 480 | if match: 481 | # A field/property is usually saved in this form 482 | # : 483 | # or 484 | # :; = 485 | 486 | dirty_value = match.group('dirtyvalue') 487 | value = "" 488 | 489 | if dirty_value: 490 | match2 = regex_value.search(dirty_value) 491 | if not match2: 492 | logging.warning("Unable to parse value for " + dirty_value) 493 | else: 494 | value = match2.group('value') 495 | if value.startswith('"') and value.endswith('"'): 496 | value = value[1:-1] 497 | 498 | modifiers = [] 499 | name = None 500 | name_modifiers = match.group('name') 501 | name_modifiers_splitted = name_modifiers.split(" ") 502 | for i in range(len(name_modifiers_splitted)): 503 | if i == len(name_modifiers_splitted) - 1: 504 | name = name_modifiers_splitted[i] 505 | else: 506 | modifiers.append(name_modifiers_splitted[i]) 507 | p = { 508 | # modifiers 509 | 'modifiers': modifiers, 510 | 511 | # Property name 512 | 'property_name': name, 513 | 514 | # Property type 515 | 'type': match.group('type') if len(match.group('type')) > 1 else '', 516 | 517 | # Value 518 | 'value': value 519 | } 520 | 521 | return p 522 | else: 523 | return None 524 | 525 | 526 | def extract_const_string(data): 527 | """Extract const string information from a string 528 | 529 | Warning: strings array seems to be practically indistinguishable from strings with ", ". 530 | e.g. 531 | 532 | The following is an array of two elements 533 | 534 | const/4 v0, 0x1 535 | new-array v0, v0, [Ljava/lang/String; 536 | const/4 v1, 0x0 537 | const-string v2, "NIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4, OIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 538 | aput-object v2, v0, v1 539 | 540 | It seems equal to this other case: 541 | 542 | const/4 v0, 0x2 543 | new-array v0, v0, [Ljava/lang/String; 544 | const/4 v1, 0x0 545 | const-string v2, "LIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 546 | aput-object v2, v0, v1 547 | 548 | But who says that in the second case the const-string last argument is just a string while in the 549 | first case the last arg are two elements of the array? 550 | 551 | :data: Data would be sth like: v0, "this is a string" 552 | 553 | :return: Returns a const string object, otherwise None 554 | :rtype: dict or list 555 | """ 556 | match = regex_var.search(data) 557 | 558 | if match: 559 | # A const string is usually saved in this form 560 | # , 561 | 562 | name = match.group('var') 563 | value = match.group('value') 564 | 565 | if ", " not in value: 566 | 567 | c = { 568 | # Variable 569 | 'name': name, 570 | 571 | # Value of string 572 | 'value': value 573 | } 574 | 575 | return c 576 | else: 577 | c = [] 578 | for val in value.split(", "): 579 | c.append({ 580 | 'name': name, 581 | 582 | 'value': val 583 | }) 584 | return c 585 | else: 586 | return None 587 | 588 | 589 | def extract_class_method(data): 590 | """Extract class method information from a string 591 | 592 | :data: Data would be sth like: 593 | public abstract isTrue(ILjava/lang/..;ILJava/string;)I 594 | 595 | :return: Returns a method object, otherwise None 596 | :rtype: dict 597 | """ 598 | method_info = data.split(" ") 599 | 600 | # A method looks like: 601 | # () 602 | m_name = method_info[-1] 603 | m_args = None 604 | m_ret = None 605 | 606 | # Search for name, arguments and return value 607 | match = regex_class_method.search(method_info[-1]) 608 | 609 | if match: 610 | m_name = match.group('name') 611 | m_args = match.group('args') 612 | m_ret = match.group('return') 613 | 614 | m = { 615 | # Method name 616 | 'name': m_name, 617 | 618 | # Arguments 619 | 'args': m_args, 620 | 621 | # Return value 622 | 'return': m_ret, 623 | 624 | # Additional info such as public static etc. 625 | 'type': " ".join(method_info[:-1]), 626 | 627 | # Calls 628 | 'calls': [] 629 | } 630 | 631 | return m 632 | 633 | 634 | def extract_method_call(data): 635 | """Extract method call information from a string 636 | 637 | :data: Data would be sth like: 638 | {v0}, Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String; 639 | 640 | :return: Returns a call object, otherwise None 641 | :rtype: dict 642 | """ 643 | # Default values 644 | c_dst_class = data 645 | c_dst_method = None 646 | c_local_args = None 647 | c_dst_args = None 648 | c_ret = None 649 | 650 | # The call looks like this 651 | # ) -> (args) 652 | match = regex_method_call.search(data) 653 | 654 | if match: 655 | c_dst_class = match.group('dst_class') 656 | c_dst_method = match.group('dst_method') 657 | c_dst_args = match.group('dst_args') 658 | c_local_args = match.group('local_args') 659 | c_ret = match.group('return') 660 | 661 | c = { 662 | # Destination class 663 | 'to_class': c_dst_class, 664 | 665 | # Destination method 666 | 'to_method': c_dst_method, 667 | 668 | # Local arguments 669 | 'local_args': c_local_args, 670 | 671 | # Destination arguments 672 | 'dst_args': c_dst_args, 673 | 674 | # Return value 675 | 'return': c_ret 676 | } 677 | 678 | return c 679 | 680 | 681 | def extract_aput_object(data): 682 | """Extract aput-object from a string 683 | 684 | :data: Data would be sth like: v2, v0, v1 685 | 686 | :return: Returns an aput-object, otherwise None 687 | :rtype: dict 688 | """ 689 | match = regex_extract_aput_object.search(data) 690 | 691 | if match: 692 | # An aput-object is usually saved in this form 693 | # , , 694 | 695 | a = { 696 | # referenced const string 697 | 'reference': match.group('reference'), 698 | 699 | # array var 700 | 'array': match.group('array'), 701 | 702 | # reference index 703 | 'index': match.group('index') 704 | } 705 | 706 | return a 707 | else: 708 | return None 709 | 710 | 711 | def extract_iput_object(data): 712 | """Extract iput-object from a string 713 | 714 | :data: Data would be sth like: v0, p0, Lcom/example/package/class;->varname:[Ljava/lang/String; 715 | 716 | :return: Returns an iput-object, otherwise None 717 | :rtype: dict 718 | """ 719 | match = regex_extract_iput_object.search(data) 720 | 721 | if match: 722 | # An aput-object is usually saved in this form 723 | # , , ;->:; 724 | 725 | i = { 726 | # referenced variable 727 | 'reference': match.group('ref'), 728 | 729 | # instance reference 730 | 'instance': match.group('instance'), 731 | 732 | # package 733 | 'package': match.group('pkg'), 734 | 735 | # variable name 736 | 'variable_name': match.group('name'), 737 | 738 | # type 739 | 'type': match.group('type') 740 | } 741 | 742 | return i 743 | else: 744 | return None 745 | 746 | 747 | def extract_sput_object(data): 748 | """Extract sput-object from a string 749 | 750 | :data: Data would be sth like: v0, Lcom/example/package/class;->varname:[Ljava/lang/String; 751 | 752 | :return: Returns a sput-object, otherwise None 753 | :rtype: dict 754 | """ 755 | match = regex_extract_sput_object.search(data) 756 | 757 | if match: 758 | # An aput-object is usually saved in this form 759 | # , ;->:; 760 | 761 | s = { 762 | # referenced variable 763 | 'reference': match.group('ref'), 764 | 765 | # package 766 | 'package': match.group('pkg'), 767 | 768 | # variable name 769 | 'variable_name': match.group('name'), 770 | 771 | # type 772 | 'type': match.group('type') 773 | } 774 | 775 | return s 776 | else: 777 | return None 778 | 779 | 780 | def extract_local_debug_info(data): 781 | """Extract .local debug information 782 | 783 | :data: Data would be sth like: .local v1, "future":Lcom/android/volley/toolbox/RequestFuture;, 784 | "Lcom/android/volley/toolbox/RequestFuture;" 785 | 786 | :return: Returns a const string object, otherwise None 787 | :rtype: dict 788 | """ 789 | match = regex_extract_local.search(data) 790 | 791 | if match: 792 | # A .local debug info is usually saved in this form 793 | # ,:; 794 | 795 | l = { 796 | # Variable 797 | 'name': match.group('var'), 798 | 799 | # Value of string 800 | 'variable_name': match.group('name'), 801 | 802 | # Type of variable 803 | 'type': match.group('type'), 804 | 805 | # Other part of the line 806 | 'type_signature': match.group('signature') 807 | } 808 | 809 | return l 810 | else: 811 | return None 812 | 813 | 814 | def main(argv): 815 | if len(argv) != 2: 816 | print("Usage: python {0} path/to/smali/file.smali".format(argv[0])) 817 | return 818 | logging.basicConfig(level=logging.DEBUG) 819 | test_instance = SmaliParser(argv[1]) 820 | strings = test_instance.get_strings() 821 | strings.sort(key=lambda x: x.value) 822 | for s in strings: 823 | print("{0}".format(s)) 824 | 825 | 826 | if __name__ == '__main__': 827 | main(sys.argv) 828 | -------------------------------------------------------------------------------- /my_tools/strings_tool.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple python3 implementation of Unix's strings utility, with some reverse-engineering utility 3 | 4 | """ 5 | import re 6 | import sys 7 | from mmap import ALLOCATIONGRANULARITY 8 | from mmap import mmap, ACCESS_READ 9 | 10 | from elftools.elf.elffile import ELFFile 11 | 12 | 13 | def strings(file_name, sections=None, min_length=4): 14 | """ 15 | Finds all strings in a file; if it's an ELF file, you can specify where (in which section) to 16 | look for the strings. 17 | 18 | :param file_name: name of the file to be examined 19 | :param sections: a list of strings which identify the ELF sections; should be used only with ELF files 20 | :param min_length: 21 | :return: 22 | """ 23 | pattern = '([\x20-\x7E]{' + str(min_length) + '}[\x20-\x7E]*)' # ASCII table from character space to tilde 24 | pattern = pattern.encode() 25 | regexp = re.compile(pattern) 26 | if not sections: 27 | with open(file_name, 'rb') as f, mmap(f.fileno(), 0, access=ACCESS_READ) as m: 28 | for match in regexp.finditer(m): 29 | yield str(match.group(0), 'utf-8') 30 | else: 31 | with open(file_name, 'rb') as f: 32 | elffile = ELFFile(f) 33 | for section in sections: 34 | try: 35 | sec = elffile.get_section_by_name(section) 36 | except AttributeError: 37 | # section not found 38 | continue 39 | # skip section if missing in elf file 40 | if not sec: 41 | continue 42 | offset = sec['sh_offset'] 43 | size = sec['sh_size'] 44 | if offset is None or size is None: 45 | continue 46 | # round to allocation granularity for mmap 47 | offset = max(offset - offset % ALLOCATIONGRANULARITY, 0) 48 | with mmap(f.fileno(), size, access=ACCESS_READ, offset=offset) as m: 49 | for match in regexp.finditer(m): 50 | yield str(match.group(0), 'utf-8') 51 | 52 | 53 | def main(argv): 54 | if len(argv) == 2: 55 | for word in strings(sys.argv[1]): 56 | print(word) 57 | elif len(argv) == 3: 58 | for word in strings(sys.argv[1], None, sys.argv[2]): 59 | print(word) 60 | else: 61 | print("Usage: python {0} path/to/file [min_string_length]".format(argv[0])) 62 | 63 | 64 | if __name__ == '__main__': 65 | main(sys.argv) 66 | -------------------------------------------------------------------------------- /my_tools/strings_xml_parser.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from lxml import etree 4 | 5 | from my_model.resource_string import ResourceString 6 | 7 | 8 | class AndroidStringsXmlParser(object): 9 | """ 10 | Parser utility for Android's strings.xml file 11 | """ 12 | 13 | def __init__(self, xml_file): 14 | """ 15 | constructor for AndroidXmlParser object 16 | 17 | :param xml_file: path of the Android's string resource (strings.xml) file to be parsed 18 | """ 19 | path = xml_file 20 | self.root = etree.parse(path, etree.XMLParser(encoding='utf-8', recover=True)).getroot() 21 | 22 | def get_all_elements(self): 23 | return list(self.root.iter()) 24 | 25 | def get_string_resources(self, value_filter=None): 26 | """ 27 | Gets all string and string-array elements from Android's strings.xml resource files 28 | 29 | Example: 30 | 31 | resources = test_instance.get_all_string_resources() 32 | 33 | for res in resources: 34 | print("{0} {1} {2}".format(res.row_type, res.name, res.value)) 35 | 36 | :return: a list of ResourceString object. 37 | :rtype: list 38 | """ 39 | resources = [] 40 | for element in self.root.findall("string"): 41 | resource = ResourceString(ResourceString.TYPE_STRING, element.get("name"), element.text) 42 | if value_filter is None or value_filter(resource): 43 | resources.append(resource) 44 | 45 | for element in self.root.findall("string-array"): 46 | array_name = element.get("name") 47 | for item in element.findall("item"): 48 | resource = ResourceString(ResourceString.TYPE_ARRAY_ELEMENT, array_name, item.text) 49 | if value_filter is None or value_filter(resource): 50 | resources.append(resource) 51 | return resources 52 | 53 | 54 | def main(argv): 55 | if len(argv) != 2 and len(argv) != 3: 56 | print("Usage: python {0} path/xml/file.xml [substring_to_find]".format(argv[0])) 57 | return 58 | test_instance = AndroidStringsXmlParser(argv[1]) 59 | if len(argv) == 2: 60 | resources = test_instance.get_string_resources() 61 | else: 62 | def value_filter(s): 63 | if argv[2] in s: 64 | return True 65 | return False 66 | 67 | resources = test_instance.get_string_resources(value_filter) 68 | for res in resources: 69 | print(res.name) 70 | print(res.value.decode('utf-8')) 71 | # print("{0} {1} {2}".format(res.row_type, res.name, res.value)) 72 | 73 | 74 | if __name__ == '__main__': 75 | main(sys.argv) 76 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rstr==2.2.6 2 | lxml==4.1.0 3 | pymongo==3.5.1 4 | numpy==1.13.3 5 | scipy==0.19.1 6 | retry_decorator==1.1.0 7 | matplotlib==2.1.0 8 | plotly==2.2.1 9 | flufl.lock==3.2 10 | pyelftools==0.24 11 | scikit_learn==0.19.1 12 | pyyaml>=4.2b1 13 | -------------------------------------------------------------------------------- /test/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/AndroidManifest2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /test/AndroidManifest3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/AndroidManifest4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/JavaKey.smali: -------------------------------------------------------------------------------- 1 | .class public Lit/uniroma2/adidiego/apikeytestapp/JavaKey; 2 | .super Ljava/lang/Object; 3 | .source "JavaKey.java" 4 | 5 | 6 | # static fields 7 | .field public static final API_KEY_FINAL_STATIC:Ljava/lang/String; = "GIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 8 | 9 | .field public static final API_KEY_FINAL_STATIC_ARRAY:[Ljava/lang/String; 10 | 11 | .field public static apiKeyStatic:Ljava/lang/String; 12 | 13 | .field public static apiKeyStaticArray:[Ljava/lang/String; 14 | 15 | 16 | # instance fields 17 | .field private apiKeyPrivate:Ljava/lang/String; 18 | 19 | .field private apiKeyPrivateArray:[Ljava/lang/String; 20 | 21 | .field public apiKeyPublic:Ljava/lang/String; 22 | 23 | .field public apiKeyPublicArray:[Ljava/lang/String; 24 | 25 | 26 | # direct methods 27 | .method static constructor ()V 28 | .locals 5 29 | 30 | .prologue 31 | const/4 v4, 0x2 32 | 33 | const/4 v3, 0x1 34 | 35 | const/4 v2, 0x0 36 | 37 | .line 13 38 | const-string v0, "FIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 39 | 40 | sput-object v0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey;->apiKeyStatic:Ljava/lang/String; 41 | 42 | .line 14 43 | new-array v0, v4, [Ljava/lang/String; 44 | 45 | const-string v1, "TIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 46 | 47 | aput-object v1, v0, v2 48 | 49 | const-string v1, "UIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 50 | 51 | aput-object v1, v0, v3 52 | 53 | sput-object v0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey;->apiKeyStaticArray:[Ljava/lang/String; 54 | 55 | .line 16 56 | new-array v0, v4, [Ljava/lang/String; 57 | 58 | const-string v1, "VIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 59 | 60 | aput-object v1, v0, v2 61 | 62 | const-string v1, "WIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 63 | 64 | aput-object v1, v0, v3 65 | 66 | sput-object v0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey;->API_KEY_FINAL_STATIC_ARRAY:[Ljava/lang/String; 67 | 68 | return-void 69 | .end method 70 | 71 | .method public constructor ()V 72 | .locals 5 73 | 74 | .prologue 75 | const/4 v4, 0x2 76 | 77 | const/4 v3, 0x1 78 | 79 | const/4 v2, 0x0 80 | 81 | .line 7 82 | invoke-direct {p0}, Ljava/lang/Object;->()V 83 | 84 | .line 9 85 | const-string v0, "DIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 86 | 87 | iput-object v0, p0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey;->apiKeyPublic:Ljava/lang/String; 88 | 89 | .line 10 90 | new-array v0, v4, [Ljava/lang/String; 91 | 92 | const-string v1, "PIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 93 | 94 | aput-object v1, v0, v2 95 | 96 | const-string v1, "QIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 97 | 98 | aput-object v1, v0, v3 99 | 100 | iput-object v0, p0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey;->apiKeyPublicArray:[Ljava/lang/String; 101 | 102 | .line 11 103 | const-string v0, "EIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 104 | 105 | iput-object v0, p0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey;->apiKeyPrivate:Ljava/lang/String; 106 | 107 | .line 12 108 | new-array v0, v4, [Ljava/lang/String; 109 | 110 | const-string v1, "RIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 111 | 112 | aput-object v1, v0, v2 113 | 114 | const-string v1, "SIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 115 | 116 | aput-object v1, v0, v3 117 | 118 | iput-object v0, p0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey;->apiKeyPrivateArray:[Ljava/lang/String; 119 | 120 | return-void 121 | .end method 122 | 123 | 124 | # virtual methods 125 | .method public getGlobalPrivateKey()Ljava/lang/String; 126 | .locals 1 127 | 128 | .prologue 129 | .line 37 130 | iget-object v0, p0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey;->apiKeyPrivate:Ljava/lang/String; 131 | 132 | return-object v0 133 | .end method 134 | 135 | .method public getGlobalPrivateKeyArray()[Ljava/lang/String; 136 | .locals 1 137 | 138 | .prologue 139 | .line 41 140 | iget-object v0, p0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey;->apiKeyPrivateArray:[Ljava/lang/String; 141 | 142 | return-object v0 143 | .end method 144 | 145 | .method public getLocalKey()Ljava/lang/String; 146 | .locals 1 147 | 148 | .prologue 149 | .line 19 150 | const-string v0, "BIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 151 | 152 | .line 20 153 | .local v0, "apiKeyLocal":Ljava/lang/String; 154 | return-object v0 155 | .end method 156 | 157 | .method public getLocalKeyArray()[Ljava/lang/String; 158 | .locals 3 159 | 160 | .prologue 161 | .line 24 162 | const/4 v1, 0x2 163 | 164 | new-array v0, v1, [Ljava/lang/String; 165 | 166 | const/4 v1, 0x0 167 | 168 | const-string v2, "LIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 169 | 170 | aput-object v2, v0, v1 171 | 172 | const/4 v1, 0x1 173 | 174 | const-string v2, "MIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 175 | 176 | aput-object v2, v0, v1 177 | 178 | .line 25 179 | .local v0, "apiKeyLocalArray":[Ljava/lang/String; 180 | return-object v0 181 | .end method 182 | 183 | .method public getLocalReturnKey()Ljava/lang/String; 184 | .locals 1 185 | 186 | .prologue 187 | .line 29 188 | const-string v0, "CIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 189 | 190 | return-object v0 191 | .end method 192 | 193 | .method public getLocalReturnKeyArray()[Ljava/lang/String; 194 | .locals 3 195 | 196 | .prologue 197 | .line 33 198 | const/4 v0, 0x1 199 | 200 | new-array v0, v0, [Ljava/lang/String; 201 | 202 | const/4 v1, 0x0 203 | 204 | const-string v2, "NIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4, OIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 205 | 206 | aput-object v2, v0, v1 207 | 208 | return-object v0 209 | .end method 210 | 211 | .method public printKey()V 212 | .locals 2 213 | 214 | .prologue 215 | .line 45 216 | const-class v0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey; 217 | 218 | invoke-virtual {v0}, Ljava/lang/Class;->getSimpleName()Ljava/lang/String; 219 | 220 | move-result-object v0 221 | 222 | const-string v1, "KIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 223 | 224 | invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 225 | 226 | .line 46 227 | return-void 228 | .end method 229 | 230 | .method public printKeyArray()V 231 | .locals 4 232 | 233 | .prologue 234 | .line 49 235 | const-class v0, Lit/uniroma2/adidiego/apikeytestapp/JavaKey; 236 | 237 | invoke-virtual {v0}, Ljava/lang/Class;->getSimpleName()Ljava/lang/String; 238 | 239 | move-result-object v0 240 | 241 | const/4 v1, 0x1 242 | 243 | new-array v1, v1, [Ljava/lang/String; 244 | 245 | const/4 v2, 0x0 246 | 247 | const-string v3, "XIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4, YIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4" 248 | 249 | aput-object v3, v1, v2 250 | 251 | invoke-static {v1}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String; 252 | 253 | move-result-object v1 254 | 255 | invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 256 | 257 | .line 50 258 | return-void 259 | .end method 260 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbhunter/apk_api_key_extractor/cbc2a8b02a5758886ac6aa08ed4b1b193e2b02aa/test/__init__.py -------------------------------------------------------------------------------- /test/test_manifest_parser.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from my_tools.manifest_parser import AndroidManifestXmlParser 5 | 6 | __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) 7 | 8 | 9 | class AbleToRetrieveMainActivity(unittest.TestCase): 10 | """AndroidManifestXmlParser should be able to retrieve the main activity name""" 11 | 12 | def test_get_main_activity_name_simple(self): 13 | parser = AndroidManifestXmlParser(os.path.join(__location__, "AndroidManifest.xml")) 14 | self.assertEqual(parser.get_main_activity_name(), "it.uniroma2.adidiego.apikeytestapp.MainActivity") 15 | 16 | def test_get_main_activity_name_alias(self): 17 | parser = AndroidManifestXmlParser(os.path.join(__location__, "AndroidManifest2.xml")) 18 | self.assertEqual(parser.get_main_activity_name(), "com.xlythe.calculator.material.Calculator") 19 | 20 | def test_get_main_activity_name_relative(self): 21 | parser = AndroidManifestXmlParser(os.path.join(__location__, "AndroidManifest3.xml")) 22 | self.assertEqual(parser.get_main_activity_name(), "com.ducky.tracedrawingLite.MainActivity") 23 | 24 | def test_get_main_activity_name_relative2(self): 25 | parser = AndroidManifestXmlParser(os.path.join(__location__, "AndroidManifest4.xml")) 26 | self.assertEqual(parser.get_main_activity_name(), "com.WiringDiagramMobil.TroneStudio.WiringDiagramMobil") 27 | -------------------------------------------------------------------------------- /test/test_smali_parser.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from my_tools.smali_parser import SmaliParser 5 | 6 | __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) 7 | 8 | 9 | class ContainsInstances(unittest.TestCase): 10 | """SmaliParser should be able to parse certain predetermined strings in a test smali file""" 11 | 12 | def test_get_strings(self): 13 | parser = SmaliParser(os.path.join(__location__, "JavaKey.smali")) 14 | mystrings = parser.get_strings() 15 | 16 | static_final_var = {'method_name': '', 'parameter_of': None, 17 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 18 | 'source': 'TYPE_STATIC_VAR', 'value': 'GIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 19 | 'in_array': False, 20 | 'name': 'API_KEY_FINAL_STATIC'} 21 | contains_static_final_var = False 22 | static_var = {'method_name': '', 'parameter_of': None, 23 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 24 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_STATIC_VAR', 25 | 'value': 'FIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': False, 'name': 'apiKeyStatic'} 26 | contains_static_var = False 27 | static_array1 = {'method_name': '', 'parameter_of': None, 28 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 29 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_STATIC_VAR', 30 | 'value': 'TIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 31 | 'name': 'apiKeyStaticArray'} 32 | contains_static_array1 = False 33 | static_array2 = {'method_name': '', 'parameter_of': None, 34 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 35 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_STATIC_VAR', 36 | 'value': 'UIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 37 | 'name': 'apiKeyStaticArray'} 38 | contains_static_array2 = False 39 | static_final_array1 = {'method_name': '', 'parameter_of': None, 40 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 41 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_STATIC_VAR', 42 | 'value': 'VIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 43 | 'name': 'API_KEY_FINAL_STATIC_ARRAY'} 44 | contains_static_final_array1 = False 45 | static_final_array2 = {'method_name': '', 'parameter_of': None, 46 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 47 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_STATIC_VAR', 48 | 'value': 'WIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 49 | 'name': 'API_KEY_FINAL_STATIC_ARRAY'} 50 | contains_static_final_array2 = False 51 | global_public_var = {'method_name': '', 'parameter_of': None, 52 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 53 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_INSTANCE_VAR', 54 | 'value': 'DIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': False, 55 | 'name': 'apiKeyPublic'} 56 | contains_global_public_var = False 57 | global_public_array1 = {'method_name': '', 'parameter_of': None, 58 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 59 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_INSTANCE_VAR', 60 | 'value': 'PIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 61 | 'name': 'apiKeyPublicArray'} 62 | contains_global_public_array1 = False 63 | global_public_array2 = {'method_name': '', 'parameter_of': None, 64 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 65 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_INSTANCE_VAR', 66 | 'value': 'QIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 67 | 'name': 'apiKeyPublicArray'} 68 | contains_global_public_array2 = False 69 | global_private_var = {'method_name': '', 'parameter_of': None, 70 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 71 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_INSTANCE_VAR', 72 | 'value': 'EIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': False, 73 | 'name': 'apiKeyPrivate'} 74 | contains_global_private_var = False 75 | global_private_array1 = {'method_name': '', 'parameter_of': None, 76 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 77 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_INSTANCE_VAR', 78 | 'value': 'RIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 79 | 'name': 'apiKeyPrivateArray'} 80 | contains_global_private_array1 = False 81 | global_private_array2 = {'method_name': '', 'parameter_of': None, 82 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 83 | 'source': 'TYPE_LOCAL_VAR', 'string_type': 'TYPE_INSTANCE_VAR', 84 | 'value': 'SIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 85 | 'name': 'apiKeyPrivateArray'} 86 | contains_global_private_array2 = False 87 | local_var = {'method_name': 'getLocalKey', 'parameter_of': None, 88 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 'source': 'TYPE_LOCAL_VAR', 89 | 'value': 'BIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': False, 'name': 'apiKeyLocal'} 90 | contains_local_var = False 91 | local_array1 = {'method_name': 'getLocalKeyArray', 'parameter_of': None, 92 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 'source': 'TYPE_LOCAL_VAR', 93 | 'value': 'LIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 94 | 'name': 'apiKeyLocalArray'} 95 | contains_local_array1 = False 96 | local_array2 = {'method_name': 'getLocalKeyArray', 'parameter_of': None, 97 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 'source': 'TYPE_LOCAL_VAR', 98 | 'value': 'MIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 99 | 'name': 'apiKeyLocalArray'} 100 | contains_local_array2 = False 101 | method_return = {'method_name': 'getLocalReturnKey', 'parameter_of': None, 102 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 'source': 'TYPE_LOCAL_VAR', 103 | 'value': 'CIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': False, 'name': 'v0'} 104 | contains_method_return = False 105 | method_return_array1 = {'method_name': 'getLocalReturnKeyArray', 'parameter_of': None, 106 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 'source': 'TYPE_LOCAL_VAR', 107 | 'value': 'NIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 'name': 'v2'} 108 | contains_method_return_array1 = False 109 | method_return_array2 = {'method_name': 'getLocalReturnKeyArray', 'parameter_of': None, 110 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 'source': 'TYPE_LOCAL_VAR', 111 | 'value': 'OIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 'name': 'v2'} 112 | contains_method_return_array2 = False 113 | method_parameter = {'method_name': 'printKey', 'parameter_of': 'Ljava/util/Arrays.toString', 114 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 'source': 'TYPE_LOCAL_VAR', 115 | 'string_type': 'TYPE_METHOD_PARAMETER', 'value': 'KIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 116 | 'in_array': False, 117 | 'name': 'v1'} 118 | contains_method_parameter = False 119 | method_parameter_array1 = {'method_name': 'printKeyArray', 'parameter_of': 'Ljava/util/Arrays.toString', 120 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 121 | 'source': 'TYPE_LOCAL_VAR', 122 | 'string_type': 'TYPE_METHOD_PARAMETER', 123 | 'value': 'XIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 124 | 'name': 'v3'} 125 | contains_method_parameter_array1 = False 126 | method_parameter_array2 = {'method_name': 'printKeyArray', 'parameter_of': 'Ljava/util/Arrays.toString', 127 | 'class_name': 'Lit/uniroma2/adidiego/apikeytestapp/JavaKey', 128 | 'source': 'TYPE_LOCAL_VAR', 129 | 'string_type': 'TYPE_METHOD_PARAMETER', 130 | 'value': 'YIzaSyCuxR_sUTfFJZBDkIsauakeuqXaFxhbur4', 'in_array': True, 131 | 'name': 'v3'} 132 | contains_method_parameter_array2 = False 133 | 134 | for mystring in mystrings: 135 | if mystring.__dict__ == static_final_var: 136 | contains_static_final_var = True 137 | elif mystring.__dict__ == static_var: 138 | contains_static_var = True 139 | elif mystring.__dict__ == static_array1: 140 | contains_static_array1 = True 141 | elif mystring.__dict__ == static_array2: 142 | contains_static_array2 = True 143 | elif mystring.__dict__ == static_final_array1: 144 | contains_static_final_array1 = True 145 | elif mystring.__dict__ == static_final_array2: 146 | contains_static_final_array2 = True 147 | elif mystring.__dict__ == global_public_var: 148 | contains_global_public_var = True 149 | elif mystring.__dict__ == global_public_array1: 150 | contains_global_public_array1 = True 151 | elif mystring.__dict__ == global_public_array2: 152 | contains_global_public_array2 = True 153 | elif mystring.__dict__ == global_private_var: 154 | contains_global_private_var = True 155 | elif mystring.__dict__ == global_private_array1: 156 | contains_global_private_array1 = True 157 | elif mystring.__dict__ == global_private_array2: 158 | contains_global_private_array2 = True 159 | elif mystring.__dict__ == local_var: 160 | contains_local_var = True 161 | elif mystring.__dict__ == local_array1: 162 | contains_local_array1 = True 163 | elif mystring.__dict__ == local_array2: 164 | contains_local_array2 = True 165 | elif mystring.__dict__ == method_return: 166 | contains_method_return = True 167 | elif mystring.__dict__ == method_return_array1: 168 | contains_method_return_array1 = True 169 | elif mystring.__dict__ == method_return_array2: 170 | contains_method_return_array2 = True 171 | elif mystring.__dict__ == method_parameter: 172 | contains_method_parameter = True 173 | elif mystring.__dict__ == method_parameter_array1: 174 | contains_method_parameter_array1 = True 175 | elif mystring.__dict__ == method_parameter_array2: 176 | contains_method_parameter_array2 = True 177 | self.assertTrue(contains_static_final_var) 178 | self.assertTrue(contains_static_var) 179 | self.assertTrue(contains_static_array1) 180 | self.assertTrue(contains_static_array2) 181 | self.assertTrue(contains_static_final_array1) 182 | self.assertTrue(contains_static_final_array2) 183 | self.assertTrue(contains_global_public_var) 184 | self.assertTrue(contains_global_public_array1) 185 | self.assertTrue(contains_global_public_array2) 186 | self.assertTrue(contains_global_private_var) 187 | self.assertTrue(contains_global_private_array1) 188 | self.assertTrue(contains_global_private_array2) 189 | self.assertTrue(contains_local_var) 190 | self.assertTrue(contains_local_array1) 191 | self.assertTrue(contains_local_array2) 192 | self.assertTrue(contains_method_return) 193 | self.assertTrue(contains_method_return_array1) 194 | self.assertTrue(contains_method_return_array2) 195 | self.assertTrue(contains_method_parameter) 196 | self.assertTrue(contains_method_parameter_array1) 197 | self.assertTrue(contains_method_parameter_array2) 198 | --------------------------------------------------------------------------------