├── .gitignore ├── LICENSE ├── README.md ├── adb.py ├── api-android-monitor ├── api-monitor-native.json ├── api-monitor.json ├── frida_script_template.js └── papi-monitor.js ├── api-example └── hooks.json ├── img_repo └── papimonitor.gif ├── papi-monitor.png ├── papi_monitor.py ├── requirements.txt ├── resources └── frida-server │ └── .gitkeep ├── scripts-android ├── file-system-monitor.js ├── get-app-env-information.js ├── load-stetho.js ├── native-hook-template.js └── string-compare.js └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | frida-env/ 4 | logs/ 5 | venv-linux/ 6 | test.sh 7 | cert-proxy/ 8 | api-example/api-example-conf.json 9 | # User-specific stuff: 10 | .idea/workspace.xml 11 | .idea/tasks.xml 12 | 13 | # Sensitive or high-churn files: 14 | .idea/dataSources/ 15 | .idea/dataSources.ids 16 | .idea/dataSources.xml 17 | .idea/dataSources.local.xml 18 | .idea/sqlDataSources.xml 19 | .idea/dynamic.xml 20 | .idea/uiDesigner.xml 21 | 22 | # Gradle: 23 | .idea/gradle.xml 24 | .idea/libraries 25 | 26 | # Mongo Explorer plugin: 27 | .idea/mongoSettings.xml 28 | 29 | ## File-based project format: 30 | *.iws 31 | 32 | ## Plugin-specific files: 33 | 34 | # IntelliJ 35 | /out/ 36 | 37 | # mpeltonen/sbt-idea plugin 38 | .idea_modules/ 39 | 40 | # JIRA plugin 41 | atlassian-ide-plugin.xml 42 | 43 | # Crashlytics plugin (for Android Studio and IntelliJ) 44 | com_crashlytics_export_strings.xml 45 | crashlytics.properties 46 | crashlytics-build.properties 47 | fabric.properties 48 | 49 | # Byte-compiled / optimized / DLL files 50 | __pycache__/ 51 | *.py[cod] 52 | *$py.class 53 | 54 | # C extensions 55 | *.so 56 | 57 | # Distribution / packaging 58 | .Python 59 | env/ 60 | build/ 61 | develop-eggs/ 62 | dist/ 63 | downloads/ 64 | eggs/ 65 | .eggs/ 66 | lib/ 67 | lib64/ 68 | parts/ 69 | sdist/ 70 | var/ 71 | *.egg-info/ 72 | .installed.cfg 73 | *.egg 74 | 75 | # PyInstaller 76 | # Usually these files are written by a python script from a template 77 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 78 | *.manifest 79 | *.spec 80 | 81 | # Installer logs 82 | pip-log.txt 83 | pip-delete-this-directory.txt 84 | 85 | # Unit test / coverage reports 86 | htmlcov/ 87 | .tox/ 88 | .coverage 89 | .coverage.* 90 | .cache 91 | nosetests.xml 92 | coverage.xml 93 | *,cover 94 | .hypothesis/ 95 | 96 | # Translations 97 | *.mo 98 | *.pot 99 | 100 | # Django stuff: 101 | *.log 102 | local_settings.py 103 | 104 | # Flask stuff: 105 | instance/ 106 | .webassets-cache 107 | 108 | # Scrapy stuff: 109 | .scrapy 110 | 111 | # Sphinx documentation 112 | docs/_build/ 113 | 114 | # PyBuilder 115 | target/ 116 | 117 | # Jupyter Notebook 118 | .ipynb_checkpoints 119 | 120 | # pyenv 121 | .python-version 122 | 123 | # celery beat schedule file 124 | celerybeat-schedule 125 | 126 | # dotenv 127 | .env 128 | 129 | # virtualenv 130 | .venv/ 131 | venv/ 132 | ENV/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # Files and folders used during testing 141 | test.apk 142 | /test_output 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Davide Caputo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://raw.githubusercontent.com/Dado1513/PAPIMonitor/master/papi-monitor.png) 2 | 3 | 4 | [![Python Version](https://img.shields.io/badge/Python-3.5%2B-green.svg?logo=python&logoColor=white)](https://www.python.org/downloads/) 5 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Dado1513/AndroidApiMonitoring/blob/master/LICENSE) 6 | 7 | **PAPIMonitor** (**P**ython **API** **Monitor** for Android apps) is a python tool based on [Frida](https://frida.re/docs/android/) for monitoring user-select APIs during the app execution. 8 | The app should be installed within an emulator already connected through ADB to the pc host. 9 | The user can choose to monitor a predefined list of APIs divided into several categories (e.g., Device Data, Device Info, SMS) or a custom list of APIs passed through the command line to the script. 10 | The tool stores the invoked API, the parameters, the return value, and the line and file from where it was called. 11 | 12 | Below is an example of output: 13 | ```json 14 | { 15 | "category": "Custom", 16 | "class": "com.dave.popupre.MainActivity", 17 | "method": "getText", 18 | "args": [], 19 | "calledFrom": "com.dave.popupre.MainActivity$1.onClick(MainActivity.java:26)", 20 | "returnValue": "Hello Toast!", 21 | "time": "03/09/2021, 14:43:06" 22 | } 23 | 24 | ``` 25 | 26 | :warning: **Warning with Google Emulator** 27 | 28 | | Google Emulator | Ubuntu | Windows | MacOS | 29 | |:----------------------:|:------------------------:|:------------------------:|:------------------------:| 30 | | **7.x x86** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 31 | | **8.x x86** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 32 | | **9.0 x86** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 33 | | **10.0 x86** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 34 | | **11.0 x86** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 35 | | **12.0 x86** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 36 | 37 | :warning: Frida seems broken on Android 11-12 (x86_64) (Google Emulatore). 38 | - [issue-1917](https://github.com/frida/frida/issues/1917) 39 | - [issue-1977](https://github.com/frida/frida/issues/1977) 40 | - [issue-1982](https://github.com/frida/frida/issues/1982) 41 | 42 | --- 43 | 44 | ## Installation 45 | General requirements: 46 | ```bash 47 | sudo apt-get install libjpeg-dev zlib1g-dev 48 | ``` 49 | 50 | ### Installation with pyenv and virtualenv 51 | - Install [pyenv](https://github.com/pyenv/pyenv) and [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv) 52 | ```bash 53 | pyenv install 3.8.0 54 | pyenv virtualenv 3.8.0 papi-monitor 55 | pyenv activate 3.8.0/envs/papi-monitor 56 | pip3 install -r requirements 57 | ``` 58 | 59 | ### Installation with virtualenv 60 | 61 | - install virtualenv 62 | 63 | ```bash 64 | sudo apt-get install python3-virtualenv 65 | ``` 66 | - activate virtualenv 67 | ```bash 68 | virtualenv env 69 | source env/bin/activate 70 | ``` 71 | - install requirements 72 | 73 | ```bash 74 | pip install -r requirements 75 | ``` 76 | 77 | - Download frida-server in `resources/frida-server/` 78 | 79 | ## Post Installation 80 | - adb in path file 81 | - emulator/device already running and connect 82 | 83 | --- 84 | 85 | ## Usage 86 | 87 | ```bash 88 | python papi_monitor.py --package-name com.package.name --filter "Crypto" 89 | python papi_monitor.py --file-apk app.apk --api-monitor api_personalized.json 90 | python papi_monitor.py --package-name com.package.name --api-monitor api_personalized.json 91 | python papi_monitor.py --package-name com.package.name --filter "ALL" 92 | python papi_monitor.py --package-name com.package.name --api-monitor api_personalized.json --store-script True --filter "Crypto" "Crypto - Hash" 93 | python papi_monitor.py --package-name com.package.name --api-monitor api_personalized.json --pinning-bypass --antiroot-bypass 94 | ``` 95 | 96 | ### Predefined Categories 97 | - Device Data 98 | - Device Info 99 | - SMS 100 | - System Manager 101 | - Base64 encode/decode 102 | - Dex Class Loader 103 | - Network 104 | - Crypto 105 | - Crypto - Hash 106 | - Binder 107 | - IPC 108 | - Database 109 | - SharedPreferences 110 | - WebView 111 | - Java Native Interface 112 | - Command 113 | - Process 114 | - FileSytem - Java 115 | 116 | 117 | --- 118 | 119 | ### Frida Script 120 | 121 | A lot of Frida Script for Android can be found [here](https://github.com/Dado1513/frida-script-android). 122 | 123 | --- 124 | -------------------------------------------------------------------------------- /adb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import logging 5 | import os 6 | import re 7 | import shutil 8 | import subprocess 9 | import time 10 | 11 | from typing import Optional, Union, List 12 | 13 | 14 | class ADB(object): 15 | def __init__(self, device: str = None, debug: bool = False): 16 | """ 17 | Android Debug Bridge (adb) object constructor. 18 | 19 | :param device: The name of the Android device (serial number) for which to execute adb commands. Can be 20 | omitted if there is only one Android device connected to adb. 21 | :param debug: When set to True, more debug messages will be shown for each executed operation. 22 | """ 23 | 24 | self.logger = logging.getLogger( 25 | "{0}.{1}".format(__name__, self.__class__.__name__) 26 | ) 27 | 28 | self._device = device 29 | 30 | if debug: 31 | self.logger.setLevel(logging.DEBUG) 32 | 33 | # If adb executable is not added to PATH variable, it can be specified by using the 34 | # ADB_PATH environment variable. 35 | if "ADB_PATH" in os.environ: 36 | self.adb_path: str = os.environ["ADB_PATH"] 37 | else: 38 | self.adb_path: str = "adb" 39 | 40 | if not self.is_available(): 41 | raise FileNotFoundError( 42 | "Adb executable is not available! Make sure to have adb (Android Debug Bridge) " 43 | "installed and added to the PATH variable, or specify the adb path by using the " 44 | "ADB_PATH environment variable." 45 | ) 46 | 47 | @property 48 | def target_device(self) -> str: 49 | return self._device 50 | 51 | @target_device.setter 52 | def target_device(self, new_device: str): 53 | self._device = new_device 54 | 55 | def is_available(self) -> bool: 56 | """ 57 | Check if adb executable is available. 58 | 59 | :return: True if abd executable is available for usage, False otherwise. 60 | """ 61 | 62 | return shutil.which(self.adb_path) is not None 63 | 64 | def execute( 65 | self, command: List[str], is_async: bool = False, timeout: Optional[int] = None 66 | ) -> Optional[str]: 67 | """ 68 | Execute an adb command and return the output of the command as a string. 69 | 70 | :param command: The command to execute, formatted as a list of strings. 71 | :param is_async: When set to True, the adb command will run in background and the program will continue its 72 | execution. If False (default), the program will wait until the adb command returns a result. 73 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 74 | :return: The (string) output of the command. If the method is called with the parameter is_async = True, 75 | None will be returned. 76 | """ 77 | 78 | if not isinstance(command, list) or any( 79 | not isinstance(command_token, str) for command_token in command 80 | ): 81 | raise TypeError( 82 | "The command to execute should be passed as a list of strings" 83 | ) 84 | 85 | if timeout is not None and (not isinstance(timeout, int) or timeout <= 0): 86 | raise ValueError("If a timeout is provided, it must be a positive integer") 87 | 88 | if is_async and timeout: 89 | raise RuntimeError( 90 | "The timeout cannot be used when executing the program in background" 91 | ) 92 | 93 | try: 94 | # Use the specified Android device serial number (if any). 95 | if self.target_device: 96 | command[0:0] = ["-s", self.target_device] 97 | 98 | command.insert(0, self.adb_path) 99 | self.logger.debug( 100 | "Running command `{0}` (async={1}, timeout={2})".format( 101 | " ".join(command), is_async, timeout 102 | ) 103 | ) 104 | 105 | if is_async: 106 | # Adb command will run in background, nothing to return. 107 | subprocess.Popen(command) 108 | return None 109 | else: 110 | process = subprocess.Popen( 111 | command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT 112 | ) 113 | output = ( 114 | process.communicate(timeout=timeout)[0] 115 | .strip() 116 | .decode(errors="backslashreplace") 117 | ) 118 | if process.returncode != 0: 119 | raise subprocess.CalledProcessError( 120 | process.returncode, command, output.encode() 121 | ) 122 | self.logger.debug( 123 | "Command `{0}` successfully returned: {1}".format( 124 | " ".join(command), output 125 | ) 126 | ) 127 | 128 | # This is needed to make sure the adb command actually terminated before continuing the execution. 129 | time.sleep(1) 130 | 131 | return output 132 | except subprocess.TimeoutExpired as e: 133 | self.logger.error( 134 | "Command `{0}` timed out: {1}".format( 135 | " ".join(command), 136 | e.output.decode(errors="backslashreplace") if e.output else e, 137 | ) 138 | ) 139 | raise 140 | except subprocess.CalledProcessError as e: 141 | self.logger.error( 142 | "Command `{0}` exited with error: {1}".format( 143 | " ".join(command), 144 | e.output.decode(errors="backslashreplace") if e.output else e, 145 | ) 146 | ) 147 | raise 148 | except Exception as e: 149 | self.logger.error( 150 | "Generic error during `{0}` command execution: {1}".format( 151 | " ".join(command), e 152 | ) 153 | ) 154 | raise 155 | 156 | def get_version(self, timeout: Optional[int] = None) -> str: 157 | """ 158 | Get the version of the installed adb. 159 | 160 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 161 | :return: A string containing the version of the installed adb. 162 | """ 163 | 164 | output = self.execute(["version"], timeout=timeout) 165 | 166 | match = re.search(r"version\s(\S+)", output) 167 | if match: 168 | return match.group(1) 169 | else: 170 | raise RuntimeError("Unable to determine adb version") 171 | 172 | def get_available_devices(self, timeout: Optional[int] = None) -> List[str]: 173 | """ 174 | Get a list with the serials of the devices currently connected to adb. 175 | 176 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 177 | :return: A list of strings, each string is a device serial number. 178 | """ 179 | 180 | output = self.execute(["devices"], timeout=timeout) 181 | 182 | devices = [] 183 | for line in output.splitlines(): 184 | tokens = line.strip().split() 185 | if len(tokens) == 2 and tokens[1] == "device": 186 | # Add to the list the name / ip and port of the device. 187 | devices.append(tokens[0]) 188 | return devices 189 | 190 | def shell( 191 | self, command: List[str], is_async: bool = False, timeout: Optional[int] = None 192 | ) -> Optional[str]: 193 | """ 194 | Execute an adb shell command on the Android device connected through adb and return the output 195 | of the command as a string. 196 | 197 | :param command: The command to execute, formatted as a list of strings. 198 | :param is_async: When set to True, the adb shell command will run in background and the program will continue 199 | its execution. If False (default), the program will wait until the adb shell command returns 200 | a result. This can be useful when running background scripts on the Android device. 201 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 202 | :return: The (string) output of the command. If the method is called with the parameter is_async = True, 203 | None will be returned. 204 | """ 205 | 206 | if not isinstance(command, list) or any( 207 | not isinstance(command_token, str) for command_token in command 208 | ): 209 | raise TypeError( 210 | "The command to execute should be passed as a list of strings" 211 | ) 212 | 213 | command.insert(0, "shell") 214 | 215 | return self.execute(command, is_async=is_async, timeout=timeout) 216 | 217 | def shell_su(self, command: str, is_async: bool = False, timeout: Optional[int] = None): 218 | if not isinstance(command, str) or any( 219 | not isinstance(command_token, str) for command_token in command 220 | ): 221 | raise TypeError( 222 | "The command to execute should be passed as a list of strings" 223 | ) 224 | 225 | command_list = ["shell", "su", "-c", f"'{command}'"] 226 | 227 | return self.execute(command_list, is_async=is_async, timeout=timeout) 228 | 229 | def get_property(self, property_name: str, timeout: Optional[int] = None) -> str: 230 | """ 231 | Get the value of a property on the Android device connected through adb. 232 | 233 | :param property_name: The name of the property. 234 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 235 | :return: The value of the property. 236 | """ 237 | 238 | return self.shell(["getprop", property_name], timeout=timeout) 239 | 240 | def get_device_sdk_version(self, timeout: Optional[int] = None) -> int: 241 | """ 242 | Get the version of the SDK installed on the Android device (e.g., 23 for Android Marshmallow). 243 | 244 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 245 | :return: An int with the version number. 246 | """ 247 | 248 | return int(self.get_property("ro.build.version.sdk", timeout=timeout)) 249 | 250 | def wait_for_device(self, timeout: Optional[int] = None) -> None: 251 | """ 252 | Wait until the Android device connected through adb is ready to receive commands. 253 | 254 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 255 | """ 256 | 257 | self.execute(["wait-for-device"], timeout=timeout) 258 | 259 | def kill_server(self, timeout: Optional[int] = None) -> None: 260 | """ 261 | Kill the adb server if it is running. 262 | 263 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 264 | """ 265 | 266 | self.execute(["kill-server"], timeout=timeout) 267 | 268 | def connect(self, host: str = None, timeout: Optional[int] = None) -> str: 269 | """ 270 | Start an adb server and (optionally) connect to an Android device. 271 | 272 | :param host: (Optional) Host address of the Android device (in host[:port] format). This parameter is not 273 | needed in simple scenarios when connecting to the default emulator or to a device connected 274 | through usb cable, since in this case the connection is automatic. 275 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 276 | :return: The string with the result of the connect operation. 277 | """ 278 | 279 | if host: 280 | connect_cmd = ["connect", host] 281 | else: 282 | connect_cmd = ["start-server"] 283 | 284 | output = self.execute(connect_cmd, timeout=timeout) 285 | 286 | # Make sure the connect operation ended successfully. 287 | if output and "unable to connect" in output.lower(): 288 | raise RuntimeError( 289 | "Something went wrong during the connect operation: {0}".format(output) 290 | ) 291 | else: 292 | return output 293 | 294 | def remount(self, timeout: Optional[int] = None) -> str: 295 | """ 296 | Remount system partitions in writable mode (system partitions are read-only by default). This command needs 297 | adb with root privileges. 298 | 299 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 300 | :return: The string with the result of the remount operation. 301 | """ 302 | 303 | output = self.execute(["remount"], timeout=timeout) 304 | 305 | # Make sure the remount operation ended successfully. 306 | if output and "remount succeeded" in output.lower(): 307 | return output 308 | else: 309 | raise RuntimeError( 310 | "Something went wrong during the remount operation: {0}".format(output) 311 | ) 312 | 313 | def reboot(self, timeout: Optional[int] = None) -> None: 314 | """ 315 | Reboot the Android device connected through adb. 316 | 317 | :param timeout: How many seconds to wait for the command to finish execution before throwing an exception. 318 | """ 319 | 320 | return self.execute(["reboot"], timeout=timeout) 321 | 322 | def push_file( 323 | self, 324 | host_path: Union[str, List[str]], 325 | device_path: str, 326 | timeout: Optional[int] = None, 327 | ) -> str: 328 | """ 329 | Copy a file (or a list of files) from the computer to the Android device connected through adb. 330 | 331 | :param host_path: The path of the file on the host computer. This parameter also accepts a list of paths 332 | (strings) to copy more files at the same time. 333 | :param device_path: The path on the Android device where the file(s) should be copied. 334 | :param timeout: How many seconds to wait for the file copy operation before throwing an exception. 335 | :return: The string with the result of the copy operation. 336 | """ 337 | 338 | # Make sure the files to copy exist on the host computer. 339 | if isinstance(host_path, list): 340 | for p in host_path: 341 | if not os.path.exists(p): 342 | raise FileNotFoundError( 343 | 'Cannot copy "{0}" to the Android device: no such file or directory'.format( 344 | p 345 | ) 346 | ) 347 | 348 | if isinstance(host_path, str) and not os.path.exists(host_path): 349 | raise FileNotFoundError( 350 | 'Cannot copy "{0}" to the Android device: no such file or directory'.format( 351 | host_path 352 | ) 353 | ) 354 | 355 | push_cmd = ["push"] 356 | if isinstance(host_path, list): 357 | push_cmd.extend(host_path) 358 | else: 359 | push_cmd.append(host_path) 360 | 361 | push_cmd.append(device_path) 362 | 363 | output = self.execute(push_cmd, timeout=timeout) 364 | 365 | # Make sure the push operation ended successfully. 366 | match = re.search(r"\d+ files? pushed\.", output.splitlines()[-1]) 367 | if match: 368 | return output 369 | else: 370 | raise RuntimeError("Something went wrong during the file push operation") 371 | 372 | def pull_file( 373 | self, 374 | device_path: Union[str, List[str]], 375 | host_path: str, 376 | timeout: Optional[int] = None, 377 | ) -> str: 378 | """ 379 | Copy a file (or a list of files) from the Android device to the computer connected through adb. 380 | 381 | :param device_path: The path of the file on the Android device. This parameter also accepts a list of paths 382 | (strings) to copy more files at the same time. 383 | :param host_path: The path on the host computer where the file(s) should be copied. If multiple files are 384 | copied at the same time, this path should refer to an existing directory on the host. 385 | :param timeout: How many seconds to wait for the file copy operation before throwing an exception. 386 | :return: The string with the result of the copy operation. 387 | """ 388 | 389 | # When copying multiple files at the same time, make sure the host path refers to an existing directory. 390 | if isinstance(device_path, list) and not os.path.isdir(host_path): 391 | raise NotADirectoryError( 392 | "When copying multiple files, the destination host path should be an " 393 | 'existing directory: "{0}" directory was not found'.format(host_path) 394 | ) 395 | 396 | # Make sure the destination directory on the host exists (adb won't create the missing directories specified 397 | # on the host path). For example, if test/ directory exists on host, it can be used, but test/nested/ can be 398 | # used only if it already exists on the host, otherwise adb won't create the nested/ directory. 399 | if not os.path.isdir(os.path.dirname(host_path)): 400 | raise NotADirectoryError( 401 | 'The destination host directory "{0}" was not found'.format( 402 | os.path.dirname(host_path) 403 | ) 404 | ) 405 | 406 | pull_cmd = ["pull"] 407 | if isinstance(device_path, list): 408 | pull_cmd.extend(device_path) 409 | else: 410 | pull_cmd.append(device_path) 411 | 412 | pull_cmd.append(host_path) 413 | 414 | output = self.execute(pull_cmd, timeout=timeout) 415 | 416 | # Make sure the pull operation ended successfully. 417 | match = re.search(r"\d+ files? pulled\.", output.splitlines()[-1]) 418 | if match: 419 | return output 420 | else: 421 | raise RuntimeError("Something went wrong during the file pull operation") 422 | 423 | def install_app( 424 | self, 425 | apk_path: str, 426 | replace_existing: bool = False, 427 | grant_permissions: bool = False, 428 | timeout: Optional[int] = None, 429 | ): 430 | """ 431 | Install an application into the Android device. 432 | 433 | :param apk_path: The path on the host computer to the application file to be installed. 434 | :param replace_existing: When set to True, any old version of the application installed on the Android device 435 | will be replaced by the new application being installed. 436 | :param grant_permissions: When set to True, all the runtime permissions of the application will be granted. 437 | :param timeout: How many seconds to wait for the install operation before throwing an exception. 438 | :return: The string with the result of the install operation. 439 | """ 440 | 441 | # Make sure the application to install is an existing file on the host computer. 442 | if not os.path.isfile(apk_path): 443 | raise FileNotFoundError('"{0}" apk file was not found'.format(apk_path)) 444 | 445 | install_cmd = ["install"] 446 | 447 | # Additional installation flags. 448 | if replace_existing: 449 | install_cmd.append("-r") 450 | if grant_permissions and self.get_device_sdk_version() >= 23: 451 | # Runtime permissions exist since SDK version 23 (Android Marshmallow). 452 | install_cmd.append("-g") 453 | 454 | install_cmd.append(apk_path) 455 | 456 | output = self.execute(install_cmd, timeout=timeout) 457 | 458 | # Make sure the install operation ended successfully. Complete list of error messages: 459 | # https://android.googlesource.com/platform/frameworks/base/+/lollipop-release/core/java/android/content/pm/PackageManager.java 460 | match = re.search(r"Failure \[.+?\]", output, flags=re.IGNORECASE) 461 | if not match: 462 | return output 463 | else: 464 | raise RuntimeError( 465 | "Application installation failed: {0}".format(match.group()) 466 | ) 467 | 468 | def uninstall_app(self, package_name: str, timeout: Optional[int] = None): 469 | """ 470 | Uninstall an application from the Android device. 471 | 472 | :param package_name: The package name of the application to uninstall. 473 | :param timeout: How many seconds to wait for the uninstall operation before throwing an exception. 474 | :return: The string with the result of the uninstall operation. 475 | """ 476 | 477 | uninstall_cmd = ["uninstall", package_name] 478 | 479 | output = self.execute(uninstall_cmd, timeout=timeout) 480 | 481 | # Make sure the uninstall operation ended successfully. Complete list of error messages: 482 | # https://android.googlesource.com/platform/frameworks/base/+/lollipop-release/core/java/android/content/pm/PackageManager.java 483 | match = re.search(r"Failure \[.+?\]", output, flags=re.IGNORECASE) 484 | if not match: 485 | return output 486 | else: 487 | raise RuntimeError("Application removal failed: {0}".format(match.group())) 488 | -------------------------------------------------------------------------------- /api-android-monitor/api-monitor-native.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Category": "FileSytem Native", 4 | "HookType": "Native", 5 | "Comment": "Hook it alone! Do not combine with other Categories", 6 | "hooks": [ 7 | { 8 | "clazz": "libc.so", 9 | "method": "open" 10 | }, 11 | { 12 | "clazz": "libc.so", 13 | "method": "close" 14 | }, 15 | { 16 | "clazz": "libc.so", 17 | "method": "read" 18 | }, 19 | { 20 | "clazz": "libc.so", 21 | "method": "write" 22 | }, 23 | { 24 | "clazz": "libc.so", 25 | "method": "unlink" 26 | }, 27 | { 28 | "clazz": "libc.so", 29 | "method": "remove" 30 | } 31 | ] 32 | } 33 | ] -------------------------------------------------------------------------------- /api-android-monitor/api-monitor.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Category": "Process", 4 | "HookType": "Java", 5 | "hooks": [ 6 | { 7 | "clazz": "android.os.Process", 8 | "method": "start" 9 | }, 10 | { 11 | "clazz": "android.app.ActivityManager", 12 | "method": "killBackgroundProcesses" 13 | }, 14 | { 15 | "clazz": "android.os.Process", 16 | "method": "killProcess" 17 | } 18 | ] 19 | }, 20 | { 21 | "Category": "Command", 22 | "HookType": "Java", 23 | "hooks": [ 24 | { 25 | "clazz": "java.lang.Runtime", 26 | "method": "exec" 27 | }, 28 | { 29 | "clazz": "java.lang.ProcessBuilder", 30 | "method": "start" 31 | } 32 | ] 33 | }, 34 | { 35 | "Category": "Java Native Interface", 36 | "HookType": "Java", 37 | "hooks": [ 38 | { 39 | "clazz": "java.lang.Runtime", 40 | "method": "loadLibrary" 41 | }, 42 | { 43 | "clazz": "java.lang.Runtime", 44 | "method": "load" 45 | } 46 | ] 47 | }, 48 | { 49 | "Category": "WebView", 50 | "HookType": "Java", 51 | "hooks": [ 52 | { 53 | "clazz": "android.webkit.WebView", 54 | "method": "loadUrl" 55 | }, 56 | { 57 | "clazz": "android.webkit.WebView", 58 | "method": "loadData" 59 | }, 60 | { 61 | "clazz": "android.webkit.WebView", 62 | "method": "loadDataWithBaseURL" 63 | }, 64 | { 65 | "clazz": "android.webkit.WebView", 66 | "method": "addJavascriptInterface" 67 | }, 68 | { 69 | "clazz": "android.webkit.WebView", 70 | "method": "evaluateJavascript" 71 | }, 72 | { 73 | "clazz": "android.webkit.WebView", 74 | "method": "postUrl" 75 | }, 76 | { 77 | "clazz": "android.webkit.WebView", 78 | "method": "postWebMessage" 79 | }, 80 | { 81 | "clazz": "android.webkit.WebView", 82 | "method": "savePassword" 83 | }, 84 | { 85 | "clazz": "android.webkit.WebView", 86 | "method": "setHttpAuthUsernamePassword" 87 | }, 88 | { 89 | "clazz": "android.webkit.WebView", 90 | "method": "getHttpAuthUsernamePassword" 91 | }, 92 | { 93 | "clazz": "android.webkit.WebView", 94 | "method": "setWebContentsDebuggingEnabled" 95 | } 96 | ] 97 | }, 98 | { 99 | "Category": "SharedPreferences", 100 | "HookType": "Java", 101 | "hooks": [ 102 | { 103 | "clazz": "android.app.SharedPreferencesImpl", 104 | "method": "getString" 105 | }, 106 | { 107 | "clazz": "android.app.SharedPreferencesImpl", 108 | "method": "contains" 109 | }, 110 | { 111 | "clazz": "android.app.SharedPreferencesImpl", 112 | "method": "getInt" 113 | }, 114 | { 115 | "clazz": "android.app.SharedPreferencesImpl", 116 | "method": "getFloat" 117 | }, 118 | { 119 | "clazz": "android.app.SharedPreferencesImpl", 120 | "method": "getLong" 121 | }, 122 | { 123 | "clazz": "android.app.SharedPreferencesImpl", 124 | "method": "getBoolean" 125 | }, 126 | { 127 | "clazz": "android.app.SharedPreferencesImpl", 128 | "method": "getStringSet" 129 | }, 130 | { 131 | "clazz": "android.app.SharedPreferencesImpl$EditorImpl", 132 | "method": "putString" 133 | }, 134 | { 135 | "clazz": "android.app.SharedPreferencesImpl$EditorImpl", 136 | "method": "putStringSet" 137 | }, 138 | { 139 | "clazz": "android.app.SharedPreferencesImpl$EditorImpl", 140 | "method": "putInt" 141 | }, 142 | { 143 | "clazz": "android.app.SharedPreferencesImpl$EditorImpl", 144 | "method": "putFloat" 145 | }, 146 | { 147 | "clazz": "android.app.SharedPreferencesImpl$EditorImpl", 148 | "method": "putBoolean" 149 | }, 150 | { 151 | "clazz": "android.app.SharedPreferencesImpl$EditorImpl", 152 | "method": "putLong" 153 | }, 154 | { 155 | "clazz": "android.app.SharedPreferencesImpl$EditorImpl", 156 | "method": "remove" 157 | } 158 | 159 | ] 160 | }, 161 | { 162 | "Category": "Database", 163 | "HookType": "Java", 164 | "hooks": [ 165 | { 166 | "clazz": "android.content.ContextWrapper", 167 | "method": "openOrCreateDatabase" 168 | }, 169 | { 170 | "clazz": "android.content.ContextWrapper", 171 | "method": "databaseList" 172 | }, 173 | { 174 | "clazz": "android.content.ContextWrapper", 175 | "method": "deleteDatabase" 176 | }, 177 | { 178 | "clazz": "android.database.sqlite.SQLiteDatabase", 179 | "method": "execSQL" 180 | }, 181 | { 182 | "clazz": "android.database.sqlite.SQLiteDatabase", 183 | "method": "deleteDatabase" 184 | }, 185 | { 186 | "clazz": "android.database.sqlite.SQLiteDatabase", 187 | "method": "getPath" 188 | }, 189 | { 190 | "clazz": "android.database.sqlite.SQLiteDatabase", 191 | "method": "insert" 192 | }, 193 | { 194 | "clazz": "android.database.sqlite.SQLiteDatabase", 195 | "method": "insertOrThrow" 196 | }, 197 | { 198 | "clazz": "android.database.sqlite.SQLiteDatabase", 199 | "method": "insertWithOnConflict" 200 | }, 201 | { 202 | "clazz": "android.database.sqlite.SQLiteDatabase", 203 | "method": "openDatabase" 204 | }, 205 | { 206 | "clazz": "android.database.sqlite.SQLiteDatabase", 207 | "method": "openOrCreateDatabase" 208 | }, 209 | { 210 | "clazz": "android.database.sqlite.SQLiteDatabase", 211 | "method": "query" 212 | }, 213 | { 214 | "clazz": "android.database.sqlite.SQLiteDatabase", 215 | "method": "queryWithFactory" 216 | }, 217 | { 218 | "clazz": "android.database.sqlite.SQLiteDatabase", 219 | "method": "rawQuery" 220 | }, 221 | { 222 | "clazz": "android.database.sqlite.SQLiteDatabase", 223 | "method": "rawQueryWithFactory" 224 | }, 225 | { 226 | "clazz": "android.database.sqlite.SQLiteDatabase", 227 | "method": "update" 228 | }, 229 | { 230 | "clazz": "android.database.sqlite.SQLiteDatabase", 231 | "method": "updateWithOnConflict" 232 | }, 233 | { 234 | "clazz": "android.database.sqlite.SQLiteDatabase", 235 | "method": "compileStatement" 236 | }, 237 | { 238 | "clazz": "android.database.sqlite.SQLiteDatabase", 239 | "method": "create" 240 | } 241 | ] 242 | }, 243 | { 244 | "Category": "IPC", 245 | "HookType": "Java", 246 | "hooks": [ 247 | { 248 | "clazz": "android.content.ContextWrapper", 249 | "method": "sendBroadcast" 250 | }, 251 | { 252 | "clazz": "android.content.ContextWrapper", 253 | "method": "sendStickyBroadcast" 254 | }, 255 | { 256 | "clazz": "android.content.ContextWrapper", 257 | "method": "startActivity" 258 | }, 259 | { 260 | "clazz": "android.content.ContextWrapper", 261 | "method": "startService" 262 | }, 263 | { 264 | "clazz": "android.content.ContextWrapper", 265 | "method": "stopService" 266 | }, 267 | { 268 | "clazz": "android.content.ContextWrapper", 269 | "method": "registerReceiver" 270 | } 271 | ] 272 | }, 273 | { 274 | "Category": "Binder", 275 | "HookType": "Java", 276 | "hooks": [ 277 | { 278 | "clazz": "android.app.ContextImpl", 279 | "method": "registerReceiver" 280 | }, 281 | { 282 | "clazz": "android.app.ActivityThread", 283 | "method": "handleReceiver" 284 | }, 285 | { 286 | "clazz": "android.app.Activity", 287 | "method": "startActivity" 288 | } 289 | ] 290 | }, 291 | { 292 | "Category": "Crypto", 293 | "HookType": "Java", 294 | "hooks": [ 295 | { 296 | "clazz": "javax.crypto.spec.SecretKeySpec", 297 | "method": "$init" 298 | }, 299 | { 300 | "clazz": "javax.crypto.Cipher", 301 | "method": "doFinal" 302 | } 303 | ] 304 | }, 305 | { 306 | "Category": "Crypto - Hash", 307 | "HookType": "Java", 308 | "hooks": [ 309 | { 310 | "clazz": "java.security.MessageDigest", 311 | "method": "digest" 312 | }, 313 | { 314 | "clazz": "java.security.MessageDigest", 315 | "method": "update" 316 | } 317 | ] 318 | }, 319 | { 320 | "Category": "Network", 321 | "HookType": "Java", 322 | "hooks": [ 323 | { 324 | "clazz": "java.net.URL", 325 | "method": "openConnection" 326 | }, 327 | { 328 | "clazz": "org.apache.http.impl.client.AbstractHttpClient", 329 | "method": "execute" 330 | }, 331 | { 332 | "clazz": "com.android.okhttp.internal.huc.HttpURLConnectionImpl", 333 | "method": "getInputStream" 334 | }, 335 | { 336 | "clazz": "com.android.okhttp.internal.huc.HttpURLConnectionImpl", 337 | "method": "getInputStream" 338 | } 339 | ] 340 | }, 341 | { 342 | "Category": "Dex Class Loader", 343 | "HookType": "Java", 344 | "hooks": [ 345 | { 346 | "clazz": "dalvik.system.BaseDexClassLoader", 347 | "method": "findResource" 348 | }, 349 | { 350 | "clazz": "dalvik.system.BaseDexClassLoader", 351 | "method": "findResources" 352 | }, 353 | { 354 | "clazz": "dalvik.system.BaseDexClassLoader", 355 | "method": "findLibrary" 356 | }, 357 | { 358 | "clazz": "dalvik.system.DexFile", 359 | "method": "loadDex" 360 | }, 361 | { 362 | "clazz": "dalvik.system.DexFile", 363 | "method": "loadClass" 364 | }, 365 | { 366 | "clazz": "dalvik.system.DexClassLoader", 367 | "method": "$init" 368 | } 369 | ] 370 | }, 371 | { 372 | "Category": "Base64 encode/decode", 373 | "HookType": "Java", 374 | "hooks": [ 375 | { 376 | "clazz": "android.util.Base64", 377 | "method": "decode" 378 | }, 379 | { 380 | "clazz": "android.util.Base64", 381 | "method": "encode" 382 | }, 383 | { 384 | "clazz": "android.util.Base64", 385 | "method": "encodeToString" 386 | } 387 | ] 388 | }, 389 | { 390 | "Category": "System Manager", 391 | "HookType": "Java", 392 | "hooks": [ 393 | { 394 | "clazz": "android.app.ApplicationPackageManager", 395 | "method": "setComponentEnabledSetting" 396 | }, 397 | { 398 | "clazz": "android.app.NotificationManager", 399 | "method": "notify" 400 | }, 401 | { 402 | "clazz": "android.telephony.TelephonyManager", 403 | "method": "listen" 404 | }, 405 | { 406 | "clazz": "android.content.BroadcastReceiver", 407 | "method": "abortBroadcast" 408 | } 409 | ] 410 | }, 411 | { 412 | "Category": "SMS", 413 | "HookType": "Java", 414 | "hooks": [ 415 | { 416 | "clazz": "android.telephony.SmsManager", 417 | "method": "sendTextMessage" 418 | }, 419 | { 420 | "clazz": "android.telephony.SmsManager", 421 | "method": "sendMultipartTextMessage" 422 | } 423 | ] 424 | }, 425 | { 426 | "Category": "Device Info", 427 | "HookType": "Java", 428 | "hooks": [ 429 | { 430 | "clazz": "android.telephony.TelephonyManager", 431 | "method": "getDeviceId" 432 | }, 433 | { 434 | "clazz": "android.telephony.TelephonyManager", 435 | "method": "getSubscriberId" 436 | }, 437 | { 438 | "clazz": "android.telephony.TelephonyManager", 439 | "method": "getLine1Number" 440 | }, 441 | { 442 | "clazz": "android.telephony.TelephonyManager", 443 | "method": "getNetworkOperator" 444 | }, 445 | { 446 | "clazz": "android.telephony.TelephonyManager", 447 | "method": "getNetworkOperatorName" 448 | }, 449 | { 450 | "clazz": "android.telephony.TelephonyManager", 451 | "method": "getSimOperatorName" 452 | }, 453 | { 454 | "clazz": "android.net.wifi.WifiInfo", 455 | "method": "getMacAddress" 456 | }, 457 | { 458 | "clazz": "android.net.wifi.WifiInfo", 459 | "method": "getBSSID" 460 | }, 461 | { 462 | "clazz": "android.net.wifi.WifiInfo", 463 | "method": "getIpAddress" 464 | }, 465 | { 466 | "clazz": "android.net.wifi.WifiInfo", 467 | "method": "getNetworkId" 468 | }, 469 | { 470 | "clazz": "android.telephony.TelephonyManager", 471 | "method": "getSimCountryIso" 472 | }, 473 | { 474 | "clazz": "android.telephony.TelephonyManager", 475 | "method": "getSimSerialNumber" 476 | }, 477 | { 478 | "clazz": "android.telephony.TelephonyManager", 479 | "method": "getNetworkCountryIso" 480 | }, 481 | { 482 | "clazz": "android.telephony.TelephonyManager", 483 | "method": "getDeviceSoftwareVersion" 484 | }, 485 | { 486 | "clazz": "android.os.Debug", 487 | "method": "isDebuggerConnected" 488 | }, 489 | { 490 | "clazz": "android.content.pm.PackageManager", 491 | "method": "getInstallerPackageName" 492 | }, 493 | { 494 | "clazz": "android.content.pm.PackageManager", 495 | "method": "getInstalledApplications" 496 | }, 497 | { 498 | "clazz": "android.content.pm.PackageManager", 499 | "method": "getInstalledModules", 500 | "target": 10 501 | }, 502 | { 503 | "clazz": "android.content.pm.PackageManager", 504 | "method": "getInstalledPackages" 505 | } 506 | ] 507 | }, 508 | { 509 | "Category": "Device Data", 510 | "HookType": "Java", 511 | "hooks": [ 512 | { 513 | "clazz": "android.content.ContentResolver", 514 | "method": "query" 515 | }, 516 | { 517 | "clazz": "android.content.ContentResolver", 518 | "method": "registerContentObserver" 519 | }, 520 | { 521 | "clazz": "android.content.ContentResolver", 522 | "method": "insert" 523 | }, 524 | { 525 | "clazz": "android.content.ContentResolver", 526 | "method": "delete" 527 | }, 528 | { 529 | "clazz": "android.accounts.AccountManager", 530 | "method": "getAccountsByType" 531 | }, 532 | { 533 | "clazz": "android.accounts.AccountManager", 534 | "method": "getAccounts" 535 | }, 536 | { 537 | "clazz": "android.location.Location", 538 | "method": "getLatitude" 539 | }, 540 | { 541 | "clazz": "android.location.Location", 542 | "method": "getLongitude" 543 | }, 544 | { 545 | "clazz": "android.media.AudioRecord", 546 | "method": "startRecording" 547 | }, 548 | { 549 | "clazz": "android.media.MediaRecorder", 550 | "method": "start" 551 | }, 552 | { 553 | "clazz": "android.os.SystemProperties", 554 | "method": "get" 555 | }, 556 | { 557 | "clazz": "android.app.ApplicationPackageManager", 558 | "method": "getInstalledPackages" 559 | } 560 | ] 561 | }, 562 | { 563 | "Category": "FileSytem - Java", 564 | "HookType": "Java", 565 | "hooks": [ 566 | { 567 | "clazz": "ibcore.io.IoBridge", 568 | "method": "open" 569 | }, 570 | { 571 | "clazz": "java.io.FileOutputStream", 572 | "method": "write" 573 | }, 574 | { 575 | "clazz": "java.io.FileInputStream'", 576 | "method": "read" 577 | }, 578 | { 579 | "clazz": "android.content.ContextWrapper", 580 | "method": "openFileInput" 581 | }, 582 | { 583 | "clazz": "android.content.ContextWrapper", 584 | "method": "openFileOutput" 585 | }, 586 | { 587 | "clazz": "android.content.ContextWrapper", 588 | "method": "deleteFile" 589 | } 590 | 591 | ] 592 | } 593 | ] -------------------------------------------------------------------------------- /api-android-monitor/frida_script_template.js: -------------------------------------------------------------------------------- 1 | Java.perform(function () { 2 | var cn = class_name; 3 | var clazz = Java.use(cn); 4 | var func = method_name; 5 | var overloads = clazz[func].overloads; 6 | for (var i in overloads) { 7 | if (overloads[i].hasOwnProperty('argumentTypes')) { 8 | var parameters = []; 9 | var curArgumentTypes = overloads[i].argumentTypes; 10 | var args = []; 11 | var argLog = '['; 12 | var value_parameters = "["; 13 | for (var j in curArgumentTypes) { 14 | var cName = curArgumentTypes[j].className; 15 | parameters.push(cName); 16 | argLog += "'(" + cName + ") ' + v" + j + ","; 17 | value_parameters += "v" + j + " ,"; 18 | args.push('v' + j); 19 | } 20 | 21 | argLog += ']'; 22 | value_parameters += "]"; 23 | 24 | 25 | var script = "var ret = this." + func + '(' + args.join(',') + ") || '';\n" 26 | + "send({className:'" + cn + "', method:'" + func + "', parameters: " + value_parameters + ", return: ret});\n" 27 | + "return ret;"; 28 | args.push(script); 29 | clazz[func].overload.apply(this, parameters).implementation = Function.apply(null, args); 30 | } 31 | } 32 | 33 | }); -------------------------------------------------------------------------------- /api-android-monitor/papi-monitor.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Exported APIs 3 | 1. loadclasses 4 | 2. loadclasseswithfilter([filters]) 5 | 3. loadcustomfridascript(frida_script) 6 | 4. loadmethods([loaded_classes]) 7 | 5. hookclassesandmethods([loaded_classes], [loaded_methods], template) 8 | 6. generatehooktemplate([loaded_classes], [loaded_methods], template) 9 | ******************************************************************************/ 10 | 11 | rpc.exports = { 12 | loadclasses: function () { 13 | var loaded_classes = [] 14 | 15 | Java.perform(function () { 16 | Java.enumerateLoadedClasses({ 17 | onMatch: function (className) { 18 | 19 | //Remove too generics 20 | if (className.length > 5) 21 | loaded_classes.push(className) 22 | 23 | }, 24 | onComplete: function () { 25 | loaded_classes.sort() 26 | } 27 | }); 28 | }) 29 | return loaded_classes; 30 | }, 31 | loadclasseswithfilter: function (filter) { 32 | var loaded_classes = [] 33 | Java.perform(function () { 34 | Java.enumerateLoadedClasses({ 35 | onMatch: function (className) { 36 | 37 | //check if a filter exists 38 | if (filter != null) { 39 | //check if we have multiple filters (comma separated list) 40 | var filter_array = filter.split(","); 41 | filter_array.forEach(function (f) { 42 | //f.trim() is needed to remove possibile spaces after the comma 43 | if (className.startsWith(f.trim())) { 44 | loaded_classes.push(className) 45 | } 46 | }); 47 | } 48 | }, 49 | onComplete: function () { 50 | loaded_classes.sort() 51 | } 52 | }); 53 | }) 54 | return loaded_classes; 55 | }, 56 | loadcustomfridascript: function (frida_script) { 57 | Java.perform(function () { 58 | console.log("FRIDA script LOADED") 59 | eval(frida_script) 60 | }) 61 | }, 62 | loadmethods: function (loaded_classes) { 63 | var loaded_methods = {}; 64 | Java.perform(function () { 65 | loaded_classes.forEach(function (className, index) { 66 | var jClass; 67 | var classMethods_dirty; 68 | 69 | //catch possible issues 70 | try{ 71 | jClass = Java.use(className); 72 | classMethods_dirty = jClass.class.getDeclaredMethods(); 73 | }catch(err){ 74 | console.log("[*] Exception while loading methods for "+className); 75 | //skip current loop 76 | loaded_methods[className] = [] 77 | return; 78 | } 79 | var classMethods = [] 80 | 81 | classMethods_dirty.forEach(function (m) { 82 | var method_and_args = {}; 83 | //Cleaning up 84 | m = m.toString(); 85 | //add info for the UI 86 | method_and_args["ui_name"] = m.replace(className + ".", "") 87 | // Remove generics from the method 88 | while (m.includes("<")) { 89 | m = m.replace(/<.*?>/g, ""); 90 | } 91 | // remove "Throws" 92 | if (m.indexOf(" throws ") !== -1) { 93 | m = m.substring(0, m.indexOf(" throws ")); 94 | } 95 | // remove scope and return type declarations 96 | m = m.slice(m.lastIndexOf(" ")); 97 | // remove the class name 98 | m = m.replace(className + ".", ""); 99 | 100 | // remove the signature (args) 101 | method_and_args["name"] = m.split("(")[0].trim() 102 | 103 | // get the args 104 | var args_dirty = ((/\((.*?)\)/.exec(m)[1]).trim()) 105 | 106 | // add quotes between every arg 107 | var args_array = args_dirty.split(",") 108 | var args_srt = "" 109 | for (var i = 0; i < args_array.length; i++) { 110 | args_srt = args_srt + ("\"" + args_array[i] + "\"") 111 | //add a comma if the current item is not the last one 112 | if (i + 1 < args_array.length) args_srt = args_srt + ","; 113 | } 114 | 115 | method_and_args["args"] = args_srt 116 | classMethods.push(method_and_args); 117 | 118 | }); 119 | 120 | loaded_methods[className] = classMethods; 121 | }); 122 | 123 | }) 124 | //DEBUG console.log("loaded_classes.length: " + loaded_classes.length) 125 | //DEBUG console.log("loaded_methods.length: " + Object.keys(loaded_methods).length) 126 | return loaded_methods; 127 | }, 128 | hookclassesandmethods: function (loaded_classes, loaded_methods, template) { 129 | Java.perform(function () { 130 | 131 | console.log("Hook Template setup") 132 | 133 | loaded_classes.forEach(function (clazz) { 134 | loaded_methods[clazz].forEach(function (dict) { 135 | var t = template //template1 136 | 137 | // replace className 138 | t = t.replace("{className}", clazz); 139 | // replace classMethod x3 140 | t = t.replace("{classMethod}", dict["name"]); 141 | t = t.replace("{classMethod}", dict["name"]); 142 | t = t.replace("{classMethod}", dict["name"]); 143 | 144 | //check if the method has args 145 | if (dict["args"] != "\"\"") { 146 | //check if the method has overloads 147 | t = t.replace("{overload}", "overload(" + dict["args"] + ")."); 148 | // Check args length 149 | var args_len = (dict["args"].split(",")).length 150 | 151 | //args creation (method inputs) - v[i] to N 152 | var args = ""; 153 | for (var i = 0; i < args_len; i++) { 154 | if (i + 1 == args_len) args = args + "v" + i; 155 | else args = args + "v" + i + ","; 156 | } 157 | 158 | //replace x2 159 | t = t.replace("{args}", args); 160 | t = t.replace("{args}", args); 161 | 162 | } else { 163 | //Current methods has NO args 164 | // no need to overload 165 | t = t.replace("{overload}", "overload()."); 166 | //replace x2 and no args 167 | t = t.replace("{args}", ""); 168 | t = t.replace("{args}", ""); 169 | } 170 | 171 | //Debug - print FRIDA template 172 | //send(t); 173 | 174 | // ready to eval! 175 | eval(t); 176 | }); 177 | }); 178 | 179 | }) 180 | }, 181 | generatehooktemplate: function (loaded_classes, loaded_methods, template) { 182 | var hto = "" //hto stands for hooks template output 183 | Java.perform(function () { 184 | loaded_classes.forEach(function (clazz) { 185 | loaded_methods[clazz].forEach(function (dict) { 186 | var t = template //template2 187 | 188 | // replace className 189 | t = t.replace("{className}", clazz); 190 | // replace classMethod x3 191 | t = t.replace("{classMethod}", dict["name"]); 192 | t = t.replace("{classMethod}", dict["name"]); 193 | t = t.replace("{classMethod}", dict["name"]); 194 | 195 | t = t.replace("{methodSignature}", dict["ui_name"]); 196 | 197 | //check if the method has args 198 | if (dict["args"] != "\"\"") { 199 | //check if the method has overloads 200 | t = t.replace("{overload}", "overload(" + dict["args"] + ")."); 201 | // Check args length 202 | var args_len = (dict["args"].split(",")).length 203 | 204 | //args creation (method inputs) - v[i] to N 205 | var args = ""; 206 | for (var i = 0; i < args_len; i++) { 207 | if (i + 1 == args_len) args = args + "v" + i; 208 | else args = args + "v" + i + ","; 209 | } 210 | 211 | //replace x3 212 | t = t.replace("{args}", args); 213 | t = t.replace("{args}", args); 214 | t = t.replace("{args}", args); 215 | } else { 216 | //Current methods has NO args 217 | // no need to overload 218 | t = t.replace("{overload}", "overload()."); 219 | //replace x3 220 | t = t.replace("{args}", ""); 221 | t = t.replace("{args}", ""); 222 | t = t.replace("{args}", "\"\""); 223 | } 224 | 225 | //Debug - print FRIDA template 226 | //send(t); 227 | 228 | // hooks concat 229 | hto = hto + t; 230 | }); 231 | }); 232 | 233 | }) 234 | // return HOOK template 235 | return hto; 236 | }, 237 | heapsearchtemplate: function (loaded_classes, loaded_methods, template) { 238 | var hto = "" //hto stands for hooks template output 239 | Java.perform(function () { 240 | loaded_classes.forEach(function (clazz) { 241 | loaded_methods[clazz].forEach(function (dict) { 242 | var t = template //template2 243 | 244 | // replace className 245 | t = t.replace("{className}", clazz); 246 | // replace classMethod x2 247 | t = t.replace("{classMethod}", dict["name"]); 248 | t = t.replace("{classMethod}", dict["name"]); 249 | 250 | t = t.replace("{methodSignature}", dict["ui_name"]); 251 | 252 | //check if the method has args 253 | if (dict["args"] != "\"\"") { 254 | 255 | // Check args length 256 | var args_len = (dict["args"].split(",")).length 257 | 258 | //args creation (method inputs) - v[i] to N 259 | var args = ""; 260 | for (var i = 0; i < args_len; i++) { 261 | if (i + 1 == args_len) args = args + "v" + i; 262 | else args = args + "v" + i + ","; 263 | } 264 | 265 | //replace 266 | t = t.replace("{args}", args); 267 | 268 | } else { 269 | //Current methods has NO args 270 | 271 | //replace 272 | t = t.replace("{args}", ""); 273 | 274 | } 275 | 276 | //Debug - print FRIDA template 277 | //send(t); 278 | 279 | // hooks concat 280 | hto = hto + t; 281 | }); 282 | }); 283 | 284 | }) 285 | // return HOOK template 286 | return hto; 287 | }, 288 | 289 | apimonitor: function (api_to_monitor) { 290 | Java.perform(function () { 291 | console.log("[*] api-monitor") 292 | api_to_monitor.forEach(function (e) { 293 | e["hooks"].forEach(function (hook) { 294 | // Java or Native Hook? 295 | 296 | // Native - File System only at the moment 297 | if (e["HookType"] == "Native") { 298 | nativedynamichook(hook, e["Category"]); 299 | } 300 | 301 | // Java 302 | if (e["HookType"] == "Java") { 303 | javadynamichook(hook, e["Category"], function (realRetval, to_print) { 304 | to_print.returnValue = realRetval 305 | 306 | //check if type object if yes convert it to string 307 | if (realRetval && typeof realRetval === 'object') { 308 | var retval_string = []; 309 | for (var k = 0, l = realRetval.length; k < l; k++) { 310 | retval_string.push(realRetval[k]); 311 | } 312 | to_print.returnValue = '' + retval_string.join(''); 313 | } 314 | if (!to_print.result) to_print.result = undefined 315 | if (!to_print.returnValue) to_print.returnValue = undefined 316 | 317 | send(JSON.stringify(to_print)); 318 | return realRetval; 319 | }); 320 | } // end javadynamichook 321 | 322 | }); 323 | 324 | }); 325 | 326 | }) 327 | }, 328 | 329 | pinningbypass: function() { 330 | setTimeout(function(){ 331 | Java.perform(function (){ 332 | console.log(""); 333 | console.log("[.][pinning-bypass] Cert Pinning Bypass/Re-Pinning"); 334 | 335 | var CertificateFactory = Java.use("java.security.cert.CertificateFactory"); 336 | var FileInputStream = Java.use("java.io.FileInputStream"); 337 | var BufferedInputStream = Java.use("java.io.BufferedInputStream"); 338 | var X509Certificate = Java.use("java.security.cert.X509Certificate"); 339 | var KeyStore = Java.use("java.security.KeyStore"); 340 | var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory"); 341 | var SSLContext = Java.use("javax.net.ssl.SSLContext"); 342 | 343 | // Load CAs from an InputStream 344 | console.log("[+][pinning-bypass] Loading our CA...") 345 | var cf = CertificateFactory.getInstance("X.509"); 346 | 347 | try { 348 | var fileInputStream = FileInputStream.$new("/data/local/tmp/cert-der.crt"); 349 | } 350 | catch(err) { 351 | console.log("[o][pinning-bypass] " + err); 352 | } 353 | 354 | var bufferedInputStream = BufferedInputStream.$new(fileInputStream); 355 | var ca = cf.generateCertificate(bufferedInputStream); 356 | bufferedInputStream.close(); 357 | 358 | var certInfo = Java.cast(ca, X509Certificate); 359 | console.log("[o][pinning-bypass] Our CA Info: " + certInfo.getSubjectDN()); 360 | 361 | // Create a KeyStore containing our trusted CAs 362 | console.log("[+][pinning-bypass] Creating a KeyStore for our CA..."); 363 | var keyStoreType = KeyStore.getDefaultType(); 364 | var keyStore = KeyStore.getInstance(keyStoreType); 365 | keyStore.load(null, null); 366 | keyStore.setCertificateEntry("ca", ca); 367 | 368 | // Create a TrustManager that trusts the CAs in our KeyStore 369 | console.log("[+][pinning-bypass] Creating a TrustManager that trusts the CA in our KeyStore..."); 370 | var tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); 371 | var tmf = TrustManagerFactory.getInstance(tmfAlgorithm); 372 | tmf.init(keyStore); 373 | console.log("[+][pinning-bypass] Our TrustManager is ready..."); 374 | 375 | console.log("[+][pinning-bypass] Hijacking SSLContext methods now...") 376 | console.log("[-][pinning-bypass] Waiting for the app to invoke SSLContext.init()...") 377 | 378 | SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").implementation = function(a,b,c) { 379 | console.log("[o][pinning-bypass] App invoked javax.net.ssl.SSLContext.init..."); 380 | SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").call(this, a, tmf.getTrustManagers(), c); 381 | console.log("[+][pinning-bypass] SSLContext initialized with our custom TrustManager!"); 382 | } 383 | 384 | // okhttp3 pinning 385 | var okhttp3_CertificatePinner_class = null; 386 | try { 387 | okhttp3_CertificatePinner_class = Java.use('okhttp3.CertificatePinner'); 388 | } catch (err) { 389 | console.log('[-][pinning-bypass] OkHTTPv3 CertificatePinner class not found. Skipping.'); 390 | okhttp3_CertificatePinner_class = null; 391 | } 392 | 393 | if(okhttp3_CertificatePinner_class != null) { 394 | 395 | try{ 396 | okhttp3_CertificatePinner_class.check.overload('java.lang.String', 'java.util.List').implementation = function (str,list) { 397 | console.log('[+][pinning-bypass] Bypassing OkHTTPv3 1: ' + str); 398 | return true; 399 | }; 400 | console.log('[+][pinning-bypass] Loaded OkHTTPv3 hook 1'); 401 | } catch(err) { 402 | console.log('[-][pinning-bypass] Skipping OkHTTPv3 hook 1'); 403 | } 404 | 405 | try{ 406 | okhttp3_CertificatePinner_class.check.overload('java.lang.String', 'java.security.cert.Certificate').implementation = function (str,cert) { 407 | console.log('[+][pinning-bypass] Bypassing OkHTTPv3 2: ' + str); 408 | return true; 409 | }; 410 | console.log('[+][pinning-bypass] Loaded OkHTTPv3 hook 2'); 411 | } catch(err) { 412 | console.log('[-][pinning-bypass] Skipping OkHTTPv3 hook 2'); 413 | } 414 | 415 | try { 416 | okhttp3_CertificatePinner_class.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function (str,cert_array) { 417 | console.log('[+][pinning-bypass] Bypassing OkHTTPv3 3: ' + str); 418 | return true; 419 | }; 420 | console.log('[+][pinning-bypass] Loaded OkHTTPv3 hook 3'); 421 | } catch(err) { 422 | console.log('[-][pinning-bypass] Skipping OkHTTPv3 hook 3'); 423 | } 424 | 425 | try { 426 | okhttp3_CertificatePinner_class['check$okhttp'].implementation = function (str,obj) { 427 | console.log('[+][pinning-bypass] Bypassing OkHTTPv3 4 (4.2+): ' + str); 428 | }; 429 | console.log('[+][pinning-bypass] Loaded OkHTTPv3 hook 4 (4.2+)'); 430 | } catch(err) { 431 | console.log('[-][pinning-bypass] Skipping OkHTTPv3 hook 4 (4.2+)'); 432 | } 433 | 434 | } 435 | 436 | }); 437 | },0); 438 | }, 439 | 440 | jailmonkeybypass: function(){ 441 | Java.perform(() => { 442 | const klass = Java.use("com.gantix.JailMonkey.JailMonkeyModule"); 443 | const hashmap_klass = Java.use("java.util.HashMap"); 444 | const false_obj = Java.use("java.lang.Boolean").FALSE.value; 445 | 446 | klass.getConstants.implementation = function () { 447 | var h = hashmap_klass.$new(); 448 | h.put("isJailBroken", false_obj); 449 | h.put("hookDetected", false_obj); 450 | h.put("canMockLocation", false_obj); 451 | h.put("isOnExternalStorage", false_obj); 452 | h.put("AdbEnabled", false_obj); 453 | return h; 454 | }; 455 | }); 456 | }, 457 | 458 | antirootbypass: function(){ 459 | // CHANGELOG by Pichaya Morimoto (p.morimoto@sth.sh): 460 | // - I added extra whitelisted items to deal with the latest versions 461 | // of RootBeer/Cordova iRoot as of August 6, 2019 462 | // - The original one just fucked up (kill itself) if Magisk is installed lol 463 | // Credit & Originally written by: https://codeshare.frida.re/@dzonerzy/fridantiroot/ 464 | // If this isn't working in the future, check console logs, rootbeer src, or libtool-checker.so 465 | Java.perform(function() { 466 | 467 | var RootPackages = ["com.noshufou.android.su", "com.noshufou.android.su.elite", "eu.chainfire.supersu", 468 | "com.koushikdutta.superuser", "com.thirdparty.superuser", "com.yellowes.su", "com.koushikdutta.rommanager", 469 | "com.koushikdutta.rommanager.license", "com.dimonvideo.luckypatcher", "com.chelpus.lackypatch", 470 | "com.ramdroid.appquarantine", "com.ramdroid.appquarantinepro", "com.devadvance.rootcloak", "com.devadvance.rootcloakplus", 471 | "de.robv.android.xposed.installer", "com.saurik.substrate", "com.zachspong.temprootremovejb", "com.amphoras.hidemyroot", 472 | "com.amphoras.hidemyrootadfree", "com.formyhm.hiderootPremium", "com.formyhm.hideroot", "me.phh.superuser", 473 | "eu.chainfire.supersu.pro", "com.kingouser.com", "com.android.vending.billing.InAppBillingService.COIN","com.topjohnwu.magisk" 474 | ]; 475 | 476 | var RootBinaries = ["su", "busybox", "supersu", "Superuser.apk", "KingoUser.apk", "SuperSu.apk","magisk", "/system/xbin/su"]; 477 | 478 | var RootProperties = { 479 | "ro.build.selinux": "1", 480 | "ro.debuggable": "0", 481 | "service.adb.root": "0", 482 | "ro.secure": "1" 483 | }; 484 | 485 | var RootPropertiesKeys = []; 486 | 487 | for (var k in RootProperties) RootPropertiesKeys.push(k); 488 | 489 | var PackageManager = Java.use("android.app.ApplicationPackageManager"); 490 | var Runtime = Java.use('java.lang.Runtime'); 491 | var NativeFile = Java.use('java.io.File'); 492 | var String = Java.use('java.lang.String'); 493 | var SystemProperties = Java.use('android.os.SystemProperties'); 494 | var BufferedReader = Java.use('java.io.BufferedReader'); 495 | var ProcessBuilder = Java.use('java.lang.ProcessBuilder'); 496 | var StringBuffer = Java.use('java.lang.StringBuffer'); 497 | var loaded_classes = Java.enumerateLoadedClassesSync(); 498 | 499 | console.log("[*][antiroot-bypass] Loaded " + loaded_classes.length + " classes!"); 500 | 501 | var useKeyInfo = false; 502 | 503 | var useProcessManager = false; 504 | 505 | console.log("[*][antiroot-bypass] loaded: " + loaded_classes.indexOf('java.lang.ProcessManager')); 506 | 507 | if (loaded_classes.indexOf('java.lang.ProcessManager') != -1) { 508 | try { 509 | //useProcessManager = true; 510 | //var ProcessManager = Java.use('java.lang.ProcessManager'); 511 | } catch (err) { 512 | console.log("[*][antiroot-bypass] ProcessManager Hook failed: " + err); 513 | } 514 | } else { 515 | console.log("[*][antiroot-bypass] ProcessManager hook not loaded"); 516 | } 517 | 518 | var KeyInfo = null; 519 | 520 | if (loaded_classes.indexOf('android.security.keystore.KeyInfo') != -1) { 521 | try { 522 | //useKeyInfo = true; 523 | //var KeyInfo = Java.use('android.security.keystore.KeyInfo'); 524 | } catch (err) { 525 | console.log("[*][antiroot-bypass] KeyInfo Hook failed: " + err); 526 | } 527 | } else { 528 | console.log("[*][antiroot-bypass] KeyInfo hook not loaded"); 529 | } 530 | 531 | PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pname, flags) { 532 | var shouldFakePackage = (RootPackages.indexOf(pname) > -1); 533 | if (shouldFakePackage) { 534 | console.log("[*][antiroot-bypass] Bypass root check for package: " + pname); 535 | pname = "set.package.name.to.a.fake.one.so.we.can.bypass.it"; 536 | } 537 | return this.getPackageInfo.call(this, pname, flags); 538 | }; 539 | 540 | NativeFile.exists.implementation = function() { 541 | var name = NativeFile.getName.call(this); 542 | var shouldFakeReturn = RootBinaries.find(element => { 543 | if (element.includes(name)) 544 | return true; 545 | }); 546 | // var shouldFakeReturn = (RootBinaries.indexOf(name) > -1); 547 | if (shouldFakeReturn) { 548 | console.log("[*][antiroot-bypass] Bypass return value for binary: " + name); 549 | return false; 550 | } else { 551 | return this.exists.call(); 552 | } 553 | }; 554 | 555 | var exec = Runtime.exec.overload('[Ljava.lang.String;'); 556 | var exec1 = Runtime.exec.overload('java.lang.String'); 557 | var exec2 = Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;'); 558 | var exec3 = Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;'); 559 | var exec4 = Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.io.File'); 560 | var exec5 = Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;', 'java.io.File'); 561 | 562 | exec5.implementation = function(cmd, env, dir) { 563 | if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") { 564 | var fakeCmd = "grep"; 565 | console.log("[*][antiroot-bypass] Bypass " + cmd + " command"); 566 | return exec1.call(this, fakeCmd); 567 | } 568 | if (cmd == "su") { 569 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 570 | console.log("[*][antiroot-bypass] Bypass " + cmd + " command"); 571 | return exec1.call(this, fakeCmd); 572 | } 573 | if (cmd == "which") { 574 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 575 | console.log("[*][antiroot-bypass] Bypass which command"); 576 | return exec1.call(this, fakeCmd); 577 | } 578 | return exec5.call(this, cmd, env, dir); 579 | }; 580 | 581 | exec4.implementation = function(cmdarr, env, file) { 582 | for (var i = 0; i < cmdarr.length; i = i + 1) { 583 | var tmp_cmd = cmdarr[i]; 584 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") { 585 | var fakeCmd = "grep"; 586 | console.log("[*][antiroot-bypass] Bypass " + cmdarr + " command"); 587 | return exec1.call(this, fakeCmd); 588 | } 589 | 590 | if (tmp_cmd == "su") { 591 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 592 | console.log("[*][antiroot-bypass] Bypass " + cmdarr + " command"); 593 | return exec1.call(this, fakeCmd); 594 | } 595 | } 596 | return exec4.call(this, cmdarr, env, file); 597 | }; 598 | 599 | exec3.implementation = function(cmdarr, envp) { 600 | for (var i = 0; i < cmdarr.length; i = i + 1) { 601 | var tmp_cmd = cmdarr[i]; 602 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") { 603 | var fakeCmd = "grep"; 604 | console.log(" [*][antiroot-bypass] Bypass " + cmdarr + " command"); 605 | return exec1.call(this, fakeCmd); 606 | } 607 | 608 | if (tmp_cmd == "su") { 609 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 610 | console.log(" [*][antiroot-bypass] Bypass " + cmdarr + " command"); 611 | return exec1.call(this, fakeCmd); 612 | } 613 | } 614 | return exec3.call(this, cmdarr, envp); 615 | }; 616 | 617 | exec2.implementation = function(cmd, env) { 618 | if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") { 619 | var fakeCmd = "grep"; 620 | console.log(" [*][antiroot-bypass] Bypass " + cmd + " command"); 621 | return exec1.call(this, fakeCmd); 622 | } 623 | if (cmd == "su") { 624 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 625 | console.log(" [*][antiroot-bypass] Bypass " + cmd + " command"); 626 | return exec1.call(this, fakeCmd); 627 | } 628 | return exec2.call(this, cmd, env); 629 | }; 630 | 631 | exec.implementation = function(cmd) { 632 | for (var i = 0; i < cmd.length; i = i + 1) { 633 | var tmp_cmd = cmd[i]; 634 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") { 635 | var fakeCmd = "grep"; 636 | console.log(" [*][antiroot-bypass] Bypass " + cmd + " command"); 637 | return exec1.call(this, fakeCmd); 638 | } 639 | 640 | if (tmp_cmd == "su") { 641 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 642 | console.log(" [*][antiroot-bypass] Bypass " + cmd + " command"); 643 | return exec1.call(this, fakeCmd); 644 | } 645 | } 646 | 647 | return exec.call(this, cmd); 648 | }; 649 | 650 | exec1.implementation = function(cmd) { 651 | if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") { 652 | var fakeCmd = "grep"; 653 | console.log(" [*][antiroot-bypass] Bypass " + cmd + " command"); 654 | return exec1.call(this, fakeCmd); 655 | } 656 | if (cmd == "su") { 657 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 658 | console.log(" [*][antiroot-bypass] Bypass " + cmd + " command"); 659 | return exec1.call(this, fakeCmd); 660 | } 661 | return exec1.call(this, cmd); 662 | }; 663 | 664 | String.contains.implementation = function(name) { 665 | if (name == "test-keys") { 666 | console.log(" [*][antiroot-bypass] Bypass test-keys check"); 667 | return false; 668 | } 669 | return this.contains.call(this, name); 670 | }; 671 | 672 | var get = SystemProperties.get.overload('java.lang.String'); 673 | 674 | get.implementation = function(name) { 675 | if (RootPropertiesKeys.indexOf(name) != -1) { 676 | console.log(" [*][antiroot-bypass] Bypass " + name); 677 | return RootProperties[name]; 678 | } 679 | return this.get.call(this, name); 680 | }; 681 | 682 | Interceptor.attach(Module.findExportByName("libc.so", "fopen"), { 683 | onEnter: function(args) { 684 | var path1 = Memory.readCString(args[0]); 685 | var path = path1.split("/"); 686 | var executable = path[path.length - 1]; 687 | var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1) 688 | if (shouldFakeReturn) { 689 | Memory.writeUtf8String(args[0], "/ggezxxx"); 690 | console.log(" [*][antiroot-bypass] Bypass native fopen >> "+path1); 691 | } 692 | }, 693 | onLeave: function(retval) { 694 | 695 | } 696 | }); 697 | 698 | Interceptor.attach(Module.findExportByName("libc.so", "fopen"), { 699 | onEnter: function(args) { 700 | var path1 = Memory.readCString(args[0]); 701 | var path = path1.split("/"); 702 | var executable = path[path.length - 1]; 703 | var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1) 704 | if (shouldFakeReturn) { 705 | Memory.writeUtf8String(args[0], "/ggezxxx"); 706 | console.log(" [*][antiroot-bypass] Bypass native fopen >> "+path1); 707 | } 708 | }, 709 | onLeave: function(retval) { 710 | 711 | } 712 | }); 713 | 714 | Interceptor.attach(Module.findExportByName("libc.so", "system"), { 715 | onEnter: function(args) { 716 | var cmd = Memory.readCString(args[0]); 717 | console.log(" [*][antiroot-bypass] SYSTEM CMD: " + cmd); 718 | if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") { 719 | console.log(" [*][antiroot-bypass] Bypass native system: " + cmd); 720 | Memory.writeUtf8String(args[0], "grep"); 721 | } 722 | if (cmd == "su") { 723 | console.log(" [*][antiroot-bypass] Bypass native system: " + cmd); 724 | Memory.writeUtf8String(args[0], "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"); 725 | } 726 | }, 727 | onLeave: function(retval) { 728 | 729 | } 730 | }); 731 | 732 | /* 733 | 734 | TO IMPLEMENT: 735 | 736 | Exec Family 737 | 738 | int execl(const char *path, const char *arg0, ..., const char *argn, (char *)0); 739 | int execle(const char *path, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]); 740 | int execlp(const char *file, const char *arg0, ..., const char *argn, (char *)0); 741 | int execlpe(const char *file, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]); 742 | int execv(const char *path, char *const argv[]); 743 | int execve(const char *path, char *const argv[], char *const envp[]); 744 | int execvp(const char *file, char *const argv[]); 745 | int execvpe(const char *file, char *const argv[], char *const envp[]); 746 | 747 | */ 748 | 749 | 750 | BufferedReader.readLine.overload().implementation = function() { 751 | var text = this.readLine.call(this); 752 | if (text === null) { 753 | // just pass , i know it's ugly as hell but test != null won't work :( 754 | } else { 755 | var shouldFakeRead = (text.indexOf("ro.build.tags=test-keys") > -1); 756 | if (shouldFakeRead) { 757 | console.log(" [*][antiroot-bypass] Bypass build.prop file read"); 758 | text = text.replace("ro.build.tags=test-keys", "ro.build.tags=release-keys"); 759 | } 760 | } 761 | return text; 762 | }; 763 | 764 | var executeCommand = ProcessBuilder.command.overload('java.util.List'); 765 | 766 | ProcessBuilder.start.implementation = function() { 767 | var cmd = this.command.call(this); 768 | var shouldModifyCommand = false; 769 | for (var i = 0; i < cmd.size(); i = i + 1) { 770 | var tmp_cmd = cmd.get(i).toString(); 771 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd.indexOf("mount") != -1 || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd.indexOf("id") != -1) { 772 | shouldModifyCommand = true; 773 | } 774 | } 775 | if (shouldModifyCommand) { 776 | console.log(" [*][antiroot-bypass] Bypass ProcessBuilder " + cmd); 777 | this.command.call(this, ["grep"]); 778 | return this.start.call(this); 779 | } 780 | if (cmd.indexOf("su") != -1) { 781 | console.log(" [*][antiroot-bypass] Bypass ProcessBuilder " + cmd); 782 | this.command.call(this, ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"]); 783 | return this.start.call(this); 784 | } 785 | 786 | return this.start.call(this); 787 | }; 788 | 789 | if (useProcessManager) { 790 | var ProcManExec = ProcessManager.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.io.File', 'boolean'); 791 | var ProcManExecVariant = ProcessManager.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.lang.String', 'java.io.FileDescriptor', 'java.io.FileDescriptor', 'java.io.FileDescriptor', 'boolean'); 792 | 793 | ProcManExec.implementation = function(cmd, env, workdir, redirectstderr) { 794 | var fake_cmd = cmd; 795 | for (var i = 0; i < cmd.length; i = i + 1) { 796 | var tmp_cmd = cmd[i]; 797 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id") { 798 | var fake_cmd = ["grep"]; 799 | console.log(" [*][antiroot-bypass] Bypass " + cmdarr + " command"); 800 | } 801 | 802 | if (tmp_cmd == "su") { 803 | var fake_cmd = ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"]; 804 | console.log(" [*][antiroot-bypass] Bypass " + cmdarr + " command"); 805 | } 806 | } 807 | return ProcManExec.call(this, fake_cmd, env, workdir, redirectstderr); 808 | }; 809 | 810 | ProcManExecVariant.implementation = function(cmd, env, directory, stdin, stdout, stderr, redirect) { 811 | var fake_cmd = cmd; 812 | for (var i = 0; i < cmd.length; i = i + 1) { 813 | var tmp_cmd = cmd[i]; 814 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id") { 815 | var fake_cmd = ["grep"]; 816 | console.log(" [*][antiroot-bypass] Bypass " + cmdarr + " command"); 817 | } 818 | 819 | if (tmp_cmd == "su") { 820 | var fake_cmd = ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"]; 821 | console.log(" [*][antiroot-bypass] Bypass " + cmdarr + " command"); 822 | } 823 | } 824 | return ProcManExecVariant.call(this, fake_cmd, env, directory, stdin, stdout, stderr, redirect); 825 | }; 826 | } 827 | 828 | if (useKeyInfo) { 829 | KeyInfo.isInsideSecureHardware.implementation = function() { 830 | console.log(" [*][antiroot-bypass] Bypass isInsideSecureHardware"); 831 | return true; 832 | } 833 | } 834 | }); 835 | 836 | }, 837 | 838 | rootbeerbypass: function(){ 839 | 840 | setTimeout(function(){ 841 | Java.perform(function (){ 842 | 843 | console.log("[*] root beer bypass") 844 | var RootPackages = ["com.noshufou.android.su", "com.noshufou.android.su.elite", "eu.chainfire.supersu", 845 | "com.koushikdutta.superuser", "com.thirdparty.superuser", "com.yellowes.su", "com.koushikdutta.rommanager", 846 | "com.koushikdutta.rommanager.license", "com.dimonvideo.luckypatcher", "com.chelpus.lackypatch", 847 | "com.ramdroid.appquarantine", "com.ramdroid.appquarantinepro", "com.devadvance.rootcloak", "com.devadvance.rootcloakplus", 848 | "de.robv.android.xposed.installer", "com.saurik.substrate", "com.zachspong.temprootremovejb", "com.amphoras.hidemyroot", 849 | "com.amphoras.hidemyrootadfree", "com.formyhm.hiderootPremium", "com.formyhm.hideroot", "me.phh.superuser", 850 | "eu.chainfire.supersu.pro", "com.kingouser.com", "com.android.vending.billing.InAppBillingService.COIN","com.topjohnwu.magisk" 851 | ]; 852 | 853 | var RootBinaries = ["su", "busybox", "supersu", "Superuser.apk", "KingoUser.apk", "SuperSu.apk","magisk"]; 854 | 855 | var RootProperties = { 856 | "ro.build.selinux": "1", 857 | "ro.debuggable": "0", 858 | "service.adb.root": "0", 859 | "ro.secure": "1" 860 | }; 861 | 862 | var RootPropertiesKeys = []; 863 | 864 | for (var k in RootProperties) RootPropertiesKeys.push(k); 865 | 866 | var PackageManager = Java.use("android.app.ApplicationPackageManager"); 867 | var Runtime = Java.use('java.lang.Runtime'); 868 | var NativeFile = Java.use('java.io.File'); 869 | 870 | NativeFile.exists.implementation = function() { 871 | var name = NativeFile.getAbsolutePath.call(this); 872 | console.log(name); 873 | var shouldFakeReturn = RootBinaries.find(element => { 874 | if (name.includes(element)) 875 | return true; 876 | }); 877 | 878 | if (shouldFakeReturn) { 879 | console.log("[*][antiroot-bypass] Bypass return value for binary: " + name); 880 | return false; 881 | } else { 882 | return this.exists.call(this); 883 | } 884 | }; 885 | 886 | }); 887 | }, 0); 888 | 889 | }, 890 | 891 | antifridabypass: function(){ 892 | Java.perform(() => { 893 | Interceptor.attach(Module.findExportByName("libc.so", "strstr"), { 894 | 895 | onEnter: function(args) { 896 | 897 | this.haystack = args[0]; 898 | this.needle = args[1]; 899 | this.frida = Boolean(0); 900 | 901 | haystack = Memory.readUtf8String(this.haystack); 902 | needle = Memory.readUtf8String(this.needle); 903 | 904 | if (haystack.indexOf("frida") !== -1 || haystack.indexOf("xposed") !== -1) { 905 | this.frida = Boolean(1); 906 | } 907 | }, 908 | 909 | onLeave: function(retval) { 910 | 911 | if (this.frida) { 912 | retval.replace(0); 913 | } 914 | return retval; 915 | } 916 | }); 917 | }); 918 | } 919 | }; 920 | 921 | function nativedynamichook(hook, category) { 922 | // File System monitor only - libc.so 923 | Interceptor.attach( 924 | Module.findExportByName(hook["clazz"], hook["method"]), { 925 | onEnter: function (args) { 926 | var file = Memory.readCString(args[0]); 927 | //bypass ashem and prod if libc.so - open 928 | if (hook["clazz"] == "libc.so" && 929 | hook["method"] == "open" && 930 | !file.includes("/dev/ashmem") && 931 | !file.includes("/proc/")) 932 | console.log("[API Monitor] - " + category + " - " + hook["clazz"] + " - " + hook["method"] + " - " + file); 933 | } 934 | } 935 | ); 936 | } 937 | 938 | function javadynamichook(hook, category, callback) { 939 | var Exception = Java.use('java.lang.Exception'); 940 | var toHook; 941 | try { 942 | var clazz = hook.clazz; 943 | var method = hook.method; 944 | 945 | try { 946 | if (hook.target && 947 | parseInt(Java.androidVersion, 10) < hook.target) { 948 | send('API Monitor - Android Version not supported - Cannot hook - ' + clazz + '.' + method) 949 | return 950 | } 951 | // Check if class and method is available 952 | toHook = Java.use(clazz)[method]; 953 | if (!toHook) { 954 | send('API Monitor - Cannot find ' + clazz + '.' + method); 955 | return 956 | } 957 | } catch (err) { 958 | send('API Monitor - Cannot find ' + clazz + '.' + method); 959 | return 960 | } 961 | 962 | // if contains only one methods 963 | 964 | 965 | if (toHook.overloads == undefined){ 966 | toHook.implementation = function () { 967 | if (arguments !== undefined) { 968 | var args = [].slice.call(arguments); 969 | 970 | if (args !== undefined){ 971 | for (var k = 0, l = args.length; k < l; k++) { 972 | args_string_value.push(args[k] + ""); 973 | } 974 | } 975 | } 976 | 977 | // Call original method 978 | var retval = this[method].apply(this, arguments); 979 | if (callback) { 980 | var calledFrom = Exception.$new().getStackTrace().toString().split(',')[1]; 981 | 982 | var to_print = { 983 | category: category, 984 | class: clazz, 985 | method: method, 986 | args: args_string_value, 987 | calledFrom: calledFrom, 988 | result: retval ? retval.toString() : null, 989 | }; 990 | retval = callback(retval, to_print); 991 | 992 | } 993 | return retval; 994 | } 995 | } else { 996 | 997 | for (var i = 0; i < toHook.overloads.length; i++) { 998 | var args_string_value = [] 999 | toHook.overloads[i].implementation = function () { 1000 | 1001 | if (arguments !== undefined) { 1002 | var args = [].slice.call(arguments); 1003 | // send('API Monitor - '+ typeof args) 1004 | 1005 | if (args !== undefined){ 1006 | for (var k = 0, l = args.length; k < l; k++) { 1007 | args_string_value.push(args[k] + ""); 1008 | } 1009 | } 1010 | } 1011 | 1012 | // Call original method 1013 | var retval = this[method].apply(this, arguments); 1014 | 1015 | if (callback) { 1016 | var calledFrom = Exception.$new().getStackTrace().toString().split(',')[1]; 1017 | var to_print = { 1018 | category: category, 1019 | class: clazz, 1020 | method: method, 1021 | args: args_string_value, 1022 | calledFrom: calledFrom, 1023 | result: retval ? retval.toString() : null, 1024 | }; 1025 | retval = callback(retval, to_print); 1026 | } 1027 | return retval; 1028 | } 1029 | } 1030 | } 1031 | } catch (err) { 1032 | send('API Monitor - ERROR: ' + clazz + "." + method + " [\"Error\"] => " + err); 1033 | } 1034 | } -------------------------------------------------------------------------------- /api-example/hooks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "className": "android.widget.Toast", 4 | "methodName": "makeText", 5 | "thisObject": false, 6 | "category": "generic" 7 | }, 8 | { 9 | "className": "com.scottyab.rootbeer.RootBeerNative", 10 | "methodName": "checkForRoot", 11 | "thisObject": false, 12 | "category": "generic" 13 | }, 14 | 15 | { 16 | "className": "android.webkit.WebView", 17 | "methodName": "loadUrl", 18 | "thisObject": false, 19 | "category": "generic" 20 | }, 21 | { 22 | "className": "android.webkit.WebViewClient", 23 | "methodName": "onReceivedSslError", 24 | "thisObject": false, 25 | "category": "generic" 26 | } 27 | 28 | ] -------------------------------------------------------------------------------- /img_repo/papimonitor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dado1513/PAPIMonitor/e2b68a5929d56588160e6da632575e2634f36ea4/img_repo/papimonitor.gif -------------------------------------------------------------------------------- /papi-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dado1513/PAPIMonitor/e2b68a5929d56588160e6da632575e2634f36ea4/papi-monitor.png -------------------------------------------------------------------------------- /papi_monitor.py: -------------------------------------------------------------------------------- 1 | from statistics import median 2 | import frida 3 | import json 4 | from datetime import datetime 5 | import argparse 6 | from rich import print 7 | from rich.console import Console 8 | from loguru import logger 9 | import json 10 | from utils import * 11 | 12 | console = Console() 13 | file_log_frida = os.path.join(os.path.dirname(__file__), "logs") 14 | 15 | api_monitor_hooked = [] 16 | 17 | def on_message(message, data): 18 | """ 19 | 20 | Parameters 21 | ---------- 22 | message 23 | data 24 | 25 | Returns 26 | ------- 27 | 28 | """ 29 | 30 | if message["type"] == "error": 31 | # logger.error(message["description"]) 32 | logger.error(message) 33 | 34 | if message["type"] == "send": 35 | 36 | if type(message["payload"]) is str: 37 | if "API Monitor" not in message["payload"]: 38 | try: 39 | message_dict = json.loads(message["payload"]) 40 | message_dict["time"] = datetime.now().strftime("%m/%d/%Y, %H:%M:%S") 41 | console.log(message_dict) 42 | api_monitor_hooked.append(message_dict) 43 | except json.decoder.JSONDecodeError as e: 44 | logger.info(message["payload"]) 45 | pass 46 | # logger.info(message["payload"]) 47 | else: 48 | try: 49 | message_dict = json.loads(message["payload"]) 50 | message_dict["time"] = datetime.now().strftime("%m/%d/%Y, %H:%M:%S") 51 | console.log(message_dict) 52 | api_monitor_hooked.append(message_dict) 53 | except json.decoder.JSONDecodeError as e: 54 | logger.info("[* error]"+ str(message["payload"])) 55 | pass 56 | return 57 | else: 58 | # general message here 59 | message_dict = message["payload"] 60 | logger.info("[*]" +str(message_dict)) 61 | 62 | 63 | 64 | 65 | def main( 66 | app_path: str, 67 | api_monitor_file : str = None, 68 | is_app_to_install: bool = True, 69 | is_google_emulator: bool = False, 70 | category: list = ["NONE"], 71 | pinning_bypass: bool = False, 72 | antiroot_bypass: bool = False 73 | ): 74 | """ 75 | Parameters 76 | ---------- 77 | app_path 78 | api_monitor_file 79 | is_app_to_install 80 | is_google_emulator 81 | category 82 | Returns 83 | ------- 84 | """ 85 | if is_app_to_install: 86 | package_name = install_app_and_install_frida(app_path=app_path, is_google_emulator=is_google_emulator) 87 | else: 88 | package_name = create_adb_and_start_frida(package_name=app_path, is_google_emulator=is_google_emulator) 89 | 90 | pid = None 91 | device = None 92 | session = None 93 | 94 | try: 95 | device = frida.get_usb_device() 96 | pid = device.spawn([package_name]) 97 | session = device.attach(pid) 98 | except Exception as e: 99 | logger.error("Error {}".format(e)) 100 | device = frida.get_usb_device() 101 | pid = device.spawn([package_name]) 102 | session = device.attach(pid) 103 | 104 | logger.debug("[*] Succesfully attacched frida to app") 105 | 106 | global file_log_frida 107 | dir_frida = os.path.join(file_log_frida, package_name.replace(".", "_")) 108 | if not os.path.exists(dir_frida): 109 | os.makedirs(dir_frida) 110 | time_file = datetime.now().strftime("%H_%M_%S_%m_%d_%Y") 111 | file_log_frida = os.path.join( 112 | dir_frida, f"api-monitor-{package_name.replace('.', '_')}_{time_file}.txt" 113 | ) 114 | 115 | with open( 116 | os.path.join(os.path.dirname(__file__), "api-android-monitor", "papi-monitor.js") 117 | ) as f: 118 | frida_code = f.read() 119 | 120 | script = session.create_script(frida_code) 121 | script.on("message", on_message) 122 | script.load() 123 | device.resume(pid) 124 | api = script.exports 125 | api_monitor = [] 126 | 127 | if api_monitor_file is not None: 128 | json_api_monitor = create_json_api_monitor(api_monitor_file) 129 | if json_api_monitor is not None: 130 | api_monitor.extend(json_api_monitor) 131 | # append all category 132 | for e in json_api_monitor: 133 | category.append(e["Category"]) 134 | if "NONE" not in category: 135 | # add api_monitor default 136 | with open( 137 | os.path.join( 138 | os.path.dirname(__file__), "api-android-monitor", "api-monitor.json" 139 | ) 140 | ) as f: 141 | api_monitor = api_monitor + json.load(f) 142 | 143 | # remove app filtered 144 | if "ALL" not in category: 145 | api_filter = [e for e in api_monitor if e["Category"] in category] 146 | api_to_hook = json.loads(json.dumps(api_filter)) 147 | api.apimonitor(api_to_hook) 148 | else: 149 | api.apimonitor(api_monitor) 150 | else: 151 | api.apimonitor(api_monitor) 152 | 153 | # TODO antiroot_bypass 154 | if antiroot_bypass: 155 | api.rootbeerbypass() 156 | # api.antirootbypass() 157 | # api.nativefile() 158 | 159 | # TODO bypass pinning 160 | if pinning_bypass: 161 | pass 162 | # push der certificate 163 | # api.pinningbypass() 164 | 165 | time.sleep(3) 166 | 167 | while True: 168 | try: 169 | command = input("[Press 0 to exit] > \n\n") 170 | if command == "0": 171 | if len(api_monitor_hooked) > 0: 172 | logger.info(f"[*] Saving api-hooked on {file_log_frida}") 173 | file_log = open(file_log_frida, "a") 174 | json.dump(api_monitor_hooked, file_log, indent=4) 175 | api_monitor = [] 176 | break 177 | except KeyboardInterrupt as e: 178 | if len(api_monitor_hooked) > 0: 179 | logger.info(f"[*] Saving api-hooked on {file_log_frida}") 180 | file_log = open(file_log_frida, "a") 181 | json.dump(api_monitor_hooked, file_log, indent=4) 182 | api_monitor = [] 183 | break 184 | except Exception as e: 185 | 186 | logger.error(f"[*] Error as {e}") 187 | if len(api_monitor_hooked) > 0: 188 | logger.info(f"[*] Saving api-hooked on {file_log_frida}") 189 | file_log = open(file_log_frida, "a") 190 | json.dump(api_monitor_hooked, file_log, indent=4) 191 | api_monitor = [] 192 | break 193 | 194 | def get_cmd_args(args: list = None): 195 | """ 196 | Parse and return the command line parameters needed for the script execution. 197 | :param args: List of arguments to be parsed (by default sys.argv is used). 198 | :return: The command line needed parameters. 199 | """ 200 | 201 | parser = argparse.ArgumentParser( 202 | prog="Python API Monitor for Android apps", 203 | description="Start dynamic API monitoring", 204 | usage=""" 205 | python papi_monitor.py --package-name com.package.name --filter "Crypto" 206 | python papi_monitor.py --file-apk app.apk --api-monitor api_personalized.json 207 | python papi_monitor.py --package-name com.package.name --api-monitor api_personalized.json 208 | python papi_monitor.py --package-name com.package.name --filter "ALL" 209 | python papi_monitor.py --package-name com.package.name ---api-monitor api_personalized.json --store-script True --filter "Crypto" "Crypto - Hash" 210 | python papi_monitor.py --package-name com.package.name --api-monitor api_personalized.json --pinning-bypass --antiroot-bypass 211 | 212 | """, 213 | formatter_class=argparse.RawTextHelpFormatter, 214 | ) 215 | 216 | parser.add_argument( 217 | "-f", "--file-apk", type=str, metavar="APK", help="file apk to analyze" 218 | ) 219 | 220 | parser.add_argument( 221 | "-p", 222 | "--package-name", 223 | type=str, 224 | metavar="PACKAGENAME", 225 | help="Package Name of app to analyze", 226 | ) 227 | 228 | parser.add_argument( 229 | "--api-monitor", 230 | type=str, 231 | metavar="API", 232 | help="File that contain the list of API to monitoring, \ne.g., hooks.json", 233 | ) 234 | 235 | parser.add_argument( 236 | "--filter", 237 | type=str, 238 | nargs="+", 239 | choices=[ 240 | "Device Data", 241 | "Device Info", 242 | "SMS", 243 | "System Manager", 244 | "Base64 encode/decode", 245 | "Dex Class Loader", 246 | "Network", 247 | "Crypto", 248 | "Crypto - Hash", 249 | "Binder", 250 | "IPC", 251 | "Database", 252 | "SharedPreferences", 253 | "WebView", 254 | "Java Native Interface", 255 | "Command", 256 | "Process", 257 | "FileSytem - Java", 258 | "ALL", 259 | "NONE", 260 | ], 261 | default=["NONE"], 262 | ) 263 | 264 | parser.add_argument("--store-script", type=bool, default=False) 265 | parser.add_argument("--google-emulator", action="store_true") 266 | 267 | parser.add_argument("--pinning-bypass", action="store_true", help="Flag for bypass app ssl pinning") 268 | parser.add_argument("--antiroot-bypass", action="store_true", help="Flag for bypass app root detection") 269 | 270 | 271 | return parser.parse_args(args) 272 | 273 | 274 | if __name__ == "__main__": 275 | 276 | arguments = get_cmd_args() 277 | app = None 278 | is_app_to_install = False 279 | 280 | if arguments.file_apk is not None and os.path.exists(arguments.file_apk): 281 | logger.info("[*] Start PAPIMonitor with App Installation") 282 | app = arguments.file_apk 283 | is_app_to_install = True 284 | elif arguments.package_name is not None: 285 | logger.info("[*] Start PAPIMonitor without App Installation") 286 | app = arguments.package_name 287 | is_app_to_install = False 288 | 289 | if app is not None: 290 | 291 | # if app is not None mean that the app path exist or package name is set 292 | if arguments.api_monitor is not None: 293 | # argument is a file of API to monitor hook.txt/json 294 | monitor_file = arguments.api_monitor 295 | main( 296 | app, 297 | api_monitor_file=arguments.api_monitor, 298 | is_app_to_install=is_app_to_install, 299 | category=arguments.filter, 300 | pinning_bypass=arguments.pinning_bypass, 301 | antiroot_bypass=arguments.antiroot_bypass 302 | ) 303 | 304 | else: 305 | # used list api of chosen by user (default is NONE) 306 | main( 307 | app, 308 | None, 309 | is_app_to_install=is_app_to_install, 310 | category=arguments.filter, 311 | pinning_bypass=arguments.pinning_bypass, 312 | antiroot_bypass=arguments.antiroot_bypass 313 | ) 314 | 315 | else: 316 | print( 317 | "[bold][*] Usage: python papi_monitor.py --package-name com.package.name[/bold]" 318 | ) 319 | print( 320 | "[bold][*] Usage: python papi_monitor.py --file-apk app.apk --list-api api_personalized_1.txt [/bold]" 321 | ) 322 | print( 323 | "[bold][*] Usage: python papi_monitor.py --package-name com.package.name " 324 | "--api-monitor api_personalized.json [/bold]" 325 | ) 326 | print( 327 | "[bold][*] Usage: python papi_monitor.py --package-name com.package.name --filter \"Crypto\" [/bold]" 328 | ) 329 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | androguard==3.3.5 2 | asn1crypto==1.5.1 3 | asttokens==2.2.1 4 | backcall==0.2.0 5 | click==8.1.3 6 | colorama==0.4.6 7 | commonmark==0.9.1 8 | contourpy==1.0.7 9 | cycler==0.11.0 10 | decorator==5.1.1 11 | executing==1.2.0 12 | fonttools==4.38.0 13 | frida==16.0.10 14 | frida-tools==12.1.1 15 | future==0.18.3 16 | ipython==8.10.0 17 | jedi==0.18.2 18 | kiwisolver==1.4.4 19 | loguru==0.6.0 20 | lxml==4.9.2 21 | matplotlib==3.7.0 22 | matplotlib-inline==0.1.6 23 | networkx==3.0 24 | numpy==1.24.2 25 | packaging==23.0 26 | parso==0.8.3 27 | pexpect==4.8.0 28 | pickleshare==0.7.5 29 | Pillow==9.4.0 30 | prompt-toolkit==3.0.36 31 | ptyprocess==0.7.0 32 | pure-eval==0.2.2 33 | pydot==1.4.2 34 | Pygments==2.14.0 35 | pyparsing==3.0.9 36 | python-dateutil==2.8.2 37 | rich==9.13.0 38 | six==1.16.0 39 | stack-data==0.6.2 40 | traitlets==5.9.0 41 | typing_extensions==4.5.0 42 | wcwidth==0.2.6 43 | -------------------------------------------------------------------------------- /resources/frida-server/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dado1513/PAPIMonitor/e2b68a5929d56588160e6da632575e2634f36ea4/resources/frida-server/.gitkeep -------------------------------------------------------------------------------- /scripts-android/file-system-monitor.js: -------------------------------------------------------------------------------- 1 | 2 | /************************************************************************ 3 | * Name: File System Monitor 4 | * OS: Android 5 | * Author: @mobilesecurity_ 6 | * Source: https://github.com/m0bilesecurity 7 | * Info: (libc.so - open, close, read, write, unlink, remove) 8 | *************************************************************************/ 9 | 10 | Java.perform(function () { 11 | Interceptor.attach( 12 | Module.findExportByName("libc.so", "open"), { 13 | onEnter: function (args) { 14 | var file = Memory.readCString(args[0]); 15 | if(!file.includes("/dev/ashmem") && !file.includes("/proc/")) 16 | print("open",file); 17 | }, 18 | onLeave: function (retval) { 19 | 20 | } 21 | } 22 | ); 23 | 24 | Interceptor.attach( 25 | Module.findExportByName("libc.so", "close"), { 26 | onEnter: function (args) { 27 | var file = Memory.readCString(args[0]); 28 | print("close",file); 29 | }, 30 | onLeave: function (retval) { 31 | 32 | } 33 | } 34 | ); 35 | 36 | Interceptor.attach( 37 | Module.findExportByName("libc.so", "read"), { 38 | onEnter: function (args) { 39 | var file = Memory.readCString(args[0]); 40 | print("read",file); 41 | }, 42 | onLeave: function (retval) { 43 | 44 | } 45 | } 46 | ); 47 | 48 | Interceptor.attach( 49 | Module.findExportByName("libc.so", "write"), { 50 | onEnter: function (args) { 51 | var file = Memory.readCString(args[0]); 52 | print("write",file); 53 | }, 54 | onLeave: function (retval) { 55 | 56 | } 57 | } 58 | ); 59 | 60 | Interceptor.attach( 61 | Module.findExportByName("libc.so", "unlink"), { 62 | onEnter: function (args) { 63 | var file = Memory.readCString(args[0]); 64 | print("remove",file); 65 | }, 66 | onLeave: function (retval) { 67 | 68 | } 69 | } 70 | ); 71 | 72 | Interceptor.attach( 73 | Module.findExportByName("libc.so", "remove"), { 74 | onEnter: function (args) { 75 | var file = Memory.readCString(args[0]); 76 | print("remove",file); 77 | }, 78 | onLeave: function (retval) { 79 | 80 | } 81 | } 82 | ); 83 | 84 | 85 | function print(method,file){ 86 | send("API Monitor | "+ 87 | "FileSystem" + " | " + 88 | method + " - " + 89 | file 90 | ); 91 | } 92 | }); -------------------------------------------------------------------------------- /scripts-android/get-app-env-information.js: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | * Name: Android App Environment 3 | * OS: Android 4 | * Author: @mobilesecurity_ 5 | * Source: https://github.com/m0bilesecurity 6 | * Info: 7 | * filesDirectory 8 | * cacheDirectory and externalCacheDirectory 9 | * codeCacheDirectory 10 | * obbDir 11 | * packageCodePath 12 | *************************************************************************/ 13 | 14 | Java.perform(function() { 15 | var context = null 16 | var ActivityThread = Java.use('android.app.ActivityThread'); 17 | var targetApp = ActivityThread.currentApplication(); 18 | 19 | if (targetApp != null) { 20 | context = targetApp.getApplicationContext(); 21 | var env = { 22 | filesDirectory: context.getFilesDir().getAbsolutePath().toString(), 23 | cacheDirectory: context.getCacheDir().getAbsolutePath().toString(), 24 | externalCacheDirectory: context.getExternalCacheDir().getAbsolutePath().toString(), 25 | codeCacheDirectory: 26 | 'getCodeCacheDir' in context ? 27 | context.getCodeCacheDir().getAbsolutePath().toString() : 'N/A', 28 | obbDir: context.getObbDir().getAbsolutePath().toString(), 29 | packageCodePath: context.getPackageCodePath().toString(), 30 | }; 31 | 32 | send("******************* App Environment Info *******************") 33 | send("filesDirectory: "+env.filesDirectory); 34 | send("cacheDirectory: "+env.cacheDirectory); 35 | send("externalCacheDirectory: "+env.externalCacheDirectory); 36 | send("codeCacheDirectory: "+env.codeCacheDirectory); 37 | send("obbDir: "+env.obbDir); 38 | send("packageCodePath: "+env.packageCodePath); 39 | send("************************************************************") 40 | 41 | } else console.log("Error: App Environment Info - N/A") 42 | 43 | }); -------------------------------------------------------------------------------- /scripts-android/load-stetho.js: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | * Name: Load Stetho by Facebook - a debug bridge for Android apps 3 | * OS: Android 4 | * Author: @mobilesecurity_ 5 | * Source: https://github.com/m0bilesecurity 6 | * Info: How to use Stetho? 7 | 1. Download Stetho - http://facebook.github.io/stetho/ 8 | 2. Rename to stetho.jar 9 | 3. Download dextojar https://sourceforge.net/projects/dex2jar/ 10 | 4. Convert the jar file to dex - d2j-jar2dex.sh stetho.jar 11 | 5. Push the dex file in /data/local/tmp/ 12 | adb push stetho-jar2dex.dex /data/local/tmp/stetho.jar 13 | 6. Open chrome at this address - chrome://inspect/#devices 14 | 7. Inspect your app! 15 | *************************************************************************/ 16 | 17 | Java.perform(function () { 18 | 19 | const stethoJarFilePath = "/data/local/tmp/stetho.jar" 20 | 21 | const stethoClassName = "com.facebook.stetho.Stetho"; 22 | const pathClassLoader = Java.use("dalvik.system.PathClassLoader"); 23 | const javaFile = Java.use("java.io.File"); 24 | const activityThread = Java.use("android.app.ActivityThread"); 25 | const app = activityThread.currentApplication(); 26 | const context = app.getApplicationContext(); 27 | 28 | const stethoJarFile = javaFile.$new(stethoJarFilePath); 29 | const loader = pathClassLoader.$new(stethoJarFile.getAbsolutePath(), 30 | null, 31 | app.getClassLoader()); 32 | try { 33 | loader.loadClass(stethoClassName); 34 | 35 | var classLoaders = Java.enumerateClassLoadersSync(); 36 | classLoaders=classLoaders.filter(function (cl) { 37 | return cl.toString().includes("stetho"); 38 | }); 39 | 40 | Java.classFactory.loader = classLoaders[0]; 41 | const stetho = Java.use(stethoClassName); 42 | stetho.initializeWithDefaults(context); 43 | send("Stetho successfully loaded!"); 44 | send("Open Chrome at chrome://inspect/#devices") 45 | 46 | } catch (err) { 47 | send("Stetho NOT loaded!"); 48 | send(err.toString()); 49 | } 50 | }); -------------------------------------------------------------------------------- /scripts-android/native-hook-template.js: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | * Name: Native Hook Template 3 | * OS: Android 4 | * Author: @mobilesecurity_ 5 | * Source: https://github.com/m0bilesecurity 6 | * Info: 7 | * {native_library} = e.g. libc.so 8 | * {native_function} = e.g. open 9 | * args is an array containing arguments passed to native function 10 | * retval contains return value 11 | *************************************************************************/ 12 | 13 | var native_library="{native_library}" 14 | var native_function="{native_function}" 15 | 16 | Interceptor.attach( 17 | Module.findExportByName(native_library, native_function), { 18 | onEnter: function (args) { 19 | send(native_library + " - " + native_function); 20 | send("arg0 "+Memory.readCString(args[0])); 21 | 22 | }, 23 | onLeave: function (retval) { 24 | send("Return Value: "+Memory.readCString(retval)); 25 | //retval.replace(0); 26 | } 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /scripts-android/string-compare.js: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | * Name: Strings Compare 3 | * OS: Android 4 | * Author: iddoeldor 5 | * Source: https://github.com/iddoeldor/frida-snippets#string-comparison 6 | *************************************************************************/ 7 | 8 | Java.perform(function() { 9 | 10 | var str = Java.use('java.lang.String'); 11 | str.equals.overload('java.lang.Object').implementation = function(obj) { 12 | var result = str.equals.overload('java.lang.Object').call(this, obj); 13 | if (obj) { 14 | if (obj.toString().length > 8) { 15 | send(str.toString.call(this)+" == "+obj.toString()+" ? "+ result); 16 | } 17 | } 18 | return result; 19 | } 20 | 21 | }); -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from cmath import log 2 | import imp 3 | from loguru import logger 4 | from adb import ADB 5 | import json 6 | from androguard.core.bytecodes.apk import APK 7 | import time 8 | import os 9 | 10 | 11 | def push_and_start_frida_server(adb: ADB): 12 | """ 13 | Push and start adb server on device 14 | Parameters 15 | ---------- 16 | adb 17 | 18 | Returns 19 | ------- 20 | 21 | """ 22 | frida_server = os.path.join( 23 | os.path.dirname(__file__), "resources", "frida-server", "frida-server" 24 | ) 25 | 26 | cmd_output = adb.shell("ps -e | grep frida".split()) 27 | 28 | if "frida-server" in cmd_output: 29 | logger.warning("[*] frida-server is already running on device") 30 | return 31 | 32 | try: 33 | adb.execute(["root"]) 34 | except Exception as e: 35 | adb.kill_server() 36 | logger.error("Error on adb {}".format(e)) 37 | 38 | logger.info("[*] Push frida server") 39 | try: 40 | adb.push_file(frida_server, "/data/local/tmp") 41 | except Exception as e: 42 | pass 43 | logger.info("[*] Add execution permission to frida-server") 44 | chmod_frida = ["chmod 755 /data/local/tmp/frida-server"] 45 | adb.shell(chmod_frida) 46 | logger.info("Start frida server") 47 | start_frida = ["cd /data/local/tmp && ./frida-server &"] 48 | adb.shell(start_frida, is_async=True) 49 | time.sleep(4) 50 | 51 | 52 | def push_and_start_frida_server_google_emulator(adb: ADB): 53 | """ 54 | Parameters 55 | ---------- 56 | adb 57 | Returns 58 | ------- 59 | """ 60 | frida_server = os.path.join( 61 | os.path.dirname(__file__), "resources", "frida-server-15-1-17", "frida-server" 62 | ) 63 | 64 | logger.info("[*] Checking if frida-server is already running") 65 | cmd_output = adb.shell("ps -e | grep frida") 66 | 67 | if "frida-server" in cmd_output: 68 | logger.warning("[*] frida-server is already running on device") 69 | return 70 | 71 | logger.info("[*] Push frida-server (google-emulator)") 72 | try: 73 | adb.push_file(frida_server, "/sdcard") 74 | adb.shell_su("mv /sdcard/frida-server /data/local/tmp/frida-server") 75 | except Exception as e: 76 | pass 77 | 78 | cmd_set_enforce = "setenforce 0" 79 | adb.shell_su(cmd_set_enforce) 80 | 81 | cmd_enforce_echo = "echo 0 > /sys/fs/selinux/enforce" 82 | adb.shell_su(cmd_enforce_echo) 83 | 84 | chmod_frida = "chmod 755 /data/local/tmp/frida-server" 85 | adb.shell_su(chmod_frida) 86 | logger.info("[*] Start frida server") 87 | start_frida = "/data/local/tmp/frida-server &" 88 | adb.shell_su(start_frida, is_async=True) 89 | time.sleep(4) 90 | 91 | 92 | def install_app_and_install_frida(app_path, is_google_emulator: bool = False): 93 | """ 94 | Install app and Frida script 95 | 96 | Parameters 97 | ---------- 98 | app_path 99 | 100 | Returns 101 | ------- 102 | 103 | """ 104 | app = APK(app_path) 105 | package_name = app.get_package() 106 | logger.info("[*] Start ADB") 107 | adb = ADB() 108 | logger.info(f"[*] Install App {package_name}") 109 | adb.install_app(app_path) 110 | logger.info("[*] Frida Initialization") 111 | if not is_google_emulator: 112 | push_and_start_frida_server(adb) 113 | else: 114 | push_and_start_frida_server_google_emulator(adb) 115 | return package_name 116 | 117 | 118 | def create_adb_and_start_frida(package_name, is_google_emulator: bool = False): 119 | """ 120 | 121 | Parameters 122 | ---------- 123 | package_name 124 | 125 | Returns 126 | ------- 127 | 128 | """ 129 | logger.warning(f"[*] App Already Installed, start to monitoring ${package_name}") 130 | adb = ADB() 131 | logger.info("[*] Frida Initialization") 132 | if not is_google_emulator: 133 | push_and_start_frida_server(adb) 134 | else: 135 | push_and_start_frida_server_google_emulator(adb) 136 | return package_name 137 | 138 | 139 | def create_script_frida(list_api_to_monitoring: list, path_frida_script_template: str): 140 | """ 141 | 142 | Parameters 143 | ---------- 144 | list_api_to_monitoring 145 | path_frida_script_template 146 | 147 | Returns 148 | ------- 149 | 150 | """ 151 | with open(path_frida_script_template) as frida_script_file: 152 | script_frida_template = frida_script_file.read() 153 | 154 | script_frida = "" 155 | for tuple_class_method in list_api_to_monitoring: 156 | script_frida += ( 157 | script_frida_template.replace( 158 | "class_name", '"' + tuple_class_method[0] + '"' 159 | ).replace("method_name", '"' + tuple_class_method[1] + '"') 160 | + "\n\n" 161 | ) 162 | return script_frida 163 | 164 | 165 | 166 | def create_json_custom(list_api_to_monitoring): 167 | """ 168 | 169 | Parameters 170 | ---------- 171 | list_api_to_monitoring 172 | 173 | Returns 174 | ------- 175 | 176 | """ 177 | dict_category_custom = {"Category": "Custom", "HookType": "Java", "hooks": []} 178 | 179 | for api in list_api_to_monitoring: 180 | dict_method = {"clazz": api[0], "method": api[1]} 181 | dict_category_custom["hooks"].append(dict_method) 182 | 183 | return dict_category_custom 184 | 185 | 186 | def create_json_api_monitor(json_list_api_file: str): 187 | """ 188 | Parameters 189 | ---------- 190 | json_list_api_file 191 | Returns 192 | ------- 193 | """ 194 | 195 | if not os.path.exists(json_list_api_file): 196 | return None 197 | 198 | # load file 199 | json_list_api = json.load(open(json_list_api_file, "r")) 200 | 201 | api_monitor = [] 202 | dict_template = {"Category": "", "HookType": "Java", "hooks": []} 203 | dict_data_category = {} 204 | for api in json_list_api: 205 | 206 | category = api["category"] 207 | clazz = api["className"] 208 | method = api["methodName"] 209 | # logger.debug(f"{category} {clazz} {method}") 210 | 211 | if category.lower() in dict_data_category: 212 | dict_data_category[category.lower()]["hooks"].append({"clazz": clazz, "method": method}) 213 | # monitor_api_config["hooks"].append({"clazz": clazz, "method": method}) 214 | # dict_data_category[category.lower()] = monitor_api_config 215 | 216 | else: 217 | monitor_api_config = { 218 | "Category": category.lower(), 219 | "HookType": "Java", 220 | "hooks": [{"clazz": clazz, "method": method}] 221 | } 222 | dict_data_category[category.lower()] = monitor_api_config 223 | 224 | for key, item in dict_data_category.items(): 225 | api_monitor.append(item) 226 | return api_monitor 227 | 228 | 229 | 230 | 231 | def read_api_to_monitoring(file_api_to_monitoring): 232 | """ 233 | 234 | Parameters 235 | ---------- 236 | file_api_to_monitoring 237 | 238 | Returns 239 | ------- 240 | 241 | """ 242 | if os.path.exists(file_api_to_monitoring): 243 | list_api_to_monitoring = [] 244 | content = [] 245 | with open(file_api_to_monitoring) as file_api: 246 | content = file_api.readlines() 247 | content = [x.strip() for x in content] 248 | for class_method in content: 249 | list_api_to_monitoring.append( 250 | (class_method.split(",")[0], class_method.split(",")[1]) 251 | ) 252 | return list_api_to_monitoring 253 | else: 254 | return None 255 | --------------------------------------------------------------------------------