Repository: sensepost/objection Branch: master Commit: 3c3cde806f15 Files: 316 Total size: 696.5 KB Directory structure: gitextract_j8lfqr1u/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── codeql-analysis.yml │ └── pypi.yml ├── .gitignore ├── .python-version ├── .vscode/ │ └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── agent/ │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── android/ │ │ │ ├── clipboard.ts │ │ │ ├── filesystem.ts │ │ │ ├── general.ts │ │ │ ├── heap.ts │ │ │ ├── hooking.ts │ │ │ ├── intent.ts │ │ │ ├── keystore.ts │ │ │ ├── lib/ │ │ │ │ ├── intentUtils.ts │ │ │ │ ├── interfaces.ts │ │ │ │ ├── libjava.ts │ │ │ │ └── types.ts │ │ │ ├── monitor.ts │ │ │ ├── pinning.ts │ │ │ ├── proxy.ts │ │ │ ├── root.ts │ │ │ ├── shell.ts │ │ │ └── userinterface.ts │ │ ├── generic/ │ │ │ ├── custom.ts │ │ │ ├── environment.ts │ │ │ ├── http.ts │ │ │ ├── memory.ts │ │ │ └── ping.ts │ │ ├── index.ts │ │ ├── ios/ │ │ │ ├── binary.ts │ │ │ ├── binarycookies.ts │ │ │ ├── bundles.ts │ │ │ ├── credentialstorage.ts │ │ │ ├── crypto.ts │ │ │ ├── filesystem.ts │ │ │ ├── heap.ts │ │ │ ├── hooking.ts │ │ │ ├── jailbreak.ts │ │ │ ├── keychain.ts │ │ │ ├── lib/ │ │ │ │ ├── constants.ts │ │ │ │ ├── helpers.ts │ │ │ │ ├── interfaces.ts │ │ │ │ ├── libobjc.ts │ │ │ │ └── types.ts │ │ │ ├── nsuserdefaults.ts │ │ │ ├── pasteboard.ts │ │ │ ├── pinning.ts │ │ │ ├── plist.ts │ │ │ └── userinterface.ts │ │ ├── lib/ │ │ │ ├── color.ts │ │ │ ├── constants.ts │ │ │ ├── helpers.ts │ │ │ ├── interfaces.ts │ │ │ └── jobs.ts │ │ └── rpc/ │ │ ├── android.ts │ │ ├── environment.ts │ │ ├── ios.ts │ │ ├── jobs.ts │ │ ├── memory.ts │ │ └── other.ts │ ├── tsconfig.json │ └── tslint.json ├── objection/ │ ├── __init__.py │ ├── api/ │ │ ├── __init__.py │ │ ├── app.py │ │ ├── rpc.py │ │ └── script.py │ ├── commands/ │ │ ├── __init__.py │ │ ├── android/ │ │ │ ├── __init__.py │ │ │ ├── clipboard.py │ │ │ ├── command.py │ │ │ ├── general.py │ │ │ ├── generate.py │ │ │ ├── heap.py │ │ │ ├── hooking.py │ │ │ ├── intents.py │ │ │ ├── keystore.py │ │ │ ├── monitor.py │ │ │ ├── pinning.py │ │ │ ├── proxy.py │ │ │ └── root.py │ │ ├── command_history.py │ │ ├── custom.py │ │ ├── device.py │ │ ├── filemanager.py │ │ ├── frida_commands.py │ │ ├── http.py │ │ ├── ios/ │ │ │ ├── __init__.py │ │ │ ├── binary.py │ │ │ ├── bundles.py │ │ │ ├── cookies.py │ │ │ ├── generate.py │ │ │ ├── heap.py │ │ │ ├── hooking.py │ │ │ ├── jailbreak.py │ │ │ ├── keychain.py │ │ │ ├── monitor.py │ │ │ ├── nsurlcredentialstorage.py │ │ │ ├── nsuserdefaults.py │ │ │ ├── pasteboard.py │ │ │ ├── pinning.py │ │ │ └── plist.py │ │ ├── jobs.py │ │ ├── memory.py │ │ ├── mobile_packages.py │ │ ├── plugin_manager.py │ │ ├── sqlite.py │ │ └── ui.py │ ├── console/ │ │ ├── __init__.py │ │ ├── cli.py │ │ ├── commands.py │ │ ├── completer.py │ │ ├── helpfiles/ │ │ │ ├── !.txt │ │ │ ├── android.clipboard.monitor.txt │ │ │ ├── android.clipboard.txt │ │ │ ├── android.heap.search.instances.txt │ │ │ ├── android.hooking.list.activities.txt │ │ │ ├── android.hooking.list.class_methods.txt │ │ │ ├── android.hooking.list.classes.txt │ │ │ ├── android.hooking.list.receivers.txt │ │ │ ├── android.hooking.list.services.txt │ │ │ ├── android.hooking.list.txt │ │ │ ├── android.hooking.search.classes.txt │ │ │ ├── android.hooking.search.methods.txt │ │ │ ├── android.hooking.search.txt │ │ │ ├── android.hooking.set.return_value.txt │ │ │ ├── android.hooking.txt │ │ │ ├── android.hooking.watch.class.txt │ │ │ ├── android.hooking.watch.class_method.txt │ │ │ ├── android.hooking.watch.txt │ │ │ ├── android.intent.implicit_intents.txt │ │ │ ├── android.intent.launch_activity.txt │ │ │ ├── android.intent.launch_service.txt │ │ │ ├── android.intent.txt │ │ │ ├── android.keystore.clear.txt │ │ │ ├── android.keystore.detail.txt │ │ │ ├── android.keystore.list.txt │ │ │ ├── android.keystore.txt │ │ │ ├── android.keystore.watch.txt │ │ │ ├── android.root.disable.txt │ │ │ ├── android.root.simulate.txt │ │ │ ├── android.shell_exec.txt │ │ │ ├── android.sslpinning.disable.txt │ │ │ ├── android.sslpinning.txt │ │ │ ├── android.txt │ │ │ ├── android.ui.FLAG_SECURE.txt │ │ │ ├── android.ui.screenshot.txt │ │ │ ├── cd.txt │ │ │ ├── env.txt │ │ │ ├── exit.txt │ │ │ ├── file.download.txt │ │ │ ├── file.txt │ │ │ ├── file.upload.txt │ │ │ ├── frida.txt │ │ │ ├── import.txt │ │ │ ├── ios.bundles.list_bundles.txt │ │ │ ├── ios.bundles.list_frameworks.txt │ │ │ ├── ios.bundles.txt │ │ │ ├── ios.cookies.get.txt │ │ │ ├── ios.cookies.txt │ │ │ ├── ios.hooking.list.class_methods.txt │ │ │ ├── ios.hooking.list.classes.txt │ │ │ ├── ios.hooking.list.txt │ │ │ ├── ios.hooking.search.classes.txt │ │ │ ├── ios.hooking.search.methods.txt │ │ │ ├── ios.hooking.search.txt │ │ │ ├── ios.hooking.set.return_value.txt │ │ │ ├── ios.hooking.set.txt │ │ │ ├── ios.hooking.txt │ │ │ ├── ios.hooking.watch.class.txt │ │ │ ├── ios.hooking.watch.method.txt │ │ │ ├── ios.hooking.watch.txt │ │ │ ├── ios.jailbreak.disable.txt │ │ │ ├── ios.jailbreak.simulate.txt │ │ │ ├── ios.jailbreak.txt │ │ │ ├── ios.keychain.add.txt │ │ │ ├── ios.keychain.clear.txt │ │ │ ├── ios.keychain.dump.txt │ │ │ ├── ios.keychain.txt │ │ │ ├── ios.monitor.crypto.txt │ │ │ ├── ios.nsuserdefaults.get.txt │ │ │ ├── ios.nsuserdefaults.txt │ │ │ ├── ios.pasteboard.monitor.txt │ │ │ ├── ios.pasteboard.txt │ │ │ ├── ios.plist.cat.txt │ │ │ ├── ios.plist.txt │ │ │ ├── ios.sslpinning.disable.txt │ │ │ ├── ios.sslpinning.txt │ │ │ ├── ios.txt │ │ │ ├── ios.ui.alert.txt │ │ │ ├── ios.ui.dump.txt │ │ │ ├── ios.ui.screenshot.txt │ │ │ ├── ios.ui.touchid_bypass.txt │ │ │ ├── ios.ui.txt │ │ │ ├── jobs.kill.txt │ │ │ ├── jobs.list.txt │ │ │ ├── jobs.txt │ │ │ ├── ls.txt │ │ │ ├── memory.dump.all.txt │ │ │ ├── memory.dump.from_base.txt │ │ │ ├── memory.dump.txt │ │ │ ├── memory.list.exports.txt │ │ │ ├── memory.list.modules.txt │ │ │ ├── memory.list.txt │ │ │ ├── memory.search.txt │ │ │ ├── memory.txt │ │ │ ├── memory.write.txt │ │ │ ├── plugin.load.txt │ │ │ ├── plugin.txt │ │ │ ├── pwd.print.txt │ │ │ ├── pwd.txt │ │ │ ├── reconnect.txt │ │ │ ├── rm.txt │ │ │ ├── sqlite.connect.txt │ │ │ ├── sqlite.disconnect.txt │ │ │ ├── sqlite.execute.query.txt │ │ │ ├── sqlite.execute.schema.txt │ │ │ ├── sqlite.execute.txt │ │ │ ├── sqlite.status.txt │ │ │ ├── sqlite.sync.txt │ │ │ ├── sqlite.txt │ │ │ ├── ui.alert.txt │ │ │ └── ui.txt │ │ └── repl.py │ ├── state/ │ │ ├── __init__.py │ │ ├── api.py │ │ ├── app.py │ │ ├── connection.py │ │ ├── device.py │ │ ├── filemanager.py │ │ └── jobs.py │ └── utils/ │ ├── __init__.py │ ├── agent.py │ ├── assets/ │ │ ├── javahookmanager.js │ │ ├── network_security_config.xml │ │ ├── objchookmanager.js │ │ └── objection.jks │ ├── helpers.py │ ├── patchers/ │ │ ├── __init__.py │ │ ├── android.py │ │ ├── base.py │ │ ├── github.py │ │ └── ios.py │ ├── plugin.py │ └── update_checker.py ├── plugins/ │ ├── README.md │ ├── api/ │ │ ├── __init__.py │ │ └── index.js │ ├── flex/ │ │ ├── README.md │ │ ├── __init__.py │ │ ├── index.js │ │ ├── libFlex.h │ │ └── libFlex.m │ ├── mettle/ │ │ ├── README.md │ │ ├── __init__.py │ │ └── index.js │ └── stetho/ │ ├── README.md │ ├── __init__.py │ └── index.js ├── pyproject.toml └── tests/ ├── __init__.py ├── commands/ │ ├── __init__.py │ ├── android/ │ │ ├── __init__.py │ │ ├── test_clipboard.py │ │ ├── test_command.py │ │ ├── test_heap.py │ │ ├── test_hooking.py │ │ ├── test_intents.py │ │ ├── test_keystore.py │ │ ├── test_pinning.py │ │ └── test_root.py │ ├── ios/ │ │ ├── __init__.py │ │ ├── test_bundles.py │ │ ├── test_cookies.py │ │ ├── test_hooking.py │ │ ├── test_jailbreak.py │ │ ├── test_keychain.py │ │ ├── test_nsurlcredentialstorage.py │ │ ├── test_nsuserdefaults.py │ │ ├── test_pasteboard.py │ │ ├── test_pinning.py │ │ └── test_plist.py │ ├── test_command_history.py │ ├── test_device.py │ ├── test_filemanager.py │ ├── test_frida_commands.py │ ├── test_jobs.py │ ├── test_memory.py │ ├── test_mobile_packages.py │ ├── test_plugin_manager.py │ └── test_ui.py ├── console/ │ ├── __init__.py │ ├── test_cli.py │ ├── test_completer.py │ └── test_repl.py ├── data/ │ └── plugin/ │ └── __init__.py ├── helpers.py ├── state/ │ ├── __init__.py │ ├── test_app.py │ └── test_jobs.py └── utils/ ├── __init__.py ├── patchers/ │ ├── __init__.py │ ├── test_android.py │ ├── test_base.py │ ├── test_github.py │ └── test_ios.py └── test_helpers.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "[bug] Bug Description Here" labels: freshissue assignees: '' --- - Please, fill in all of the sections in this template. - Please read each section carefully. Each has a description of what information will help. - Windows support is limited. There is a good chance that a pull request would help! - The more information you give, the better. - Remove this section when you are done. **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Run command '...' 2. Run command '...' **Similar issues** Please link the issues in this repository that is similar to yours. For example: #358, #229 etc. **Expected behavior** A clear and concise description of what you expected to happen. **Evidence / Logs / Screenshots** Any output from objection, such as stack traces or errors that occurred. Be sure to run objection with the `--debug` flag so that errors from the agent are verbose enough to debug. For example: ```text objection --debug explore ``` **Environment (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Frida Version [e.g. 22] - Objection Version [e.g. 1.6.2] **Application** If possible, please attach the target application where you can reproduce this bug to the issue. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. name: "CodeQL" on: push: branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - cron: '0 7 * * 6' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['python', 'javascript'] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .github/workflows/pypi.yml ================================================ name: Release to PyPi on: push: tags: - '*' jobs: pypi: name: Publish to PyPI runs-on: ubuntu-latest environment: name: pypi permissions: id-token: write contents: read steps: - name: Checkout uses: actions/checkout@v5 - name: Ensure node for agent uses: actions/setup-node@v4 with: node-version: '24' cache: npm cache-dependency-path: agent/package-lock.json - name: Install uv uses: astral-sh/setup-uv@v6 - name: Set tagged version run: | VERSION="$GITHUB_REF_NAME" uv version "$VERSION" - name: Build Agent run: cd agent && npm ci - name: Build run: uv build - name: Publish run: uv publish ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # extra settings.ini python/ .idea/ .DS_Store # Ignore the compiled agent. objection/agent.js ================================================ FILE: .python-version ================================================ 3.14 ================================================ FILE: .vscode/settings.json ================================================ { "editor.tabSize": 2 } ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Objection First off, thanks for taking the time to contribute! 🎉💥 The following are some simple guidelines for contributing to the project. Before you get started though, it is highly recommended that you read the Wiki article entry available [here](https://github.com/sensepost/objection/wiki/Hacking) to get an idea of how the project is put structured and to learn about the various components. Finally, when submitting your pull request, please try and be as descriptive as possible about what is changing/is fixed. Ideally, including tests greatly helps facilitate this process. Thanks! 🤘 ## Code Structure Objection consists of two major parts. The Python command line environment and the TypeScript agent. Both of these parts live in this single, monorepo. - The Python command line lives [here](https://github.com/sensepost/objection/tree/master/objection). - The TypeScript agent lives [here](https://github.com/sensepost/objection/tree/master/agent). ## Environment Setup Whether you want to contribute to the TypeScript agent or the Python command line, both components would require some setup. ### Python Command Line Any Python 3 environment should do, but we recommend you use the latest version of Python. To satisfy all of the dependencies that you may need, install those defined in the [`requirements-dev.txt`](https://github.com/sensepost/objection/blob/master/requirements-dev.txt) file in the project's root. This would make all of the code dependencies available, as well as some useful debugging helpers. ### TypeScript Agent The objection agent is written using TypeScript 3. It is recommended that you download [Visual Studio Code](https://code.visualstudio.com/) for agent development given the excellent TypeScript support that it has. For more information on developing for the agent, please see the Wiki article [here](https://github.com/sensepost/objection/wiki/Agent-Development-Environment). ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: MANIFEST.in ================================================ include requirements.txt ================================================ FILE: Makefile ================================================ DIST_DIR := dist default: clean frida-agent sdist clean: $(RM) $(DIST_DIR)/* frida-agent: cd agent && npm run build sdist: uv build testupload: uv publish --index testpypi upload: uv publish ================================================ FILE: README.md ================================================ # 📱objection - Runtime Mobile Exploration `objection` is a runtime mobile exploration toolkit, powered by [Frida](https://www.frida.re/), built to help you assess the security posture of your mobile applications, without needing a jailbreak. [![Twitter](https://img.shields.io/badge/twitter-%40leonjza-blue.svg)](https://twitter.com/leonjza) [![PyPi](https://badge.fury.io/py/objection.svg)](https://pypi.python.org/pypi/objection) [![Black Hat Arsenal](https://raw.githubusercontent.com/toolswatch/badges/master/arsenal/europe/2017.svg?sanitize=true)](https://www.blackhat.com/eu-17/arsenal-overview.html) [![Black Hat Arsenal](https://raw.githubusercontent.com/toolswatch/badges/master/arsenal/usa/2019.svg?sanitize=true)](https://www.blackhat.com/us-19/arsenal-overview.html) objection - Supports both iOS and Android. - Inspect and interact with container file systems. - Bypass SSL pinning. - Dump keychains. - Perform memory related tasks, such as dumping & patching. - Explore and manipulate objects on the heap. - And much, much [more](https://github.com/sensepost/objection/wiki/Features)... Screenshots are available in the [wiki](https://github.com/sensepost/objection/wiki/Screenshots). ## installation Installation is simply a matter of `pip3 install objection`. This will give you the `objection` command. You can update an existing `objection` installation with `pip3 install --upgrade objection`. For more detailed update and installation instructions, please refer to the wiki page [here](https://github.com/sensepost/objection/wiki/Installation). ## license `objection` is licensed under a [GNU General Public v3 License](https://www.gnu.org/licenses/gpl-3.0.en.html). Permissions beyond the scope of this license may be available at [http://sensepost.com/contact/](http://sensepost.com/contact/). ================================================ FILE: agent/.gitignore ================================================ node_modules/ ================================================ FILE: agent/README.md ================================================ # objection agent This directory contains the source code for the agent used within `objection`. These sources are compiled and shipped with a distribution of `objection`, and live in `/objection/agent.js` in its compiled state. For more information, such as development environment setup instructions, please refer to the project wiki [here](https://github.com/sensepost/objection/wiki/Agent-Development-Environment). ================================================ FILE: agent/package.json ================================================ { "name": "objection", "version": "0.0.0", "description": "Runtime Mobile Exploration", "private": true, "type": "module", "main": "src/index.ts", "scripts": { "prepare": "npm run build", "build": "frida-compile src/index.ts -o ../objection/agent.js -T none", "watch": "frida-compile src/index.ts -o ../objection/agent.js -w", "lint": "tslint -c tslint.json 'src/**/*.ts'" }, "repository": { "type": "git", "url": "git+https://github.com/sensepost/objection.git" }, "keywords": [ "frida", "runtime", "mobile", "security", "objection" ], "author": "Leon Jacobs", "license": "GPL-3.0", "bugs": { "url": "https://github.com/sensepost/objection/issues" }, "homepage": "https://github.com/sensepost/objection#readme", "dependencies": { "frida-fs": "^7.0.0", "frida-java-bridge": "^7", "frida-objc-bridge": "^8", "frida-screenshot": "^6", "macho-ts": "^0.1.0" }, "devDependencies": { "@types/frida-gum": "^19", "@types/node": "^24", "frida-compile": "^19", "tslint": "^6" } } ================================================ FILE: agent/src/android/clipboard.ts ================================================ import { colors as c } from "../lib/color.js"; import { getApplicationContext, wrapJavaPerform, Java } from "./lib/libjava.js"; import { ClipboardManager } from "./lib/types.js"; export const monitor = (): Promise => { // -- Sample Java // // ClipboardManager f = (ClipboardManager)getApplicationContext().getSystemService(CLIPBOARD_SERVICE); // ClipData.Item i = f.getPrimaryClip().getItemAt(0); // Log.e("t", "?:" + i.getText()); send(`${c.yellowBright("Warning!")} This module is still broken. A pull request fixing it would be awesome!`); // https://developer.android.com/reference/android/content/Context.html#CLIPBOARD_SERVICE const CLIPBOARD_SERVICE: string = "clipboard"; // a variable for clipboard text let data: string; return wrapJavaPerform(() => { const clipboardManager: ClipboardManager = Java.use("android.content.ClipboardManager"); const context = getApplicationContext(); const clipboardHandle = context.getApplicationContext().getSystemService(CLIPBOARD_SERVICE); const cp = Java.cast(clipboardHandle, clipboardManager); setInterval(() => { const primaryClip = cp.getPrimaryClip(); // Check if there is at least some data if (primaryClip == null || primaryClip.getItemCount() <= 0) { return; } // If we have managed to get the primary clipboard and there are // items stored in it, process an update. const currentString = primaryClip.getItemAt(0).coerceToText(context).toString(); // If the data is the same, just stop. if (data === currentString) { return; } // Update the data with the new string and report back. data = currentString; send(`${c.blackBright(`[pasteboard-monitor]`)} Data: ${c.greenBright(data.toString())}`); }, 1000 * 5); }); }; ================================================ FILE: agent/src/android/filesystem.ts ================================================ import * as fs from "frida-fs"; import { Buffer } from "buffer"; import { hexStringToBytes } from "../lib/helpers.js"; import { IAndroidFilesystem } from "./lib/interfaces.js"; import { getApplicationContext, wrapJavaPerform, Java } from "./lib/libjava.js"; import { File, JavaClass } from "./lib/types.js"; export const exists = (path: string): Promise => { // -- Sample Java // // File path = new File("."); // Boolean e = path.exists(); return wrapJavaPerform(() => { const file: File = Java.use("java.io.File"); const currentFile: JavaClass = file.$new(path); return currentFile.exists(); }); }; export const readable = (path: string): Promise => { // -- Sample Java Code // // File d = new File("."); // d.canRead(); return wrapJavaPerform(() => { const file: File = Java.use("java.io.File"); const currentFile: JavaClass = file.$new(path); return currentFile.canRead(); }); }; export const writable = (path: string): Promise => { // -- Sample Java Code // // File d = new File("."); // d.canWrite(); return wrapJavaPerform(() => { const file: File = Java.use("java.io.File"); const currentFile: JavaClass = file.$new(path); return currentFile.canWrite(); }); }; export const pathIsFile = (path: string): Promise => { // -- Sample Java Code // // File d = new File("."); // d.isFile(); return wrapJavaPerform(() => { const file: File = Java.use("java.io.File"); const currentFile: JavaClass = file.$new(path); return currentFile.isFile(); }); }; export const pwd = (): Promise => { // -- Sample Java // // getApplicationContext().getFilesDir().getAbsolutePath() return wrapJavaPerform(() => { const context = getApplicationContext(); return context.getFilesDir().getAbsolutePath().toString(); }); }; // heavy lifting is done in frida-fs here. export const readFile = (path: string): string | Buffer => { if (fs.statSync(path).size == 0) return Buffer.alloc(0); return fs.readFileSync(path); }; // heavy lifting is done in frida-fs here. export const writeFile = (path: string, data: string): void => { const writeStream: any = fs.createWriteStream(path); writeStream.on("error", (error: Error) => { throw error; }); writeStream.write(hexStringToBytes(data)); writeStream.end(); }; export const deleteFile = (path: string): Promise => { // -- Sample Java Code // // File d = new File("."); // d.delete(); return wrapJavaPerform(() => { const file: File = Java.use("java.io.File"); const currentFile: JavaClass = file.$new(path); return currentFile.delete(); }); }; export const ls = (p: string): Promise => { // -- Sample Java Code // // File d = new File("."); // File[] files = d.listFiles(); // Log.e(getClass().getName(), "Files: " + files.length); // for (int i = 0; i < files.length; i++) { // Log.e(getClass().getName(), // files[i].getName() + ": " + files[i].canRead() // + " " + files[i].lastModified() // + " " + files[i].length() // ); // } return wrapJavaPerform(() => { const file: File = Java.use("java.io.File"); const directory: JavaClass = file.$new(p); const response: IAndroidFilesystem = { files: {}, path: p, readable: directory.canRead(), writable: directory.canWrite(), }; if (!response.readable) { return response; } // get a listing of the files in the directory const files: any[] = directory.listFiles(); for (const f of files) { response.files[f.getName()] = { attributes: { isDirectory: f.isDirectory(), isFile: f.isFile(), isHidden: f.isHidden(), lastModified: f.lastModified(), size: f.length(), }, fileName: f.getName(), readable: f.canRead(), writable: f.canWrite(), }; } return response; }); }; ================================================ FILE: agent/src/android/general.ts ================================================ import { wrapJavaPerform, Java } from "./lib/libjava.js"; export const deoptimize = (): Promise => { return wrapJavaPerform(() => { Java.deoptimizeEverything(); }); }; ================================================ FILE: agent/src/android/heap.ts ================================================ import { colors as c } from "../lib/color.js"; import { IHeapClassDictionary, IHeapObject, IJavaField, IHeapNormalised } from "./lib/interfaces.js"; import { wrapJavaPerform, Java } from "./lib/libjava.js"; export let handles: IHeapClassDictionary = {}; import type { default as JavaTypes } from "frida-java-bridge"; const getInstance = (hashcode: number): JavaTypes.Wrapper | null => { const matches: IHeapObject[] = []; // Search for this handle, and push the results to matches Object.keys(handles).forEach((clazz) => { handles[clazz].filter((heapObject) => { if (heapObject.hashcode === hashcode) { matches.push(heapObject); } }); }); if (matches.length > 1) { c.log(`Found ${c.redBright(matches.length.toString())} handles, this is probably a bug, please report it!`); } if (matches.length > 0) { wrapJavaPerform(() => { c.log(`${c.blackBright(`Handle ` + hashcode + ` is to class `)} ${c.greenBright(matches[0].instance.$className)}`); }); return matches[0].instance; } c.log(`${c.yellowBright(`Warning:`)} Could not find a known handle for ${hashcode}. ` + `Try searching class instances first.`); return null; }; export const getInstances = (clazz: string): Promise => { return wrapJavaPerform(() => { handles[clazz] = []; // tslint:disable:only-arrow-functions // tslint:disable:object-literal-shorthand // tslint:disable:no-empty Java.choose(clazz, { onComplete: function () { c.log(`Class instance enumeration complete for ${c.green(clazz)}`); }, onMatch: function (instance) { handles[clazz].push({ instance: instance, hashcode: instance.hashCode(), }); }, }); // tslint:enable return handles[clazz].map((h): IHeapNormalised => { return { hashcode: h.hashcode, classname: clazz, tostring: h.instance.toString(), }; }); }); }; export const methods = (handle: number): Promise => { return wrapJavaPerform(() => { const clazz = getInstance(handle); if (clazz == null) { return []; } return clazz.class.getDeclaredMethods().map((method: any) => { return method.toGenericString(); }); }); }; export const execute = (handle: number, method: string, returnString: boolean = false): Promise => { return wrapJavaPerform(() => { const clazz = getInstance(handle); if (clazz == null) { return; } c.log(`${c.blackBright(`Executing method:`)} ${c.greenBright(`${method}()`)}`); const returnValue = clazz[method](); if (returnString && returnValue) { return returnValue.toString(); } return returnValue; }); }; export const fields = (handle: number): Promise => { return wrapJavaPerform(() => { const clazz = getInstance(handle); if (clazz == null) { return; } return clazz.class.getDeclaredFields().map((field: any): IJavaField => { const fieldName: string = field.getName(); const fieldInstance: JavaTypes.Wrapper = clazz.class.getDeclaredField(fieldName); fieldInstance.setAccessible(true); let fieldValue = fieldInstance.get(clazz); // Cast a string if possible if (fieldValue) { fieldValue = fieldValue.toString(); } return { name: fieldName, value: fieldValue, }; }); }); }; export const evaluate = (handle: number, js: string): Promise => { return wrapJavaPerform(() => { const clazz = getInstance(handle); if (clazz == null) { return; } // tslint:disable-next-line:no-eval eval(js); }); }; ================================================ FILE: agent/src/android/hooking.ts ================================================ import { colors as c } from "../lib/color.js"; import * as jobs from "../lib/jobs.js"; import { ICurrentActivityFragment } from "./lib/interfaces.js"; import { getApplicationContext, R, wrapJavaPerform, Java } from "./lib/libjava.js"; import { Activity, ActivityClientRecord, ActivityThread, ArrayMap, JavaClass, PackageManager, Throwable, JavaMethodsOverloadsResult, } from "./lib/types.js"; import type { default as JavaTypes } from "frida-java-bridge"; enum PatternType { Regex = 'regex', Klass = 'klass', } const splitClassMethod = (fqClazz: string): string[] => { // split a fully qualified class name, assuming the last period denotes the method const methodSeperatorIndex: number = fqClazz.lastIndexOf("."); const clazz: string = fqClazz.substring(0, methodSeperatorIndex); const method: string = fqClazz.substring(methodSeperatorIndex + 1); // Increment by 1 to exclude the leading period return [clazz, method]; }; export const getClasses = (): Promise => { return wrapJavaPerform(() => { return Java.enumerateLoadedClassesSync(); }); }; export const getClassLoaders = (): Promise => { return wrapJavaPerform(() => { const loaders: string[] = []; Java.enumerateClassLoaders({ onMatch: function (l) { if (l == null) { return; } loaders.push(l.toString()); }, onComplete: function () { } }); return loaders; }); }; const getPatternType = (pattern: string): PatternType => { if (pattern.indexOf('!') !== -1) { return PatternType.Regex; } return PatternType.Klass; }; export const lazyWatchForPattern = (query: string, watch: boolean, dargs: boolean, dret: boolean, dbt: boolean): void => { // TODO: Use param to control interval let found = false; const job: jobs.Job = new jobs.Job(jobs.identifier(),`notify-class for: ${query}`); // This method loops over all enumerate matches and then calls watch // with the arguments specified in the parent function const watchMatches = (matches: JavaTypes.EnumerateMethodsMatchGroup[]) => { matches.forEach(match => { match.classes.forEach(_class => { _class.methods.forEach(_method => { watchMethod(_class.name + "." + _method, job, dargs, dbt, dret); }) }) }) } // Check if the pattern is found before starting an interval javaEnumerate(query).then(matches => { if (matches.length > 0) { found = true; send(`${c.green(query)} is already loaded / available`); if (watch) { watchMatches(matches); jobs.add(job); } } }); if (found) return; send(`Watching for ${c.green(query)} ${c.blackBright(`(not starting a job)`)}`); // TODO: The javaEnumerate promise makes this racy. Figure it out one day. const interval = setInterval(() => { javaEnumerate(query).then(matches => { // Only notify if we haven't before if (!found && matches.length > 0) { send(`${c.green(query)} is now available`); found = true; if (watch) { watchMatches(matches); jobs.add(job); } } if (found) clearInterval(interval); }); }, 1000 * 5); }; export const javaEnumerate = (query: string): Promise => { // If the query is just a classname, strongarm it into a pattern. if (getPatternType(query) === PatternType.Klass) { query = `*${query}*!*`; } return wrapJavaPerform(() => { return Java.enumerateMethods(query); }); }; export const getClassMethods = (className: string): Promise => { return wrapJavaPerform(() => { const clazz: JavaClass = Java.use(className); return clazz.class.getDeclaredMethods().map((method) => { return method.toGenericString(); }); }); }; // This function takes in a method such as package.class.perform() // and extracts only the method name, ie "perform" const genericMethodNameToMethodOnly = (fullMethodName: string): string => { // Reduces [package, class, perform()] to "perform()" const method = fullMethodName.split('.').filter((part: string) => part.includes('('))[0]; // Now extract everything before the first '(' return method.substring(0, method.indexOf('(')); }; // This method assumes that it's being called from inside wrapJavaPerform // TODO: Not in use yet, but this is a proposal to replace Java.use() to // support multiple classloaders transparently. export const getClassHandle = (className: string): JavaClass | null => { let clazz: JavaClass = null; const loaders = Java.enumerateClassLoadersSync(); let found = false; // Try to get a handle using each of the class loaders for (let i = 0; i < loaders.length; i++) { const loader = loaders[i]; const factory = Java.ClassFactory.get(loader); try { clazz = factory.use(className); found = true; break; } catch { } } if (found) { return clazz; } else { return null; } }; // This method assumes that it's being called from inside wrapJavaPerform // It behaves the same as the above, except only uses the specified class // loader export const getClassHandleWithLoaderClassName = (className: string, loaderClassName: any): JavaClass | null => { let clazz: JavaClass = null; const loaders = Java.enumerateClassLoadersSync() .filter(loader => loaderClassName === loader.$className); if (loaders.length == 0) return null; let found = false; // Try to get a handle using each of the class loaders // This is still required because some loaders may have the // same name, so distinguishing between them using this is // incorrect. I'm sure there is a way of finding the correct // one efficiently. for (let i = 0; i < loaders.length; i++) { const loader = loaders[i]; const factory = Java.ClassFactory.get(loader); try { clazz = factory.use(className); found = true; break; } catch { } } if (found) return clazz; return null; }; export const getClassMethodsOverloads = (className: string, methodsAllowList: string[] = [], loader?: string): Promise => { return wrapJavaPerform(() => { const result: JavaMethodsOverloadsResult = {}; const clazz = loader !== null ? getClassHandleWithLoaderClassName(className, loader) : Java.use(className); if (clazz === null) { throw new Error("Could not find class!"); } // TODO(cduplooy): The below line can fail with Error: java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/datastore/core/DataStore; // This seems to involve custom class loaders... const methods = clazz.class.getDeclaredMethods() .map(method => genericMethodNameToMethodOnly(method.toGenericString())); methods.forEach(methodName => { if (methodsAllowList.length === 0 || (methodsAllowList.length > 0 && methodsAllowList.includes(methodName))) { const overloads = clazz[methodName].overloads; result[methodName] = { 'argTypes': overloads.map(overload => overload.argumentTypes), 'returnType': overloads.map(overload => overload.returnType), 'methodName': overloads.map(overload => overload.methodName), 'handle': overloads.map(overload => overload.handle), 'holder': overloads.map(overload => overload.holder), 'type': overloads.map(overload => overload.type), }; } }); // Finally append the constructor details if (clazz.class.getConstructors().length > 0) { if (methodsAllowList.length === 0 || (methodsAllowList.length > 0 && methodsAllowList.includes("$init"))) { const overloads = clazz['$init'].overloads; result['$init'] = { 'argTypes': overloads.map(overload => overload.argumentTypes), 'returnType': overloads.map(overload => overload.returnType), 'methodName': overloads.map(overload => overload.methodName), 'handle': overloads.map(overload => overload.handle), 'holder': overloads.map(overload => overload.holder), 'type': overloads.map(overload => overload.type), }; } } return result; }); }; export const watch = (pattern: string, dargs: boolean, dbt: boolean, dret: boolean): Promise => { // The general idea here is that we enumerate the total functions (based on the pattern type) // and via watchClass (which calls wathMethod) apply hooks. const patternType = getPatternType(pattern); if (patternType === PatternType.Klass) { // start a new job container const job: jobs.Job = new jobs.Job(jobs.identifier(),`watch-class for: ${pattern}`); const w = watchClass(pattern, job, dargs, dbt, dret); jobs.add(job); return w; } // assume we have PatternType.Regex const job: jobs.Job = new jobs.Job(jobs.identifier(),`watch-pattern for: ${pattern}`); jobs.add(job); return new Promise((resolve, reject) => { javaEnumerate(pattern).then((matches: JavaTypes.EnumerateMethodsMatchGroup[]) => { matches.forEach((match: JavaTypes.EnumerateMethodsMatchGroup) => { match.classes.forEach((klass: JavaTypes.EnumerateMethodsMatchClass) => { klass.methods.forEach(method => { // Only watch matched methods watchMethod(`${klass.name}.${method}`, job, dargs, dbt, dret); }); }); }); resolve(); }).catch((error) => { reject(error); }); }); }; const watchClass = (clazz: string, job: jobs.Job, dargs: boolean = false, dbt: boolean = false, dret: boolean = false): Promise => { return wrapJavaPerform(() => { const clazzInstance: JavaClass = Java.use(clazz); clazzInstance.class.getDeclaredMethods().map((method) => { // perform a cleanup of the method. An example after toGenericString() would be: // public void android.widget.ScrollView.draw(android.graphics.Canvas) throws Exception // public final rx.c.b com.apple.android.music.icloud.a.a(rx.c.b) let m: string = method.toGenericString(); // Remove generics from the method while (m.includes("<")) { m = m.replace(/<.*?>/g, ""); } // remove any "Throws" the method may have if (m.indexOf(" throws ") !== -1) { m = m.substring(0, m.indexOf(" throws ")); } // remove scope and return type declarations (aka: first two words) // remove the class name // remove the signature and return m = m.slice(m.lastIndexOf(" ")); m = m.replace(` ${clazz}.`, ""); return m.split("(")[0]; }).filter((value, index, self) => { return self.indexOf(value) === index; }).forEach((method) => { // get the argument types for this overload // send(`Watching ${c.green(clazz)}.${c.greenBright(method)}()`); const fqClazz = `${clazz}.${method}`; watchMethod(fqClazz, job, dargs, dbt, dret); }); }); }; const watchMethod = ( fqClazz: string, job: jobs.Job, dargs: boolean, dbt: boolean, dret: boolean, ): Promise => { const [clazz, method] = splitClassMethod(fqClazz); // send(`Attempting to watch class ${c.green(clazz)} and method ${c.green(method)}.`); return wrapJavaPerform(() => { const throwable: Throwable = Java.use("java.lang.Throwable"); const targetClass: JavaClass = Java.use(clazz); // Ensure that the method exists on the class if (targetClass[method] === undefined) { send(`${c.red("Error:")} Unable to find method ${c.redBright(method)} in class ${c.green(clazz)}`); return; } targetClass[method].overloads.forEach((m: any) => { // get the argument types for this overload const calleeArgTypes: string[] = m.argumentTypes.map((arg) => arg.className); send(`Watching ${c.green(clazz)}.${c.greenBright(method)}(${c.red(calleeArgTypes.join(", "))})`); // replace the implementation of this method // tslint:disable-next-line:only-arrow-functions m.implementation = function () { send( c.blackBright(`[${job.identifier}] `) + `Called ${c.green(clazz)}.${c.greenBright(m.methodName)}(${c.red(calleeArgTypes.join(", "))})`, ); // dump a backtrace if (dbt) { send( c.blackBright(`[${job.identifier}] `) + "Backtrace:\n\t" + throwable.$new().getStackTrace().map((traceElement) => traceElement.toString() + "\n\t").join(""), ); } // dump arguments if (dargs && calleeArgTypes.length > 0) { const argValues: string[] = []; for (const h of arguments) { argValues.push((h || "(none)").toString()); } send( c.blackBright(`[${job.identifier}] `) + `Arguments ${c.green(clazz)}.${c.greenBright(m.methodName)}(${c.red(argValues.join(", "))})`, ); } // actually run the intended method const retVal: any = m.apply(this, arguments); // dump the return value if (dret) { const retValStr: string = (retVal || "(none)").toString(); send(c.blackBright(`[${job.identifier}] `) + `Return Value: ${c.red(retValStr)}`); } // also return the captured return value return retVal; }; // Push the implementation so that it can be nulled later job.addImplementation(m); }); }); }; export const getCurrentActivity = (): Promise => { return wrapJavaPerform(() => { const activityThread: ActivityThread = Java.use("android.app.ActivityThread"); const activity: Activity = Java.use("android.app.Activity"); const activityClientRecord: ActivityClientRecord = Java.use("android.app.ActivityThread$ActivityClientRecord"); const currentActivityThread = activityThread.currentActivityThread(); const activityRecords = currentActivityThread.mActivities.value.values().toArray(); let currentActivity; for (const i of activityRecords) { const activityRecord = Java.cast(i, activityClientRecord); if (!activityRecord.paused.value) { currentActivity = Java.cast(Java.cast(activityRecord, activityClientRecord).activity.value, activity); break; } } if (currentActivity) { // Discover an active fragment const fm = currentActivity.getFragmentManager(); const fragment = fm.findFragmentById(R("content_frame", "id")); return { activity: currentActivity.$className, fragment: fragment.$className, }; } return { activity: null, fragment: null, }; }); }; export const getActivities = (): Promise => { return wrapJavaPerform(() => { const packageManager: PackageManager = Java.use("android.content.pm.PackageManager"); const GET_ACTIVITIES = packageManager.GET_ACTIVITIES.value; const context = getApplicationContext(); return Array.prototype.concat(context.getPackageManager() .getPackageInfo(context.getPackageName(), GET_ACTIVITIES).activities.value.map((activityInfo) => { return activityInfo.name.value; }), ); }); }; export const getServices = (): Promise => { return wrapJavaPerform(() => { const activityThread: ActivityThread = Java.use("android.app.ActivityThread"); const arrayMap: ArrayMap = Java.use("android.util.ArrayMap"); const packageManager: PackageManager = Java.use("android.content.pm.PackageManager"); const GET_SERVICES = packageManager.GET_SERVICES.value; const currentApplication = activityThread.currentApplication(); // not using the helper as we need other variables too const context = currentApplication.getApplicationContext(); var services: string[] = []; currentApplication.mLoadedApk.value.mServices.value.values().toArray().map((potentialServices) => { Java.cast(potentialServices, arrayMap).keySet().toArray().map((service) => { services.push(service.$className); }); }); services = services.concat(context.getPackageManager() .getPackageInfo(context.getPackageName(), GET_SERVICES).services.value.map((activityInfo) => { return activityInfo.name.value; }), ); return services; }); }; export const getBroadcastReceivers = (): Promise => { return wrapJavaPerform(() => { const activityThread: ActivityThread = Java.use("android.app.ActivityThread"); const arrayMap: ArrayMap = Java.use("android.util.ArrayMap"); const packageManager: PackageManager = Java.use("android.content.pm.PackageManager"); const GET_RECEIVERS = packageManager.GET_RECEIVERS.value; const currentApplication = activityThread.currentApplication(); // not using the helper as we need other variables too const context = currentApplication.getApplicationContext(); const receiversFromContext = context.getPackageManager().getPackageInfo( context.getPackageName(), GET_RECEIVERS ).receivers.value var receivers: string[] = []; currentApplication.mLoadedApk.value.mReceivers.value.values().toArray().map((potentialReceivers) => { Java.cast(potentialReceivers, arrayMap).keySet().toArray().map((receiver) => { receivers.push(receiver.$className); }); }); if (receiversFromContext != null) receivers = receivers.concat(receiversFromContext.map((activityInfo) => { return activityInfo.name.value; })); return receivers; }); }; export const setReturnValue = (fqClazz: string, filterOverload: string | null, newRet: boolean): Promise => { const [clazz, method] = splitClassMethod(fqClazz); send(`Attempting to modify return value for class ${c.green(clazz)} and method ${c.green(method)}.`); if (filterOverload != null) { send(c.blackBright(`Will filter for method overload with arguments:`) + ` ${c.green(filterOverload)}`); } return wrapJavaPerform(() => { const job: jobs.Job = new jobs.Job(jobs.identifier(), `set-return for: ${fqClazz}`); const targetClazz: JavaClass = Java.use(clazz); targetClazz[method].overloads.forEach((m: any) => { // get the argument types for this method const calleeArgTypes: string[] = m.argumentTypes.map((arg) => arg.className); // check if we need to filter on a specific overload if (filterOverload != null && calleeArgTypes.join(",") !== filterOverload) { return; } send(`Hooking ${c.green(clazz)}.${c.greenBright(method)}(${c.red(calleeArgTypes.join(", "))})`); // tslint:disable-next-line:only-arrow-functions m.implementation = function () { let retVal = m.apply(this, arguments); // Override retval if needed if (retVal !== newRet) { send( c.blackBright(`[${job.identifier}] `) + `Return value was not ${c.red(newRet.toString())}, ` + `setting to ${c.green(newRet.toString())}.`, ); // update the return value retVal = newRet; } return retVal; }; // record override job.addImplementation(m); }); jobs.add(job); }); }; ================================================ FILE: agent/src/android/intent.ts ================================================ import { colors as c } from "../lib/color.js"; import { getApplicationContext, wrapJavaPerform, Java } from "./lib/libjava.js"; import { Intent, FridaOverload } from "./lib/types.js"; import { analyseIntent } from "./lib/intentUtils.js"; import * as jobs from "../lib/jobs.js"; import type { default as JavaTypes } from "frida-java-bridge"; // https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK const FLAG_ACTIVITY_NEW_TASK = 0x10000000; // starts an Android activity // This method does not yet allow for 'extra' data to be send along // with the intent. export const startActivity = (activityClass: string): Promise => { // -- Sample Java // // Intent intent = new Intent(this, DisplayMessageActivity.class); // intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // // startActivity(intent); return wrapJavaPerform(() => { const context = getApplicationContext(); // Setup a new Intent const androidIntent: Intent = Java.use("android.content.Intent"); // Get the Activity class's .class const newActivity: JavaTypes.Wrapper = Java.use(activityClass).class; send(`Starting activity ${c.green(activityClass)}...`); // Init and launch the intent const newIntent: Intent = androidIntent.$new(context, newActivity); newIntent.setFlags(FLAG_ACTIVITY_NEW_TASK); context.startActivity(newIntent); send(c.blackBright(`Activity successfully asked to start.`)); }); }; // starts an Android service export const startService = (serviceClass: string): Promise => { // -- Sample Java // // Intent intent = new Intent(this, Service.class); // intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // // startService(intent); return wrapJavaPerform(() => { const context = getApplicationContext(); // Setup a new Intent const androidIntent: Intent = Java.use("android.content.Intent"); // Get the Activity class's .class const newService: string = Java.use(serviceClass).$className; send(`Starting service ${c.green(serviceClass)}...`); // Init and launch the intent const newIntent: Intent = androidIntent.$new(context, newService); newIntent.setFlags(FLAG_ACTIVITY_NEW_TASK); context.startService(newIntent); send(c.blackBright(`Service successfully asked to start.`)); }); }; // Analyzes and Detects Android Implicit Intents // https://developer.android.com/guide/components/intents-filters#Types export const analyzeImplicits = (backtrace = false): Promise => { const job = new jobs.Job(jobs.identifier(),`implicit-intent-analyser`) jobs.add(job) return wrapJavaPerform(() => { const classesToHook = [ { className: "android.app.Activity", methodName: "startActivityForResult" }, { className: "android.app.Activity", methodName: "onActivityResult" }, { className: "androidx.activity.ComponentActivity", methodName: "onActivityResult" }, { className: "android.content.Context", methodName: "startActivity" }, { className: "android.content.BroadcastReceiver", methodName: "onReceive" } // Add other classes and methods as needed ]; classesToHook.forEach(hook => { try { const clazz = Java.use(hook.className); const method = clazz[hook.methodName]; method.overloads.forEach((overload: FridaOverload) => { overload.implementation = function (...args: any[]): any { args.forEach(arg => { if (arg && arg.$className === "android.content.Intent") { analyseIntent(`${hook.className}::${hook.methodName}`, arg, backtrace = backtrace); } }); return overload.apply(this, args); }; job.addImplementation(overload); }); } catch (e) { send(`[-] Error hooking ${c.redBright(`${hook.className}.${hook.methodName}: ${e}`)}`); } }); }); }; ================================================ FILE: agent/src/android/keystore.ts ================================================ import { colors as c } from "../lib/color.js"; import { IKeyStoreDetail, IKeyStoreEntry } from "./lib/interfaces.js"; import { wrapJavaPerform, Java } from "./lib/libjava.js"; import { KeyFactory, KeyInfo, KeyStore, SecretKeyFactory } from "./lib/types.js"; import * as jobs from "../lib/jobs.js"; // Dump entries in the Android Keystore, together with a flag // indicating if its a key or a certificate. // // Ref: https://developer.android.com/reference/java/security/KeyStore.html export const list = (): Promise => { // - Sample Java // // KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); // ks.load(null); // Enumeration aliases = ks.aliases(); // // while(aliases.hasMoreElements()) { // Log.e("E", "Aliases = " + aliases.nextElement()); // } return wrapJavaPerform(() => { const keyStore: KeyStore = Java.use("java.security.KeyStore"); const entries: IKeyStoreEntry[] = []; // Prepare the AndroidKeyStore keystore provider and load it. // Maybe at a later stage we should support adding other stores // like from file or JKS. const ks: KeyStore = keyStore.getInstance("AndroidKeyStore"); ks.load(null, null); // Get the aliases and loop through them. The aliases() method // return an Enumeration type. const aliases = ks.aliases(); while (aliases.hasMoreElements()) { const alias = aliases.nextElement(); entries.push({ alias: alias.toString(), is_certificate: ks.isCertificateEntry(alias), is_key: ks.isKeyEntry(alias), }); } return entries; }); }; // Dump detailed information about keystore entries per alias. // // Refs: // https://labs.f-secure.com/blog/how-secure-is-your-android-keystore-authentication // https://github.com/FSecureLABS/android-keystore-audit export const detail = (): Promise => { // helper function to extract keystore alias information const keystore_info = (alias): IKeyStoreDetail => { const r: IKeyStoreDetail = {}; wrapJavaPerform(() => { // java class handles const keyStore: KeyStore = Java.use('java.security.KeyStore'); const keyFactory: KeyFactory = Java.use('java.security.KeyFactory'); const keyInfo: KeyInfo = Java.use('android.security.keystore.KeyInfo'); const keySecretKeyFactory: SecretKeyFactory = Java.use('javax.crypto.SecretKeyFactory'); // load the keystore entry const keyStoreObj = keyStore.getInstance('AndroidKeyStore'); keyStoreObj.load(null); const key = keyStoreObj.getKey(alias, null); if (key == null) return null; let keySpec = null; try { keySpec = keyFactory.getInstance(key.getAlgorithm(), 'AndroidKeyStore') .getKeySpec(key, keyInfo.class); } catch (err) { keySpec = keySecretKeyFactory.getInstance(key.getAlgorithm(), 'AndroidKeyStore') .getKeySpec(key, keyInfo.class); } // set result fields r.keyAlgorithm = key.getAlgorithm(); r.keySize = keyInfo['getKeySize'].call(keySpec); r.blockModes = keyInfo['getBlockModes'].call(keySpec); r.digests = keyInfo['getDigests'].call(keySpec); r.encryptionPaddings = keyInfo['getEncryptionPaddings'].call(keySpec); r.keyValidityForConsumptionEnd = keyInfo['getKeyValidityForConsumptionEnd'].call(keySpec); r.keyValidityForOriginationEnd = keyInfo['getKeyValidityForOriginationEnd'].call(keySpec); r.keyValidityStart = keyInfo['getKeyValidityStart'].call(keySpec); r.keystoreAlias = keyInfo['getKeystoreAlias'].call(keySpec); r.origin = keyInfo['getOrigin'].call(keySpec); r.purposes = keyInfo['getPurposes'].call(keySpec); r.signaturePaddings = keyInfo['getSignaturePaddings'].call(keySpec); r.userAuthenticationValidityDurationSeconds = keyInfo['getUserAuthenticationValidityDurationSeconds'].call(keySpec); r.isInsideSecureHardware = keyInfo['isInsideSecureHardware'].call(keySpec); r.isInvalidatedByBiometricEnrollment = keyInfo['isInvalidatedByBiometricEnrollment'].call(keySpec); r.isUserAuthenticationRequired = keyInfo['isUserAuthenticationRequired'].call(keySpec); r.isUserAuthenticationRequirementEnforcedBySecureHardware = keyInfo['isUserAuthenticationRequirementEnforcedBySecureHardware'].call(keySpec); r.isUserAuthenticationValidWhileOnBody = keyInfo['isUserAuthenticationValidWhileOnBody'].call(keySpec); // "crashy" calls that's ok if they fail try { r.isTrustedUserPresenceRequired = keyInfo['isTrustedUserPresenceRequired'].call(keySpec); } catch (err) { } try { r.isUserConfirmationRequired = keyInfo['isUserConfirmationRequired'].call(keySpec); } catch (err) { } // translate some values to string representation if they are not empty if (r.keyValidityForConsumptionEnd != null) r.keyValidityForConsumptionEnd = r.keyValidityForConsumptionEnd.toString(); if (r.keyValidityForOriginationEnd != null) r.keyValidityForOriginationEnd = r.keyValidityForOriginationEnd.toString(); if (r.keyValidityStart != null) r.keyValidityStart = r.keyValidityStart.toString(); }); return r; }; return wrapJavaPerform((): IKeyStoreDetail[] => { const keyStore: KeyStore = Java.use("java.security.KeyStore"); const ks: KeyStore = keyStore.getInstance("AndroidKeyStore"); ks.load(null, null); const aliases = ks.aliases(); const info: IKeyStoreDetail[] = []; while (aliases.hasMoreElements()) { var a = aliases.nextElement(); info.push(keystore_info(a.toString())); } return info; }); }; // Delete all entries in the Android Keystore // // Ref: https://developer.android.com/reference/java/security/KeyStore.html#deleteEntry(java.lang.String) export const clear = () => { // - Sample Java // // KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); // ks.load(null); // Enumeration aliases = ks.aliases(); // // while(aliases.hasMoreElements()) { // ks.deleteEntry(aliases.nextElement()); // } return wrapJavaPerform(() => { const keyStore: KeyStore = Java.use("java.security.KeyStore"); // Prepare the AndroidKeyStore keystore provider and load it. // Maybe at a later stage we should support adding other stores // like from file or JKS. const ks: KeyStore = keyStore.getInstance("AndroidKeyStore"); ks.load(null, null); // Get the aliases and loop through them. The aliases() method // return an Enumeration type. const aliases = ks.aliases(); while (aliases.hasMoreElements()) { ks.deleteEntry(aliases.nextElement()); } send(c.blackBright(`Keystore entries cleared`)); }); }; // keystore watch methods // Watch for KeyStore.load(); // TODO: Store the keystores themselves maybe? const keystoreLoad = (ident: number): Promise => { return wrapJavaPerform(() => { const ks: KeyStore = Java.use("java.security.KeyStore"); const ksLoad = ks.load.overload("java.io.InputStream", "[C"); send(c.blackBright(`[${ident}] Watching Keystore.load("java.io.InputStream", "[C")`)); ksLoad.implementation = function (stream, password) { send(c.blackBright(`[${ident}] `) + `Keystore.load(${c.greenBright(stream)}, ${c.redBright(password || `null`)}) ` + `called, loading a ${c.cyanBright(this.getType())} keystore.`); return this.load(stream, password); }; return ksLoad }); }; // Watch for Keystore.getKey(). // TODO: Extract more information, like the key itself maybe? const keystoreGetKey = (ident: number): Promise => { return wrapJavaPerform(() => { const ks: KeyStore = Java.use("java.security.KeyStore"); const ksGetKey = ks.getKey.overload("java.lang.String", "[C"); send(c.blackBright(`[${ident}] Watching Keystore.getKey("java.lang.String", "[C")`)); ksGetKey.implementation = function (alias, password) { const key = this.getKey(alias, password); send(c.blackBright(`[${ident}] `) + `Keystore.getKey(${c.greenBright(alias)}, ${c.redBright(password || `null`)}) ` + `called, returning a ${c.greenBright(key.$className)} instance.`); return key; }; return ksGetKey; }); }; // Android KeyStore watcher. // Many, many more methods can be added here.. export const watchKeystore = async (): Promise => { const job: jobs.Job = new jobs.Job(jobs.identifier(), "android-keystore-watch"); job.addImplementation(await keystoreLoad(job.identifier)); job.addImplementation(await keystoreGetKey(job.identifier)); jobs.add(job); }; ================================================ FILE: agent/src/android/lib/intentUtils.ts ================================================ import { Java } from "./libjava.js"; import { colors as c } from "../../lib/color.js"; export const analyseIntent = (methodName: string, intent: any, backtrace: boolean = false): void => { try { send(`\nAnalyzing Intent from: ${c.green(`${methodName}`)}`); // Get Component const component = intent.getComponent(); if (component) { send(`[-] ${c.green('Intent Type: Explicit Intent')}`); } else { send(`[+] ${c.redBright('Intent Type: Implicit Intent Detected!')}`); if (backtrace) { send( Java.use('android.util.Log') .getStackTraceString(Java.use('java.lang.Exception').$new()) ) } // Log intent details send(`[+] Action: ${`${c.green(`${intent.getAction()}`)}` || `${c.redBright(`[None]`)}`}`); send(`[+] Data URI: ${`${c.green(`${intent.getDataString()}`)}` || `${c.redBright(`[None]`)}`}`); send(`[+] Type: ${`${c.green(`${intent.getType()}`)}` || `${c.redBright(`[None]`)}`}`); send(`[+] Flags: ${c.green(`0x${intent.getFlags().toString(16)}`)}`); // Categories const categories = intent.getCategories(); if (categories) { send("\n[+] Categories:"); const iterator = categories.iterator(); while (iterator.hasNext()) { send(`[+] Category: ${c.green(`${iterator.next()}`)} `); } } else { send(`[-] Category: ${`${c.redBright(`[None]`)}`}`); } // Extras const extras = intent.getExtras(); if (extras) { send(`[+] Extras: ${c.green(`${extras}`)}`); } else { send(`[-] Extras: ${`${c.redBright(`[None]`)}`}`); } // Resolving implicit intents const activityContext = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext(); if (activityContext) { const packageManager = activityContext.getPackageManager(); const resolveInfoList = packageManager.queryIntentActivities(intent, Java.use("android.content.pm.PackageManager").MATCH_ALL.value); send("[+] Responding apps:"); for (let i = 0; i < resolveInfoList.size(); i++) { const resolveInfo = resolveInfoList.get(i); send(`[*] Resolve Info List at position ${i}: ${c.green(`${resolveInfo.toString()}`)}`); } } else { send("[-] No activity context available"); } } } catch (e) { send(`[!] Error analyzing intent: ${e}`); } }; ================================================ FILE: agent/src/android/lib/interfaces.ts ================================================ import type { default as JavaTypes } from "frida-java-bridge"; export interface IAndroidFilesystem { files: any; path: string; readable: boolean; writable: boolean; } export interface IExecutedCommand { command: string; stdOut: string; stdErr: string; } export interface IKeyStoreEntry { alias: string; is_certificate: boolean; is_key: boolean; } export interface ICurrentActivityFragment { activivity: string | null; fragment: string | null; } export interface IHeapClassDictionary { [index: string]: IHeapObject[]; } export interface IHeapObject { hashcode: number; instance: JavaTypes.Wrapper; } export interface IHeapNormalised { hashcode: number; classname: string; tostring: string; } export interface IJavaField { name: string; value: string; } export interface IKeyStoreDetail { keyAlgorithm?: string; keySize?: string; blockModes?: string; digests?: string; encryptionPaddings?: string; keyValidityForConsumptionEnd?: string; keyValidityForOriginationEnd?: string; keyValidityStart?: string; keystoreAlias?: string; origin?: string; purposes?: string; signaturePaddings?: string; userAuthenticationValidityDurationSeconds?: string; isInsideSecureHardware?: string; isInvalidatedByBiometricEnrollment?: string; isUserAuthenticationRequired?: string; isUserAuthenticationRequirementEnforcedBySecureHardware?: string; isUserAuthenticationValidWhileOnBody?: string; // "crashy" fields isTrustedUserPresenceRequired?: string; isUserConfirmationRequired?: string; } ================================================ FILE: agent/src/android/lib/libjava.ts ================================================ import Java_bridge from "frida-java-bridge"; import { colors as c } from "../../lib/color.js"; let Java: typeof Java_bridge; // Compatibility with frida < 17 if (globalThis.Java) { send(c.blackBright("Pre-v17 version of Frida detected. Attempting to use old bridge interface.")) Java = globalThis.Java } else { Java = Java_bridge } export { Java } // all Java calls need to be wrapped in a Java.perform(). // this helper just wraps that into a Promise that the // rpc export will sniff and resolve before returning // the result when its ready. export const wrapJavaPerform = (fn: any): Promise => { return new Promise((resolve, reject) => { Java.perform(() => { try { resolve(fn()); } catch (e) { reject(e); } }); }); }; export const getApplicationContext = (): any => { const ActivityThread = Java.use("android.app.ActivityThread"); const currentApplication = ActivityThread.currentApplication(); return currentApplication.getApplicationContext(); }; // A helper method to access the R class for the app. // Typical usage within an app would be something like: // R.id.content_frame. // // Using this method, the above example would be: // R("content_frame", "id") export const R = (name: string, type: string): any => { const context = getApplicationContext(); // https://github.com/bitpay/android-sdk/issues/14#issue-202495610 return context.getResources().getIdentifier(name, type, context.getPackageName()); }; ================================================ FILE: agent/src/android/lib/types.ts ================================================ export type JavaClass = any; export type JavaMethodsOverloadsResult = any; export type ClipboardManager = JavaClass | any; export type File = JavaClass | any; export type Throwable = JavaClass | any; export type PackageManager = JavaClass | any; export type ArrayMap = JavaClass | any; export type ActivityThread = JavaClass | any; export type Intent = JavaClass | any; export type KeyStore = JavaClass | any; export type KeyFactory = JavaClass | any; export type KeyInfo = JavaClass | any; export type SecretKeyFactory = JavaClass | any; export type X509TrustManager = JavaClass | any; export type SSLContext = JavaClass | any; export type CertificatePinner = JavaClass | any; export type PinningTrustManager = JavaClass | any; export type SSLCertificateChecker = JavaClass | any; export type TrustManagerImpl = JavaClass | any; export type ArrayList = JavaClass | any; export type JavaString = JavaClass | any; export type Runtime = JavaClass | any; export type IOException = JavaClass | any; export type InputStreamReader = JavaClass | any; export type BufferedReader = JavaClass | any; export type StringBuilder = JavaClass | any; export type Activity = JavaClass | any; export type ActivityClientRecord = JavaClass | any; export type Bitmap = JavaClass | any; export type ByteArrayOutputStream = JavaClass | any; export type CompressFormat = JavaClass | any; export type FridaOverload = { implementation: (...args: any[]) => any; apply: (thisArg: any, args: any[]) => any; }; ================================================ FILE: agent/src/android/monitor.ts ================================================ import { wrapJavaPerform } from "./lib/libjava.js"; export namespace monitor { export const stringCanary = (can: string): Promise => { return wrapJavaPerform(() => { }); }; } ================================================ FILE: agent/src/android/pinning.ts ================================================ import { colors as c } from "../lib/color.js"; import { qsend } from "../lib/helpers.js"; import * as jobs from "../lib/jobs.js"; import { wrapJavaPerform, Java } from "./lib/libjava.js"; import { ArrayList, CertificatePinner, PinningTrustManager, SSLCertificateChecker, SSLContext, TrustManagerImpl, X509TrustManager, } from "./lib/types.js"; // a simple flag to control if we should be quiet or not let quiet: boolean = false; const sslContextEmptyTrustManager = (ident: number): Promise => { // -- Sample Java // // "Generic" TrustManager Example // // TrustManager[] trustAllCerts = new TrustManager[] { // new X509TrustManager() { // public java.security.cert.X509Certificate[] getAcceptedIssuers() { // return null; // } // public void checkClientTrusted(X509Certificate[] certs, String authType) { } // public void checkServerTrusted(X509Certificate[] certs, String authType) { } // } // }; // SSLContext sslcontect = SSLContext.getInstance("TLS"); // sslcontect.init(null, trustAllCerts, null); return wrapJavaPerform(() => { const x509TrustManager: X509TrustManager = Java.use("javax.net.ssl.X509TrustManager"); const sSLContext: SSLContext = Java.use("javax.net.ssl.SSLContext"); // Some 'anti-frida' detections will scan /proc//maps. // Rename the tempFileNaming prefix as this could end up in maps. // https://github.com/frida/frida-java-bridge/blob/8b3790f7489ff5be7b19ddaccf5149d4e7738460/lib/class-factory.js#L94 if (Java.classFactory.tempFileNaming.prefix == 'frida') { Java.classFactory.tempFileNaming.prefix = 'onetwothree'; } // Implement a new TrustManager // ref: https://gist.github.com/oleavr/3ca67a173ff7d207c6b8c3b0ca65a9d8 const TrustManager: X509TrustManager = Java.registerClass({ implements: [x509TrustManager], methods: { // tslint:disable-next-line:no-empty checkClientTrusted(chain, authType) { }, // tslint:disable-next-line:no-empty checkServerTrusted(chain, authType) { }, getAcceptedIssuers() { return []; }, }, name: "com.sensepost.test.TrustManager", }); // Prepare the TrustManagers array to pass to SSLContext.init() const TrustManagers: X509TrustManager[] = [TrustManager.$new()]; send(c.blackBright("Custom TrustManager ready, overriding SSLContext.init()")); // Get a handle on the init() on the SSLContext class const SSLContextInit = sSLContext.init.overload( "[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom"); // Override the init method, specifying our new TrustManager SSLContextInit.implementation = function (keyManager, trustManager, secureRandom) { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`SSLContext.init()`) + `, overriding TrustManager with empty one.`, ); SSLContextInit.call(this, keyManager, TrustManagers, secureRandom); }; return SSLContextInit; }); }; const okHttp3CertificatePinnerCheck = (ident: number): Promise => { // -- Sample Java // // Example used to test this bypass. // // String hostname = "swapi.co"; // CertificatePinner certificatePinner = new CertificatePinner.Builder() // .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // .build(); // OkHttpClient client = new OkHttpClient.Builder() // .certificatePinner(certificatePinner) // .build(); // Request request = new Request.Builder() // .url("https://swapi.co/api/people/1") // .build(); // Response response = client.newCall(request).execute(); return wrapJavaPerform(() => { try { const certificatePinner: CertificatePinner = Java.use("okhttp3.CertificatePinner"); send(c.blackBright(`Found okhttp3.CertificatePinner, overriding CertificatePinner.check()`)); if(!certificatePinner.check) { return null; } const CertificatePinnerCheck = certificatePinner.check.overload("java.lang.String", "java.util.List"); // tslint:disable-next-line:only-arrow-functions CertificatePinnerCheck.implementation = function () { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`OkHTTP 3.x CertificatePinner.check()`) + `, not throwing an exception.`, ); }; return CertificatePinnerCheck; } catch (err) { if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } return null; } }); }; const okHttp3CertificatePinnerCheckOkHttp = (ident: number): Promise => { // -- Sample Java // // Example used to test this bypass. // // String hostname = "swapi.co"; // CertificatePinner certificatePinner = new CertificatePinner.Builder() // .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // .build(); // OkHttpClient client = new OkHttpClient.Builder() // .certificatePinner(certificatePinner) // .build(); // Request request = new Request.Builder() // .url("https://swapi.co/api/people/1") // .build(); // Response response = client.newCall(request).execute(); return wrapJavaPerform(() => { try { const certificatePinner: CertificatePinner = Java.use("okhttp3.CertificatePinner"); if(!certificatePinner.check$okhttp) { return null; } send(c.blackBright(`Found okhttp3.CertificatePinner, overriding CertificatePinner.check$okhttp()`)); const CertificatePinnerCheckOkHttp = certificatePinner.check$okhttp.overload("java.lang.String", "u15"); // tslint:disable-next-line:only-arrow-functions CertificatePinnerCheckOkHttp.implementation = function () { qsend(quiet, c.blackBright(`[${ident}] `) + `Called check$okhttp ` + c.green(`OkHTTP 3.x CertificatePinner.check$okhttp()`) + `, not throwing an exception.`, ); }; return CertificatePinnerCheckOkHttp; } catch (err) { if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } return null; } }); }; const appceleratorTitaniumPinningTrustManager = (ident: number): Promise => { return wrapJavaPerform(() => { try { const pinningTrustManager: PinningTrustManager = Java.use("appcelerator.https.PinningTrustManager"); const PinningTrustManagerCheckServerTrusted = pinningTrustManager.checkServerTrusted; if(!PinningTrustManagerCheckServerTrusted) { return null; } send( c.blackBright(`Found appcelerator.https.PinningTrustManager, ` + `overriding PinningTrustManager.checkServerTrusted()`), ); // tslint:disable-next-line:only-arrow-functions PinningTrustManagerCheckServerTrusted.implementation = function () { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`PinningTrustManager.checkServerTrusted()`) + `, not throwing an exception.`, ); }; return PinningTrustManagerCheckServerTrusted; } catch (err) { if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } return null; } }); }; // Android 7+ TrustManagerImpl.verifyChain() // The work in the following NCC blog post was a great help for this hook! // hattip @AdriVillaB :) // https://www.nccgroup.trust/uk/about-us/newsroom-and-events/ // blogs/2017/november/bypassing-androids-network-security-configuration/ // // More information: https://sensepost.com/blog/2018/tip-toeing-past-android-7s-network-security-configuration/ const trustManagerImplVerifyChainCheck = (ident: number): Promise => { return wrapJavaPerform(() => { try { const trustManagerImpl: TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl"); // https://github.com/google/conscrypt/blob/c88f9f55a523f128f0e4dace76a34724bfa1e88c/ // platform/src/main/java/org/conscrypt/TrustManagerImpl.java#L650 const TrustManagerImplverifyChain = trustManagerImpl.verifyChain; if((!TrustManagerImplverifyChain)) { return null; } send( c.blackBright(`Found com.android.org.conscrypt.TrustManagerImpl, ` + `overriding TrustManagerImpl.verifyChain()`), ); // tslint:disable-next-line:only-arrow-functions TrustManagerImplverifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) { qsend(quiet, c.blackBright(`[${ident}] `) + `Called (Android 7+) ` + c.green(`TrustManagerImpl.verifyChain()`) + `, not throwing an exception.`, ); // Skip all the logic and just return the chain again :P return untrustedChain; }; return TrustManagerImplverifyChain; } catch (err) { if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } return null; } }); }; // Android 7+ TrustManagerImpl.checkTrustedRecursive() // The work in the following method is based on: // https://techblog.mediaservice.net/2018/11/universal-android-ssl-pinning-bypass-2/ const trustManagerImplCheckTrustedRecursiveCheck = (ident: number): Promise => { return wrapJavaPerform(() => { try { const arrayList: ArrayList = Java.use("java.util.ArrayList"); const trustManagerImpl: TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl"); if(!trustManagerImpl.checkTrustedRecursive) { return null; } // https://android.googlesource.com/platform/external/conscrypt/+/1186465/src/ // platform/java/org/conscrypt/TrustManagerImpl.java#391 const TrustManagerImplcheckTrustedRecursive = trustManagerImpl.checkTrustedRecursive; send( c.blackBright(`Found com.android.org.conscrypt.TrustManagerImpl, ` + `overriding TrustManagerImpl.checkTrustedRecursive()`), ); // tslint:disable-next-line:only-arrow-functions TrustManagerImplcheckTrustedRecursive.implementation = function (certs, host, clientAuth, untrustedChain, trustAnchorChain, used) { qsend(quiet, c.blackBright(`[${ident}] `) + `Called (Android 7+) ` + c.green(`TrustManagerImpl.checkTrustedRecursive()`) + `, not throwing an exception.`, ); // Return an empty list return arrayList.$new(); }; return TrustManagerImplcheckTrustedRecursive; } catch (err) { if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } return null; } }); }; const phoneGapSSLCertificateChecker = (ident: number): Promise => { return wrapJavaPerform(() => { try { const sslCertificateChecker: SSLCertificateChecker = Java.use("nl.xservices.plugins.SSLCertificateChecker"); if(!sslCertificateChecker.execute) { return null; } send( c.blackBright(`Found nl.xservices.plugins.SSLCertificateChecker, ` + `overriding SSLCertificateChecker.execute()`), ); const SSLCertificateCheckerExecute = sslCertificateChecker.execute.overload("java.lang.String", "org.json.JSONArray", "org.apache.cordova.CallbackContext"); SSLCertificateCheckerExecute.implementation = function (str, jsonArray, callBackContext) { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`SSLCertificateChecker.execute()`) + `, not throwing an exception.`, ); callBackContext.success("CONNECTION_SECURE"); return true; }; return SSLCertificateCheckerExecute; } catch (err) { if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } return null; } }); }; // the main exported function to run all of the pinning bypass methods known export const disable = async (q: boolean): Promise => { if (q) { send(c.yellow(`Quiet mode enabled. Not reporting invocations.`)); quiet = true; } const job: jobs.Job = new jobs.Job(jobs.identifier(), "android-sslpinning-disable"); job.addImplementation(await sslContextEmptyTrustManager(job.identifier)); // Exceptions can cause undefined values if classes are not found. Thus addImplementation only adds if function was hooked job.addImplementation(await okHttp3CertificatePinnerCheck(job.identifier)); job.addImplementation(await okHttp3CertificatePinnerCheckOkHttp(job.identifier)); job.addImplementation(await appceleratorTitaniumPinningTrustManager(job.identifier)); job.addImplementation(await trustManagerImplVerifyChainCheck(job.identifier)); job.addImplementation(await trustManagerImplCheckTrustedRecursiveCheck(job.identifier)); job.addImplementation(await phoneGapSSLCertificateChecker(job.identifier)); jobs.add(job); }; ================================================ FILE: agent/src/android/proxy.ts ================================================ import { wrapJavaPerform, Java } from "./lib/libjava.js"; import { colors as c } from "../lib/color.js"; export const set = (host: string, port: string): Promise => { return wrapJavaPerform(() => { var proxyHost = host; var proxyPort = port; var System = Java.use("java.lang.System"); if (System != undefined) { send(c.green(`Setting properties for a proxy`)); System.setProperty("http.proxyHost", proxyHost); System.setProperty("http.proxyPort", proxyPort); System.setProperty("https.proxyHost", proxyHost); System.setProperty("https.proxyPort", proxyPort); send(`${c.green(`Proxy configured to ` + proxyHost + ` ` + proxyPort)}`); } }); }; ================================================ FILE: agent/src/android/root.ts ================================================ import { colors as c } from "../lib/color.js"; import * as jobs from "../lib/jobs.js"; import { wrapJavaPerform, Java } from "./lib/libjava.js"; import { File, IOException, JavaString, Runtime } from "./lib/types.js"; const commonPaths = [ "/data/local/bin/su", "/data/local/su", "/data/local/xbin/su", "/dev/com.koushikdutta.superuser.daemon/", "/sbin/su", "/system/app/Superuser.apk", "/system/bin/failsafe/su", "/system/bin/su", "/system/etc/init.d/99SuperSUDaemon", "/system/sd/xbin/su", "/system/xbin/busybox", "/system/xbin/daemonsu", "/system/xbin/su", ]; const testKeysCheck = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const JavaString: JavaString = Java.use("java.lang.String"); JavaString.contains.implementation = function (name) { if (name !== "test-keys") { return this.contains.call(this, name); } if (success) { send(c.blackBright(`[${ident}] `) + `Marking "test-keys" check as ` + c.green(`successful`) + `.`); return true; } send(c.blackBright(`[${ident}] `) + `Marking "test-keys" check as ` + c.green(`failed`) + `.`); return false; }; return JavaString.contains; }); }; const execSuCheck = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const JavaRuntime: Runtime = Java.use("java.lang.Runtime"); const iOException: IOException = Java.use("java.io.IOException"); const JavaRuntime_exec = JavaRuntime.exec.overload("java.lang.String"); JavaRuntime_exec.implementation = function (command: string) { if (command.endsWith("su")) { if (success) { send(c.blackBright(`[${ident}] `) + `Check for 'su' using command exec detected, allowing.`); return this.apply(this, arguments); } send(c.blackBright(`[${ident}] `) + `Check for 'su' using command exec detected, throwing IOException.`); throw iOException.$new("objection anti-root"); } // call the original method return this.exec.overload("java.lang.String").call(this, command); }; return JavaRuntime_exec; }); }; const fileExistsCheck = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const JavaFile: File = Java.use("java.io.File"); JavaFile.exists.implementation = function () { const filename = this.getAbsolutePath(); if (commonPaths.indexOf(filename) >= 0) { if (success) { send( c.blackBright(`[${ident}] `) + `File existence check for ${filename} detected, marking as ${c.green("true")}.`, ); return true; } send( c.blackBright(`[${ident}] `) + `File existence check for ${filename} detected, marking as ${c.green("false")}.`, ); return false; } // call the original method return this.exists.call(this); }; return JavaFile.exists; }); }; // RootBeer: https://github.com/scottyab/rootbeer const rootBeerIsRooted = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); const RootBeer_isRooted = RootBeer.isRooted.overload(); RootBeer_isRooted.implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + `RootBeer->isRooted() check detected, marking as ${c.green("true")}.`, ); return true; } send( c.blackBright(`[${ident}] `) + `RootBeer->isRooted() check detected, marking as ${c.green("false")}.`, ); return false; }; return RootBeer_isRooted; }); }; const rootBeerCheckForBinary = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.checkForBinary.overload('java.lang.String').implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + `RootBeer->checkForBinary() check detected, marking as ${c.green("true")}.`, ); return true; } send( c.blackBright(`[${ident}] `) + `RootBeer->checkForBinary() check detected, marking as ${c.green("false")}.`, ); return false; }; return RootBeer.checkForBinary; }); }; const rootBeerCheckForDangerousProps = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.checkForDangerousProps.overload().implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + `RootBeer->checkForDangerousProps() check detected, marking as ${c.green("true")}.`, ); return true; } send( c.blackBright(`[${ident}] `) + `RootBeer->checkForDangerousProps() check detected, marking as ${c.green("false")}.`, ); return false; }; return RootBeer.checkForDangerousProps; }); }; const rootBeerDetectRootCloakingApps = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); const RootBeer_detectRootCloakingApps = RootBeer.detectRootCloakingApps.overload(); RootBeer_detectRootCloakingApps.implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + `RootBeer->detectRootCloakingApps() check detected, marking as ${c.green("true")}.`, ); return true; } send( c.blackBright(`[${ident}] `) + `RootBeer->detectRootCloakingApps() check detected, marking as ${c.green("false")}.`, ); return false; }; return RootBeer_detectRootCloakingApps; }); }; const rootBeerCheckSuExists = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.checkSuExists.overload().implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + `RootBeer->checkSuExists() check detected, marking as ${c.green("true")}.`, ); return true; } send( c.blackBright(`[${ident}] `) + `RootBeer->checkSuExists() check detected, marking as ${c.green("false")}.`, ); return false; }; return RootBeer.checkSuExists; }); }; const rootBeerDetectTestKeys = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.detectTestKeys.overload().implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + `RootBeer->detectTestKeys() check detected, marking as ${c.green("true")}.`, ); return true; } send( c.blackBright(`[${ident}] `) + `RootBeer->detectTestKeys() check detected, marking as ${c.green("false")}.`, ); return false; }; return RootBeer.detectTestKeys; }); }; const rootBeerCheckSeLinux = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { try { const Util = Java.use("com.scottyab.rootbeer.util"); Util.isSelinuxFlagInEnabled.overload().implementation = function () { if (success) { send( c.blackBright(`[${ident}]`) + `Rootbeer.util->isSelinuxFlagInEnabled() check detected, marking as ${c.green("true")}`, ); return true; } send( c.blackBright(`[${ident}] `) + `Rootbeer.util->isSelinuxFlagInEnabled() check detected, marking as ${c.green("false")}`, ); return false; }; return Util.isSelinuxFlagInEnabled; } catch (err) { if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") === 0) { return null; }; throw err; } }); }; const rootBeerNative = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeerNative = Java.use("com.scottyab.rootbeer.RootBeerNative"); const RootBeerNative_checkForRoot = RootBeerNative.checkForRoot.overload('[Ljava.lang.Object;'); RootBeerNative_checkForRoot.implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + `RootBeerNative->checkForRoot() check detected, marking as ${c.green("1")}.`, ); return 1; } send( c.blackBright(`[${ident}] `) + `RootBeerNative->checkForRoot() check detected, marking as ${c.green("0")}.`, ); return 0; }; return RootBeerNative_checkForRoot; }); }; // ref: https://www.ayrx.me/gantix-jailmonkey-root-detection-bypass/ const jailMonkeyBypass = (success: boolean, ident: number): Promise => { return wrapJavaPerform(() => { try { const JavaJailMonkeyModule = Java.use("com.gantix.JailMonkey.JailMonkeyModule"); const JavaHashMap = Java.use("java.util.HashMap"); const JavaBoolean = Java.use("java.lang.Boolean") const JavaFalseObject = JavaBoolean.FALSE.value; const JavaTrueObject = JavaBoolean.TRUE.value; JavaJailMonkeyModule.getConstants.implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + `RootBeer->checkForDangerousProps() check detected, marking as ${c.green("true")} for all keys.`, ); const hm = JavaHashMap.$new(); hm.put("isJailBroken", JavaTrueObject); hm.put("hookDetected", JavaTrueObject); hm.put("canMockLocation", JavaTrueObject); hm.put("isOnExternalStorage", JavaTrueObject); hm.put("AdbEnabled", JavaTrueObject); return hm; } send( c.blackBright(`[${ident}] `) + `JailMonkeyModule.getConstants() called, returning ${c.green("false")} for all keys.` ); const hm = JavaHashMap.$new(); hm.put("isJailBroken", JavaFalseObject); hm.put("hookDetected", JavaFalseObject); hm.put("canMockLocation", JavaFalseObject); hm.put("isOnExternalStorage", JavaFalseObject); hm.put("AdbEnabled", JavaFalseObject); return hm; }; return JavaJailMonkeyModule.getConstants; } catch (err) { if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") === 0) { return null; }; throw err; } }); }; export const disable = async (): Promise => { const job: jobs.Job = new jobs.Job(jobs.identifier(), 'root-detection-disable'); job.addImplementation(await testKeysCheck(false, job.identifier)); job.addImplementation(await execSuCheck(false, job.identifier)); job.addImplementation(await fileExistsCheck(false, job.identifier)); job.addImplementation(await jailMonkeyBypass(false, job.identifier)); // RootBeer functions job.addImplementation(await rootBeerIsRooted(false, job.identifier)); job.addImplementation(await rootBeerCheckForBinary(false, job.identifier)); job.addImplementation(await rootBeerCheckForDangerousProps(false, job.identifier)); job.addImplementation(await rootBeerDetectRootCloakingApps(false, job.identifier)); job.addImplementation(await rootBeerCheckSuExists(false, job.identifier)); job.addImplementation(await rootBeerDetectTestKeys(false, job.identifier)); job.addImplementation(await rootBeerNative(false, job.identifier)); job.addImplementation(await rootBeerCheckSeLinux(false, job.identifier)); jobs.add(job); }; export const enable = async (): Promise => { const job: jobs.Job = new jobs.Job(jobs.identifier(), "root-detection-enable"); job.addImplementation(await testKeysCheck(true, job.identifier)); job.addImplementation(await execSuCheck(true, job.identifier)); job.addImplementation(await fileExistsCheck(true, job.identifier)); job.addImplementation(await jailMonkeyBypass(true, job.identifier)); // RootBeer functions job.addImplementation(await rootBeerIsRooted(true, job.identifier)); job.addImplementation(await rootBeerCheckForBinary(true, job.identifier)); job.addImplementation(await rootBeerCheckForDangerousProps(true, job.identifier)); job.addImplementation(await rootBeerDetectRootCloakingApps(true, job.identifier)); job.addImplementation(await rootBeerCheckSuExists(true, job.identifier)); job.addImplementation(await rootBeerDetectTestKeys(true, job.identifier)); job.addImplementation(await rootBeerNative(true, job.identifier)); job.addImplementation(await rootBeerCheckSeLinux(false, job.identifier)); jobs.add(job); }; ================================================ FILE: agent/src/android/shell.ts ================================================ import { IExecutedCommand } from "./lib/interfaces.js"; import { wrapJavaPerform, Java } from "./lib/libjava.js"; import { BufferedReader, InputStreamReader, Runtime, StringBuilder } from "./lib/types.js"; // Executes shell commands on an Android device using Runtime.getRuntime().exec() export const execute = (cmd: string): Promise => { // -- Sample Java // // Process command = Runtime.getRuntime().exec("ls -l /"); // InputStreamReader isr = new InputStreamReader(command.getInputStream()); // BufferedReader br = new BufferedReader(isr); // // StringBuilder sb = new StringBuilder(); // String line = ""; // // while ((line = br.readLine()) != null) { // sb.append(line + "\n"); // } // // String output = sb.toString(); return wrapJavaPerform(() => { const runtime: Runtime = Java.use("java.lang.Runtime"); const inputStreamReader: InputStreamReader = Java.use("java.io.InputStreamReader"); const bufferedReader: BufferedReader = Java.use("java.io.BufferedReader"); const stringBuilder: StringBuilder = Java.use("java.lang.StringBuilder"); // Run the command const command = runtime.getRuntime().exec(cmd); // Read 'stderr' const stdErrInputStreamReader: InputStreamReader = inputStreamReader.$new(command.getErrorStream()); let bufferedReaderInstance: BufferedReader = bufferedReader.$new(stdErrInputStreamReader); const stdErrStringBuilder: StringBuilder = stringBuilder.$new(); let lineBuffer: string; // tslint:disable-next-line:no-conditional-assignment while ((lineBuffer = bufferedReaderInstance.readLine()) != null) { stdErrStringBuilder.append(lineBuffer + "\n"); } // Read 'stdout' const stdOutInputStreamReader: InputStreamReader = inputStreamReader.$new(command.getInputStream()); bufferedReaderInstance = bufferedReader.$new(stdOutInputStreamReader); const stdOutStringBuilder = stringBuilder.$new(); lineBuffer = ""; // tslint:disable-next-line:no-conditional-assignment while ((lineBuffer = bufferedReaderInstance.readLine()) != null) { stdOutStringBuilder.append(lineBuffer + "\n"); } return { command: cmd, stdErr: stdErrStringBuilder.toString(), stdOut: stdOutStringBuilder.toString(), }; }); }; ================================================ FILE: agent/src/android/userinterface.ts ================================================ import { colors as c } from "../lib/color.js"; import { wrapJavaPerform, Java } from "./lib/libjava.js"; import { Activity, ActivityClientRecord, ActivityThread, Bitmap, ByteArrayOutputStream, CompressFormat, } from "./lib/types.js"; // https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_SECURE const FLAG_SECURE = 0x00002000; export const screenshot = (): Promise => { return wrapJavaPerform(() => { // Take a screenshot by making use of a View's drawing cache: // ref: https://developer.android.com/reference/android/view/View.html#getDrawingCache(boolean) const activityThread: ActivityThread = Java.use("android.app.ActivityThread"); const activity: Activity = Java.use("android.app.Activity"); const activityClientRecord: ActivityClientRecord = Java.use("android.app.ActivityThread$ActivityClientRecord"); const bitmap: Bitmap = Java.use("android.graphics.Bitmap"); const byteArrayOutputStream: ByteArrayOutputStream = Java.use("java.io.ByteArrayOutputStream"); const compressFormat: CompressFormat = Java.use("android.graphics.Bitmap$CompressFormat"); let bytes; const currentActivityThread = activityThread.currentActivityThread(); const activityRecords = currentActivityThread.mActivities.value.values().toArray(); let currentActivity; for (const i of activityRecords) { const activityRecord = Java.cast(i, activityClientRecord); if (!activityRecord.paused.value) { currentActivity = Java.cast(Java.cast(activityRecord, activityClientRecord).activity.value, activity); break; } } if (currentActivity) { const view = currentActivity.getWindow().getDecorView().getRootView(); view.setDrawingCacheEnabled(true); const bitmapInstance = bitmap.createBitmap(view.getDrawingCache()); view.setDrawingCacheEnabled(false); const outputStream: ByteArrayOutputStream = byteArrayOutputStream.$new(); bitmapInstance.compress(compressFormat.PNG.value, 100, outputStream); bytes = outputStream.buf.value; } return bytes; }); }; export const setFlagSecure = (v: boolean): Promise => { return wrapJavaPerform(() => { const activityThread: ActivityThread = Java.use("android.app.ActivityThread"); const activity: Activity = Java.use("android.app.Activity"); const activityClientRecord: ActivityClientRecord = Java.use("android.app.ActivityThread$ActivityClientRecord"); const currentActivityThread = activityThread.currentActivityThread(); const activityRecords = currentActivityThread.mActivities.value.values().toArray(); let currentActivity; for (const i of activityRecords) { const activityRecord = Java.cast(i, activityClientRecord); if (!activityRecord.paused.value) { currentActivity = Java.cast(Java.cast(activityRecord, activityClientRecord).activity.value, activity); break; } } if (currentActivity) { // Somehow the next line prevents Frida from throwing an abort error currentActivity.getWindow(); // Set flag and trigger update (Throws abort without first calling getWindow()) Java.scheduleOnMainThread(() => { currentActivity.getWindow().setFlags(v ? FLAG_SECURE : 0, FLAG_SECURE); send(`FLAG_SECURE set to ${c.green(v.toString())}`); }); } }); }; ================================================ FILE: agent/src/generic/custom.ts ================================================ export const evaluate = (js: string): void => { // tslint:disable-next-line:no-eval eval(js); }; ================================================ FILE: agent/src/generic/environment.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import { getApplicationContext, wrapJavaPerform, Java } from "../android/lib/libjava.js"; import { NSSearchPaths, NSUserDomainMask } from "../ios/lib/constants.js"; import { getNSFileManager, getNSMainBundle } from "../ios/lib/helpers.js"; import { NSBundle } from "../ios/lib/types.js"; import { DeviceType } from "../lib/constants.js"; import { IAndroidPackage, IFridaInfo, IIosBundlePaths, IIosPackage } from "../lib/interfaces.js"; // small helper function to lookup ios bundle paths const getPathForNSLocation = (NSPath: NSSearchPaths): string => { const p = getNSFileManager().URLsForDirectory_inDomains_(NSPath, NSUserDomainMask).lastObject(); if (p) { return p.path().toString(); } return ""; }; export const runtime = (): string => { if (ObjC.available) { return DeviceType.IOS; } if (Java.available) { return DeviceType.ANDROID; } return DeviceType.UNKNOWN; }; export const frida = (): IFridaInfo => { return { arch: Process.arch, debugger: Process.isDebuggerAttached(), heap: Frida.heapSize, platform: Process.platform, runtime: Script.runtime, version: Frida.version, }; }; export const iosPackage = (): IIosPackage => { // -- Sample Objective-C // // NSFileManager *fm = [NSFileManager defaultManager]; // NSString *pictures = [[fm URLsForDirectory:NSPicturesDirectory inDomains:NSUserDomainMask] lastObject].path; // NSBundle *bundle = [NSBundle mainBundle]; // NSString *bundlePath = [bundle bundlePath]; // NSString *receipt = [bundle appStoreReceiptURL].path; // NSString *resourcePath = [bundle resourcePath]; const { UIDevice } = ObjC.classes; const mb: NSBundle = getNSMainBundle(); return { applicationName: mb.objectForInfoDictionaryKey_("CFBundleIdentifier").toString(), deviceName: UIDevice.currentDevice().name().toString(), identifierForVendor: UIDevice.currentDevice().identifierForVendor().toString(), model: UIDevice.currentDevice().model().toString(), systemName: UIDevice.currentDevice().systemName().toString(), systemVersion: UIDevice.currentDevice().systemVersion().toString(), }; }; export const iosPaths = (): IIosBundlePaths => { const mb: NSBundle = getNSMainBundle(); return { BundlePath: mb.bundlePath().toString(), CachesDirectory: getPathForNSLocation(NSSearchPaths.NSCachesDirectory), DocumentDirectory: getPathForNSLocation(NSSearchPaths.NSDocumentDirectory), LibraryDirectory: getPathForNSLocation(NSSearchPaths.NSLibraryDirectory), }; }; export const androidPackage = (): Promise => { return wrapJavaPerform(() => { // https://developer.android.com/reference/android/os/Build.html const Build: any = Java.use("android.os.Build"); return { application_name: getApplicationContext().getPackageName(), board: Build.BOARD.value.toString(), brand: Build.BRAND.value.toString(), device: Build.DEVICE.value.toString(), host: Build.HOST.value.toString(), id: Build.ID.value.toString(), model: Build.MODEL.value.toString(), product: Build.PRODUCT.value.toString(), user: Build.USER.value.toString(), version: Java.androidVersion, }; }); }; export const androidPaths = (): Promise => { // -- Sample Java // // getApplicationContext().getFilesDir().getAbsolutePath() return wrapJavaPerform(() => { const context = getApplicationContext(); return { cacheDirectory: context.getCacheDir().getAbsolutePath().toString(), codeCacheDirectory: "getCodeCacheDir" in context ? context.getCodeCacheDir() .getAbsolutePath().toString() : "n/a", externalCacheDirectory: context.getExternalCacheDir().getAbsolutePath().toString(), filesDirectory: context.getFilesDir().getAbsolutePath().toString(), obbDir: context.getObbDir().getAbsolutePath().toString(), packageCodePath: context.getPackageCodePath().toString(), }; }); }; ================================================ FILE: agent/src/generic/http.ts ================================================ import * as fs from "frida-fs"; import * as httpLib from "http"; import * as url from "url"; import { colors as c } from "../lib/color.js"; let httpServer: httpLib.Server; let listenPort: number; let servePath: string; const log = (m: string): void => { c.log(`[http server] ${m}`); }; const dirListingHTML = (pwd: string, path: string): string => { let h = `

Index Of ${path}

{file_listing} `; h = h.replace(`{file_listing}`, () => { return fs.list(pwd + decodeURIComponent(path)).map((f) => { if (f.name === '.') return; // Add a slash at the end if it is a directory. var fname = f.name + (f.type == 4 ? '/' : ''); if (path !== '/') { return `${fname}`; } else if (fname !== '../') { return `${fname}`; } }).join("
"); }); return h; }; export const start = (pwd: string, port: number = 9000): void => { log(c.redBright(`httpServer module not currently available.`)) if (httpServer) { log(c.redBright(`Server appears to already be running`)); return; } // if (!pwd.endsWith("/")) { // pwd = pwd + "/"; // } // log(`${c.blackBright(`Starting HTTP server in: ${pwd}`)}`); // servePath = pwd; // httpServer = httpLib.createServer((req, res) => { // if (req.method && req.url) { // log(`${c.greenBright(req.method)} ${req.url}`); // } else { // log(`${c.redBright('Missing URL or request method.')}`); // return; // } // try { // const parsedUrl = url.parse(req.url); // const fileLocation = pwd + decodeURIComponent(parsedUrl.path); // if (fs.statSync(fileLocation).isDirectory()) { // res.end(dirListingHTML(pwd, parsedUrl.path)); // return; // } // res.setHeader("Content-type", "application/octet-stream"); // // Check that we are not reading an empty file // if (fs.statSync(fileLocation).size !== 0) { // const file = fs.readFileSync(fileLocation); // res.write(file, 'utf-8') // } // res.end(); // } catch (error) { // if (error instanceof Error && error.message == "No such file or directory") { // res.statusCode = 404; // res.end("File not found") // } else { // if (error instanceof Error) { // log(c.redBright(`${error.stack}`)); // } else { // log(c.redBright(`${error}`)); // } // res.statusCode = 500; // res.end("Internal Server Error") // } // } // }); // httpServer.listen(port); // listenPort = port; }; export const stop = (): void => { if (!httpServer) { log(c.yellowBright(`Server does not appear to be running.`)); return; } log(c.blackBright(`Waiting for client connections to close then stopping...`)); httpServer.close() .once("close", () => { log(c.blackBright(`Server closed.`)); httpServer = undefined; }); }; export const status = (): void => { if (httpServer && httpServer.listening) { log(`Server is running on port ` + `${c.greenBright(listenPort.toString())} serving ${c.greenBright(servePath)}`); return; } log(c.yellowBright(`Server does not appear to be running.`)); }; ================================================ FILE: agent/src/generic/memory.ts ================================================ import { colors } from "../lib/color.js" export const listModules = (): Module[] => { return Process.enumerateModules(); }; export const listExports = (name: string): ModuleExportDetails[] => { const mod: Module[] = Process.enumerateModules().filter((m) => m.name === name); if (mod.length <= 0) { return []; } return mod[0].enumerateExports(); }; export const listRanges = (protection: string = "rw-"): RangeDetails[] => { return Process.enumerateRanges(protection); }; export const dump = (address: string, size: number): ArrayBuffer => { // Originally part of Frida <=11 but got removed in 12. // https://github.com/frida/frida-python/commit/72899a4315998289fb171149d62477ba7d1fcb91 const data = new NativePointer(address).readByteArray(size); if (data) { return data; } else { return new ArrayBuffer(0); } }; export const search = (pattern: string, onlyOffsets: boolean = false): string[] => { const addresses = listRanges("rw-") .map((range) => { return Memory.scanSync(range.base, range.size, pattern) .map((match) => { if (!onlyOffsets) { colors.log(hexdump(match.address, { ansi: true, header: false, length: 48, })); } return match.address.toString(); }); }).filter((m) => m.length !== 0); if (addresses.length <= 0) { return []; } return addresses.reduce((a, b) => a.concat(b)); }; export const replace = (pattern: string, replace: number[]): string[] => { return search(pattern, true).map((match) => { write(match, replace); return match; }) }; export const write = (address: string, value: number[]): void => { new NativePointer(address).writeByteArray(value); }; ================================================ FILE: agent/src/generic/ping.ts ================================================ export const ping = (): boolean => true; ================================================ FILE: agent/src/index.ts ================================================ import { ping } from "./generic/ping.js"; import { android } from "./rpc/android.js"; import { env } from "./rpc/environment.js"; import { ios } from "./rpc/ios.js"; import { jobs } from "./rpc/jobs.js"; import { memory } from "./rpc/memory.js"; import { other } from "./rpc/other.js"; rpc.exports = { ...android, ...ios, ...env, ...jobs, ...memory, ...other, ping: (): boolean => ping(), }; ================================================ FILE: agent/src/ios/binary.ts ================================================ import macho from "macho-ts"; import * as iosfilesystem from "./filesystem.js"; import { IBinaryModuleDictionary } from "./lib/interfaces.js"; const isEncrypted = (cmds: any[]): boolean => { for (const cmd of cmds) { // https://opensource.apple.com/source/cctools/cctools-921/include/mach-o/loader.h.auto.html // struct encryption_info_command { // [ ... ] // uint32_t cryptid; /* which enryption system, 0 means not-encrypted yet */ // }; if (cmd.type === "encryption_info" || cmd.type === "encryption_info_64") { if (cmd.id !== 0) { return true; } } } return false; }; export const info = (): IBinaryModuleDictionary => { const modules = Process.enumerateModules(); const parsedModules: IBinaryModuleDictionary = {}; modules.forEach((a) => { if (!a.path.includes(".app")) { return; } const imports: Set = new Set(a.enumerateImports().map((i) => i.name)); const fb = iosfilesystem.readFile(a.path); if (typeof(fb) == 'string') { return; } try { const exe = macho.parse(fb); parsedModules[a.name] = { arc: imports.has("objc_release"), canary: imports.has("__stack_chk_fail"), encrypted: isEncrypted(exe.cmds), pie: exe.flags.pie ? true : false, rootSafe: exe.flags.root_safe ? true : false, stackExec: exe.flags.allow_stack_execution ? true : false, type: exe.filetype, }; } catch (e) { // ignore any errors. especially ones where // the target path is not a mach-o } }); return parsedModules; }; ================================================ FILE: agent/src/ios/binarycookies.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import { IIosCookie } from "./lib/interfaces.js"; import { NSArray, NSData, NSHTTPCookieStorage } from "./lib/types.js"; export const get = (): IIosCookie[] => { // -- Sample Objective-C // // NSHTTPCookieStorage *cs = [NSHTTPCookieStorage sharedHTTPCookieStorage]; // NSArray *cookies = [cs cookies]; const cookies: IIosCookie[] = []; const HTTPCookieStorage = ObjC.classes.NSHTTPCookieStorage; const cookieStore: NSHTTPCookieStorage = HTTPCookieStorage.sharedHTTPCookieStorage(); const cookieJar: NSArray = cookieStore.cookies(); if (cookieJar.count() <= 0) { return cookies; } for (let i = 0; i < cookieJar.count(); i++) { // get the actual cookie from the jar const cookie: NSData = cookieJar.objectAtIndex_(i); // const cookieData: IIosCookie = { domain: cookie.domain().toString(), expiresDate: cookie.expiresDate() ? cookie.expiresDate().toString() : "null", isHTTPOnly: cookie.isHTTPOnly().toString(), isSecure: cookie.isSecure().toString(), name: cookie.name().toString(), path: cookie.path().toString(), value: cookie.value().toString(), version: cookie.version().toString(), }; cookies.push(cookieData); } return cookies; }; ================================================ FILE: agent/src/ios/bundles.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import { BundleType } from "./lib/constants.js"; import { IFramework } from "./lib/interfaces.js"; import { NSArray, NSBundle, NSDictionary } from "./lib/types.js"; // https://developer.apple.com/documentation/foundation/nsbundle/1408056-allframeworks?language=objc // https://developer.apple.com/documentation/foundation/nsbundle/1413705-allbundles?language=objc export const getBundles = (type: BundleType): IFramework[] => { // -- Sample ObjC // // for (id ob in [NSBundle allBundles]) { // NSDictionary *i = [ob infoDictionary]; // NSString *p = [ob bundlePath]; // NSLog(@"%@:%@ @ %@", [i objectForKey:@"CFBundleIdentifier"], // [i objectForKey:@"CFBundleShortVersionString"], p); // } // Figure out which bundle type to enumerate let frameworks: NSArray; if (type === BundleType.NSBundleFramework) { frameworks = ObjC.classes.NSBundle.allFrameworks(); } else if (type === BundleType.NSBundleAllBundles) { frameworks = ObjC.classes.NSBundle.allBundles(); } const appBundles: IFramework[] = []; const frameworksLength: number = frameworks.count().valueOf(); for (let i = 0; i !== frameworksLength; i++) { // get information about the bundle itself const bundle: NSBundle = frameworks.objectAtIndex_(i); const bundleInfo: NSDictionary = bundle.infoDictionary(); // get values for the keys we are interested in const bundlePath: string = bundle.bundlePath(); const CFBundleIdentifier: string = bundleInfo.objectForKey_("CFBundleIdentifier"); const CFBundleShortVersionString: string = bundleInfo.objectForKey_("CFBundleShortVersionString"); const CFBundleExecutable: string = bundleInfo.objectForKey_("CFBundleExecutable"); appBundles.push({ bundle: CFBundleIdentifier ? CFBundleIdentifier.toString() : null, executable: CFBundleExecutable ? CFBundleExecutable.toString() : null, path: bundlePath.toString(), version: CFBundleShortVersionString ? CFBundleShortVersionString.toString() : null, }); } return appBundles; }; ================================================ FILE: agent/src/ios/credentialstorage.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import { ICredential } from "./lib/interfaces.js"; import { NSArray, NSData, NSURLCredentialStorage } from "./lib/types.js"; export const dump = (): ICredential[] => { // -- Sample ObjC to create and dump a credential // NSURLProtectionSpace *ps = [[NSURLProtectionSpace alloc] // initWithHost:@"foo.com" port:80 protocol:@"https" realm:NULL // authenticationMethod:NSURLAuthenticationMethodHTTPBasic]; // NSURLCredential *creds = [[NSURLCredential alloc] // initWithUser:@"user" password:@"password" persistence:NSURLCredentialPersistencePermanent]; // NSURLCredentialStorage *cs = [NSURLCredentialStorage sharedCredentialStorage]; // [cs setCredential:creds forProtectionSpace:ps]; // NSDictionary *allcreds = [cs allCredentials]; // NSLog(@"%@", allcreds); const credentialStorage: NSURLCredentialStorage = ObjC.classes.NSURLCredentialStorage; const data: ICredential[] = []; const credentialsDict: NSArray = credentialStorage.sharedCredentialStorage().allCredentials(); if (credentialsDict.count() <= 0) { return data; } const protectionSpaceEnumerator = credentialsDict.keyEnumerator(); let urlProtectionSpace; // tslint:disable-next-line:no-conditional-assignment while ((urlProtectionSpace = protectionSpaceEnumerator.nextObject()) !== null) { const userNameEnumerator = credentialsDict.objectForKey_(urlProtectionSpace).keyEnumerator(); let userName; // tslint:disable-next-line:no-conditional-assignment while ((userName = userNameEnumerator.nextObject()) !== null) { const creds: NSData = credentialsDict.objectForKey_(urlProtectionSpace).objectForKey_(userName); // Add the creds for this protection space. const credentialData: ICredential = { authMethod: urlProtectionSpace.authenticationMethod().toString(), host: urlProtectionSpace.host().toString(), password: creds.password().toString(), port: urlProtectionSpace.port(), protocol: urlProtectionSpace.protocol().toString(), user: creds.user().toString(), }; data.push(credentialData); } } return data; }; ================================================ FILE: agent/src/ios/crypto.ts ================================================ import { colors as c } from "../lib/color.js"; import { fsend } from "../lib/helpers.js"; import * as jobs from "../lib/jobs.js"; import { arrayBufferToHex, hexToString } from "./lib/helpers.js"; type CCAlgorithm = { [key: number]: { name: string; blocksize: number }; }; type AlgorithmType = { [key: number]: string; }; // Encryption algorithms implemented by this module. const CCAlgorithm: CCAlgorithm = { 0: { name: "kCCAlgorithmAES128", blocksize: 16 }, 1: { name: "kCCAlgorithmDES", blocksize: 8 }, 2: { name: "kCCAlgorithm3DES", blocksize: 8 }, 3: { name: "kCCAlgorithmCAST", blocksize: 8 }, 4: { name: "kCCAlgorithmRC4", blocksize: 8 }, 5: { name: "kCCAlgorithmRC2", blocksize: 8 } }; // Encryption algorithms implemented by this module. const CCOperation: AlgorithmType = { 0: "kCCEncrypt", 1: "kCCDecrypt" }; // Options flags, passed to CCCryptorCreate(). const CCOption: AlgorithmType = { 1: "kCCOptionPKCS7Padding", 2: "kCCOptionECBMode" }; // alg for pbkdf. Right now only pbkdf2 is supported by CommonCrypto const CCPBKDFAlgorithm: AlgorithmType = { 2: "kCCPBKDF2" }; // alg for prt for pbkdf const CCPseudoRandomAlgorithm: AlgorithmType = { 1: "kCCPRFHmacAlgSHA1", 2: "kCCPRFHmacAlgSHA224", 3: "kCCPRFHmacAlgSHA256", 4: "kCCPRFHmacAlgSHA384", 5: "kCCPRFHmacAlgSHA512" }; // ident for crypto hooks job let cryptoidentifier: number = 0; // operation being performed 0=encrypt 1=decrypt let op = 0; // needed to keep track of CCAlgorithm so we can know // blocksize from CCCryptorCreate to CCCryptorUpdate let alg = 0; // keep track of all the output bytes. // this is necessary because CCCryptorUpdate needs to be // append the final block from CCCryptorFinal let dataOutBytes: string = ""; // Compatibility with frida < 16.7 if (!Module.getGlobalExportByName) { Module.getGlobalExportByName = function(name) { return Module['getExportByName'](null, name); } } const secrandomcopybytes = (ident: number): InvocationListener => { const hook = "SecRandomCopyBytes"; return Interceptor.attach( Module.getGlobalExportByName(hook), { onEnter(args) { this.secrandomcopybytes = {}; this.secrandomcopybytes.rnd = args[0].toInt32(); this.secrandomcopybytes.count = args[1].toInt32(); this.bytes = args[2]; }, onLeave(retval) { this.secrandomcopybytes.bytes = arrayBufferToHex(this.bytes.readByteArray(this.secrandomcopybytes.count)); fsend(ident, hook, this.secrandomcopybytes); } }); }; const cckeyderivationpbkdf = (ident: number): InvocationListener => { const hook = "CCKeyDerivationPBKDF"; return Interceptor.attach( Module.getGlobalExportByName(hook), { onEnter(args) { this.cckeyderivationpbkdf = {}; // args[0] "kCCPBKDF2" is the only alg supported by CommonCrypto this.cckeyderivationpbkdf.algorithm = CCPBKDFAlgorithm[args[0].toInt32()]; // args[1] The text password used as input to the derivation // function. The actual octets present in this string // will be used with no additional processing. It's // extremely important that the same encoding and // normalization be used each time this routine is // called if the same key is expected to be derived. // args[2] The length of the text password in bytes. const passwordPtr = args[1]; const passwordLen = args[2].toInt32(); const passwordBytes = arrayBufferToHex(passwordPtr.readByteArray(passwordLen)); try { this.cckeyderivationpbkdf.password = hexToString(passwordBytes); } catch { this.cckeyderivationpbkdf.password = passwordBytes; } // args[3] The salt byte values used as input to the derivation function. // args[4] The length of the salt in bytes. const saltPtr = args[3]; const saltLen = args[4].toInt32(); this.cckeyderivationpbkdf.saltBytes = arrayBufferToHex(saltPtr.readByteArray(saltLen)); // args[5] The Pseudo Random Algorithm to use for the derivation iterations. this.cckeyderivationpbkdf.prf = CCPseudoRandomAlgorithm[args[5].toInt32()]; // args[6] The number of rounds of the Pseudo Random Algorithm to use. this.cckeyderivationpbkdf.rounds = args[6].toInt32(); // args[7] The resulting derived key produced by the function. // The space for this must be provided by the caller. this.derivedKeyPtr = args[7]; // args[8] The expected length of the derived key in bytes. this.derivedKeyLen = args[8].toInt32(); }, onLeave(retval) { this.cckeyderivationpbkdf.derivedKey = arrayBufferToHex(this.derivedKeyPtr.readByteArray(this.derivedKeyLen)); fsend(ident, hook, this.cckeyderivationpbkdf); } }); }; const cccrypt = (ident: number): InvocationListener => { const hook = "CCCrypt"; return Interceptor.attach( Module.getGlobalExportByName(hook), { onEnter(args) { this.cccrpyt = {}; // args[0] Defines the basic operation: kCCEncrypt or kCCDecrypt. this.op = args[0].toInt32(); this.cccrpyt.op = CCOperation[this.op]; // args[1] Defines the encryption algorithm. this.alg = args[1].toInt32(); this.cccrpyt.alg = CCAlgorithm[alg].name; // args[2] A word of flags defining options. See discussion for the CCOptions type. this.cccrpyt.options = CCOption[args[2].toInt32()]; // args[3] Raw key material, length keyLength bytes. // args[4] Length of key material. Must be appropriate // for the select algorithm. Some algorithms may // provide for varying key lengths. const key = args[3]; this.cccrpyt.keyLength = args[4].toInt32(); this.cccrpyt.key = arrayBufferToHex(key.readByteArray(this.cccrpyt.keyLength)); // args[5] Initialization vector, optional. Used for // Cipher Block Chaining (CBC) mode. If present, // must be the same length as the selected // algorithm's block size. If CBC mode is // selected (by the absence of any mode bits in // the options flags) and no IV is present, a // NULL (all zeroes) IV will be used. This is // ignored if ECB mode is used or if a stream // cipher algorithm is selected. const iv = args[5]; this.cccrpyt.iv = arrayBufferToHex(iv.readByteArray(CCAlgorithm[alg].blocksize)); // args[6] Data to encrypt or decrypt, length dataInLength bytes. // args[7] Length of data to encrypt or decrypt. const dataInPtr = args[6]; const dataInLength = args[7].toInt32(); const dataInHex = arrayBufferToHex(dataInPtr.readByteArray(dataInLength)); this.cccrpyt.dataIn = this.op ? dataInHex : hexToString(dataInHex); // args[8] Result is written here. Allocated by caller. // Encryption and decryption can be performed // "in-place", with the same buffer used for // input and output. this.dataOut = args[8]; // args[9] The size of the dataOut buffer in bytes. this.dataOutAvailable = args[9].toInt32(); // args[10] On successful return, the number of bytes written // to dataOut. If kCCBufferTooSmall is returned as // a result of insufficient buffer space being // provided, the required buffer space is returned // here. this.dataOutMoved = args[10]; }, onLeave(retval) { const dataOutHex = arrayBufferToHex(this.dataOut.readByteArray(this.dataOutAvailable)); this.cccrpyt.dataOut = this.op ? hexToString(dataOutHex) : dataOutHex; fsend(ident, hook, this.cccrpyt); } }); }; const cccryptorcreate = (ident: number): InvocationListener => { const hook = "CCCryptorCreate"; return Interceptor.attach( Module.getGlobalExportByName(hook), { onEnter(args) { this.cccryptorcreate = {}; // args[0] Defines the basic operation: kCCEncrypt or kCCDecrypt. op = args[0].toInt32(); this.cccryptorcreate.op = CCOperation[op]; // args[1] Defines the encryption algorithm. alg = args[1].toInt32(); this.cccryptorcreate.alg = CCAlgorithm[alg].name; // args[2] A word of flags defining options. See discussion for the CCOptions type. const option = args[2].toInt32(); this.cccryptorcreate.options = CCOption[option]; // args[3] Raw key material, length keyLength bytes. // args[4] Length of key material. Must be appropriate // for the select algorithm. Some algorithms may // provide for varying key lengths. const keyPtr = args[3]; this.cccryptorcreate.keyLength = args[4].toInt32(); this.cccryptorcreate.key = arrayBufferToHex(keyPtr.readByteArray(this.cccryptorcreate.keyLength)); // args[5] Initialization vector, optional. Used for // Cipher Block Chaining (CBC) mode. If present, // must be the same length as the selected // algorithm's block size. If CBC mode is // selected (by the absence of any mode bits in // the options flags) and no IV is present, a // NULL (all zeroes) IV will be used. This is // ignored if ECB mode is used or if a stream // cipher algorithm is selected. const ivPtr = args[5]; this.cccryptorcreate.iv = arrayBufferToHex(ivPtr.readByteArray(CCAlgorithm[alg].blocksize)); }, onLeave(retval) { fsend(ident, hook, this.cccryptorcreate); } }); }; const cccryptorupdate = (ident: number): InvocationListener => { const hook = "CCCryptorUpdate"; return Interceptor.attach( Module.getGlobalExportByName(hook), { onEnter(args) { this.cccryptorupdate = {}; // reset for the next operation. dataOutBytes = ""; // args[1] Data to process, length dataInLength bytes. const dataInPtr = args[1]; // args[2] Length of data to process. this.dataInLength = args[2].toInt32(); // args[3] Result is written here. Allocated by caller. // Encryption and decryption can be performed // "in-place", with the same buffer used for // input and output. this.dataOutPtr = args[3]; // args[4] The size of the dataOut buffer in bytes. this.dataOutAvailable = args[4].toInt32(); const dataIn = arrayBufferToHex(dataInPtr.readByteArray(this.dataInLength)); this.cccryptorupdate.dataIn = op ? dataIn : hexToString(dataIn); }, onLeave(retval) { const blocksize = CCAlgorithm[alg].blocksize; // if the message is longer than 1 block then we need to // remember everything before the final block if (this.dataInLength > blocksize) { // TODO: There is sometimes padding added to the end of this message // someone please fix this in a pull request. it is super hacky. dataOutBytes = arrayBufferToHex(this.dataOutPtr.readByteArray(this.dataOutAvailable)).split("000000")[0]; this.cccryptorupdate.dataOut = dataOutBytes; } fsend(ident, hook, this.cccryptorupdate); } }); }; const cccryptorfinal = (ident: number): InvocationListener => { const hook = "CCCryptorFinal"; return Interceptor.attach( Module.getGlobalExportByName(hook), { onEnter(args) { this.cccryptorfinal = {}; // args[1] Result is written here. Allocated by caller. // Encryption and decryption can be performed // "in-place", with the same buffer used for // input and output. this.dataOutPtr = args[1]; // args[2] The size of the dataOut buffer in bytes. this.dataOutAvailable = args[2].toInt32(); }, onLeave(retval) { // var dataOutHex = arrayBufferToHex(this.dataOutPtr.readByteArray(this.dataOutAvailable)) // this.cccryptorfinal.dataOut = op ? hexToString(dataOutHex) : dataOutHex // append the final block the any previous blocks that might exist dataOutBytes += arrayBufferToHex(this.dataOutPtr.readByteArray(this.dataOutAvailable)); this.cccryptorfinal.dataOut = this.op ? hexToString(dataOutBytes) : dataOutBytes; // this.cccryptorfinal.dataOut = dataOutBytes fsend(ident, hook, this.cccryptorfinal); } }); }; export const monitor = (): void => { // if we already have a job registered then return if (jobs.hasIdent(cryptoidentifier)) { send(`${c.greenBright("Job already registered")}: ${c.blueBright(cryptoidentifier.toString())}`); return; } const job: jobs.Job = new jobs.Job(jobs.identifier(), "ios-crypto-monitor"); cryptoidentifier = job.identifier; job.addInvocation(secrandomcopybytes(job.identifier)); job.addInvocation(cckeyderivationpbkdf(job.identifier)); job.addInvocation(cccrypt(job.identifier)); job.addInvocation(cccryptorcreate(job.identifier)); job.addInvocation(cccryptorupdate(job.identifier)); job.addInvocation(cccryptorfinal(job.identifier)); jobs.add(job); }; ================================================ FILE: agent/src/ios/filesystem.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import * as fs from "frida-fs"; import { Buffer } from "buffer"; import { hexStringToBytes } from "../lib/helpers.js"; import { getNSFileManager } from "./lib/helpers.js"; import { IIosFilePath, IIosFileSystem } from "./lib/interfaces.js"; import { NSDictionary, NSFileManager, NSString as NSStringType } from "./lib/types.js"; // a resolved nsfilemanager instance let fileManager: NSFileManager; const getFileManager = (): NSFileManager => { if (fileManager === undefined) { fileManager = getNSFileManager(); return fileManager; } return fileManager; }; export const exists = (path: string): boolean => { // -- Sample Objective-C // // NSFileManager *fm = [NSFileManager defaultManager]; // if ([fm fileExistsAtPath:@"/"]) { // NSLog(@"Yep!"); // } const fm: NSFileManager = getFileManager(); const p: NSStringType = ObjC.classes.NSString.stringWithString_(path); return fm.fileExistsAtPath_(p); }; export const readable = (path: string): boolean => { // -- Sample Objective-C // // NSFileManager *fm = [NSFileManager defaultManager]; // NSLog(@"%d / readable?", [fm isReadableFileAtPath:@"/"]); const fm: NSFileManager = getFileManager(); const p: NSStringType = ObjC.classes.NSString.stringWithString_(path); return fm.isReadableFileAtPath_(p); }; export const writable = (path: string): boolean => { // -- Sample Objective-C // // NSFileManager *fm = [NSFileManager defaultManager]; // NSLog(@"%d / readable?", [fm isReadableFileAtPath:@"/"]); const fm: NSFileManager = getFileManager(); const p: NSStringType = ObjC.classes.NSString.stringWithString_(path); return fm.isWritableFileAtPath_(p); }; export const pathIsFile = (path: string): boolean => { const fm: NSFileManager = getFileManager(); const isDir: NativePointer = Memory.alloc(Process.pointerSize); fm.fileExistsAtPath_isDirectory_(path, isDir); // deref the isDir pointer to get the bool // *isDir === 1 means the path is a directory return isDir.readInt() === 0; }; // returns a 'pwd' that assumes the current bundle's path // is the directory we are interested in. the handling of // pwd is actually handled in the python world and this // method is only really called as a starting point. export const pwd = (): string => { // -- Sample Objective-C // // NSURL *bundleURL = [[NSBundle mainBundle] bundleURL]; const NSBundle = ObjC.classes.NSBundle; return NSBundle.mainBundle().bundlePath().toString(); }; // heavy lifting is done in frida-fs here. export const readFile = (path: string): string | Buffer => { if (fs.statSync(path).size == 0) return Buffer.alloc(0); return fs.readFileSync(path); }; // heavy lifting is done in frida-fs here. export const writeFile = (path: string, data: string): void => { const writeStream: any = fs.createWriteStream(path); writeStream.on("error", (error: Error) => { throw error; }); writeStream.write(hexStringToBytes(data)); writeStream.end(); }; export const deleteFile = (path: string): boolean => { const fm: NSFileManager = getFileManager(); const err: NativePointer = Memory.alloc(Process.pointerSize); fm.removeItemAtPath_error_(path, err); // deref the isDir pointer to get the bool // *isDir === 1 means the path is a directory return err.readInt() === 0; }; export const ls = (path: string): IIosFileSystem => { // -- Sample Objective-C // // NSFileManager *fm = [NSFileManager defaultManager]; // NSString *bundleURL = [[NSBundle mainBundle] bundlePath]; // NSArray *contents = [fm contentsOfDirectoryAtPath:bundleURL error:nil]; // for (id item in contents) { // NSString *p = [[NSString alloc] initWithFormat:@"%@/%@",bundleURL, item]; // NSDictionary *attribs = [fm attributesOfItemAtPath:p error:nil]; // NSLog(@"%@ - %@", p, attribs); // } const fm: NSFileManager = getFileManager(); const p: NSStringType = ObjC.classes.NSString.stringWithString_(path); const response: IIosFileSystem = { files: {}, path: `${path}`, readable: fm.isReadableFileAtPath_(p), writable: fm.isWritableFileAtPath_(p), }; // not being able to read the path should leave us bailing early if (!response.readable) { return response; } const pathContents: NSDictionary = fm.contentsOfDirectoryAtPath_error_(path, NULL); const fileCount: number = pathContents.count(); // loop-de-loop files for (let i = 0; i < fileCount; i++) { // pick a file off contents const file: string = pathContents.objectAtIndex_(i); const pathFileData: IIosFilePath = { attributes: {}, fileName: file.toString(), readable: undefined, writable: undefined, }; // generate a full path to the file const filePath: string = [path, "/", file].join(""); const currentFilePath: NSStringType = ObjC.classes.NSString.stringWithString_(filePath); // check read / write pathFileData.readable = fm.isReadableFileAtPath_(currentFilePath); pathFileData.writable = fm.isWritableFileAtPath_(currentFilePath); // get attributes const attributes = fm.attributesOfItemAtPath_error_(currentFilePath, NULL); // if we were able to get attributes for the item, // append them to those for this file. (example is listing // files in / have some that cant have attributes read for :|) if (attributes) { // loop the attributes and set them in the file_data // dictionary const enumerator = attributes.keyEnumerator(); let key; // tslint:disable-next-line:no-conditional-assignment while ((key = enumerator.nextObject()) !== null) { // get attribute data const value: any = attributes.objectForKey_(key); // add it to the attributes for this item pathFileData.attributes[key] = value.toString(); } } // finally, add the file to the final response response.files[file] = pathFileData; } return response; }; ================================================ FILE: agent/src/ios/heap.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import type { default as ObjCTypes } from "frida-objc-bridge"; import { colors as c } from "../lib/color.js"; import { bytesToUTF8 } from "./lib/helpers.js"; import { IHeapObject } from "./lib/interfaces.js"; const enumerateInstances = (clazz: string): ObjCTypes.Object[] => { if (!ObjC.classes.hasOwnProperty(clazz)) { c.log(`Unknown Objective-C class: ${c.redBright(clazz)}`); return []; } const specifier: ObjCTypes.DetailedChooseSpecifier = { class: ObjC.classes[clazz], subclasses: true, // don't skip subclasses }; return ObjC.chooseSync(specifier); }; export const getInstances = (clazz: string): IHeapObject[] => { c.log(`${c.blackBright(`Enumerating live instances of`)} ${c.greenBright(clazz)}...`); const instances: IHeapObject[] = []; enumerateInstances(clazz).map((instance) => { try { instances.push({ className: instance.$className, handle: instance.handle.toString(), ivars: instance.$ivars, kind: instance.$kind, methods: instance.$ownMethods, superClass: instance.$superClass.$className, }); } catch (err) { c.log(`Warning: ${c.yellowBright((err as Error).message)}`); } }); return instances; }; const resolvePointer = (pointer: string): ObjCTypes.Object => { const o = new ObjC.Object(new NativePointer(pointer)); c.log(`${c.blackBright(`Pointer ` + pointer + ` is to class `)}${c.greenBright(o.$className)}`); return o; }; export const getIvars = (pointer: string, toUTF8: boolean): [string, any[string]] => { const { $className, $ivars } = resolvePointer(pointer); // if we need to get utf8 representations, start a new object with // which cloned properties will have utf8 values. we _could_ have // just gone and replaces values in $ivars, but there are some // access errors for that. if (toUTF8) { const $clonedIvars: {[name: string]: any} = {}; c.log(c.blackBright(`Converting ivar values to UTF8 strings...`)); for (const k in $ivars) { if ($ivars.hasOwnProperty(k)) { const v = $ivars[k]; $clonedIvars[k] = bytesToUTF8(v); } } return [$className, $clonedIvars]; } return [$className, $ivars]; }; export const getMethods = (pointer: string): [string, any[string]] => { const { $className, $ownMethods } = resolvePointer(pointer); return [$className, $ownMethods]; }; export const callInstanceMethod = (pointer: string, method: string, returnString: boolean): void => { const i = resolvePointer(pointer); c.log(`${c.blackBright(`Executing:`)} ${c.greenBright(`[${i.$className} ${method}]`)}`); const result = i[method](); if (returnString) { return result.toString(); } return i[method](); }; export const evaluate = (pointer: string, js: string): void => { const ptr = resolvePointer(pointer); // tslint:disable-next-line:no-eval eval(js); }; ================================================ FILE: agent/src/ios/hooking.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import { colors as c } from "../lib/color.js"; import * as jobs from "../lib/jobs.js"; export const getClasses = () => { return ObjC.classes; }; export const getClassMethods = (className: string, includeParents: boolean): string[] => { if (ObjC.classes[className] === undefined) { return []; } // Show all methods of the class if (includeParents) { return ObjC.classes[className].$methods; } return ObjC.classes[className].$ownMethods; }; const objcEnumerate = (pattern: string): ApiResolverMatch[] => { return new ApiResolver('objc').enumerateMatches(pattern); }; export const search = (patternOrClass: string): ApiResolverMatch[] => { // if we didnt get a pattern, make one assuming its meant to be a class if (!patternOrClass.includes('[')) patternOrClass = `*[*${patternOrClass}* *]`; return objcEnumerate(patternOrClass); }; export const watch = (patternOrClass: string, dargs: boolean = false, dbt: boolean = false, dret: boolean = false, watchParents: boolean = false): void => { // Add the job // We init a new job here as the child watch* calls will be grouped in a single job. // mostly commandline fluff const job: jobs.Job = new jobs.Job(jobs.identifier(), `ios-watch for: ${patternOrClass}`); jobs.add(job); const isPattern = patternOrClass.includes('['); // if we have a patterm we'll loop the methods, hook and push a listener to the job if (isPattern === true) { const matches = objcEnumerate(patternOrClass); matches.forEach((match: ApiResolverMatch) => { watchMethod(match.name, job, dargs, dbt, dret); }); return; } watchClass(patternOrClass, job, dargs, dbt, dret, watchParents); }; const watchClass = (clazz: string, job: jobs.Job, dargs: boolean = false, dbt: boolean = false, dret: boolean = false, parents: boolean = false): void => { const target = ObjC.classes[clazz]; if (!target) { send(`${c.red(`Error!`)} Unable to find class ${c.redBright(clazz)}!`); return; } // with parents as true, include methods from a parent class, // otherwise simply hook the target class' own methods (parents ? target.$methods : target.$ownMethods).forEach((method) => { // filter and make sure we have a type and name. Looks like some methods can // have '' as name... am expecting something like "- isJailBroken" const fullMethodName = `${method[0]}[${clazz} ${method.substring(2)}]`; watchMethod(fullMethodName, job, dargs, dbt, dret); }); }; const watchMethod = (selector: string, job: jobs.Job, dargs: boolean, dbt: boolean, dret: boolean): void => { const resolver = new ApiResolver("objc"); let matchedMethod: ApiResolverMatch = { address: NULL, name: '', }; // handle the resolvers error it may throw if the selector format is off. try { // select the first match const resolved = resolver.enumerateMatches(selector); if (resolved.length <= 0) { send(`${c.red(`Error:`)} No matches for selector ${c.redBright(`${selector}`)}. ` + `Double check the name, or try "ios hooking list class_methods" first.`); return; } // not sure if this will ever be the case... but lets log it // anyways if (resolved.length > 1) { send(`${c.yellow(`Warning:`)} More than one result for selector ${c.redBright(`${selector}`)}!`); } matchedMethod = resolved[0]; } catch (error) { send( `${c.red(`Error:`)} Unable to find address for selector ${c.redBright(`${selector}`)}! ` + `The error was:\n` + c.red((error as Error).message), ); return; } // Attach to the discovered match // TODO: loop correctly when globbing send(`Found selector at ${c.green(matchedMethod.address.toString())} as ${c.green(matchedMethod.name)}`); const watchInvocation: InvocationListener = Interceptor.attach(matchedMethod.address, { // tslint:disable-next-line:object-literal-shorthand onEnter: function (args) { // how many arguments do we have in this selector? const argumentCount: number = (selector.match(/:/g) || []).length; const receiver = new ObjC.Object(args[0]); send( c.blackBright(`[${job.identifier}] `) + `Called: ${c.green(`${selector}`)} ${c.blue(`${argumentCount}`)} arguments` + `(Kind: ${c.cyan(receiver.$kind)}) (Super: ${c.cyan(receiver.$superClass.$className)})`, ); // if we should include a backtrace to here, do that. if (dbt) { send( c.blackBright(`[${job.identifier}] `) + `${c.green(`${selector}`)} Backtrace:\n\t` + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n\t"), ); } if (dargs && argumentCount > 0) { const methodSplit = ObjC.selectorAsString(args[1]).split(":").filter((val) => val); const r = methodSplit.map((argName, position) => { // As this is an ObjectiveC method, the arguments are as follows: // 0. 'self' // 1. The selector (object.name:) // 2. The first arg // // For this reason do we shift it by 2 positions to get an 'instance' for // the argument value. const t = new ObjC.Object(args[position + 2]); return `${argName}: ${c.greenBright(`${t}`)}`; }); send(c.blackBright(`[${job.identifier}] `) + `Argument dump: [${c.green(receiver.$className)} ${r.join(" ")}]`); } }, onLeave: (retval) => { // do nothing if we are not expected to dump return values if (!dret) { return; } send(c.blackBright(`[${job.identifier}] `) + `Return Value: ${c.red(retval.toString())}`); }, }); job.addInvocation(watchInvocation); }; export const setMethodReturn = (selector: string, returnValue: boolean): void => { const TRUE = new NativePointer(0x1); const FALSE = new NativePointer(0x0); const resolver = new ApiResolver("objc"); let matchedMethod: ApiResolverMatch = { address: NULL, name: '', }; // handle the resolvers error it may throw if the selector format // is off. try { // select the first match matchedMethod = resolver.enumerateMatches(selector)[0]; } catch (error) { send( `${c.red(`Error!`)} Unable to find address for selector ${c.redBright(`${selector}`)}! ` + `The error was:\n` + c.red((error as Error).message), ); return; } // no match? then just leave. if (!matchedMethod.address) { send(`${c.red(`Error!`)} Unable to find address for selector ${c.redBright(`${selector}`)}!`); return; } // Start a new Job const job: jobs.Job = new jobs.Job(jobs.identifier(), `set-method-return for: ${selector}`); // Attach to the discovered match // TODO: loop correctly when globbing send(`Found selector at ${c.green(matchedMethod.address.toString())} as ${c.green(matchedMethod.name)}`); const watchInvocation: InvocationListener = Interceptor.attach(matchedMethod.address, { onLeave: (retval) => { switch (returnValue) { case true: if (retval.equals(TRUE)) { return; } send( c.blackBright(`[${job.identifier}] `) + `${c.green(selector)} ` + `Return value was: ${c.red(retval.toString())}, overriding to ${c.green(TRUE.toString())}`, ); retval.replace(TRUE); break; case false: if (retval.equals(FALSE)) { return; } send( c.blackBright(`[${job.identifier}] `) + `${c.green(selector)} ` + `Return value was: ${c.red(retval.toString())}, overriding to ${c.green(FALSE.toString())}`, ); retval.replace(FALSE); break; } }, }); // register the job job.addInvocation(watchInvocation); jobs.add(job); }; ================================================ FILE: agent/src/ios/jailbreak.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import { colors as c } from "../lib/color.js"; import * as jobs from "../lib/jobs.js"; // Attempts to disable Jailbreak detection. // This seems like an odd thing to do on a device that is probably not // jailbroken. However, in the case of a device losing a jailbreak due to // an OS upgrade, some filesystem artifacts may still exist, causing some // of the typical checks to incorrectly detect the jailbreak status! // Hook NSFileManager and fopen calls and check if it is to a common path. // Hook canOpenURL for Cydia deep link. const jailbreakPaths = [ "/Applications/Cydia.app", "/Applications/FakeCarrier.app", "/Applications/Icy.app", "/Applications/IntelliScreen.app", "/Applications/MxTube.app", "/Applications/RockApp.app", "/Applications/SBSetttings.app", "/Applications/WinterBoard.app", "/Applications/blackra1n.app", "/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist", "/Library/MobileSubstrate/DynamicLibraries/Veency.plist", "/Library/MobileSubstrate/MobileSubstrate.dylib", "/System/Library/LaunchDaemons/com.ikey.bbot.plist", "/System/Library/LaunchDaemons/com.saurik.Cy@dia.Startup.plist", "/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist", "/bin/bash", "/bin/sh", "/etc/apt", "/etc/ssh/sshd_config", "/private/var/stash", "/private/var/tmp/cydia.log", "/private/var/lib/apt", "/usr/bin/cycript", "/usr/bin/ssh", "/usr/bin/sshd", "/usr/libexec/sftp-server", "/usr/libexec/sftp-server", "/usr/libexec/ssh-keysign", "/usr/sbin/sshd", "/var/cache/apt", "/var/lib/cydia", "/var/log/syslog", "/var/tmp/cydia.log", ]; // toggles replies to fileExistsAtPath: for the paths in jailbreakPaths const fileExistsAtPath = (success: boolean, ident: number): InvocationListener => { return Interceptor.attach( ObjC.classes.NSFileManager["- fileExistsAtPath:"].implementation, { onEnter(args) { // Use a marker to check onExit if we need to manipulate // the response. this.is_common_path = false; // Extract the path this.path = new ObjC.Object(args[2]).toString(); // check if the looked up path is in the list of common_paths if (jailbreakPaths.indexOf(this.path) >= 0) { // Mark this path as one that should have its response // modified if needed. this.is_common_path = true; } }, onLeave(retval) { // stop if we dont care about the path if (!this.is_common_path) { return; } // depending on the desired state, we flip retval switch (success) { case (true): // ignore successful lookups if (!retval.isNull()) { return; } send( c.blackBright(`[${ident}] `) + `fileExistsAtPath: check for ` + c.green(this.path) + ` failed with: ` + c.red(retval.toString()) + `, marking it as successful.`, ); retval.replace(new NativePointer(0x01)); break; case (false): // ignore failed lookups if (retval.isNull()) { return; } send( c.blackBright(`[${ident}] `) + `fileExistsAtPath: check for ` + c.green(this.path) + ` was successful with: ` + c.red(retval.toString()) + `, marking it as failed.`, ); retval.replace(new NativePointer(0x00)); break; } }, }, ); }; // toggles replies to fopen: for the paths in jailbreakPaths const fopen = (success: boolean, ident: number): InvocationListener | null => { // Compatibility with frida < 16.7 if (!Module.findGlobalExportByName) { Module.findGlobalExportByName = function(name) { return Module['findExportByName'](null, name); } } const fopen_addr = Module.findGlobalExportByName("fopen"); if (!fopen_addr) { send(c.red(`fopen function not found!`)); return null; } return Interceptor.attach(fopen_addr, { onEnter(args) { this.is_common_path = false; // Extract the path this.path = args[0].readCString(); // check if the looked up path is in the list of common_paths if (jailbreakPaths.indexOf(this.path) >= 0) { // Mark this path as one that should have its response // modified if needed. this.is_common_path = true; } }, onLeave(retval) { // stop if we dont care about the path if (!this.is_common_path) { return; } // depending on the desired state, we flip retval switch (success) { case (true): // ignore successful lookups if (!retval.isNull()) { return; } send( c.blackBright(`[${ident}] `) + `fopen: check for ` + c.green(this.path) + ` failed with: ` + c.red(retval.toString()) + `, marking it as successful.`, ); retval.replace(new NativePointer(0x01)); break; case (false): // ignore failed lookups if (retval.isNull()) { return; } send( c.blackBright(`[${ident}] `) + `fopen: check for ` + c.green(this.path) + ` was successful with: ` + c.red(retval.toString()) + `, marking it as failed.`, ); retval.replace(new NativePointer(0x00)); break; } }, }, ); }; // toggles replies to canOpenURL for Cydia const canOpenURL = (success: boolean, ident: number): InvocationListener => { return Interceptor.attach( ObjC.classes.UIApplication["- canOpenURL:"].implementation, { onEnter(args) { this.is_flagged = false; // Extract the path this.path = new ObjC.Object(args[2]).toString(); if (this.path.startsWith('cydia') || this.path.startsWith('Cydia')) { this.is_flagged = true; } }, onLeave(retval) { if (!this.is_flagged) { return; } // depending on the desired state, we flip retval switch (success) { case (true): // ignore successful lookups if (!retval.isNull()) { return; } send( c.blackBright(`[${ident}] `) + `canOpenURL: check for ` + c.green(this.path) + ` failed with: ` + c.red(retval.toString()) + `, marking it as successful.`, ); retval.replace(new NativePointer(0x01)); break; case (false): // ignore failed if (retval.isNull()) { return; } send( c.blackBright(`[${ident}] `) + `canOpenURL: check for ` + c.green(this.path) + ` was successful with: ` + c.red(retval.toString()) + `, marking it as failed.`, ); retval.replace(new NativePointer(0x00)); break; } }, }, ); }; const libSystemBFork = (success: boolean, ident: number): InvocationListener | null => { // Hook fork() in libSystem.B.dylib and return 0 // TODO: Hook vfork const libSystemBdylib = Process.findModuleByName("libSystem.B.dylib"); if (!libSystemBdylib) return null; const libSystemBdylibFork = libSystemBdylib.findExportByName("fork"); if (!libSystemBdylibFork) return null; return Interceptor.attach(libSystemBdylibFork, { onLeave(retval) { switch (success) { case (true): // already successful forks are ok if (!retval.isNull()) { return; } send( c.blackBright(`[${ident}] `) + `Call to ` + c.green(`libSystem.B.dylib::fork()`) + ` failed with ` + c.red(retval.toString()) + ` marking it as successful.`, ); retval.replace(new NativePointer(0x1)); break; case (false): // already failed forks are ok if (retval.isNull()) { return; } send( c.blackBright(`[${ident}] `) + `Call to ` + c.green(`libSystem.B.dylib::fork()`) + ` was successful with ` + c.red(retval.toString()) + ` marking it as failed.`, ); retval.replace(new NativePointer(0x0)); break; } }, }); }; // ref: https://www.ayrx.me/gantix-jailmonkey-root-detection-bypass/ const jailMonkeyBypass = (success: boolean, ident: number): InvocationListener | null => { const JailMonkeyClass = ObjC.classes.JailMonkey; if (JailMonkeyClass === undefined) return null; return Interceptor.attach(JailMonkeyClass["- isJailBroken"].implementation, { onLeave(retval) { send( c.blackBright(`[${ident}] `) + `JailMonkey.isJailBroken called, returning false.` ); retval.replace(new NativePointer(0x00)); } }); }; export const disable = (): void => { const job: jobs.Job = new jobs.Job(jobs.identifier(), "ios-jailbreak-disable"); job.addInvocation(fileExistsAtPath(false, job.identifier)); job.addInvocation(libSystemBFork(false, job.identifier)); job.addInvocation(fopen(false, job.identifier)); job.addInvocation(canOpenURL(false, job.identifier)); job.addInvocation(jailMonkeyBypass(false, job.identifier)); jobs.add(job); }; export const enable = (): void => { const job: jobs.Job = new jobs.Job(jobs.identifier(), "ios-jailbreak-enable"); job.addInvocation(fileExistsAtPath(true, job.identifier)); job.addInvocation(libSystemBFork(true, job.identifier)); job.addInvocation(fopen(true, job.identifier)); job.addInvocation(canOpenURL(true, job.identifier)); job.addInvocation(jailMonkeyBypass(true, job.identifier)); jobs.add(job); }; ================================================ FILE: agent/src/ios/keychain.ts ================================================ // dumps all of the keychain items available to the current // application. import { colors as c } from "../lib/color.js"; import { reverseEnumLookup } from "../lib/helpers.js"; import { kSec, NSUTF8StringEncoding } from "./lib/constants.js"; import { bytesToHexString, bytesToUTF8, smartDataToString } from "./lib/helpers.js"; import { IKeychainData, IKeychainItem } from "./lib/interfaces.js"; import { libObjc, ObjC } from "./lib/libobjc.js"; import { NSDictionary, NSMutableDictionary as NSMutableDictionaryType, NSString as NSStringType, } from "./lib/types.js"; // keychain item times to query for const itemClasses = [ kSec.kSecClassKey, kSec.kSecClassIdentity, kSec.kSecClassCertificate, kSec.kSecClassGenericPassword, kSec.kSecClassInternetPassword, ]; // The parent method that enumerates the iOS keychain const enumerateKeychain = (): IKeychainData[] => { // -- Sample Objective-C // // NSMutableDictionary *query = [[NSMutableDictionary alloc] init]; // [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes]; // [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData]; // [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnRef]; // [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit]; // NSArray *itemClasses = [NSArray arrayWithObjects: // (__bridge id)kSecClassKey, // (__bridge id)kSecClassIdentity, // (__bridge id)kSecClassCertificate, // (__bridge id)kSecClassGenericPassword, // (__bridge id)kSecClassInternetPassword, // nil]; // for (id itemClass in itemClasses) { // NSLog(@"Querying: %@", itemClass); // [query setObject:itemClass forKey:(__bridge id)kSecClass]; // CFTypeRef result = NULL; // OSStatus findStatus = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); // if(findStatus != errSecSuccess) { // NSLog(@"Failed to query keychain for types %@", itemClass); // continue; // } // // loopy-loop the results // for (NSDictionary *entry in (__bridge NSDictionary *)result) { // NSString *stringRes = [[NSString alloc] initWithData:[entry objectForKey:@"v_Data"] // encoding:NSUTF8StringEncoding]; // NSLog(@"%@", stringRes); // } // if (result != NULL) { // CFRelease(result); // } // } // http://nshipster.com/bool/ const kCFBooleanTrue = ObjC.classes.__NSCFBoolean.numberWithBool_(true); // the base query dictionary to use for the keychain lookups const searchDictionary: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init(); searchDictionary.setObject_forKey_(kCFBooleanTrue, kSec.kSecReturnAttributes); searchDictionary.setObject_forKey_(kCFBooleanTrue, kSec.kSecReturnData); searchDictionary.setObject_forKey_(kCFBooleanTrue, kSec.kSecReturnRef); searchDictionary.setObject_forKey_(kSec.kSecMatchLimitAll, kSec.kSecMatchLimit); searchDictionary.setObject_forKey_(kSec.kSecAttrSynchronizableAny, kSec.kSecAttrSynchronizable); // loop each of the keychain class types and extract data const itemClassResults: IKeychainData[][] = itemClasses.map((clazz) => { const data: IKeychainData[] = []; // start empty. searchDictionary.setObject_forKey_(clazz, kSec.kSecClass); // prepare a pointer for the results and call SecItemCopyMatching to get them const resultsPointer: NativePointer = Memory.alloc(Process.pointerSize); const copyResult: NativePointer = libObjc.SecItemCopyMatching(searchDictionary, resultsPointer); // without results (aka non-zero OSStatus) we just move along. if (!copyResult.isNull()) { return data; } // read the resultant dict of the lookup from memory const searchResults: NSDictionary = new ObjC.Object(resultsPointer.readPointer()); // if the results in the dict is empty (which is not something I expect), // fail fast too. if (searchResults.length <= 0) { return data; } // read each key chain entry for the current item_class and populate // the item_class items we will return for (let i: number = 0; i < searchResults.count(); i++) { data.push({ clazz, data: searchResults.objectAtIndex_(i), }); } return data; }); const keyChainData: IKeychainData[] = []; return keyChainData.concat(...itemClassResults).filter((n) => n !== undefined); }; // print raw entries using some Frida magic // to do the toString() repr... export const listRaw = (): void => { enumerateKeychain().forEach((e) => { c.log(e.data); }); }; // dump the contents of the iOS keychain, returning the // results as an array representation. export const list = (smartDecode: boolean = false): IKeychainItem[] => { return enumerateKeychain().map((entry) => { const { data, clazz } = entry; return { access_control: (data.containsKey_(kSec.kSecAttrAccessControl)) ? decodeAcl(data) : "", accessible_attribute: reverseEnumLookup(kSec, bytesToUTF8(data.objectForKey_(kSec.kSecAttrAccessible))), account: bytesToUTF8(data.objectForKey_(kSec.kSecAttrAccount)), alias: bytesToUTF8(data.objectForKey_(kSec.kSecAttrAlias)), comment: bytesToUTF8(data.objectForKey_(kSec.kSecAttrComment)), create_date: bytesToUTF8(data.objectForKey_(kSec.kSecAttrCreationDate)), creator: bytesToUTF8(data.objectForKey_(kSec.kSecAttrCreator)), custom_icon: bytesToUTF8(data.objectForKey_(kSec.kSecAttrHasCustomIcon)), data: (clazz !== "keys") ? (smartDecode) ? smartDataToString(data.objectForKey_(kSec.kSecValueData)) : bytesToUTF8(data.objectForKey_(kSec.kSecValueData)) : "(Key data not displayed)", dataHex: bytesToHexString(data.objectForKey_(kSec.kSecValueData)), description: bytesToUTF8(data.objectForKey_(kSec.kSecAttrDescription)), entitlement_group: bytesToUTF8(data.objectForKey_(kSec.kSecAttrAccessGroup)), generic: bytesToUTF8(data.objectForKey_(kSec.kSecAttrGeneric)), invisible: bytesToUTF8(data.objectForKey_(kSec.kSecAttrIsInvisible)), item_class: reverseEnumLookup(kSec, clazz), label: bytesToUTF8(data.objectForKey_(kSec.kSecAttrLabel)), modification_date: bytesToUTF8(data.objectForKey_(kSec.kSecAttrModificationDate)), negative: bytesToUTF8(data.objectForKey_(kSec.kSecAttrIsNegative)), protected: bytesToUTF8(data.objectForKey_(kSec.kSecProtectedDataItemAttr)), script_code: bytesToUTF8(data.objectForKey_(kSec.kSecAttrScriptCode)), service: bytesToUTF8(data.objectForKey_(kSec.kSecAttrService)), type: bytesToUTF8(data.objectForKey_(kSec.kSecAttrType)), }; }); }; // clean out the keychain export const empty = (): void => { const searchDictionary: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init(); searchDictionary.setObject_forKey_(kSec.kSecAttrSynchronizableAny, kSec.kSecAttrSynchronizable); itemClasses.forEach((clazz) => { // set the class-type we are querying for now & delete searchDictionary.setObject_forKey_(clazz, kSec.kSecClass); libObjc.SecItemDelete(searchDictionary); }); }; // remove matching itemms from the keychain export const remove = (account: string, service: string): void => { const searchDictionary: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init(); searchDictionary.setObject_forKey_(kSec.kSecAttrSynchronizableAny, kSec.kSecAttrSynchronizable); itemClasses.forEach((clazz) => { // set the class-type we are querying for now & delete searchDictionary.setObject_forKey_(clazz, kSec.kSecClass); searchDictionary.setObject_forKey_(account, kSec.kSecAttrAccount); searchDictionary.setObject_forKey_(service, kSec.kSecAttrService); libObjc.SecItemDelete(searchDictionary); }); }; // update matching item from the keychain export const update = (account: string, service: string, newData: string): void => { const searchDictionary: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init(); searchDictionary.setObject_forKey_(kSec.kSecAttrSynchronizableAny, kSec.kSecAttrSynchronizable); // set the class-type we are querying for now & update searchDictionary.setObject_forKey_(kSec.kSecClassGenericPassword, kSec.kSecClass); searchDictionary.setObject_forKey_(account, kSec.kSecAttrAccount); searchDictionary.setObject_forKey_(service, kSec.kSecAttrService); // set the dictionary with new value const itemDict: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init(); const v: NSStringType = ObjC.classes.NSString.stringWithString_(newData).dataUsingEncoding_(NSUTF8StringEncoding); itemDict.setObject_forKey_(account, kSec.kSecAttrAccount); itemDict.setObject_forKey_(v, kSec.kSecValueData); libObjc.SecItemUpdate(searchDictionary, itemDict); }; // add a string entry to the keychain export const add = (account: string, service: string, data: string): boolean => { // prepare the dictionary for SecItemAdd() const itemDict: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init(); itemDict.setObject_forKey_(kSec.kSecClassGenericPassword, kSec.kSecClass); [ { "type": "account", "value": account, "ksec": kSec.kSecAttrAccount }, { "type": "service", "value": service, "ksec": kSec.kSecAttrService }, { "type": "data", "value": data, "ksec": kSec.kSecValueData } ].forEach(e => { if (e.value == null) return; const v: NSStringType = ObjC.classes.NSString.stringWithString_(e.value) .dataUsingEncoding_(NSUTF8StringEncoding); itemDict.setObject_forKey_(v, e.ksec); }); // Add the keychain entry const result: any = libObjc.SecItemAdd(itemDict, NULL); return result.isNull(); }; // decode the access control attributes on a keychain // entry into a human readable string. Getting an idea of what the // constraints actually are is done using an undocumented method, // SecAccessControlGetConstraints. const decodeAcl = (entry: NSDictionary): string => { const acl = new ObjC.Object( libObjc.SecAccessControlGetConstraints(entry.objectForKey_(kSec.kSecAttrAccessControl))); // Ensure we were able to get the SecAccessControlRef if (acl.handle.isNull()) { return "None"; } const flags: string[] = []; const aclEnum: NSDictionary = acl.keyEnumerator(); let aclItemkey: any; // tslint:disable-next-line:no-conditional-assignment while ((aclItemkey = aclEnum.nextObject()) !== null) { const aclItem: NSDictionary = acl.objectForKey_(aclItemkey); switch (smartDataToString(aclItemkey)) { // Defaults? case "dacl": break; case "osgn": flags.push("kSecAttrKeyClassPrivate"); break; case "od": const constraints: NSDictionary = aclItem; const constraintEnum = constraints.keyEnumerator(); let constraintItemKey; // tslint:disable-next-line:no-conditional-assignment while ((constraintItemKey = constraintEnum.nextObject()) !== null) { switch (smartDataToString(constraintItemKey)) { case "cpo": flags.push("kSecAccessControlUserPresence"); break; case "cup": flags.push("kSecAccessControlDevicePasscode"); break; case "pkofn": constraints.objectForKey_("pkofn") === 1 ? flags.push("Or") : flags.push("And"); break; case "cbio": constraints.objectForKey_("cbio").count().valueOf() === 1 ? flags.push("kSecAccessControlBiometryAny") : flags.push("kSecAccessControlBiometryCurrentSet"); break; default: break; } } break; case "prp": flags.push("kSecAccessControlApplicationPassword"); break; default: break; } } return flags.join(" "); }; ================================================ FILE: agent/src/ios/lib/constants.ts ================================================ // constants used for Security Attributes etc. // NSLog(@"kSecAttrService: %@", kSecAttrService); export enum kSec { // To reference some of the constants, the had to be echoed to // get their values. // NSLog(@"Constants Dump"); // NSLog(@"kSecAttrService: %@", kSecAttrService); // NSLog(@"End Constants Dump"); kSecReturnAttributes = "r_Attributes", kSecReturnData = "r_Data", kSecReturnRef = "r_Ref", kSecMatchLimit = "m_Limit", kSecMatchLimitAll = "m_LimitAll", kSecClass = "class", kSecClassKey = "keys", kSecClassIdentity = "idnt", kSecClassCertificate = "cert", kSecClassGenericPassword = "genp", kSecClassInternetPassword = "inet", kSecAttrService = "svce", kSecAttrAccount = "acct", kSecAttrAccessGroup = "agrp", kSecAttrLabel = "labl", kSecAttrCreationDate = "cdat", kSecAttrAccessControl = "accc", kSecAttrGeneric = "gena", kSecAttrSynchronizable = "sync", kSecAttrSynchronizableAny = "syna", kSecAttrModificationDate = "mdat", kSecAttrServer = "srvr", kSecAttrDescription = "desc", kSecAttrComment = "icmt", kSecAttrCreator = "crtr", kSecAttrType = "type", kSecAttrScriptCode = "scrp", kSecAttrAlias = "alis", kSecAttrIsInvisible = "invi", kSecAttrIsNegative = "nega", kSecAttrHasCustomIcon = "cusi", kSecProtectedDataItemAttr = "prot", kSecAttrAccessible = "pdmn", kSecAttrAccessibleWhenUnlocked = "ak", kSecAttrAccessibleAfterFirstUnlock = "ck", kSecAttrAccessibleAlways = "dk", kSecAttrAccessibleWhenUnlockedThisDeviceOnly = "aku", kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly = "akpu", kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly = "cku", kSecAttrAccessibleAlwaysThisDeviceOnly = "dku", kSecValueData = "v_Data", } // typedef NS_ENUM(NSUInteger, NSSearchPathDirectory) { // NSApplicationDirectory = 1, // supported applications (Applications) // NSDemoApplicationDirectory, // unsupported applications, demonstration versions (Demos) // tslint:disable-next-line:max-line-length // NSDeveloperApplicationDirectory, // developer applications (Developer/Applications). DEPRECATED - there is no one single Developer directory. // NSAdminApplicationDirectory, // system and network administration applications (Administration) // tslint:disable-next-line:max-line-length // NSLibraryDirectory, // various documentation, support, and configuration files, resources (Library) // tslint:disable-next-line:max-line-length // NSDeveloperDirectory, // developer resources (Developer) DEPRECATED - there is no one single Developer directory. // NSUserDirectory, // user home directories (Users) // NSDocumentationDirectory, // documentation (Documentation) // NSDocumentDirectory, // documents (Documents) // NSCoreServiceDirectory, // location of CoreServices directory (System/Library/CoreServices) // tslint:disable-next-line:max-line-length // NSAutosavedInformationDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 11, // location of autosaved documents (Documents/Autosaved) // NSDesktopDirectory = 12, // location of user's desktop // NSCachesDirectory = 13, // location of discardable cache files (Library/Caches) // tslint:disable-next-line:max-line-length // NSApplicationSupportDirectory = 14, // location of application support files (plug-ins, etc) (Library/Application Support) // // [... snip ...] // // }; export enum NSSearchPaths { NSApplicationDirectory = 1, NSDemoApplicationDirectory, NSDeveloperApplicationDirectory, NSAdminApplicationDirectory, NSLibraryDirectory, NSDeveloperDirectory, NSUserDirectory, NSDocumentationDirectory, NSDocumentDirectory, NSCoreServiceDirectory, NSAutosavedInformationDirectory, NSDesktopDirectory, NSCachesDirectory, NSApplicationSupportDirectory, } export const NSUserDomainMask = 1; export const NSUTF8StringEncoding = 4; export enum BundleType { NSBundleFramework = 1, NSBundleAllBundles, } ================================================ FILE: agent/src/ios/lib/helpers.ts ================================================ import { ObjC } from "./libobjc.js"; import type { default as ObjCTypes } from "frida-objc-bridge"; import { NSUTF8StringEncoding } from "./constants.js"; import { NSBundle, NSDictionary, NSFileManager, NSString as NSStringType } from "./types.js"; // Attempt to unarchive data. Returning a string of `` indicates that the // unarchiving failed. export const unArchiveDataAndGetString = (data: ObjCTypes.Object | any): string => { try { // tslint:disable-next-line:max-line-length // https://developer.apple.com/documentation/foundation/nskeyedunarchiver/1574811-unarchivetoplevelobjectwithdata // This one is marked as DEPRECATED, but seems to still be a thing in // iOS 12. Ok for now. const NSKeyedUnarchiver = ObjC.classes.NSKeyedUnarchiver; const unArchivedData: any = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData_error_(data, NULL); // if we have a null value, this data is probably not archived if (unArchivedData === null) { return ``; } switch (unArchivedData.$className) { case "__NSDictionary": case "__NSDictionaryI": const dict: NSDictionary = new ObjC.Object(unArchivedData); const enumerator = dict.keyEnumerator(); let key: string; const s: {[key: string]: any } = {}; // tslint:disable-next-line:no-conditional-assignment while ((key = enumerator.nextObject()) !== null) { s[key] = `${dict.objectForKey_(key)}`; } return JSON.stringify(s); default: return ``; } } catch (e) { return data.toString(); } }; export const smartDataToString = (raw: any): string => { if (raw === null) { return ""; } try { const dataObject: ObjCTypes.Object | any = new ObjC.Object(raw); switch (dataObject.$className) { case "__NSCFData": try { const unarchivedData: string = unArchiveDataAndGetString(dataObject); if (unarchivedData.length > 0) { return unarchivedData; } // tslint:disable-next-line:no-empty } catch (e) { } try { const data: string = dataObject.readUtf8String(dataObject.length()); if (data.length > 0) { return data; } // tslint:disable-next-line:no-empty } catch (e) { } case "__NSCFNumber": return dataObject.integerValue(); case "NSTaggedPointerString": case "__NSDate": case "__NSCFString": case "__NSTaggedDate": return dataObject.toString(); default: return `(could not get string for class: ${dataObject.$className})`; } } catch (e) { return "(failed to decode)"; } }; export const bytesToUTF8 = (data: any): string => { // Sample Objective-C // // char buf[] = "\x41\x42\x43\x44"; // NSString *p = [[NSString alloc] initWithBytes:buf length:5 encoding:NSUTF8StringEncoding]; if (data === null) { return ""; } if (!data.hasOwnProperty("bytes")) { return data.toString(); } const s: NSStringType = ObjC.classes.NSString.alloc().initWithBytes_length_encoding_( data.bytes(), data.length(), NSUTF8StringEncoding); if (s) { return s.UTF8String(); } return ""; }; export const bytesToHexString = (data: any): string => { // https://stackoverflow.com/a/50767210 if (data == null) { return ""; } const buffer: ArrayBuffer = data.bytes().readByteArray(data.length()); return Array.from(new Uint8Array(buffer)).map((b) => ("0" + b.toString(16)).substr(-2)).join(""); }; export const getNSFileManager = (): NSFileManager => { const NSFM = ObjC.classes.NSFileManager; return NSFM.defaultManager(); }; export const getNSMainBundle = (): NSBundle => { const bundle = ObjC.classes.NSBundle; return bundle.mainBundle(); }; export const arrayBufferToHex = (arrayBuffer: ArrayBuffer | null): string => { if (typeof arrayBuffer !== 'object' || arrayBuffer === null || typeof arrayBuffer.byteLength !== 'number') { throw new TypeError('Expected input to be an ArrayBuffer'); } const buffer = new Uint8Array(arrayBuffer); let result = ''; let value; for (const byte of buffer) { value = byte.toString(16); result += (value.length === 1 ? '0' + value : value); } return result; }; export const hexToString = (hexx: string): string => { const hex = hexx.toString(); // force conversion let str = ''; for (let i = 0; (i < hex.length && hex.substring(i, i+2) !== '00'); i += 2) str += String.fromCharCode(parseInt(hex.substring(i, i+2), 16)); return str; }; ================================================ FILE: agent/src/ios/lib/interfaces.ts ================================================ import { NSDictionary } from "./types.js"; export interface IKeychainData { clazz: string; data: NSDictionary; } export interface IKeychainItem { item_class: string; create_date: string; modification_date: string; description: string; comment: string; creator: string; type: string; script_code: string; alias: string; invisible: string; negative: string; custom_icon: string; protected: string; access_control: string; accessible_attribute: string; entitlement_group: string; generic: string; service: string; account: string; label: string; data: string; dataHex: string; } export interface IIosFileSystem { files: any; path: string; readable: boolean; writable: boolean; } export interface IIosFilePath { attributes: any; fileName: string; readable: boolean | undefined; writable: boolean | undefined; } export interface IIosCookie { name: string; version: string; value: string; expiresDate: string | undefined; domain: string; path: string; isSecure: boolean; isHTTPOnly: boolean; } export interface ICredential { authMethod: string; host: string; password: string; port: string; protocol: string; user: string; } export interface IFramework { version: string | null; executable: string | null; bundle: string | null; path: string | null; } export interface IHeapObject { className: string; handle: string; ivars: any[string]; kind: string; methods: string[]; superClass: string; } export interface IBinaryModuleDictionary { [index: string]: IBinaryInfo; } export interface IBinaryInfo { arc: boolean; canary: boolean; encrypted: boolean; pie: boolean; rootSafe: boolean; stackExec: boolean; type: string; } ================================================ FILE: agent/src/ios/lib/libobjc.ts ================================================ import ObjC_bridge from "frida-objc-bridge"; let ObjC: typeof ObjC_bridge; // Compatibility with frida < 17 if (globalThis.ObjC) { ObjC = globalThis.ObjC } else { ObjC = ObjC_bridge } export { ObjC } const nativeExports: any = { // iOS keychain methods SecAccessControlGetConstraints: { argTypes: ["pointer"], exportName: "SecAccessControlGetConstraints", moduleName: "Security", retType: "pointer", }, SecItemAdd: { argTypes: ["pointer", "pointer"], exportName: "SecItemAdd", moduleName: "Security", retType: "pointer", }, SecItemCopyMatching: { argTypes: ["pointer", "pointer"], exportName: "SecItemCopyMatching", moduleName: "Security", retType: "pointer", }, SecItemDelete: { argTypes: ["pointer"], exportName: "SecItemDelete", moduleName: "Security", retType: "pointer", }, SecItemUpdate: { argTypes: ["pointer", "pointer"], exportName: "SecItemUpdate", moduleName: "Security", retType: "pointer", }, // SSL pinning methods SSLCreateContext: { argTypes: ["pointer", "int", "int"], exportName: "SSLCreateContext", moduleName: "Security", retType: "pointer", }, SSLHandshake: { argTypes: ["pointer"], exportName: "SSLHandshake", moduleName: "Security", retType: "int", }, SSLSetSessionOption: { argTypes: ["pointer", "int", "bool"], exportName: "SSLSetSessionOption", moduleName: "Security", retType: "int", }, // iOS 10+ TLS methods nw_tls_create_peer_trust: { argTypes: ["pointer", "bool", "pointer"], exportName: "nw_tls_create_peer_trust", moduleName: "libnetwork.dylib", retType: "int", }, tls_helper_create_peer_trust: { argTypes: ["pointer", "bool", "pointer"], exportName: "tls_helper_create_peer_trust", moduleName: "libcoretls_cfhelpers.dylib", retType: "int", }, // iOS 11+ libboringssl methods SSL_CTX_set_custom_verify: { argTypes: ["pointer", "int", "pointer"], exportName: "SSL_CTX_set_custom_verify", moduleName: "libboringssl.dylib", retType: "void", }, SSL_get_psk_identity: { argTypes: ["pointer"], exportName: "SSL_get_psk_identity", moduleName: "libboringssl.dylib", retType: "pointer", }, // iOS 13+ libboringssl methods SSL_set_custom_verify: { argTypes: ["pointer", "int", "pointer"], exportName: "SSL_set_custom_verify", moduleName: "libboringssl.dylib", retType: "void", }, }; const api: any = { SecAccessControlGetConstraints: null, SecItemAdd: null, SecItemCopyMatching: null, SecItemUpdate: null, SecItemDelete: null, SSLCreateContext: null, SSLHandshake: null, SSLSetSessionOption: null, nw_tls_create_peer_trust: null, tls_helper_create_peer_trust: null, SSL_CTX_set_custom_verify: null, SSL_get_psk_identity: null, SSL_set_custom_verify: null, }; // proxy method resolution export const libObjc = new Proxy(api, { get: (target, key) => { if (target[key] === null) { const mod = Process.findModuleByName(nativeExports[key].moduleName) var tgt = new NativePointer(0x00); if (mod != null) { tgt = mod.findExportByName(nativeExports[key].exportName) || new NativePointer(0x00); } target[key] = new NativeFunction(tgt, nativeExports[key].retType, nativeExports[key].argTypes); } return target[key]; }, }); ================================================ FILE: agent/src/ios/lib/types.ts ================================================ import type { default as ObjCTypes } from "frida-objc-bridge"; export type NSDictionary = ObjCTypes.Object | any; export type NSMutableDictionary = ObjCTypes.Object | any; export type NSString = ObjCTypes.Object | any; export type NSFileManager = ObjCTypes.Object | any; export type NSBundle = ObjCTypes.Object | any; export type NSUserDefaults = ObjCTypes.Object | any; export type NSHTTPCookieStorage = ObjCTypes.Object | any; export type NSURLCredentialStorage = ObjCTypes.Object | any; export type NSArray = ObjCTypes.Object | any; export type NSData = ObjCTypes.Object | any; export type CFDictionaryRef = any; export type CFTypeRef = any; ================================================ FILE: agent/src/ios/nsuserdefaults.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import { NSDictionary, NSUserDefaults } from "./lib/types.js"; export const get = (): NSUserDefaults | any => { // -- Sample Objective-C // // NSUserDefaults *d = [[NSUserDefaults alloc] init]; // NSLog(@"%@", [d dictionaryRepresentation]); const defaults: NSUserDefaults = ObjC.classes.NSUserDefaults; const data: NSDictionary = defaults.alloc().init().dictionaryRepresentation(); return data.toString(); }; ================================================ FILE: agent/src/ios/pasteboard.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import { colors as c } from "../lib/color.js"; export const monitor = (): void => { // -- Sample Objective-C // // UIPasteboard *pb = [UIPasteboard generalPasteboard]; // NSLog(@"%@", [pb string]); // NSLog(@"%@", [pb image]); const UIPasteboard = ObjC.classes.UIPasteboard; const Pasteboard = UIPasteboard.generalPasteboard(); let data: string = ""; setInterval(() => { const currentString = Pasteboard.string().toString(); // do nothing if the strings are the same as the last one // we know about if (currentString === data) { return; } // update the string_data with the new string data = currentString; // ... and send the update along send(`${c.blackBright(`[pasteboard-monitor]`)} Data: ${c.greenBright(data.toString())}`); // 5 second poll }, 1000 * 5); }; ================================================ FILE: agent/src/ios/pinning.ts ================================================ import { colors as c } from "../lib/color.js"; import { qsend } from "../lib/helpers.js"; import * as jobs from "../lib/jobs.js"; import { libObjc, ObjC } from "./lib/libobjc.js"; import type { default as ObjCTypes } from "frida-objc-bridge"; // These hooks attempt many ways to kill SSL pinning and certificate // validations. The first sections search for common libraries and // class methods used in many examples online to demonstrate how // to pin SSL certificates. // As far as libraries and classes go, this hook searches for: // // - AFNetworking. // AFNetworking has a very easy pinning feature that can be disabled // by setting the 'PinningMode' to 'None'. // // - NSURLSession. // NSURLSession makes use of a delegate method with the signature // 'URLSession:didReceiveChallenge:completionHandler:' that allows // developers to extract the server presented certificate and make // decisions to complete the request or cancel it. The hook for this // Class searches for the selector and replaces it one that will // continue regardless of the logic in this method, and apply the // original block as a callback, with a successful return. // // - NSURLConnection. // While an old method, works similar to NSURLSession, except there is // no completionHandler block, so just the successful challenge is returned. // The more 'lower level' stuff is basically a reimplementation of the commonly // known 'SSL-Killswitch2'[1], which hooks and replaces lower level certificate validation // methods with ones that will always pass. An important note should be made on the // implementation changes from iOS9 to iOS10 as detailed here[2]. This hook also tries // to implement those for iOS10. // [1] https://github.com/nabla-c0d3/ssl-kill-switch2/blob/master/SSLKillSwitch/SSLKillSwitch.m // [2] https://nabla-c0d3.github.io/blog/2017/02/05/ios10-ssl-kill-switch/ // Many apps implement the SSL pinning in interesting ways, if this hook fails, all // is not lost yet. Sometimes, there is a method that just checks some configuration // item somewhere, and returns a BOOL, indicating whether pinning is applicable or // not. So, hunt that method and hook it :) // a simple flag to control if we should be quiet or not let quiet: boolean = false; const afNetworking = (ident: number): InvocationListener[] => { const { AFHTTPSessionManager, AFSecurityPolicy } = ObjC.classes; // If AFNetworking is not a thing, just move on. if (!(AFHTTPSessionManager && AFSecurityPolicy)) { return []; } send(c.blackBright(`[${ident}] `) + `Found AFNetworking library. Hooking known pinning methods.`); // -[AFSecurityPolicy setSSLPinningMode:] const setSSLPinningmode: InvocationListener = Interceptor.attach( AFSecurityPolicy["- setSSLPinningMode:"].implementation, { onEnter(args) { // typedef NS_ENUM(NSUInteger, AFSSLPinningMode) { // AFSSLPinningModeNone, // AFSSLPinningModePublicKey, // AFSSLPinningModeCertificate, // }; qsend(quiet, c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` + c.green(`-[AFSecurityPolicy setSSLPinningMode:]`) + ` with mode ` + c.red(args[2].toString()), ); if (!args[2].isNull()) { qsend(quiet, c.blackBright(`[${ident}] `) + `[AFNetworking] ` + c.blueBright(`Altered `) + c.green(`-[AFSecurityPolicy setSSLPinningMode:]`) + ` mode to ` + c.green(`0x0`), ); // update mode to 0 (AFSSLPinningModeNone), bypassing it. args[2] = new NativePointer(0x0); } }, }); // -[AFSecurityPolicy setAllowInvalidCertificates:] const setAllowInvalidCertificates: InvocationListener = Interceptor.attach( AFSecurityPolicy["- setAllowInvalidCertificates:"].implementation, { onEnter(args) { qsend(quiet, c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` + c.green(`-[AFSecurityPolicy setAllowInvalidCertificates:]`) + ` with allow ` + c.red(args[2].toString()), ); if (args[2].equals(new NativePointer(0x0))) { qsend(quiet, c.blackBright(`[${ident}] `) + `[AFNetworking] ` + c.blueBright(`Altered `) + c.green(`-[AFSecurityPolicy setAllowInvalidCertificates:]`) + ` allow to ` + c.green(`0x1`), ); // Basically, do [policy setAllowInvalidCertificates:YES]; args[2] = new NativePointer(0x1); } }, }); // +[AFSecurityPolicy policyWithPinningMode:] const policyWithPinningMode: InvocationListener = Interceptor.attach( AFSecurityPolicy["+ policyWithPinningMode:"].implementation, { onEnter(args) { // typedef NS_ENUM(NSUInteger, AFSSLPinningMode) { // AFSSLPinningModeNone, // AFSSLPinningModePublicKey, // AFSSLPinningModeCertificate, // }; qsend(quiet, c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` + c.green(`+[AFSecurityPolicy policyWithPinningMode:]`) + ` with mode ` + c.red(args[2].toString()), ); if (!args[2].isNull()) { qsend(quiet, c.blackBright(`[${ident}] `) + `[AFNetworking] ` + c.blueBright(`Altered `) + c.green(`+[AFSecurityPolicy policyWithPinningMode:]`) + ` mode to ` + c.green(`0x0`), ); // effectively set to AFSSLPinningModeNone args[2] = new NativePointer(0x0); } }, }); // +[AFSecurityPolicy policyWithPinningMode:withPinnedCertificates:] const policyWithPinningModewithPinnedCertificates: InvocationListener | null = (AFSecurityPolicy["+ policyWithPinningMode:withPinnedCertificates:"]) ? Interceptor.attach( AFSecurityPolicy["+ policyWithPinningMode:withPinnedCertificates:"].implementation, { onEnter(args) { // typedef NS_ENUM(NSUInteger, AFSSLPinningMode) { // AFSSLPinningModeNone, // AFSSLPinningModePublicKey, // AFSSLPinningModeCertificate, // }; qsend(quiet, c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` + c.green(`+[AFSecurityPolicy policyWithPinningMode:withPinnedCertificates:]`) + ` with mode ` + c.red(args[2].toString()), ); if (!args[2].isNull()) { qsend(quiet, c.blackBright(`[${ident}] `) + `[AFNetworking] ` + c.blueBright(`Altered `) + c.green(`+[AFSecurityPolicy policyWithPinningMode:withPinnedCertificates:]`) + ` mode to ` + c.green(`0x0`), ); // effectively set to AFSSLPinningModeNone args[2] = new NativePointer(0x0); } }, }) : null; return [ setSSLPinningmode, setAllowInvalidCertificates, policyWithPinningMode, ...(policyWithPinningModewithPinnedCertificates ? [policyWithPinningModewithPinnedCertificates] : []), ]; }; const nsUrlSession = (ident: number): InvocationListener[] => { const NSURLCredential: ObjCTypes.Object = ObjC.classes.NSURLCredential; const resolver = new ApiResolver("objc"); // - [NSURLSession URLSession:didReceiveChallenge:completionHandler:] const search: ApiResolverMatch[] = resolver.enumerateMatches( "-[* URLSession:didReceiveChallenge:completionHandler:]"); // Move along if no NSURLSession usage is found if (search.length <= 0) { return []; } send(c.blackBright(`Found NSURLSession based classes. Hooking known pinning methods.`)); // hook all of the methods that matched the selector return search.map((i) => { return Interceptor.attach(i.address, { onEnter(args) { // 0 // 1 // 2 URLSession // 3 didReceiveChallenge // 4 completionHandler const receiver = new ObjC.Object(args[0]); const selector = ObjC.selectorAsString(args[1]); const challenge = new ObjC.Object(args[3]); qsend(quiet, c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` + c.green(`-[${receiver} ${selector}]`) + `, ensuring pinning is passed`, ); // get the original completion handler, and save it const completionHandler = new ObjC.Block(args[4]); const savedCompletionHandler = completionHandler.implementation; // ignore everything the original method wanted to do, // and prepare the successful arguments for the original // completion handler completionHandler.implementation = () => { // Example handler source // SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; // SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, 0); // NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); // NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"swapi.co" ofType:@"der"]; // NSData *localCertData = [NSData dataWithContentsOfFile:cerPath]; // if ([remoteCertificateData isEqualToData:localCertData]) { // NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; // [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; // completionHandler(NSURLSessionAuthChallengeUseCredential, credential); // } else { // [[challenge sender] cancelAuthenticationChallenge:challenge]; // completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); // } const credential = NSURLCredential.credentialForTrust_(challenge.protectionSpace().serverTrust()); challenge.sender().useCredential_forAuthenticationChallenge_(credential, challenge); // typedef NS_ENUM(NSInteger, NSURLSessionAuthChallengeDisposition) { // NSURLSessionAuthChallengeUseCredential = 0, // NSURLSessionAuthChallengePerformDefaultHandling = 1, // NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2, // NSURLSessionAuthChallengeRejectProtectionSpace = 3, // } NS_ENUM_AVAILABLE(NSURLSESSION_AVAILABLE, 7_0); savedCompletionHandler(0, credential); }; }, }); }); }; // TrustKit const trustKit = (ident: number): InvocationListener | null => { // https://github.com/datatheorem/TrustKit/blob/ // 71878dce8c761fc226fecc5dbb6e86fbedaee05e/TrustKit/TSKPinningValidator.m#L84 if (!ObjC.classes.TSKPinningValidator) { return null; } send(c.blackBright(`[${ident}] `) + `Found TrustKit. Hooking known pinning methods.`); return Interceptor.attach(ObjC.classes.TSKPinningValidator["- evaluateTrust:forHostname:"].implementation, { onLeave(retval) { qsend(quiet, c.blackBright(`[${ident}] `) + `[TrustKit] Called ` + c.green(`-[TSKPinningValidator evaluateTrust:forHostname:]`) + ` with result ` + c.red(retval.toString()), ); if (!retval.isNull()) { qsend(quiet, c.blackBright(`[${ident}] `) + `[TrustKit] ` + c.blueBright(`Altered `) + c.green(`-[TSKPinningValidator evaluateTrust:forHostname:]`) + ` mode to ` + c.green(`0x0`), ); retval.replace(new NativePointer(0x0)); } }, }); }; const cordovaCustomURLConnectionDelegate = (ident: number): InvocationListener | null => { // https://github.com/EddyVerbruggen/SSLCertificateChecker-PhoneGap-Plugin/blob/ // 67634bfdf4a31bb09b301db40f8f27fbd8818f61/src/ios/SSLCertificateChecker.m#L109-L116 if (!ObjC.classes.CustomURLConnectionDelegate) { return null; } send(c.blackBright(`[${ident}] `) + `Found SSLCertificateChecker-PhoneGap-Plugin.` + ` Hooking known pinning methods.`); return Interceptor.attach(ObjC.classes.CustomURLConnectionDelegate["- isFingerprintTrusted:"].implementation, { onLeave(retval) { qsend(quiet, c.blackBright(`[${ident}] `) + `[SSLCertificateChecker-PhoneGap-Plugin] Called ` + c.green(`-[CustomURLConnectionDelegate isFingerprintTrusted:]`) + ` with result ` + c.red(retval.toString()), ); if (retval.isNull()) { qsend(quiet, c.blackBright(`[${ident}] `) + `[SSLCertificateChecker-PhoneGap-Plugin] ` + c.blueBright(`Altered `) + c.green(`-[CustomURLConnectionDelegate isFingerprintTrusted:]`) + ` mode to ` + c.green(`0x1`), ); retval.replace(new NativePointer(0x1)); } }, }); }; const sSLSetSessionOption = (ident: number): NativePointerValue => { const kSSLSessionOptionBreakOnServerAuth = 0; const noErr = 0; const SSLSetSessionOption = libObjc.SSLSetSessionOption; Interceptor.replace(SSLSetSessionOption, new NativeCallback((context, option, value) => { // Remove the ability to modify the value of the kSSLSessionOptionBreakOnServerAuth option // ^ from SSL-Kill-Switch2 sources // https://github.com/nabla-c0d3/ssl-kill-switch2/blob/ // f7e73a2044340d59f2b96d972afcbc3c2f50ab27/SSLKillSwitch/SSLKillSwitch.m#L70 if (option === kSSLSessionOptionBreakOnServerAuth) { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`SSLSetSessionOption()`) + `, removing ability to modify kSSLSessionOptionBreakOnServerAuth.`, ); return noErr; } return SSLSetSessionOption(context, option, value); }, "int", ["pointer", "int", "bool"])); return SSLSetSessionOption; }; const sSLCreateContext = (ident: number): NativePointerValue => { const kSSLSessionOptionBreakOnServerAuth = 0; const SSLSetSessionOption = libObjc.SSLSetSessionOption; const SSLCreateContext = libObjc.SSLCreateContext; Interceptor.replace(SSLCreateContext, new NativeCallback((alloc, protocolSide, connectionType) => { // Immediately set the kSSLSessionOptionBreakOnServerAuth option in order to disable cert validation // ^ from SSL-Kill-Switch2 sources // https://github.com/nabla-c0d3/ssl-kill-switch2/blob/ // f7e73a2044340d59f2b96d972afcbc3c2f50ab27/SSLKillSwitch/SSLKillSwitch.m#L89 const sslContext = SSLCreateContext(alloc, protocolSide, connectionType); SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, 1); qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`SSLCreateContext()`) + `, setting kSSLSessionOptionBreakOnServerAuth to disable cert validation.`, ); return sslContext; }, "pointer", ["pointer", "int", "int"])); return SSLCreateContext; }; const sSLHandshake = (ident: number): NativePointerValue => { const errSSLServerAuthCompared = -9481; const SSLHandshake = libObjc.SSLHandshake; Interceptor.replace(SSLHandshake, new NativeCallback((context) => { const result = SSLHandshake(context); if (result === errSSLServerAuthCompared) { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`SSLHandshake()`) + `, calling again to skip certificate validation.`, ); return SSLHandshake(context); } return result; }, "int", ["pointer"])); return SSLHandshake; }; // tls_helper_create_peer_trust const tlsHelperCreatePeerTrust = (ident: number): NativePointerValue => { const noErr = 0; const tlsHelper = libObjc.tls_helper_create_peer_trust; if (tlsHelper.isNull()) { return NULL; } Interceptor.replace(tlsHelper, new NativeCallback((hdsk, server, SecTrustRef) => { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`tls_helper_create_peer_trust()`) + `, returning noErr.`, ); return noErr; }, "int", ["pointer", "bool", "pointer"])); return tlsHelper; }; // nw_tls_create_peer_trust const nwTlsCreatePeerTrust = (ident: number): InvocationListener | null => { const peerTrust = libObjc.nw_tls_create_peer_trust; if (peerTrust.isNull()) { return null; } return Interceptor.attach(peerTrust, { onEnter: () => { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`nw_tls_create_peer_trust()`) + `, ` + c.red(`no working bypass implemented yet.`), ); }, }); // TODO: nw_tls_create_peer_trust() always returns 0, but also seems to have // some internal logic that makes a simple replacement not work. // // const noErr = 0; // Interceptor.replace(peerTrust, new NativeCallback((hdsk, server, SecTrustRef) => { // send( // c.blackBright(`[${ident}] `) + `Called ` + // c.green(`nw_tls_create_peer_trust()`) + // `, returning noErr.`, // ); // return noErr; // }, "int", ["pointer", "bool", "pointer"])); // return peerTrust; }; // SSL_CTX_set_custom_verify const sSLCtxSetCustomVerify = (ident: number): NativePointerValue[] => { const getPskIdentity = libObjc.SSL_get_psk_identity; let setCustomVerify = libObjc.SSL_set_custom_verify; if (setCustomVerify.isNull()) { send(c.blackBright(`SSL_set_custom_verify not found, trying SSL_CTX_set_custom_verify`)); setCustomVerify = libObjc.SSL_CTX_set_custom_verify; } if (setCustomVerify.isNull() || getPskIdentity.isNull()) { return []; } // tslint:disable-next-line:only-arrow-functions variable-name const customVerifyCallback = new NativeCallback(function (ssl, out_alert) { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`custom SSL context verify callback`) + `, returning SSL_VERIFY_NONE.`, ); return 0; }, "int", ["pointer", "pointer"]); // tslint:disable-next-line:only-arrow-functions Interceptor.replace(setCustomVerify, new NativeCallback(function (ssl, mode, callback) { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`SSL_CTX_set_custom_verify()`) + `, setting custom callback.`, ); setCustomVerify(ssl, mode, customVerifyCallback); }, "void", ["pointer", "int", "pointer"])); // tslint:disable-next-line:only-arrow-functions Interceptor.replace(getPskIdentity, new NativeCallback(function (ssl) { qsend(quiet, c.blackBright(`[${ident}] `) + `Called ` + c.green(`SSL_get_psk_identity()`) + `, returning "fakePSKidentity".`, ); return Memory.allocUtf8String("fakePSKidentity"); }, "pointer", ["pointer"])); return [ setCustomVerify, getPskIdentity, ]; }; // exposed method to setup all of the interceptor invocations and replacements export const disable = (q: boolean): void => { if (q) { send(`Quiet mode enabled. Not reporting invocations.`); quiet = true; } const job: jobs.Job = new jobs.Job(jobs.identifier(), "ios-sslpinning-disable"); // Framework hooks. send(c.blackBright(`Hooking common framework methods`)); afNetworking(job.identifier).forEach((i) => { job.addInvocation(i); }); nsUrlSession(job.identifier).forEach((i) => { job.addInvocation(i); }); job.addInvocation(trustKit(job.identifier)); job.addInvocation(cordovaCustomURLConnectionDelegate(job.identifier)); // Low level hooks. // iOS 9< send(c.blackBright(`Hooking lower level SSL methods`)); job.addReplacement(sSLSetSessionOption(job.identifier)); job.addReplacement(sSLCreateContext(job.identifier)); job.addReplacement(sSLHandshake(job.identifier)); // iOS 10> send(c.blackBright(`Hooking lower level TLS methods`)); job.addReplacement(tlsHelperCreatePeerTrust(job.identifier)); job.addInvocation(nwTlsCreatePeerTrust(job.identifier)); // iOS 11> send(c.blackBright(`Hooking BoringSSL methods`)); // sSLCtxSetCustomVerify(job.identifier) sSLCtxSetCustomVerify(job.identifier).forEach((i) => { job.addReplacement(i); }); jobs.add(job); }; ================================================ FILE: agent/src/ios/plist.ts ================================================ import { ObjC } from "../ios/lib/libobjc.js"; import { NSMutableDictionary } from "./lib/types.js"; export const read = (path: string): string => { // -- Sample Objective-C // // NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithContentsOfFile:path]; const dictionary: NSMutableDictionary = ObjC.classes.NSMutableDictionary; return dictionary.alloc().initWithContentsOfFile_(path).toString(); }; export const write = (path: string, data: any): void => { // TODO }; ================================================ FILE: agent/src/ios/userinterface.ts ================================================ // tslint:disable-next-line:no-var-requires import { ObjC } from "../ios/lib/libobjc.js"; import type { default as ObjCTypes } from "frida-objc-bridge"; import screenshot from "frida-screenshot"; import { colors as c } from "../lib/color.js"; import * as jobs from "../lib/jobs.js"; export const take = (): any => { // heavy lifting thanks to frida-screenshot! // https://github.com/nowsecure/frida-screenshot return screenshot(null); }; export const dump = (): string => { return ObjC.classes.UIWindow.keyWindow().recursiveDescription().toString(); }; export const alert = (message: string): void => { const { UIAlertController, UIAlertAction, UIApplication } = ObjC.classes; // Defining a Block that will be passed as handler parameter // to +[UIAlertAction actionWithTitle:style:handler:] const handler = new ObjC.Block({ argTypes: ["object"], implementation: () => { return; }, retType: "void", }); // Using Grand Central Dispatch to pass messages (invoke methods) in application's main thread ObjC.schedule(ObjC.mainQueue, () => { // Using integer numerals for preferredStyle which is of type enum UIAlertControllerStyle const alertController: ObjCTypes.Object = UIAlertController.alertControllerWithTitle_message_preferredStyle_( "Alert", message, 1); // Again using integer numeral for style parameter that is enum const okButton: ObjCTypes.Object = UIAlertAction.actionWithTitle_style_handler_("OK", 0, handler); alertController.addAction_(okButton); // Instead of using `ObjC.choose()` and looking for UIViewController instances // on the heap, we have direct access through UIApplication: UIApplication.sharedApplication().keyWindow() .rootViewController().presentViewController_animated_completion_(alertController, true, NULL); }); }; export const biometricsBypass = (): void => { // -- Sample Objective-C // // LAContext *myContext = [[LAContext alloc] init]; // NSError *authError = nil; // NSString *myLocalizedReasonString = @"Please authenticate."; // if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { // [myContext evaluatePolicy:LAPolicyDeviceOwnerAuthentication // localizedReason:myLocalizedReasonString // reply:^(BOOL success, NSError *error) { // if (success) { // dispatch_async(dispatch_get_main_queue(), ^{ // [self performSegueWithIdentifier:@"LocalAuthSuccess" sender:nil]; // }); // } else { // dispatch_async(dispatch_get_main_queue(), ^{ // UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" // message:error.description // delegate:self // cancelButtonTitle:@"OK" // otherButtonTitles:nil, nil]; // [alertView show]; // // Rather than show a UIAlert here, use the // // error to determine if you should push to a keypad for PIN entry. // }); // } // }]; const policyJob: jobs.Job = new jobs.Job(jobs.identifier(), "ios-biometrics-disable-evaluatePolicy"); const lacontext1: InvocationListener = Interceptor.attach( ObjC.classes.LAContext["- evaluatePolicy:localizedReason:reply:"].implementation, { onEnter(args) { // localizedReason: const reason = new ObjC.Object(args[3]); send( c.blackBright(`[${policyJob.identifier}] `) + `Localized Reason for auth requirement (evaluatePolicy): ` + c.green(reason.toString()), ); // get the original block that should run on success for reply: // and save that block as a callback, to run once we change the reply // from the OS to a true const originalBlock = new ObjC.Block(args[4]); const savedReplyBlock = originalBlock.implementation; originalBlock.implementation = (success, error) => { send( c.blackBright(`[${policyJob.identifier}] `) + `OS authentication response: ` + c.red(success), ); if (!success === true) { send( c.blackBright(`[${policyJob.identifier}] `) + c.greenBright("Marking OS response as True instead"), ); // Change the success response from the OS to true success = true; error = null; } // and run the original block savedReplyBlock(success, error); send( c.blackBright(`[${policyJob.identifier}] `) + c.green("Biometrics bypass hook complete (evaluatePolicy)"), ); }; }, }); // register the job policyJob.addInvocation(lacontext1); jobs.add(policyJob); // -- Sample Swift // https://gist.github.com/algrid/f3f03915f264f243b9d06e875ad198c8/raw/03998319903ad9d939f85bbcc94ce9c23042b82b/KeychainBio.swift const accessControlJob: jobs.Job = new jobs.Job(jobs.identifier(), "ios-biometrics-disable-evaluateAccessControl"); const lacontext2: InvocationListener = Interceptor.attach( ObjC.classes.LAContext["- evaluateAccessControl:operation:localizedReason:reply:"].implementation, { onEnter(args) { // localizedReason: const reason = new ObjC.Object(args[4]); send( c.blackBright(`[${accessControlJob.identifier}] `) + `Localized Reason for auth requirement (evaluateAccessControl): ` + c.green(reason.toString()), ); // get the original block that should run on success for reply: // and save that block as a callback, to run once we change the reply // from the OS to a true const originalBlock = new ObjC.Block(args[5]); const savedReplyBlock = originalBlock.implementation; originalBlock.implementation = (success, error) => { send( c.blackBright(`[${accessControlJob.identifier}] `) + `OS authentication response: ` + c.red(success), ); if (!success === true) { send( c.blackBright(`[${accessControlJob.identifier}] `) + c.greenBright("Marking OS response as True instead"), ); // Change the success response from the OS to true success = true; error = null; } // and run the original block savedReplyBlock(success, error); send( c.blackBright(`[${accessControlJob.identifier}] `) + c.green("Biometrics bypass hook complete (evaluateAccessControl)"), ); }; }, }); // register the job accessControlJob.addInvocation(lacontext2); jobs.add(accessControlJob); }; ================================================ FILE: agent/src/lib/color.ts ================================================ export namespace colors { const base: string = `\x1B[%dm`; const reset: string = `\x1b[39m`; // return an ansified string export const ansify = (color: number, ...msg: string[]): string => base.replace(`%d`, color.toString()) + msg.join(``) + reset; // tslint:disable-next-line:no-eval export const clog = (color: number, ...msg: string[]): void => eval("console").log(ansify(color, ...msg)); // tslint:disable-next-line:no-eval export const log = (...msg: string[]): void => eval("console").log(msg.join(``)); // log based on a quiet flag export const qlog = (quiet: boolean, ...msg: string[]): void => { if (quiet === false) { log(...msg); } }; export const black = (message: string) => ansify(30, message); export const blue = (message: string) => ansify(34, message); export const cyan = (message: string) => ansify(36, message); export const green = (message: string) => ansify(32, message); export const magenta = (message: string) => ansify(35, message); export const red = (message: string) => ansify(31, message); export const white = (message: string) => ansify(37, message); export const yellow = (message: string) => ansify(33, message); export const blackBright = (message: string) => ansify(90, message); export const redBright = (message: string) => ansify(91, message); export const greenBright = (message: string) => ansify(92, message); export const yellowBright = (message: string) => ansify(93, message); export const blueBright = (message: string) => ansify(94, message); export const cyanBright = (message: string) => ansify(96, message); export const whiteBright = (message: string) => ansify(97, message); } ================================================ FILE: agent/src/lib/constants.ts ================================================ export enum DeviceType { IOS = "ios", ANDROID = "android", UNKNOWN = "unknown", } ================================================ FILE: agent/src/lib/helpers.ts ================================================ import util from "util"; import { colors as c } from "./color.js"; // sure, TS does not support this, but meh. // https://www.reddit.com/r/typescript/comments/87i59e/beginner_advice_strongly_typed_function_for/ export function reverseEnumLookup(enumType: T, value: string): string { for (const key in enumType) { if (Object.hasOwnProperty.call(enumType, key) && enumType[key] as any === value) { return key; } } return ""; } // converts a hexstring to a bytearray export const hexStringToBytes = (str: string): Uint8Array => { var a: number[] = []; for (let i = 0, len = str.length; i < len; i += 2) { a.push(parseInt(str.substring(i, i+2), 16)); } return new Uint8Array(a); }; // only send if quiet is not true export const qsend = (quiet: boolean, message: any): void => { if (quiet === false) { send(message); } }; // send a preformated dict export const fsend = (ident: number, hook: string, message: any): void => { send( c.blackBright(`[${ident}] `) + c.magenta(`[${hook}]`) + printArgs(message) ); }; // a small helper method to use util to dump export const debugDump = (o: any, depth: number = 2): void => { c.log(c.blackBright("\n[start debugDump]")); c.log(util.inspect(o, true, depth, true)); c.log(c.blackBright("[end debugDump]\n")); }; // a small helper method to format JSON nicely before printing function printArgs(args: {[key: string]:object}): string { let printableString: string = " (\n"; for (const arg in args) { printableString += ` ${c.blue(arg)} : ${args[arg]}\n`; } printableString += ")"; return printableString; } ================================================ FILE: agent/src/lib/interfaces.ts ================================================ export interface IFridaInfo { arch: string; debugger: boolean; heap: number; platform: string; runtime: string; version: string; } export interface IIosPackage { applicationName: string; deviceName: string; identifierForVendor: string; model: string; systemName: string; systemVersion: string; } export interface IAndroidPackage { application_name: string; board: string; brand: string; device: string; host: string; id: string; model: string; product: string; user: string; version: string; } export interface IIosBundlePaths { BundlePath: string; CachesDirectory: string; DocumentDirectory: string; LibraryDirectory: string; } ================================================ FILE: agent/src/lib/jobs.ts ================================================ import { colors as c } from "./color.js"; export class Job { identifier: number; private invocations?: InvocationListener[] = []; private replacements?: any[] = []; private implementations?: any[] = []; type: string; constructor(identifier: number, type: string) { this.identifier = identifier; this.type = type; } addInvocation(invocation: any): void { if (invocation === undefined) { // c.log(c.redBright(`[warn] Undefined Invocation!`)); c.log(c.redBright(`[warn] Undefined invocation`)); } if (invocation !== null) this.invocations.push(invocation); }; addImplementation(implementation: any): void { if (implementation !== undefined) { // Functions not found, working as expected if (implementation == null) return; this.implementations.push(implementation); } else { c.log(c.redBright(`[warn] Undefined implementation:`)); c.log(c.blackBright(new Error().stack)); } }; addReplacement(replacement: any): void { if (replacement !== undefined) this.replacements.push(replacement); }; killAll(): void { // remove all invocations if (this.invocations && this.invocations.length > 0) { this.invocations.forEach((invocation) => { (invocation) ? invocation.detach() : c.log(c.blackBright(`[warn] Skipping detach on null`)); }); } // revert any replacements if (this.replacements && this.replacements.length > 0) { this.replacements.forEach((replacement) => { Interceptor.revert(replacement); }); } // remove implementation replacements if (this.implementations && this.implementations.length > 0) { this.implementations.forEach((method) => { if (method.implementation == undefined) { c.log(c.red(`[warn] ${this.type} job missing implementation value`)); } send(c.blackBright(`(`)+ c.blueBright(this.identifier.toString())+ c.blackBright(`) Removing hook ${method.holder} `)) // TODO: May be racy if the method is currently used. method.implementation = null; }); } } } // a record of all of the jobs in the current process let currentJobs: Job[] = []; export const identifier = (): number => Number(Math.random().toString(36).substring(2, 8)); export const all = (): Job[] => currentJobs; export const add = (jobData: Job): void => { send(`Registering job ` + c.blueBright(`${jobData.identifier}`) + `. Name: ` + c.greenBright(`${jobData.type}`)); currentJobs.push(jobData); }; // determine of a job already exists based on an identifier export const hasIdent = (ident: number): boolean => { const m: Job[] = currentJobs.filter((job) => { if (job.identifier === ident) { return true; } }); return m.length > 0; }; // determine if a job already exists based on a type export const hasType = (type: string): boolean => { const m: Job[] = currentJobs.filter((job) => { if (job.type === type) { return true; } }); return m.length > 0; }; // kills a job by detaching any invocations and removing // the job by identifier export const kill = (ident: number): boolean => { currentJobs.forEach((job) => { if (job.identifier !== ident) return; send(`Killing job ` + c.blueBright(`${job.identifier}`) + `. Name: ` + c.greenBright(`${job.type}`)); // remove any hooks job.killAll(); // remove the job from the current jobs currentJobs = currentJobs.filter((j) => { return j.identifier !== job.identifier; }); }); return true; }; ================================================ FILE: agent/src/rpc/android.ts ================================================ import type { default as JavaTypes } from "frida-java-bridge"; import * as clipboard from "../android/clipboard.js"; import * as androidfilesystem from "../android/filesystem.js"; import * as heap from "../android/heap.js"; import * as hooking from "../android/hooking.js"; import * as intent from "../android/intent.js"; import * as keystore from "../android/keystore.js"; import * as sslpinning from "../android/pinning.js"; import * as root from "../android/root.js"; import * as androidshell from "../android/shell.js"; import * as userinterface from "../android/userinterface.js"; import * as proxy from "../android/proxy.js"; import * as general from "../android/general.js"; import { IHeapObject, IJavaField, IKeyStoreDetail } from "../android/lib/interfaces.js"; import { ICurrentActivityFragment, IExecutedCommand, IKeyStoreEntry } from "../android/lib/interfaces.js"; import { JavaMethodsOverloadsResult } from "../android/lib/types.js"; export const android = { // android clipboard androidMonitorClipboard: () => clipboard.monitor(), // android general androidDeoptimize: () => general.deoptimize(), // android command execution androidShellExec: (cmd: string): Promise => androidshell.execute(cmd), // android filesystem androidFileCwd: () => androidfilesystem.pwd(), androidFileDelete: (path: string) => androidfilesystem.deleteFile(path), androidFileDownload: (path: string) => androidfilesystem.readFile(path), androidFileExists: (path: string) => androidfilesystem.exists(path), androidFileLs: (path: string) => androidfilesystem.ls(path), androidFilePathIsFile: (path: string) => androidfilesystem.pathIsFile(path), androidFileReadable: (path: string) => androidfilesystem.readable(path), androidFileUpload: (path: string, data: string) => androidfilesystem.writeFile(path, data), androidFileWritable: (path: string) => androidfilesystem.writable(path), // android hooking androidHookingGetClassMethods: (className: string): Promise => hooking.getClassMethods(className), androidHookingGetClassMethodsOverloads: (className: string, methodAllowList: string[] = [], loader?: string): Promise => hooking.getClassMethodsOverloads(className, methodAllowList, loader), androidHookingGetClasses: (): Promise => hooking.getClasses(), androidHookingGetClassLoaders: (): Promise => hooking.getClassLoaders(), androidHookingGetCurrentActivity: (): Promise => hooking.getCurrentActivity(), androidHookingListActivities: (): Promise => hooking.getActivities(), androidHookingListBroadcastReceivers: (): Promise => hooking.getBroadcastReceivers(), androidHookingListServices: (): Promise => hooking.getServices(), androidHookingSetMethodReturn: (fqClazz: string, filterOverload: string | null, ret: boolean) => hooking.setReturnValue(fqClazz, filterOverload, ret), androidHookingWatch: (pattern: string, watchArgs: boolean, watchBacktrace: boolean, watchRet: boolean): Promise => hooking.watch(pattern, watchArgs, watchBacktrace, watchRet), androidHookingEnumerate: (query: string): Promise => hooking.javaEnumerate(query), androidHookingLazyWatchForPattern: (query: string, watch: boolean, dargs: boolean, dret: boolean, dbt: boolean): void => hooking.lazyWatchForPattern(query, watch, dargs, dret, dbt), // android heap methods androidHeapEvaluateHandleMethod: (handle: number, js: string): Promise => heap.evaluate(handle, js), androidHeapExecuteHandleMethod: (handle: number, method: string, returnString: boolean): Promise => heap.execute(handle, method, returnString), androidHeapGetLiveClassInstances: (clazz: string): Promise => heap.getInstances(clazz), androidHeapPrintFields: (handle: number): Promise => heap.fields(handle), androidHeapPrintMethods: (handle: number): Promise => heap.methods(handle), // android intents androidIntentStartActivity: (activityClass: string): Promise => intent.startActivity(activityClass), androidIntentStartService: (serviceClass: string): Promise => intent.startService(serviceClass), androidIntentAnalyze: (backtrace: boolean = false): Promise => intent.analyzeImplicits(backtrace), // android keystore androidKeystoreClear: () => keystore.clear(), androidKeystoreList: (): Promise => keystore.list(), androidKeystoreDetail: (): Promise => keystore.detail(), androidKeystoreWatch: (): Promise => keystore.watchKeystore(), // android ssl pinning androidSslPinningDisable: (quiet: boolean) => sslpinning.disable(quiet), // android proxy set/unset androidProxySet: (host: string, port: string): Promise => proxy.set(host, port), // android root detection androidRootDetectionDisable: () => root.disable(), androidRootDetectionEnable: () => root.enable(), // android user interface androidUiScreenshot: () => userinterface.screenshot(), androidUiSetFlagSecure: (v: boolean): Promise => userinterface.setFlagSecure(v), }; ================================================ FILE: agent/src/rpc/environment.ts ================================================ import * as environment from "../generic/environment.js"; export const env = { // environment envAndroid: () => environment.androidPackage(), envAndroidPaths: () => environment.androidPaths(), envFrida: () => environment.frida(), envIos: () => environment.iosPackage(), envIosPaths: () => environment.iosPaths(), envRuntime: () => environment.runtime(), }; ================================================ FILE: agent/src/rpc/ios.ts ================================================ import * as binary from "../ios/binary.js"; import * as binarycookies from "../ios/binarycookies.js"; import * as bundles from "../ios/bundles.js"; import * as credentialstorage from "../ios/credentialstorage.js"; import * as iosfilesystem from "../ios/filesystem.js"; import * as heap from "../ios/heap.js"; import * as hooking from "../ios/hooking.js"; import * as ioscrypto from "../ios/crypto.js"; import * as iosjailbreak from "../ios/jailbreak.js"; import * as ioskeychain from "../ios/keychain.js"; import * as nsuserdefaults from "../ios/nsuserdefaults.js"; import * as pasteboard from "../ios/pasteboard.js"; import * as sslpinning from "../ios/pinning.js"; import * as plist from "../ios/plist.js"; import * as userinterface from "../ios/userinterface.js"; import { BundleType } from "../ios/lib/constants.js"; import { NSUserDefaults } from "../ios/lib/types.js"; import { IBinaryModuleDictionary, ICredential, IFramework, IHeapObject, IIosCookie, IIosFileSystem, IKeychainItem, } from "../ios/lib/interfaces.js"; export const ios = { // binary iosBinaryInfo: (): IBinaryModuleDictionary => binary.info(), // ios binary cookies iosCookiesGet: (): IIosCookie[] => binarycookies.get(), // ios nsurlcredentialstorage iosCredentialStorage: (): ICredential[] => credentialstorage.dump(), // ios filesystem iosFileCwd: (): string => iosfilesystem.pwd(), iosFileDelete: (path: string): boolean => iosfilesystem.deleteFile(path), iosFileDownload: (path: string): string | Buffer => iosfilesystem.readFile(path), iosFileExists: (path: string): boolean => iosfilesystem.exists(path), iosFileLs: (path: string): IIosFileSystem => iosfilesystem.ls(path), iosFilePathIsFile: (path: string): boolean => iosfilesystem.pathIsFile(path), iosFileReadable: (path: string): boolean => iosfilesystem.readable(path), iosFileUpload: (path: string, data: string): void => iosfilesystem.writeFile(path, data), iosFileWritable: (path: string): boolean => iosfilesystem.writable(path), // ios heap iosHeapEvaluateJs: (pointer: string, js: string): void => heap.evaluate(pointer, js), iosHeapExecMethod: (pointer: string, method: string, returnString: boolean): void => heap.callInstanceMethod(pointer, method, returnString), iosHeapPrintIvars: (pointer: string, toUTF8: boolean): [string, any[string]] => heap.getIvars(pointer, toUTF8), iosHeapPrintLiveInstances: (clazz: string): IHeapObject[] => heap.getInstances(clazz), iosHeapPrintMethods: (pointer: string): [string, any[string]] => heap.getMethods(pointer), // ios hooking iosHookingGetClassMethods: (className: string, includeParents: boolean): string[] => hooking.getClassMethods(className, includeParents), iosHookingGetClasses: () => hooking.getClasses(), iosHookingSetReturnValue: (selector: string, returnVal: boolean): void => hooking.setMethodReturn(selector, returnVal), iosHookingWatch: (pattern: string, dargs: boolean, dbt: boolean, dret: boolean, dparents: boolean) => hooking.watch(pattern, dargs, dbt, dret, dparents), iosHookingSearch: (pattern: string): ApiResolverMatch[] => hooking.search(pattern), // ios crypto monitoring iosMonitorCryptoEnable: (): void => ioscrypto.monitor(), // jailbreak detection iosJailbreakDisable: (): void => iosjailbreak.disable(), iosJailbreakEnable: (): void => iosjailbreak.enable(), // plist files iosPlistRead: (path: string): string => plist.read(path), // ios user interface iosUiAlert: (message: string): void => userinterface.alert(message), iosUiBiometricsBypass: (): void => userinterface.biometricsBypass(), iosUiScreenshot: (): any => userinterface.take(), iosUiWindowDump: (): string => userinterface.dump(), // ios ssl pinning iosPinningDisable: (quiet: boolean): void => sslpinning.disable(quiet), // ios pasteboard iosMonitorPasteboard: (): void => pasteboard.monitor(), // ios frameworks & bundles iosBundlesGetBundles: (): IFramework[] => bundles.getBundles(BundleType.NSBundleAllBundles), iosBundlesGetFrameworks: (): IFramework[] => bundles.getBundles(BundleType.NSBundleFramework), // ios keychain iosKeychainAdd: (account: string, service: string, data: string): boolean => ioskeychain.add(account, service, data), iosKeychainRemove: (account: string, service: string): void => ioskeychain.remove(account, service), iosKeychainUpdate: (account: string, service: string, newData: string): void => ioskeychain.update(account, service, newData), iosKeychainEmpty: (): void => ioskeychain.empty(), iosKeychainList: (smartDecode: boolean = false): IKeychainItem[] => ioskeychain.list(smartDecode), iosKeychainListRaw: (): void => ioskeychain.listRaw(), // ios nsuserdefaults iosNsuserDefaultsGet: (): NSUserDefaults | any => nsuserdefaults.get(), }; ================================================ FILE: agent/src/rpc/jobs.ts ================================================ import * as j from "../lib/jobs.js"; export const jobs = { // jobs jobsGet: () => j.all(), jobsKill: (ident: number) => j.kill(ident), }; ================================================ FILE: agent/src/rpc/memory.ts ================================================ import * as m from "../generic/memory.js"; export const memory = { memoryDump: (address: string, size: number) => m.dump(address, size), memoryListExports: (name: string): ModuleExportDetails[] => m.listExports(name), memoryListModules: (): Module[] => m.listModules(), memoryListRanges: (protection: string): RangeDetails[] => m.listRanges(protection), memorySearch: (pattern: string, onlyOffsets: boolean): string[] => m.search(pattern, onlyOffsets), memoryReplace: (pattern: string, replace: number[]): string[] => m.replace(pattern, replace), memoryWrite: (address: string, value: number[]): void => m.write(address, value), }; ================================================ FILE: agent/src/rpc/other.ts ================================================ import * as custom from "../generic/custom.js"; import * as http from "../generic/http.js"; export const other = { evaluate: (js: string): void => custom.evaluate(js), // http server httpServerStart: (p: string, port: number): void => http.start(p, port), httpServerStatus: (): void => http.status(), httpServerStop: (): void => http.stop(), }; ================================================ FILE: agent/tsconfig.json ================================================ { "compilerOptions": { "target": "es2020", "lib": ["es2020"], "allowJs": true, "noEmit": true, "strict": true, "module": "Node16", "esModuleInterop": true, "noImplicitAny": false, "strictNullChecks": false } } ================================================ FILE: agent/tslint.json ================================================ { "defaultSeverity": "warning", "extends": [ "tslint:recommended" ], "jsRules": {}, "rules": { "no-namespace": false }, "rulesDirectory": [], "alwaysShowStatus": true } ================================================ FILE: objection/__init__.py ================================================ import sys from importlib import metadata from pathlib import Path import tomllib def _load_version() -> str: """ Prefer the installed package metadata and fall back to pyproject.toml when running from a checkout. """ try: return metadata.version("objection") except metadata.PackageNotFoundError: pyproject_path = Path(__file__).resolve().parent.parent / "pyproject.toml" try: with pyproject_path.open("rb") as f: return tomllib.load(f)["project"]["version"] except Exception: return "0.0.0" __version__ = _load_version() # helper containing a python 3 related warning # if this is run with python 2 if sys.version_info < (3,): raise ImportError( ''' You are running objection {0} on Python 2 Unfortunately objection {0} and above are not compatible with Python 2. That's a bummer; sorry about that. Make sure you have Python 3, pip >= and setuptools >= 24.2 to avoid these kinds of issues in the future: $ pip install pip setuptools --upgrade You could also setup a virtual Python 3 environment. $ pip install pip setuptools --upgrade $ pip install virtualenv $ virtualenv --python=python3 ~/virt-python3 $ source ~/virt-python3/bin/activate This will make an isolated Python 3 installation available and active, ready to install and use objection. '''.format(__version__)) ================================================ FILE: objection/api/__init__.py ================================================ ================================================ FILE: objection/api/app.py ================================================ from flask import Flask from . import rpc from . import script def create_app() -> Flask: """ Creates a new Flask instance for the objection API :return: """ app = Flask(__name__) app.register_blueprint(rpc.bp) app.register_blueprint(script.bp) return app ================================================ FILE: objection/api/rpc.py ================================================ from flask import Blueprint, jsonify, request, abort from objection.state.connection import state_connection from ..utils.helpers import to_snake_case bp = Blueprint('rpc', __name__, url_prefix='/rpc') @bp.route('/invoke/', methods=('GET', 'POST')) def invoke(method): """ Bridge a call to the Frida RPC. Endpoints may be sourced from the agent's RPC exports. Responses are JSON encoded by default, but can be raw by adding ?json=false as a query string parameter. :param method: :return: """ method = to_snake_case(method) # post requests require a little more validation, so do that if request.method == 'POST': # ensure we have some JSON formatted post data post_data = request.get_json(force=True, silent=True) if not post_data: return abort(jsonify(message='POST request without a valid body received')) try: rpc = state_connection.get_api() except Exception as e: return abort(jsonify(message='Failed to talk to the Frida RPC: {e}'.format(e=str(e)))) try: # invoke the method based on the http request type if request.method == 'POST': response = getattr(rpc, method)(*post_data.values()) if request.method == 'GET': response = getattr(rpc, method)() if 'json' in request.args and request.args.get('json').lower() == 'false': return response except Exception as e: return abort(jsonify(message='Failed to call method: {e}'.format(e=str(e)))) return jsonify(response) ================================================ FILE: objection/api/script.py ================================================ from flask import Blueprint, jsonify, request, abort from objection.state.connection import state_connection bp = Blueprint('script', __name__, url_prefix='/script') @bp.route('/runonce', methods=('POST',)) def runonce(): """ Run an arbitrary script in the connected frida enabled device. Responses are JSON encoded by default, but can be raw by adding ?json=false as a query string parameter. :return: """ source = request.data.decode('utf-8') if len(source) <= 0: return abort(jsonify(message='Missing or empty script received')) try: # run the script response = state_connection.get_agent().single(source) if 'json' in request.args and request.args.get('json').lower() == 'false': return response except Exception as e: return abort(jsonify(message='Script failed to run: {e}'.format(e=str(e)))) return jsonify(response) ================================================ FILE: objection/commands/__init__.py ================================================ ================================================ FILE: objection/commands/android/__init__.py ================================================ ================================================ FILE: objection/commands/android/clipboard.py ================================================ from objection.state.connection import state_connection def monitor(args: list = None) -> None: """ Starts a new objection job that monitors the Android clipboard and reports on new strings found. :param args: :return: """ api = state_connection.get_api() api.android_monitor_clipboard() ================================================ FILE: objection/commands/android/command.py ================================================ import click from objection.state.connection import state_connection def execute(args: list) -> None: """ Runs a shell command on an Android device. :param args: :return: """ command = ' '.join(args) click.secho('Running shell command: {0}\n'.format(command), dim=True) api = state_connection.get_api() response = api.android_shell_exec(command) if 'stdOut' in response and len(response['stdOut']) > 0: click.secho(response['stdOut'], bold=True) if 'stdErr' in response and len(response['stdErr']) > 0: click.secho(response['stdErr'], bold=True, fg='red') ================================================ FILE: objection/commands/android/general.py ================================================ from objection.state.connection import state_connection def deoptimise(args: list) -> None: """ Forces the VM to execute everything with its interpreter. Necessary to prevent optimizations from bypassing method hooks in some cases. Ref: https://frida.re/docs/javascript-api/ :param args: :return: """ api = state_connection.get_api() api.android_deoptimize() ================================================ FILE: objection/commands/android/generate.py ================================================ import os import click from objection.state.connection import state_connection def clazz(args: list) -> None: """ Simply echoes the source for a generic Hook Manager sample for Objective-C hooks with Frida. :param args: :return: """ js_path = os.path.join( os.path.abspath(os.path.dirname(__file__)), '../../utils/assets', 'javahookmanager.js' ) with open(js_path, 'r') as f: click.secho(f.read(), dim=True) def simple(args: list) -> None: """ Generate simple hooks for all methods in a Java class. :param args: :return: """ if len(args) <= 0: click.secho('Usage: android hooking generate simple ', bold=True) return classname = args[0] api = state_connection.get_api() methods = api.android_hooking_get_class_methods(classname, False) if len(methods) <= 0: click.secho('No class / methods found') return # nasty! :D unique_methods = set([x.split('(')[0].split('.')[-1] for x in methods]) for method in unique_methods: hook = """ Java.perform(function() { var clazz = Java.use('{clazz}'); clazz.{method}.implementation = function() { // return clazz.{method}.apply(this, arguments); } }); """.replace('{clazz}', classname).replace('{method}', method) click.secho(hook, dim=True) ================================================ FILE: objection/commands/android/heap.py ================================================ import pprint import click from prompt_toolkit import prompt from prompt_toolkit.lexers import PygmentsLexer from pygments.lexers.javascript import JavascriptLexer from tabulate import tabulate from objection.state.connection import state_connection def _should_ignore_methods_with_arguments(args) -> bool: """ Check if the --without-arguments flag exists :param args: :return: """ return len(args) > 0 and '--without-arguments' in args def _should_return_as_string(args) -> bool: """ Check if the --return-string flag exists :param args: :return: """ return len(args) > 0 and '--return-string' in args def instances(args: list) -> None: """ Asks the agent to print the currently live instances of a particular class :param args: :return: """ if len(args) < 1: click.secho('Usage: android heap search instances (eg: com.example.test)', bold=True) return target_class = args[0] api = state_connection.get_api() instance_results = api.android_heap_get_live_class_instances(target_class) if len(instance_results) <= 0: return click.secho(tabulate( [[ entry['hashcode'], entry['classname'], entry['tostring'], ] for entry in instance_results], headers=['Hashcode', 'Class', 'toString()'], )) def methods(args: list) -> None: """ Get the methods available on a handle :param args: :return: """ if len(args) < 1: click.secho('Usage: android heap print methods (eg: 24688232)', bold=True) return target_handle = int(args[0]) api = state_connection.get_api() method_results = api.android_heap_print_methods(target_handle) # apply argument filters # we assume methods that end with braces don't need arguments if _should_ignore_methods_with_arguments(args): method_results[1] = list(filter(lambda x: '()' in x, method_results[1])) click.secho(tabulate( [[ entry, ] for entry in method_results], headers=['Method'], )) def execute(args: list) -> None: """ Executes a method on a handle which is assumed to be a Java class instance. :param args: :return: """ if len(args) < 1: click.secho('Usage: android heap execute method (eg: 24688232)', bold=True) return target_handle = int(args[0]) method = args[1] api = state_connection.get_api() exec_results = api.android_heap_execute_handle_method(target_handle, method, _should_return_as_string(args)) if exec_results: if isinstance(exec_results, dict): click.secho(pprint.pformat(exec_results)) else: click.secho(str(exec_results)) def fields(args: list) -> None: """ :param args: :return: """ if len(args) < 1: click.secho('Usage: android heap print fields (eg: 24688232)', bold=True) return target_handle = int(args[0]) api = state_connection.get_api() field_results = api.android_heap_print_fields(target_handle) click.secho(tabulate( [[ value['name'], value['value'] ] for value in field_results], headers=['Name', 'Value'], )) def evaluate(args: list) -> None: """ Evaluates JavaScript on a handle :param args: :return: """ if len(args) < 1: click.secho('Usage: android heap execute js (eg: 24688232)', bold=True) return target_handle = int(args[0]) js = prompt( click.secho('(The hashcode at `{handle}` will be available as the `clazz` variable.)'.format( handle=target_handle ), dim=True), multiline=True, lexer=PygmentsLexer(JavascriptLexer), bottom_toolbar='JavaScript edit mode. [ESC] and then [ENTER] to accept. [CTRL] + C to cancel.').strip() click.secho('JavaScript capture complete. Evaluating...', dim=True) api = state_connection.get_api() api.android_heap_evaluate_handle_method(target_handle, js) ================================================ FILE: objection/commands/android/hooking.py ================================================ import json from typing import Optional import click from objection.state.connection import state_connection from objection.utils.helpers import clean_argument_flags def _is_pattern_or_constant(s: str) -> bool: """ Check if a provided pattern matches "CLASS!METHOD" :param s: :return: """ # No pattern case if "!" not in s: return True # Check if CLASS and METHOD is defined at all parts = s.split('!') if len(parts) != 2: return False elif len(parts[0]) == 0 or len(parts[1]) == 0: return False return True def _string_is_true(s: str) -> bool: """ Check if a string should be considered as "True" :param s: :return: """ return s.lower() in ('true', 'yes') def _should_dump_backtrace(args: list = None) -> bool: """ Check if --dump-backtrace is part of the arguments. :param args: :return: """ return '--dump-backtrace' in args def _should_watch(args: list = None) -> bool: """ Check if --dump-args is part of the arguments. :param args: :return: """ return '--watch' in args def _should_dump_args(args: list = None) -> bool: """ Check if --dump-args is part of the arguments. :param args: :return: """ return '--dump-args' in args def _should_dump_return_value(args: list = None) -> bool: """ Check if --dump-return is part of the arguments. :param args: :return: """ return '--dump-return' in args def _should_dump_json(args: list) -> bool: """ Check if --json is part of the arguments. :param args: :return: """ return '--json' in args def _should_be_quiet(args: list) -> bool: """ Check if --quiet is part of the arguments. :param args: :return: """ return '--quiet' in args def _should_print_only_classes(args: list = None) -> bool: """ Check if --only-classes is part of the arguments. :param args: :return: """ return '--only-classes' in args def _get_flag_value(flag: str, args: list) -> Optional[str]: """ Gets the value for a flag :param flag: :param args: :return: """ target = None for i in range(len(args)): if args[i] == flag: target = i + 1 if target is None: return None elif target < len(args): return args[target] else: return None def show_android_classes(args: list = None) -> None: """ Show the currently loaded classes. Note that Java classes are only loaded when they are used, so not all classes may be present. :return: """ api = state_connection.get_api() classes = api.android_hooking_get_classes() # print the enumerated classes for class_name in sorted(classes): click.secho(class_name) click.secho('\nFound {0} classes'.format(len(classes)), bold=True) def show_android_class_loaders(args: list = None) -> None: """ Show the currently registered class loaders. :return: """ api = state_connection.get_api() loaders = api.android_hooking_get_class_loaders() # print the enumerated classes for loader in sorted(loaders): click.secho('* {0}'.format(loader)) click.secho('\nFound {0} class loaders'.format(len(loaders)), bold=True) def show_android_class_methods(args: list = None) -> None: """ Shows the methods available on an Android class. :param args: :return: """ if len(clean_argument_flags(args)) <= 0: click.secho('Usage: android hooking list class_methods ', bold=True) return class_name = args[0] api = state_connection.get_api() methods = api.android_hooking_get_class_methods(class_name) # print the enumerated classes for class_name in sorted(methods): click.secho(class_name) click.secho('\nFound {0} method(s)'.format(len(methods)), bold=True) def notify(args: list = None) -> None: """ Notify when a class becomes available. :param args: :return: """ if len(clean_argument_flags(args)) <= 0: click.secho('Usage: android hooking notify ', bold=True) return query = args[0] if not _is_pattern_or_constant(query): click.secho('Incorrect query syntax, please use ! or just the class name', fg='red') return api = state_connection.get_api() should_watch = _should_watch(args) dump_arguments = _should_dump_args(args) dump_backtrace = _should_dump_backtrace(args) dump_return = _should_dump_return_value(args) api.android_hooking_lazy_watch_for_pattern(query, should_watch, dump_arguments, dump_return, dump_backtrace) def watch(args: list = None) -> None: """ Hook functions and print useful information when they are called. :param args: :return: """ if len(clean_argument_flags(args)) < 1: click.secho('Usage: android hooking watch ' '(eg: com.example.test, *com.example*!*, com.example.test!toString)' '(optional: --dump-args) ' '(optional: --dump-backtrace) ' '(optional: --dump-return)', bold=True) return query = args[0] if not _is_pattern_or_constant(query): click.secho('Incorrect query syntax, please use !', fg='red') return api = state_connection.get_api() api.android_hooking_watch(query, _should_dump_args(args), _should_dump_backtrace(args), _should_dump_return_value(args)) return def search(args: list = None) -> None: """ Enumerates the current Android application for classes and methods. :param args: :return: """ if len(clean_argument_flags(args)) <= 0: click.secho('Usage: android hooking search \'!\n\'' '(optional: --json )' '(optional: --only-classes)', bold=True) return query = args[0] if not _is_pattern_or_constant(query): click.secho('Incorrect query syntax, please use !', fg='red') return api = state_connection.get_api() results = api.android_hooking_enumerate(query) # Only get overloads if this flag is specified, otherwise just enumerating can be kind of slow if _should_dump_json(args): results_json = { 'meta': { 'runtime': 'java' } } for result in results: for _class in result['classes']: loader = result['loader'] if loader is not None: # # but we only care about the className start_index = loader.find('$className: ') + 12 start_part = loader[start_index:] if start_part.find('>'): end_index = start_part.find('>') else: end_index = start_part.find(' ') loader = start_part[:end_index] _class['overloads'] = api.android_hooking_get_class_methods_overloads(_class['name'], _class['methods'], loader) target_file = _get_flag_value('--json', args) if target_file: results_json['data'] = results with open(target_file, 'w') as fd: fd.write(json.dumps(results_json)) click.secho(f'JSON dumped to file {target_file}', bold=True) return # just print to the console for result in results: for _class in result['classes']: if _should_print_only_classes(args): print(_class['name']) continue for method in _class['methods']: print(f'{_class["name"]}.{method}') def show_registered_broadcast_receivers(args: list = None) -> None: """ Enumerate all registered BroadcastReceivers :return: """ api = state_connection.get_api() receivers = api.android_hooking_list_broadcast_receivers() for class_name in sorted(receivers): click.secho(class_name) click.secho('\nFound {0} classes'.format(len(receivers)), bold=True) def show_registered_services(args: list = None) -> None: """ Enumerate all registered Services :return: """ api = state_connection.get_api() services = api.android_hooking_list_services() for class_name in sorted(services): click.secho(class_name) click.secho('\nFound {0} classes'.format(len(services)), bold=True) def show_registered_activities(args: list = None) -> None: """ Enumerate all registered Activities :return: """ api = state_connection.get_api() activities = api.android_hooking_list_activities() for class_name in sorted(activities): click.secho(class_name) click.secho('\nFound {0} classes'.format(len(activities)), bold=True) def get_current_activity(args: list = None) -> None: """ Get the currently active activity :return: """ api = state_connection.get_api() activity = api.android_hooking_get_current_activity() click.secho('Activity: {0}'.format(activity['activity']), bold=True) click.secho('Fragment: {0}'.format(activity['fragment'])) def set_method_return_value(args: list = None) -> None: """ Sets a Java methods return value to a specified boolean. :param args: :return: """ if len(clean_argument_flags(args)) < 2: click.secho(('Usage: android hooking set return_value ' '"" "" (eg: "com.example.test.doLogin") ' ''), bold=True) return # make sure we got a true/false if args[-1].lower() not in ('true', 'false'): click.secho('Return value must be set to either true or false', bold=True) return class_name = args[0].replace('\'', '"') # fun! # check if we got an overload overload_filter = args[1].replace(' ', '') if len(args) == 3 else None retval = True if _string_is_true(args[-1]) else False api = state_connection.get_api() api.android_hooking_set_method_return(class_name, overload_filter, retval) ================================================ FILE: objection/commands/android/intents.py ================================================ import click from objection.state.connection import state_connection from objection.utils.helpers import clean_argument_flags def _should_dump_backtrace(args: list = None) -> bool: """ Check if --dump-backtrace is part of the arguments. :param args: :return: """ return '--dump-backtrace' in args def analyze_implicit_intents(args: list) -> None: """ Analyzes implicit intents in hooked methods. """ api = state_connection.get_api() should_backtrace = _should_dump_backtrace(args) api.android_intent_analyze(should_backtrace) if not should_backtrace: click.secho('Started implicit intent analysis', bold=True) else: click.secho('Started implicit intent analysis with backtrace', bold=True) def launch_activity(args: list) -> None: """ Launches an activity class using an Android Intent :param args: :return: """ if len(clean_argument_flags(args)) < 1: click.secho('Usage: android intent launch_activity ', bold=True) return intent_class = args[0] api = state_connection.get_api() api.android_intent_start_activity(intent_class) def launch_service(args: list) -> None: """ Launches an exported service using an Android Intent :param args: :return: """ if len(clean_argument_flags(args)) < 1: click.secho('Usage: android intent launch_service ', bold=True) return intent_class = args[0] api = state_connection.get_api() api.android_intent_start_service(intent_class) ================================================ FILE: objection/commands/android/keystore.py ================================================ import json import click from tabulate import tabulate from objection.state.connection import state_connection def _should_output_json(args: list) -> bool: """ Checks if --json is in the list of tokens received from the command line. :param args: :return: """ return len(args) > 0 and '--json' in args def entries(args: list = None) -> None: """ Lists entries in the Android KeyStore :param args: :return: """ api = state_connection.get_api() ks = api.android_keystore_list() output = [[x['alias'], x['is_key'], x['is_certificate']] for x in ks] click.secho(tabulate(output, headers=['Alias', 'Key', 'Certificate'])) def detail(args: list = None) -> None: """ Lists details of all items in the Android KeyStore :param args: :return: """ click.secho('Listing details for all items in the Android KeyStore...', dim=True) api = state_connection.get_api() ks = api.android_keystore_detail() if _should_output_json(args): click.secho(json.dumps(ks, indent=2, sort_keys=True)) return output = [[ x['keystoreAlias'], x['keyAlgorithm'], x['keySize'], ','.join(x['blockModes']), ','.join(x['encryptionPaddings']), ','.join(x['digests']), x['keyValidityStart'], x['origin'], x['purposes'], ','.join(x['signaturePaddings']), x['isInsideSecureHardware'], ] for x in ks] click.secho(tabulate(output, headers=[ 'Alias', 'Alg', 'Size', 'Modes', 'Paddings', 'Digests', 'Validity Start', 'Origin', 'Purposes', 'Sig Paddings', 'Sec Hardware' ])) def clear(args: list = None) -> None: """ Clears out an Android KeyStore :param args: :return: """ if not click.confirm('Are you sure you want to clear the Android keystore?'): return api = state_connection.get_api() api.android_keystore_clear() def watch(args: list = None) -> None: """ Watches usage of the Android KeyStore :param args: :return: """ api = state_connection.get_api() api.android_keystore_watch() ================================================ FILE: objection/commands/android/monitor.py ================================================ import click from objection.state.connection import state_connection def string_canary(args: list) -> None: """ Monitors for a string canary argument and reports when it is found. :param args: :return: """ if len(args) < 1: click.secho('Usage: android monitor canary (optional: )', bold=True) return target_class = args[0] api = state_connection.get_api() api.android_live_print_class_instances(target_class) ================================================ FILE: objection/commands/android/pinning.py ================================================ from objection.state.connection import state_connection def _should_be_quiet(args: list) -> bool: """ Checks if --quiet is part of the commands arguments. :param args: :return: """ return '--quiet' in args def android_disable(args: list = None) -> None: """ Starts a new objection job that hooks common classes and functions, applying new logic in an attempt to bypass SSL pinning. :param args: :return: """ api = state_connection.get_api() api.android_ssl_pinning_disable(_should_be_quiet(args)) ================================================ FILE: objection/commands/android/proxy.py ================================================ import click from objection.state.connection import state_connection def android_proxy_set(args: list = None) -> None: """ Sets a proxy specifically within the application. :param args: :return: """ if len(args) != 2: click.secho('Usage: android proxy set ', bold=True) return api = state_connection.get_api() api.android_proxy_set(args[0], args[1]) ================================================ FILE: objection/commands/android/root.py ================================================ from objection.state.connection import state_connection def disable(args: list = None) -> None: """ Performs a generic anti root detection. :param args: :return: """ api = state_connection.get_api() api.android_root_detection_disable() def simulate(args: list = None) -> None: """ Simulate a rooted environment. :param args: :return: """ api = state_connection.get_api() api.android_root_detection_enable() ================================================ FILE: objection/commands/command_history.py ================================================ import os import click from ..state.app import app_state def history(args: list) -> None: """ Lists the commands that have been run in the current session. :param args: :return: """ click.secho('Unique commands run in current session:', dim=True) for command in app_state.successful_commands: click.secho(command) def save(args: list) -> None: """ Save the current sessions command history to a file. :param args: :return: """ if len(args) <= 0: click.secho('Usage: commands save ', bold=True) return destination = os.path.expanduser(args[0]) if args[0].startswith('~') else args[0] with open(destination, 'w') as f: for command in app_state.successful_commands: f.write('{0}\n'.format(command)) click.secho('Saved commands to: {0}'.format(destination), fg='green') def clear(args: list) -> None: """ Clears the current sessions command history. :param args: :return: """ app_state.clear_command_history() click.secho('Command history cleared.', fg='green') ================================================ FILE: objection/commands/custom.py ================================================ import os import click import frida from prompt_toolkit import prompt from prompt_toolkit.lexers import PygmentsLexer from pygments.lexers.javascript import JavascriptLexer from ..state.connection import state_connection def evaluate(args: list) -> None: """ Evaluate JavaScript within the agent's context. :param args: :return: """ target_file = None # if we have an argument, let's assume it is a file path if len(args) > 0: target_file = args[0] p = os.path.expanduser(target_file) if os.path.exists(p): target_file = p else: click.secho('Could not find file {p}.'.format(p=target_file), fg='red') return if target_file: with open(target_file, 'r', encoding='utf-8') as f: javascript = ''.join(f.readlines()) else: javascript = prompt( multiline=True, lexer=PygmentsLexer(JavascriptLexer), bottom_toolbar='JavaScript edit mode. [ESC] and then [ENTER] to accept. [CTRL] + C to cancel.').strip() if len(javascript) <= 0: click.secho('JavaScript to evaluate appears empty. Skipping.', fg='yellow') return click.secho('JavaScript capture complete. Evaluating...', dim=True) try: state_connection.get_api().evaluate(javascript) except frida.core.RPCException as e: click.secho('Failed to load script: {}'.format(e), fg='red', bold=True) ================================================ FILE: objection/commands/device.py ================================================ import click from tabulate import tabulate from ..state.connection import state_connection from ..state.device import device_state, Android, Ios def get_environment(args: list = None) -> None: """ Get information about the current environment. This method will call the correct runtime specific method to get the information that it can. :param args: :return: """ if device_state.platform == Ios: _get_ios_environment() if device_state.platform == Android: _get_android_environment() def _get_ios_environment() -> None: """ Prints information about the iOS environment. This includes the current OS version as well as directories of interest for the current applications Documents, Library and main application bundle. :return: """ paths = state_connection.get_api().env_ios_paths() click.secho('') click.secho(tabulate(paths.items(), headers=['Name', 'Path'])) def _get_android_environment() -> None: """ Prints information about the Android environment. :return: """ paths = state_connection.get_api().env_android_paths() click.secho('') click.secho(tabulate(paths.items(), headers=['Name', 'Path'])) ================================================ FILE: objection/commands/filemanager.py ================================================ import os import tempfile import time import click from tabulate import tabulate from ..state.connection import state_connection from ..state.device import device_state, Ios, Android from ..state.filemanager import file_manager_state from ..utils.helpers import sizeof_fmt # variable used to cache entries from the ls-like # commands used in the below helpers. only used # by the _get_short_*_listing methods. _ls_cache = {} def _should_download_folder(args: list) -> bool: """ Checks if --json is in the list of tokens received from the command line. :param args: :return: """ return len(args) > 0 and '--folder' in args def cd(args: list) -> None: """ Change the current working directory of the device. While this method does not actually change any directories, it simply updates the value in the file_manager_state property that keeps record of the current directory. Before changing directories though, some checks are performed on the device to at least ensure that the destination directory exists. :param args: :return: """ if len(args) <= 0: click.secho('Usage: cd ', bold=True) return path = args[0] current_dir = pwd() # nothing to do if path == '.': return # moving one directory back device_path_separator = device_state.platform.path_separator if path == '..' or path == '..'+device_path_separator: split_path = os.path.split(current_dir) # nothing to do if we are already at root if len(split_path) == 1: return new_path = ''.join(split_path[:-1]) click.secho(new_path, fg='green', bold=True) file_manager_state.cwd = new_path return # if we got an absolute path, check if the path # actually exists, and then cd to it if we can if os.path.isabs(path): # assume the path does not exist by default does_exist = False # normalise path to remove '../' if '..'+device_path_separator in path: path = os.path.normpath(path).replace('\\', device_path_separator) # check for existence based on the runtime if device_state.platform == Ios: does_exist = _path_exists_ios(path) if device_state.platform == Android: does_exist = _path_exists_android(path) # if we checked with the device that the path exists # and it did, update the state manager, otherwise # show an error that the path may be invalid if does_exist: click.secho(path, fg='green', bold=True) file_manager_state.cwd = path return else: click.secho('Invalid path: `{0}`'.format(path), fg='red') # directory is not absolute, tack it on at the end and # see if its legit. else: proposed_path = device_path_separator.join([current_dir, path]) # normalise path to remove '../' if '..'+device_path_separator in proposed_path: proposed_path = os.path.normpath(proposed_path).replace('\\', device_path_separator) if proposed_path == '//': return # assume the proposed_path does not exist by default does_exist = False # check for existence based on the runtime if device_state.platform == Ios: does_exist = _path_exists_ios(proposed_path) if device_state.platform == Android: does_exist = _path_exists_android(proposed_path) # if we checked with the device that the path exists # and it did, update the state manager, otherwise # show an error that the path may be invalid if does_exist: click.secho(proposed_path, fg='green', bold=True) file_manager_state.cwd = proposed_path return else: click.secho('Invalid path: `{0}`'.format(proposed_path), fg='red') def path_exists(path: str) -> bool: """ Checks if a path exists on remote device. :param path: :return: """ if device_state.platform == Ios: return _path_exists_ios(path) if device_state.platform == Android: return _path_exists_android(path) def _path_exists_ios(path: str) -> bool: """ Checks an iOS device if a path exists. :param path: :return: """ api = state_connection.get_api() return api.ios_file_exists(path) def _path_exists_android(path: str) -> bool: """ Checks an Android device if a path exists. :param path: :return: """ api = state_connection.get_api() return api.android_file_exists(path) def pwd(args: list = None) -> str: """ Return the current working directory. If a record exists in the filemanager state, that directory is returned. Else, an environment specific call is made to the device to determine the directory it considers itself to be working from. :param args: :return: """ if file_manager_state.cwd is not None: return file_manager_state.cwd if device_state.platform == Ios: return _pwd_ios() if device_state.platform == Android: return _pwd_android() def pwd_print(args: list = None) -> None: """ Prints the current working directory. :param args: :return: """ click.secho('Current directory: {0}'.format(pwd())) def _pwd_ios() -> str: """ Execute a Frida hook that gets the current working directory from an iOS device. :return: """ api = state_connection.get_api() cwd = api.ios_file_cwd() # update the file_manager state's cwd file_manager_state.cwd = cwd return cwd def _pwd_android() -> str: """ Execute a Frida hook that gets the current working directory from an Android device. :return: """ api = state_connection.get_api() cwd = api.android_file_cwd() # update the file_manager state's cwd file_manager_state.cwd = cwd return cwd def ls(args: list) -> None: """ Get a directory listing for a path on a device. If no path is provided, the current working directory is used. :param args: :return: """ # check if we have received a path to ls for. if len(args) <= 0: path = pwd() else: path = args[0] if not os.path.isabs(path): path = device_state.platform.path_separator.join([pwd(), path]) # based on the runtime, execute the correct ls method. if device_state.platform == Ios: _ls_ios(path) if device_state.platform == Android: _ls_android(path) def _ls_ios(path: str) -> None: """ List files implementation for iOS. See: http://www.stanford.edu/class/cs193p/cgi-bin/drupal/system/files/lectures/09_Data.pdf :param path: :return: """ api = state_connection.get_api() data = api.ios_file_ls(path) def _get_key_if_exists(attribs, key): """ Small helper to grab keys where some may or may not exist in the file attributes. :param attribs: :param key: :return: """ if key in attribs: return attribs[key] return 'n/a' def _humanize_size_if_possible(size: str) -> str: """ Small helper method used to 'humanize' file sizes if the file size is not recorded as 'n/a' :param size: :return: """ return sizeof_fmt(int(size)) if size != 'n/a' else 'n/a' # if the directory was readable, dump the filesystem listing # and attributes to screen. click.secho(tabulate( [[ _get_key_if_exists(file_data['attributes'], 'NSFileType').replace('NSFileType', ''), _get_key_if_exists(file_data['attributes'], 'NSFilePosixPermissions'), _get_key_if_exists(file_data['attributes'], 'NSFileProtectionKey').replace('NSFileProtection', ''), # file read / write permissions file_data['readable'], file_data['writable'], # owner name and uid _get_key_if_exists(file_data['attributes'], 'NSFileOwnerAccountName') + ' (' + _get_key_if_exists(file_data['attributes'], 'NSFileOwnerAccountID') + ')', # group name and gid _get_key_if_exists(file_data['attributes'], 'NSFileGroupOwnerAccountName') + ' (' + _get_key_if_exists(file_data['attributes'], 'NSFileGroupOwnerAccountID') + ')', _humanize_size_if_possible(_get_key_if_exists(file_data['attributes'], 'NSFileSize')), _get_key_if_exists(file_data['attributes'], 'NSFileCreationDate'), file_name, ] for file_name, file_data in data['files'].items()], headers=[ 'NSFileType', 'Perms', 'NSFileProtection', 'Read', 'Write', 'Owner', 'Group', 'Size', 'Creation', 'Name' ], )) if data['readable'] else None # handle the permissions summary for this directory click.secho('\nReadable: {0} Writable: {1}'.format(data['readable'], data['writable']), bold=True) def _ls_android(path: str) -> None: """ Lit files implementation for Android devices. :param path: :return: """ api = state_connection.get_api() data = api.android_file_ls(path) def _timestamp_to_str(stamp: str) -> str: """ Small helper method to convert the timestamps we get from the Android filesystem to human readable ones. :param stamp: :return: """ # convert the time to an integer stamp = int(stamp) if stamp > 0: return time.strftime('%Y-%m-%d %H:%M:%S GMT', time.gmtime(stamp / 1000.0)) return 'n/a' click.secho(tabulate( [[ 'Directory' if file_data['attributes']['isDirectory'] else 'File', _timestamp_to_str(file_data['attributes']['lastModified']), # read / write permissions file_data['readable'], file_data['writable'], file_data['attributes']['isHidden'], sizeof_fmt(float(file_data['attributes']['size'])), file_name, ] for file_name, file_data in data['files'].items()], headers=[ 'Type', 'Last Modified', 'Read', 'Write', 'Hidden', 'Size', 'Name' ], )) if data['readable'] else None click.secho('\nReadable: {0} Writable: {1}'.format(data['readable'], data['writable']), bold=True) def download(args: list) -> None: """ Downloads a file from a remote filesystem and stores it locally. This method is simply a proxy to the actual download methods used for the appropriate environment. :param args: :return: """ if len(args) < 1: click.secho('Usage: filesystem download (optional: )', bold=True) return # determine the source and destination file names. # if we didnt get a specification of where to dump the file, # assume the same name should be used locally. source = args[0] destination = args[1] if len(args) > 1 else os.path.basename(source) should_download_folder = _should_download_folder(args) if device_state.platform == Ios: _download_ios(source, destination, should_download_folder) if device_state.platform == Android: _download_android(source, destination, should_download_folder) def _download_ios(path: str, destination: str, should_download_folder: bool, path_root: bool = True) -> None: """ Download a file from an iOS filesystem and store it locally. :param path: :param destination: :return: """ # if the path we got is not absolute, join it with the # current working directory if not os.path.isabs(path): path = device_state.platform.path_separator.join([pwd(), path]) api = state_connection.get_api() if path_root: click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) if not api.ios_file_readable(path): click.secho('Unable to download file. File is not readable.', fg='red') return if not api.ios_file_path_is_file(path): if not should_download_folder: click.secho('To download folders, specify --folder.', fg='yellow') return if os.path.exists(destination): click.secho('The target path already exists.', fg='yellow') return os.makedirs(destination) if path_root: if not click.confirm('Do you want to download the full directory?', default=True): click.secho('Download aborted.', fg='yellow') return click.secho('Downloading directory recursively...', fg='green') data = api.ios_file_ls(path) for name, _ in data['files'].items(): sub_path = device_state.platform.path_separator.join([path, name]) sub_destination = os.path.join(destination, name) _download_ios(sub_path, sub_destination, True, False) if path_root: click.secho('Recursive download finished.', fg='green') return if path_root: click.secho('Streaming file from device...', dim=True) file_data = api.ios_file_download(path) if path_root: click.secho('Writing bytes to destination...', dim=True) with open(destination, 'wb') as fh: fh.write(bytearray(file_data['data'])) click.secho('Successfully downloaded {0} to {1}'.format(path, destination), bold=True) def _download_android(path: str, destination: str, should_download_folder: bool, path_root: bool = True) -> None: """ Download a file from the Android filesystem and store it locally. :param path: :param destination: :return: """ # if the path we got is not absolute, join it with the # current working directory if not os.path.isabs(path): path = device_state.platform.path_separator.join([pwd(), path]) api = state_connection.get_api() if path_root: click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) if not api.android_file_readable(path): click.secho('Unable to download file. Target path is not readable.', fg='red') return if not api.android_file_path_is_file(path): if not should_download_folder: click.secho('To download folders, specify --folder.', fg='yellow') return if os.path.exists(destination): click.secho('The target path already exists.', fg='yellow') return os.makedirs(destination) if path_root: if not click.confirm('Do you want to download the full directory?', default=True): click.secho('Download aborted.', fg='yellow') return click.secho('Downloading directory recursively...', fg='green') data = api.android_file_ls(path) for name, _ in data['files'].items(): sub_path = device_state.platform.path_separator.join([path, name]) sub_destination = os.path.join(destination, name) _download_android(sub_path, sub_destination, True, False) if path_root: click.secho('Recursive download finished.', fg='green') return if path_root: click.secho('Streaming file from device...', dim=True) file_data = api.android_file_download(path) if path_root: click.secho('Writing bytes to destination...', dim=True) with open(destination, 'wb') as fh: fh.write(bytearray(file_data['data'])) click.secho('Successfully downloaded {0} to {1}'.format(path, destination), bold=True) def upload(args: list) -> None: """ Uploads a local file to the remote operating system. This method is just a proxy method to the real upload method used based on the runtime that is available. :param args: :return: """ if len(args) < 1: click.secho('Usage: filesystem upload (optional: )', bold=True) return source = args[0] destination = args[1] if len(args) > 1 else device_state.platform.path_separator.join( [pwd(), os.path.basename(source)]) if device_state.platform == Ios: _upload_ios(source, destination) if device_state.platform == Android: _upload_android(source, destination) def _upload_ios(path: str, destination: str) -> None: """ Upload a file to a remote iOS filesystem. :param path: :param destination: :return: """ if not os.path.isabs(destination): destination = device_state.platform.path_separator.join([pwd(), destination]) api = state_connection.get_api() click.secho('Uploading {0} to {1}'.format(path, destination), fg='green', dim=True) # if we cant read the file, just stop if not api.ios_file_writable(os.path.dirname(destination)): click.secho('Unable to upload file. Destination is not writable.', fg='red') return click.secho('Reading source file...', dim=True) with open(path, 'rb') as f: data = f.read().hex() click.secho('Sending file to device for writing...', dim=True) api.ios_file_upload(destination, data) click.secho('Uploaded: {0}'.format(destination), dim=True) # unset the cache key for this directory so the next short listing # will have updated contents if os.path.dirname(destination) in _ls_cache: del _ls_cache[os.path.dirname(destination)] def _upload_android(path: str, destination: str) -> None: """ Upload a file to a remote Android filesystem. :param path: :param destination: :return: """ if not os.path.isabs(destination): destination = device_state.platform.path_separator.join([pwd(), destination]) api = state_connection.get_api() click.secho('Uploading {0} to {1}'.format(path, destination), fg='green', dim=True) # if we cant read the file, just stop if not api.android_file_writable(os.path.dirname(destination)): click.secho('Unable to upload file. Destination is not writable.', fg='red') return click.secho('Reading source file...', dim=True) with open(path, 'rb') as f: data = f.read().hex() click.secho('Sending file to device for writing...', dim=True) api.android_file_upload(destination, data) click.secho('Uploaded: {0}'.format(destination), dim=True) # unset the cache key for this directory so the next short listing # will have updated contents if os.path.dirname(destination) in _ls_cache: del _ls_cache[os.path.dirname(destination)] def rm(args: list) -> None: """ Remove a file from the remote filesystem. :param args: :return: """ if len(args) < 1: click.secho('Usage: rm ', bold=True) return target = args[0] if not os.path.isabs(target): target = device_state.platform.path_separator.join([pwd(), target]) if not click.confirm('Really delete {0} ?'.format(target)): click.secho('Not deleting {0}'.format(target), dim=True) return if device_state.platform == Ios: _rm_ios(target) if device_state.platform == Android: _rm_android(target) def _rm_android(t: str) -> None: """ Removes a file from an Android device. :param t: :return: """ api = state_connection.get_api() if not _path_exists_android(t): click.secho('{0} does not exist'.format(t), fg='red') return if api.android_file_delete(t): click.secho('{0} successfully deleted'.format(t), fg='green') # update the file system cache entry if os.path.dirname(t) in _ls_cache: del _ls_cache[os.path.dirname(t)] def _rm_ios(t: str) -> None: """ Removes a file from an iOS device. :param t: :return: """ api = state_connection.get_api() if not _path_exists_ios(t): click.secho('{0} does not exist'.format(t), fg='red') return if api.ios_file_delete(t): click.secho('{0} successfully deleted'.format(t), fg='green') # update the file system cache entry if os.path.dirname(t) in _ls_cache: del _ls_cache[os.path.dirname(t)] def cat(args: list): """ Downloads a file from a remote filesystem and echos it's contents This method is simply a proxy to the relevant download methods that echoes the contents and cleans up after itself. :param args: :return: """ if len(args) < 1: click.secho('Usage: filesystem cat ', bold=True) return # determine the source and destination file names. # if we didnt get a specification of where to dump the file, # assume the same name should be used locally. source = args[0] _, destination = tempfile.mkstemp('.file') if device_state.platform == Ios: _download_ios(source, destination, False) if device_state.platform == Android: _download_android(source, destination, False) click.secho('====', dim=True) with open(destination, 'r', encoding='utf-8', errors='ignore') as f: print(f.read(), end='', ) click.secho('====', dim=True) os.remove(destination) def _get_short_ios_listing() -> list: """ Get a shortened file and directory listing for iOS devices. :return: """ # default to the pwd. this method is for tab # completions anyways. directory = pwd() # the response for this directory resp = [] # check our cheap cache if we have a listing if directory in _ls_cache: return _ls_cache[directory] api = state_connection.get_api() data = api.ios_file_ls(directory) # loop the response, marking entries as either being # a file or a directory. this response will be stored # in the _ls_cache too. for name, attribs in data['files'].items(): # attributes key contains the type attributes = attribs['attributes'] # if the attributes dict does not have the file type, # just continue as we cant be sure what it is. if 'NSFileType' not in attributes: continue # append a tuple with name, type resp.append((name, 'directory' if attributes['NSFileType'] == 'NSFileTypeDirectory' else 'file')) # cache the response so its faster next time! _ls_cache[directory] = resp # grab the output lets seeeeee return resp def _get_short_android_listing() -> list: """ Get a shortened file and directory listing for Android devices. :return: """ # default to the pwd. this method is for tab # completions anyways. directory = pwd() # the response for this directory resp = [] # check our cheap cache if we have a listing if directory in _ls_cache: return _ls_cache[directory] api = state_connection.get_api() data = api.android_file_ls(directory) # loop the response, marking entries as either being # a file or a directory. this response will be stored # in the _ls_cache too. for name, attribs in data['files'].items(): attributes = attribs['attributes'] # append a tuple with name, type resp.append((name, 'directory' if attributes['isDirectory'] else 'file')) # cache the response so its faster next time! _ls_cache[directory] = resp # grab the output lets seeeeee return resp def list_folders_in_current_fm_directory() -> dict: """ Return folders in the current working directory of the Frida attached device. """ resp = {} # get the folders based on the runtime if device_state.platform == Ios: response = _get_short_ios_listing() elif device_state.platform == Android: response = _get_short_android_listing() # looks like we landed in an unknown runtime. # just return. else: return resp # loop the response to get entries for the 'directory' # type. for entry in response: file_name, file_type = entry if file_type == 'directory': if ' ' in file_name: resp[f"'{file_name}'"] = file_name else: resp[file_name] = file_name return resp def list_files_in_current_fm_directory() -> dict: """ Return files in the current working directory of the Frida attached device. """ resp = {} # check for existence based on the runtime if device_state.platform == Ios: response = _get_short_ios_listing() elif device_state.platform == Android: response = _get_short_android_listing() # looks like we landed in an unknown runtime. # just return. else: return resp # loop the response to get entries for the 'directory' # type. for entry in response: file_name, file_type = entry if file_type == 'file': if ' ' in file_name: resp[f"'{file_name}'"] = file_name else: resp[file_name] = file_name return resp def list_content_in_current_fm_directory() -> dict: """ Return folders and files in the current working directory of the Frida attached device. """ resp = {} # check for existence based on the runtime if device_state.platform == Ios: response = _get_short_ios_listing() elif device_state.platform == Android: response = _get_short_android_listing() # looks like we landed in an unknown runtime. # just return. else: return resp # loop the response to get entries. for entry in response: name, _ = entry if ' ' in name: resp[f"'{name}'"] = name else: resp[name] = name return resp ================================================ FILE: objection/commands/frida_commands.py ================================================ import os import click from tabulate import tabulate from objection.state.connection import state_connection from ..utils.helpers import sizeof_fmt, clean_argument_flags def _should_disable_exception_handler(args: list = None) -> bool: """ Checks the arguments if '--no-exception-handler' is part of it. :param args: :return: """ return len(args) > 0 and '--no-exception-handler' in args def frida_environment(args: list = None) -> None: """ Prints information about the current Frida environment. :param args: :return: """ frida_env = state_connection.get_api().env_frida() click.secho(tabulate([ ('Frida Version', frida_env['version']), ('Process Architecture', frida_env['arch']), ('Process Platform', frida_env['platform']), ('Debugger Attached', frida_env['debugger']), ('Script Runtime', frida_env['runtime']), ('Frida Heap Size', sizeof_fmt(frida_env['heap'])) ])) def ping(args: list = None) -> None: """ Pings the agent. :param args: :return: """ agent = state_connection.get_api() if agent.ping(): click.secho('The agent responds ok!', fg='green') else: click.secho('The agent did not respond ok!', fg='red') def load_background(args: list = None) -> None: """ Loads a Frida script and runs it in the background. :param args: :return: """ if len(clean_argument_flags(args)) <= 0: click.secho('Usage: import (optional name)', bold=True) return source = args[0] # support ~ syntax if source.startswith('~'): source = os.path.expanduser(source) if not os.path.isfile(source): click.secho('Unable to import file {0}'.format(source), fg='red') return # read the hook sources with open(source, 'r') as f: hook = ''.join(f.read()) agent = state_connection.get_agent() agent.attach_script(source, hook) ================================================ FILE: objection/commands/http.py ================================================ import click from ..commands.filemanager import pwd from ..state.connection import state_connection def start(args: list) -> None: """ Start's an http server, exposing the mobile devices filesystem. :param args: :return: """ port = 9000 if len(args) > 0: port = int(args[0]) click.secho('Starting server on port {port}...'.format(port=port), dim=True) api = state_connection.get_api() api.http_server_start(pwd(), port) def stop(args: list) -> None: """ Stops the on device HTTP server :param args: :return: """ api = state_connection.get_api() api.http_server_stop() def status(args: list) -> None: """ Get the status of the HTTP server :param args: :return: """ api = state_connection.get_api() api.http_server_status() ================================================ FILE: objection/commands/ios/__init__.py ================================================ ================================================ FILE: objection/commands/ios/binary.py ================================================ import click from tabulate import tabulate from objection.state.connection import state_connection def info(args: list) -> None: """ Gets information about binaries and frameworks. :param args: :return: """ api = state_connection.get_api() binary_info = api.ios_binary_info() click.secho(tabulate( [[ name, information['type'], information['encrypted'], information['pie'], information['arc'], information['canary'], information['stackExec'], information['rootSafe'] ] for name, information in binary_info.items()], headers=['Name', 'Type', 'Encrypted', 'PIE', 'ARC', 'Canary', 'Stack Exec', 'RootSafe'], )) ================================================ FILE: objection/commands/ios/bundles.py ================================================ import click from tabulate import tabulate from objection.state.connection import state_connection from objection.utils.helpers import pretty_concat def _should_include_apple_bundles(args: list) -> bool: """ Checks if arguments have the --include-apple-frameworks flag :param args: :return: """ return len(args) > 0 and '--include-apple-frameworks' in args def _should_print_full_path(args: list) -> bool: """ Checks if arguments have the --full-path flag :param args: :return: """ return len(args) > 0 and '--full-path' in args def _is_apple_bundle(bundle: str) -> bool: """ Check if a string bundle identifier is considered an Apple bundle based on the fact that the bundle name starts with the string com.apple :param bundle: :return: """ # This is a bit of an assumption, but ok. if bundle is None: return False if bundle.startswith('com.apple'): return True return False def show_frameworks(args: list = None) -> None: """ Prints information about bundles that represent frameworks. https://developer.apple.com/documentation/foundation/nsbundle/1408056-allframeworks?language=objc :param args: :return: """ api = state_connection.get_api() frameworks = api.ios_bundles_get_frameworks() # apply filters if not _should_include_apple_bundles(args): frameworks = [f for f in frameworks if not _is_apple_bundle(f['bundle'])] # Just dump it to the screen click.secho(tabulate( [[ entry['executable'], entry['bundle'], entry['version'], entry['path'] if _should_print_full_path(args) else pretty_concat(entry['path'], 40, True), ] for entry in frameworks ], headers=['Executable', 'Bundle', 'Version', 'Path'], )) def show_bundles(args: list = None) -> None: """ Prints information about bundles that are not necessarily frameworks https://developer.apple.com/documentation/foundation/nsbundle/1413705-allbundles?language=objc :param args: :return: """ api = state_connection.get_api() bundles = api.ios_bundles_get_bundles() # Just dump it to the screen click.secho(tabulate( [[ entry['executable'], entry['bundle'], entry['version'], entry['path'] if _should_print_full_path(args) else pretty_concat(entry['path'], 40, True), ] for entry in bundles ], headers=['Executable', 'Bundle', 'Version', 'Path'], )) ================================================ FILE: objection/commands/ios/cookies.py ================================================ import json import click from tabulate import tabulate from objection.state.connection import state_connection def _should_dump_json(args: list) -> bool: """ Check if --json is part of the arguments. :param args: :return: """ return '--json' in args def get(args: list) -> None: """ Gets cookies using the iOS NSHTTPCookieStorage sharedHTTPCookieStorage and prints them to the screen. :param args: :return: """ api = state_connection.get_api() cookies = api.ios_cookies_get() if _should_dump_json(args): print(json.dumps(cookies, indent=4)) return if len(cookies) <= 0: click.secho('No cookies found') return click.secho(tabulate( [[ cookie['name'], cookie['value'], cookie['expiresDate'], cookie['domain'], cookie['path'], cookie['isSecure'], cookie['isHTTPOnly'] ] for cookie in cookies], headers=['Name', 'Value', 'Expires', 'Domain', 'Path', 'Secure', 'HTTPOnly'], )) ================================================ FILE: objection/commands/ios/generate.py ================================================ import os import click from objection.state.connection import state_connection def clazz(args: list) -> None: """ Simply echoes the source for a generic Hook Manager sample for Objective-C hooks with Frida. :param args: :return: """ js_path = os.path.join( os.path.abspath(os.path.dirname(__file__)), '../../utils/assets', 'objchookmanager.js' ) with open(js_path, 'r') as f: click.secho(f.read(), dim=True) def simple(args: list) -> None: """ Generate simple hooks for all methods in a class. :param args: :return: """ if len(args) <= 0: click.secho('Usage: ios hooking generate simple ', bold=True) return classname = args[0] api = state_connection.get_api() methods = api.ios_hooking_get_class_methods(classname, False) if len(methods) <= 0: click.secho('No class / methods found') return click.secho("var target = ObjC.classes.{};".format(classname), dim=True) for method in methods: hook = """ Interceptor.attach(target['{method}'].implementation, { onEnter: function (args) { console.log('Entering {method}!'); }, onLeave: function (retval) { console.log('Leaving {method}'); }, }); """.replace('{method}', method) click.secho(hook, dim=True) ================================================ FILE: objection/commands/ios/heap.py ================================================ import pprint import click from prompt_toolkit import prompt from prompt_toolkit.lexers import PygmentsLexer from pygments.lexers.javascript import JavascriptLexer from tabulate import tabulate from objection.state.connection import state_connection def _should_ignore_methods_with_arguments(args) -> bool: """ Check if the --without-arguments flag exists :param args: :return: """ return len(args) > 0 and '--without-arguments' in args def _should_print_as_utf8(args) -> bool: """ Check if the --to-utf8 flag exists :param args: :return: """ return len(args) > 0 and '--to-utf8' in args def _should_return_as_string(args) -> bool: """ Check if the --return-string flag exists :param args: :return: """ return len(args) > 0 and '--return-string' in args def _should_interpret_inline_js(args) -> bool: """ Check if we have the --inline flag :param args: :return: """ return len(args) > 0 and '--inline' in args def instances(args: list) -> None: """ Asks the agent to print the currently live instances of a particular class :param args: :return: """ if len(args) < 1: click.secho('Usage: ios heap search instances (eg: com.example.test)', bold=True) return target_class = args[0] api = state_connection.get_api() instance_results = api.ios_heap_print_live_instances(target_class) # export interface IHeapObject { # className: string; # handle: string; # ivars: any[string]; # kind: string; # methods: string[]; # superClass: string; # } if len(instance_results) <= 0: return click.secho(tabulate( [[ entry['handle'], entry['kind'], entry['className'], entry['superClass'], len(entry['ivars']), len(entry['methods']) ] for entry in instance_results], headers=['Handle', 'Kind', 'Class', 'Super', 'iVars', 'Methods'], )) def ivars(args: list) -> None: """ Get ivars for an Objective-C object at a pointer :param args: :return: """ if len(args) < 1: click.secho('Usage: ios heap print ivars (eg: 0x600001130660)', bold=True) return target_pointer = args[0] api = state_connection.get_api() ivar_results = api.ios_heap_print_ivars(target_pointer, _should_print_as_utf8(args)) click.secho(tabulate( [[ key, value ] for key, value in ivar_results[1].items()], headers=['iVar', 'Value'], )) def methods(args: list) -> None: """ Get methods for an Objective-C object at a pointer :param args: :return: """ if len(args) < 1: click.secho('Usage: ios heap print methods (eg: 0x600001130660)', bold=True) return target_pointer = args[0] api = state_connection.get_api() method_results = api.ios_heap_print_methods(target_pointer) # apply argument filters if _should_ignore_methods_with_arguments(args): method_results[1] = list(filter(lambda x: ':' not in x, method_results[1])) click.secho(tabulate( [[ entry, entry.split(" ")[0], "{type} [{clazz} {method}]".format( # hacky, right? :D type=entry.split(" ")[0], clazz=method_results[0], method=entry.split(" ")[1]) ] for entry in method_results[1]], headers=['Method', 'Type', 'Full'], )) def execute(args: list) -> None: """ Executes a method on a pointer which is assumed to be an Objective-C object. :param args: :return: """ if len(args) < 1: click.secho('Usage: ios heap execute method (eg: 0x600001130660)', bold=True) return target_pointer = args[0] method = args[1] if ':' in method: click.secho('Unfortunately, only methods that do not require arguments are supported.', fg='yellow') return api = state_connection.get_api() exec_results = api.ios_heap_exec_method(target_pointer, method, _should_return_as_string(args)) click.secho(pprint.pformat(exec_results)) def evaluate(args: list) -> None: """ Evaluate JavaScript on an Objective-C pointer. :param args: :return: """ if len(args) < 1: click.secho('Usage: ios heap execute js (eg: 0x600001130660) ' + '(optional: --inline) (optional: )', bold=True) return target_pointer = args[0] # adding the --inline flag would trigger reading the line contents # as JavaScript sources if _should_interpret_inline_js(args): args.remove('--inline') js = ''.join(args[1:]) click.secho('Reading inline JavaScript for evaluation...', dim=True) click.secho('{}\n'.format(js), fg='green', dim=True) else: js = prompt( click.secho('(The pointer at `{pointer}` will be available as the `ptr` variable.)n'.format( pointer=target_pointer ), dim=True), multiline=True, lexer=PygmentsLexer(JavascriptLexer), bottom_toolbar='JavaScript edit mode. [ESC] and then [ENTER] to accept. [CTRL] + C to cancel.').strip() click.secho('JavaScript capture complete. Evaluating...', dim=True) api = state_connection.get_api() api.ios_heap_evaluate_js(target_pointer, js) ================================================ FILE: objection/commands/ios/hooking.py ================================================ import json from typing import Optional import click from objection.state.connection import state_connection from objection.utils.helpers import clean_argument_flags # a thumb sucked list of prefixes used in Objective-C runtime # for iOS applications. This is not a science, but a gut feeling. native_prefixes = [ '_', 'NS', # '_NS', # '__NS', 'CF', 'OS_', 'UI', # '_UI', 'AWD', 'GEO', 'AC', 'AF', 'AU', 'AV', 'BK', 'BS', 'CA', 'CB', 'CI', 'CL', 'CT', 'CUI', 'DOM', 'FBS', 'LA', 'LS', 'MC', 'MTL', 'PFUbiquity', 'PKPhysics', 'SBS', 'TI', 'TXR', 'UM', 'Web', ] def _should_ignore_native_classes(args: list) -> bool: """ Checks if --ignore-native is in a list of tokens received from the commandline. :param args: :return: """ if len(args) <= 0: return False return '--ignore-native' in args def _should_include_parent_methods(args: list) -> bool: """ Checks if --include-parents exists in a list of tokens received from the commandline. :param args: :return: """ if len(args) <= 0: return False return '--include-parents' in args def _class_is_prefixed_with_native(class_name: str) -> bool: """ Check if a class name received is prefixed with one of the prefixes in the native_prefixes list. :param class_name: :return: """ for prefix in native_prefixes: if class_name.startswith(prefix): return True return False def _string_is_true(s: str) -> bool: """ Check if a string should be considered as "True" :param s: :return: """ return s.lower() in ('true', 'yes') def _should_dump_backtrace(args: list) -> bool: """ Check if --dump-backtrace is part of the arguments. :param args: :return: """ return '--dump-backtrace' in args def _should_dump_args(args: list) -> bool: """ Check if --dump-args is part of the arguments. :param args: :return: """ return '--dump-args' in args def _should_dump_return_value(args: list) -> bool: """ Check if --dump-return is part of the arguments. :param args: :return: """ return '--dump-return' in args def _should_print_only_classes(args: list) -> bool: """ Check if --only-classes is part of the arguments. :param args: :return: """ return '--only-classes' in args def _should_dump_json(args: list) -> bool: """ Check if --json is part of the arguments. :param args: :return: """ return '--json' in args def _should_be_quiet(args: list) -> bool: """ Check if --quiet is part of the arguments. :param args: :return: """ return '--quiet' in args def _get_flag_value(flag: str, args: list) -> Optional[str]: """ Gets the value for a flag :param flag: :param args: :return: """ target = None for i in range(len(args)): if args[i] == flag: target = i + 1 if target is None: return None elif target < len(args): return args[target] else: return None def show_ios_classes(args: list = None) -> None: """ Prints the classes available in the current Objective-C runtime to the screen. :param args: :return: """ api = state_connection.get_api() classes = api.ios_hooking_get_classes() # loop the class names and check if we should be ignoring it. for class_name in sorted(classes): if _should_ignore_native_classes(args): if not _class_is_prefixed_with_native(class_name): click.secho(class_name) continue else: click.secho(class_name) click.secho('\nFound {0} classes'.format(len(classes)), bold=True) def show_ios_class_methods(args: list) -> None: """ Displays the methods available in a class. :param args: :return: """ if len(clean_argument_flags(args)) <= 0: click.secho('Usage: ios hooking list class_methods (--include-parents)', bold=True) return classname = args[0] api = state_connection.get_api() methods = api.ios_hooking_get_class_methods(classname, _should_include_parent_methods(args)) if len(methods) > 0: # dump the methods to screen for method in methods: click.secho(method) click.secho('\nFound {0} methods'.format(len(methods)), bold=True) else: click.secho('No class / methods found') def set_method_return_value(args: list) -> None: """ Make an Objective-C method return a specific boolean value, always. :param args: :return: """ if len(clean_argument_flags(args)) < 2: click.secho('Usage: ios hooking set_method_return "" (eg: "-[ClassName methodName:]") ', bold=True) return selector = args[0] retval = args[1] api = state_connection.get_api() api.ios_hooking_set_return_value(selector, _string_is_true(retval)) def watch(args: list) -> None: """ Watches a pattern for invocations. :param args: :return: """ if len(clean_argument_flags(args)) <= 0: click.secho('Usage: ios hooking watch ', bold=True) return pattern = args[0] api = state_connection.get_api() api.ios_hooking_watch(pattern, _should_dump_args(args), _should_dump_backtrace(args), _should_dump_return_value(args), _should_include_parent_methods(args)) def search(args: list) -> None: """ Searches the current iOS application for classes and methods. :param args: :return: """ if len(clean_argument_flags(args)) <= 0: click.secho('Usage: ios hooking search \'\'', bold=True) return api = state_connection.get_api() pattern = args[0] results = api.ios_hooking_search(pattern) data = {} # build a list of results to print / dump later for func in results: fullname = func['name'] start_bracket = fullname.find('[') + 1 class_name = fullname[start_bracket: fullname.find(' ')] if data.get(class_name) is not None: data[class_name].append(fullname) else: data[class_name] = [fullname] if _should_dump_json(args): target_file = _get_flag_value('--json', args) if not target_file: click.secho('A file name needs to be specified with the --json flag', fg='red') return with open(target_file, 'w') as fd: fd.write(json.dumps({ 'meta': { 'runtime': 'objc' }, 'classes': data })) click.secho(f'JSON dumped to file {target_file}', bold=True) return # Print the matching methods for klass in data.keys(): if _should_print_only_classes(args): print(klass) continue methods = data[klass] for method in methods: print(f'{method}') ================================================ FILE: objection/commands/ios/jailbreak.py ================================================ from objection.state.connection import state_connection def disable(args: list = None) -> None: """ Attempts to disable jailbreak detection. :param args: :return: """ api = state_connection.get_api() api.ios_jailbreak_disable() def simulate(args: list = None) -> None: """ Attempts to simulate a Jailbroken environment :param args: :return: """ api = state_connection.get_api() api.ios_jailbreak_enable() ================================================ FILE: objection/commands/ios/keychain.py ================================================ import json import click from tabulate import tabulate from objection.state.connection import state_connection def _should_output_json(args: list) -> bool: """ Checks if --json is in the list of tokens received from the command line. :param args: :return: """ return len(args) > 0 and '--json' in args def _should_do_smart_decode(args: list) -> bool: """ Checks if --smart is in the list of tokens received from the command line. :param args: :return: """ return len(args) > 0 and '--smart' in args def _data_flag_has_identifier(args: list) -> bool: """ Checks that if the data flag is specified, an identifier is also passed. :param args: :return: """ if '--data' in args: return any(x in args for x in ['--service', '--account']) return True def _get_flag_value(args: list, flag: str) -> str: """ Returns the value for a flag. :param args: :param flag: :return: """ return args[args.index(flag) + 1] if flag in args else None def dump(args: list = None) -> None: """ Dump the iOS keychain :param args: :return: """ if _should_output_json(args) and len(args) < 2: click.secho('Usage: ios keychain dump (--json )', bold=True) return click.secho('Note: You may be asked to authenticate using the devices passcode or TouchID') if not _should_output_json(args): click.secho('Save the output by adding `--json keychain.json` to this command', dim=True) click.secho('Dumping the iOS keychain...', dim=True) api = state_connection.get_api() keychain = api.ios_keychain_list(_should_do_smart_decode(args)) if _should_output_json(args): destination = args[1] click.secho('Writing keychain as json to {0}...'.format(destination), dim=True) with open(destination, 'w') as f: f.write(json.dumps(keychain, indent=2)) click.secho('Dumped keychain to: {0}'.format(destination), fg='green') return # Just dump it to the screen click.secho(tabulate( [[ entry['create_date'], entry['accessible_attribute'].replace('kSecAttrAccessible', '') if 'accessible_attribute' in entry else None, entry['access_control'], entry['item_class'].replace('kSecClassGeneric', ''), entry['account'], entry['service'], entry['data'] ] for entry in keychain], headers=['Created', 'Accessible', 'ACL', 'Type', 'Account', 'Service', 'Data'], )) def dump_raw(args: list = None) -> None: """ Dump the iOS keychain, but without any parsing. The agent will output the entries it finds here. :param args: :return: """ click.secho('Note: You may be asked to authenticate using the devices passcode or TouchID') click.secho('Dumping the iOS keychain...', dim=True) api = state_connection.get_api() api.ios_keychain_list_raw() def clear(args: list = None) -> None: """ Clear the iOS keychain. :param args: :return: """ if not click.confirm('Are you sure you want to clear the iOS keychain?'): return click.secho('Clearing the keychain...', dim=True) api = state_connection.get_api() api.ios_keychain_empty() click.secho('Keychain cleared', fg='green') def remove(args: list) -> None: """ Remove matching keychain entries from the keychain :param args: :return: """ account = _get_flag_value(args, '--account') service = _get_flag_value(args, '--service') if not account or not service: click.secho('Either --account or --service is not set. We need both', bold=True) return click.secho('Removing entry from the iOS keychain...', dim=True) click.secho('Account: {0}'.format(account), dim=True) click.secho('Service: {0}'.format(service), dim=True) api = state_connection.get_api() api.ios_keychain_remove(account, service) click.secho('Successfully removed matching keychain items', fg='green') def update(args: list) -> None: """ Update matching keychain entry from the keychain :param args: :return: """ account = _get_flag_value(args, '--account') service = _get_flag_value(args, '--service') newdata = _get_flag_value(args, '--newdata') if not account or not service or not newdata: click.secho('All flags need to be set, incl. --account, --service and --newdata') return click.secho('Updating entries from the iOS keychain...', dim=True) click.secho('Account: {0}'.format(account), dim=True) click.secho('Service: {0}'.format(service), dim=True) click.secho('New Data: {0}'.format(newdata), dim=True) api = state_connection.get_api() api.ios_keychain_update(account, service, newdata) click.secho('Successfully updated matching keychain item', fg='green') def add(args: list) -> None: """ Adds a new kSecClassGenericPassword keychain entry to the keychain :param args: :return: """ if not _data_flag_has_identifier(args): click.secho('When specifying the --data flag, either --account or ' '--service should also be added', fg='red') return account = _get_flag_value(args, '--account') service = _get_flag_value(args, '--service') data = _get_flag_value(args, '--data') click.secho('Adding a new entry to the iOS keychain...', dim=True) click.secho('Account: {0}'.format(account), dim=True) click.secho('Service: {0}'.format(service), dim=True) click.secho('Data: {0}'.format(data), dim=True) api = state_connection.get_api() if api.ios_keychain_add(account, service, data): click.secho('Successfully added the keychain item', fg='green') return click.secho('Failed to add the keychain item', fg='red') ================================================ FILE: objection/commands/ios/monitor.py ================================================ from objection.state.connection import state_connection def crypto_enable(args: list = None) -> None: """ Attempts to enable ios crypto monitoring. :param args: :return: """ api = state_connection.get_api() api.ios_monitor_crypto_enable() ================================================ FILE: objection/commands/ios/nsurlcredentialstorage.py ================================================ import click from tabulate import tabulate from objection.state.connection import state_connection def dump(args: list = None) -> None: """ Dumps credentials stored in NSURLCredentialStorage :param args: :return: """ api = state_connection.get_api() cookies = api.ios_credential_storage() click.secho(tabulate( [[ entry['protocol'], entry['host'], entry['port'], entry['authMethod'].replace('NSURLAuthenticationMethod', ''), entry['user'], entry['password'], ] for entry in cookies], headers=[ 'Protocol', 'Host', 'Port', 'Authentication Method', 'User', 'Password' ], )) ================================================ FILE: objection/commands/ios/nsuserdefaults.py ================================================ import click from objection.state.connection import state_connection def get(args: list = None) -> None: """ Gets all of the values stored in NSUserDefaults and prints them to screen. :param args: :return: """ api = state_connection.get_api() defaults = api.ios_nsuser_defaults_get() click.secho(defaults, bold=True) ================================================ FILE: objection/commands/ios/pasteboard.py ================================================ from objection.state.connection import state_connection def monitor(args: list = None) -> None: """ Starts a new objection job that monitors the iOS pasteboard and reports on new strings found. :param args: :return: """ api = state_connection.get_api() api.ios_monitor_pasteboard() ================================================ FILE: objection/commands/ios/pinning.py ================================================ from objection.state.connection import state_connection def _should_be_quiet(args: list) -> bool: """ Checks if --quiet is part of the commands arguments. :param args: :return: """ return '--quiet' in args def ios_disable(args: list = None) -> None: """ Starts a new objection job that hooks common classes and functions, applying new logic in an attempt to bypass SSL pinning. :param args: :return: """ api = state_connection.get_api() api.ios_pinning_disable(_should_be_quiet(args)) ================================================ FILE: objection/commands/ios/plist.py ================================================ import os import click from objection.commands import filemanager from objection.state.connection import state_connection from objection.state.device import device_state def cat(args: list = None) -> None: """ Parses a plist on an iOS device and echoes it in a more human readable way. :param args: :return: """ if len(args) <= 0: click.secho('Usage: ios plist cat ', bold=True) return plist = args[0] if not os.path.isabs(plist): pwd = filemanager.pwd() plist = device_state.platform.path_separator.join([pwd, plist]) api = state_connection.get_api() plist_data = api.ios_plist_read(plist) click.secho(plist_data, bold=True) ================================================ FILE: objection/commands/jobs.py ================================================ import click from tabulate import tabulate from objection.state.connection import state_connection from ..state.jobs import job_manager_state, Job def show(args: list = None) -> None: """ Show all of the jobs that are currently running :return: """ sync_job_manager() jobs = job_manager_state.jobs # click.secho(tabulate( # [[ # entry['uuid'], # sum([ # len(entry[x]) for x in [ # 'invocations', 'replacements', 'implementations' # ] if x in entry # ]), # entry['type'], # ] for entry in jobs], headers=['Job ID', 'Hooks', 'Name'], # )) click.secho(tabulate( [[ uuid, job.job_type, job.name, ] for uuid, job in jobs.items()], headers=['Job ID', 'Type', 'Name'], )) def kill(args: list) -> None: """ Kills a specific objection job. :param args: :return: """ if len(args) <= 0: click.secho('Usage: jobs kill ', bold=True) return job_uuid = int(args[0]) job_manager_state.remove_job(job_uuid) def list_current_jobs() -> dict: """ Return a list of the currently listed objection jobs. Used for tab completion in the repl. """ sync_job_manager() resp = {} for uuid, job in job_manager_state.jobs.items(): resp[str(uuid)] = str(uuid) return resp def sync_job_manager() -> dict[int, Job]: try: api = state_connection.get_api() jobs = api.jobs_get() for job in jobs: job_uuid = int(job['identifier']) job_name = job['type'] if job_uuid not in job_manager_state.jobs: job_manager_state.jobs[job_uuid] = Job(job_name, 'hook', None, job_uuid) return job_manager_state.jobs except: print("REPL not ready") ================================================ FILE: objection/commands/memory.py ================================================ import json import os from typing import List import click from tabulate import tabulate from objection.state.connection import state_connection from ..utils.helpers import clean_argument_flags from ..utils.helpers import sizeof_fmt, pretty_concat BLOCK_SIZE = 40960000 def _is_string_input(args: list) -> bool: """ Checks if --string is in the list of tokens received form the command line. :param args: :return: """ return len(args) > 0 and '--string' in args def _should_only_dump_offsets(args: list) -> bool: """ Checks if --offsets-only is in the list pf tokens received from the command line. :param args: :return: """ return '--offsets-only' in args def _is_string_pattern(args: list) -> bool: """ Checks if --string-pattern is in the list of tokens received form the command line. :param args: :return: """ return len(args) > 0 and '--string-pattern' in args def _is_string_replace(args: list) -> bool: """ Checks if --string-replace is in the list of tokens received form the command line. :param args: :return: """ return len(args) > 0 and '--string-replace' in args def _should_output_json(args: list) -> bool: """ Checks if --json is in the list of tokens received from the command line. :param args: :return: """ return len(args) > 0 and '--json' in args def _get_chunks(addr: int, size: int, block_size: int = BLOCK_SIZE) -> List: """ Determine chunks of :param addr: :param size: :param block_size: :return: """ if size < block_size: return [(addr, size)] block_count = size // block_size extra_block = size % block_size current_address = addr ranges = [] for i in range(block_count): ranges.append((current_address, block_size)) current_address += block_size if extra_block != 0: ranges.append((current_address, extra_block)) return ranges # TODO: Dump memory on hooked methods. # A PR in the repo this method is based on has an idea for this # # https://github.com/Nightbringer21/fridump/pull/3 def dump_all(args: list) -> None: """ Dump memory from the currently injected process. Loosely based on: https://github.com/Nightbringer21/fridump :param args: :return: """ if len(clean_argument_flags(args)) <= 0: click.secho('Usage: memory dump all ', bold=True) return # the destination file to write the dump to destination = args[0] # Check for file override if os.path.exists(destination): click.secho('Destination file {dest} already exists'.format(dest=destination), fg='yellow', bold=True) if not click.confirm('Continue, appending to the file?'): return # access type used when enumerating ranges access = 'rw-' api = state_connection.get_api() ranges = api.memory_list_ranges(access) total_size = sum([x['size'] for x in ranges]) click.secho('Will dump {0} {1} images, totalling {2}'.format( len(ranges), access, sizeof_fmt(total_size)), fg='green', dim=True) with click.progressbar(ranges) as bar: for image in bar: dump = bytearray() bar.label = 'Dumping {0} from base: {1}'.format(sizeof_fmt(image['size']), hex(int(image['base'], 16))) # catch and exception thrown while dumping. # this could for a few reasons like if the protection # changes or the range is reallocated try: # grab the (size) bytes starting at the (base_address) in chunks of BLOCK_SIZE chunks = _get_chunks(int(image['base'], 16), int(image['size']), BLOCK_SIZE) for chunk in chunks: dump.extend(bytearray(api.memory_dump(chunk[0], chunk[1]))) except Exception as e: continue # append the results to the destination file with open(destination, 'ab') as f: f.write(dump) click.secho('Memory dumped to file: {0}'.format(destination), fg='green') def dump_from_base(args: list) -> None: """ Dump memory from a base address for a specific size to file :param args: :return: """ if len(clean_argument_flags(args)) < 3: click.secho('Usage: memory dump from_base ', bold=True) return # the destination file to write the dump to base_address = args[0] memory_size = args[1] destination = args[2] # Check for file override if os.path.exists(destination): click.secho('Destination file {dest} already exists'.format(dest=destination), fg='yellow', bold=True) if not click.confirm('Override?'): return click.secho('Dumping {0} from {1} to {2}'.format(sizeof_fmt(int(memory_size)), base_address, destination), fg='green', dim=True) api = state_connection.get_api() # iirc, if you don't cast the return type to a bytearray it uses the sizeof(int) per cell, which is massive dump = bytearray() chunks = _get_chunks(int(base_address, 16), int(memory_size), BLOCK_SIZE) for chunk in chunks: dump.extend(bytearray(api.memory_dump(chunk[0], chunk[1]))) # append the results to the destination file with open(destination, 'wb') as f: f.write(dump) click.secho('Memory dumped to file: {0}'.format(destination), fg='green') def list_modules(args: list = None) -> None: """ List modules loaded in the current process. :param args: :return: """ if _should_output_json(args) and len(args) < 2: click.secho('Usage: memory list modules (--json )', bold=True) return if not _should_output_json(args): click.secho('Save the output by adding `--json modules.json` to this command', dim=True) api = state_connection.get_api() modules = api.memory_list_modules() if _should_output_json(args): destination = args[args.index('--json') + 1] click.secho('Writing modules as json to {0}...'.format(destination), dim=True) with open(destination, 'w') as f: f.write(json.dumps(modules, indent=2)) click.secho('Wrote modules to: {0}'.format(destination), fg='green') return # Just dump it to the screen click.secho(tabulate( [[ entry['name'], entry['base'], str(entry['size']) + ' (' + sizeof_fmt(entry['size']) + ')', pretty_concat(entry['path']), ] for entry in modules], headers=['Name', 'Base', 'Size', 'Path'], )) def list_exports(args: list) -> None: """ Dumps the exported methods from a loaded module to screen. :param args: :return: """ if _should_output_json(args) and len(args) < 3: click.secho('Usage: memory list exports (--json )', bold=True) return if not _should_output_json(args): click.secho('Save the output by adding `--json exports.json` to this command', dim=True) if len(clean_argument_flags(args)) <= 0: click.secho('Usage: memory list exports ', bold=True) return module_to_list = args[0] api = state_connection.get_api() exports = api.memory_list_exports(module_to_list) if _should_output_json(args): destination = args[args.index('--json') + 1] click.secho('Writing exports as json to {0}...'.format(destination), dim=True) with open(destination, 'w') as f: f.write(json.dumps(exports, indent=2)) click.secho('Wrote exports to: {0}'.format(destination), fg='green') return # Just dump it to the screen click.secho(tabulate( [[ entry['type'], entry['name'], entry['address'], ] for entry in exports], headers=['Type', 'Name', 'Address'], )) def find_pattern(args: list) -> None: """ Searches the current processes accessible memory for a specific pattern. :param args: :return: """ if len(clean_argument_flags(args)) <= 0: click.secho('Usage: memory search "" (--string) (--offsets-only)', bold=True) return # if we got a string as input, convert it to hex if _is_string_input(args): pattern = ' '.join(hex(ord(x))[2:] for x in args[0]) else: pattern = args[0] click.secho('Searching for: {0}'.format(pattern), dim=True) api = state_connection.get_api() data = api.memory_search(pattern, _should_only_dump_offsets(args)) if len(data) > 0: click.secho('Pattern matched at {0} addresses'.format(len(data)), fg='green') if _should_only_dump_offsets(args): for address in data: click.secho(address) else: click.secho('Unable to find the pattern in any memory region') def replace_pattern(args: list) -> None: """ Searches the current processes accessible memory for a specific pattern and replaces it with given bytes or string. :param args: :return: """ if len(clean_argument_flags(args)) < 2: click.secho('Usage: memory replace "" "" (--string-pattern) (--string-replace)', bold=True) return # if we got a string as search pattern input, convert it to hex if _is_string_pattern(args): pattern = ' '.join(hex(ord(x))[2:] for x in args[0]) else: pattern = args[0] # if we got a string as replace input, convert it to int[], otherwise convert hex to int[] replace = args[1] if _is_string_replace(args): replace = [ord(x) for x in replace] else: replace = [int(x, 16) for x in replace.split(' ')] click.secho('Searching for: {0}, replacing with: {1}'.format(pattern, args[1]), dim=True) api = state_connection.get_api() data = api.memory_replace(pattern, replace) if len(data) > 0: click.secho('Pattern replaced at {0} addresses'.format(len(data)), fg='green') for address in data: click.secho(address) else: click.secho('Unable to find the pattern in any memory region') def write(args: list) -> None: """ Write an arbitrary amount of bytes to an arbitrary memory address. Needless to say, use with caution. =P :param args: :return: """ if len(clean_argument_flags(args)) < 2: click.secho('Usage: memory write "
" "" (--string)', bold=True) return destination = args[0] pattern = args[1] if _is_string_input(args): pattern = [ord(x) for x in pattern] else: pattern = [int(x, 16) for x in pattern.split(' ')] click.secho('Writing byte array: {0} to {1}'.format(pattern, destination), dim=True) api = state_connection.get_api() api.memory_write(destination, pattern) ================================================ FILE: objection/commands/mobile_packages.py ================================================ import os import shutil import click import delegator from packaging.version import Version from ..utils.patchers.android import AndroidGadget, AndroidPatcher from ..utils.patchers.github import Github from ..utils.patchers.ios import IosGadget, IosPatcher def patch_ios_ipa(source: str, codesign_signature: str, provision_file: str, binary_name: str, skip_cleanup: bool, unzip_unicode: bool, gadget_version: str = None, pause: bool = False, gadget_config: str = None, script_source: str = None, bundle_id: str = None) -> None: """ Patches an iOS IPA by extracting, injecting the Frida dylib, codesigning the dylib and app executable and rezipping the IPA. :param bundle_id: :param source: :param codesign_signature: :param provision_file: :param binary_name: :param skip_cleanup: :param unzip_unicode: :param gadget_version: :param pause: :param gadget_config: :param script_source: :return: """ github = Github(gadget_version=gadget_version) ios_gadget = IosGadget(github) # get the gadget version numbers # check if a gadget version was specified. if not, get the latest one. if gadget_version is not None: github_version = gadget_version click.secho('Using manually specified version: {0}'.format(gadget_version), fg='green', bold=True) else: github_version = github.get_latest_version() click.secho('Using latest Github gadget version: {0}'.format(github_version), fg='green', bold=True) # get the local version number of the stored gadget local_version = ios_gadget.get_local_version('ios_universal') # check if the local version needs updating. this can be either because # the version is outdated or we simply don't have the gadget yet if Version(github_version) != Version(local_version) or not ios_gadget.gadget_exists(): # download! click.secho('Remote FridaGadget version is v{0}, local is v{1}. Downloading...'.format( github_version, local_version), fg='green') # download, unpack, update local version and cleanup the temp files. ios_gadget.download() \ .unpack() \ .set_local_version('ios_universal', github_version) \ .cleanup() click.secho('Patcher will be using Gadget version: {0}'.format(github_version), fg='green') # start the patching process patcher = IosPatcher(skip_cleanup=skip_cleanup) # return of we do not have all of the requirements. if not patcher.are_requirements_met(): return patcher.set_provsioning_profile(provision_file=provision_file, bundle_id=bundle_id) patcher.extract_ipa(unzip_unicode, ipa_source=source) patcher.set_application_binary(binary=binary_name) patcher.patch_and_codesign_binary( frida_gadget=ios_gadget.get_gadget_path(), codesign_signature=codesign_signature, gadget_config=gadget_config) if script_source: click.secho('Copying over a custom script to use with the gadget config.', fg='green') shutil.copyfile(script_source, os.path.join(patcher.app_folder, 'Frameworks', script_source)) # give a chance to make any last minute modifications if needed if pause: click.secho(('Patching paused. The next step is to rebuild the IPA. ' 'If you require any manual fixes, the current temp ' 'directory is:'), bold=True) click.secho('{0}'.format(patcher.app_folder), fg='green', bold=True) input('Press ENTER to continue...') patcher.archive_and_codesign(original_name=source, codesign_signature=codesign_signature) click.secho('Copying final ipa from {0} to current directory...'.format(patcher.get_patched_ipa_path())) shutil.copyfile( patcher.get_patched_ipa_path(), os.path.join(os.path.abspath('.'), os.path.basename(patcher.get_patched_ipa_path()))) def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup: bool = True, enable_debug: bool = True, gadget_version: str = None, skip_resources: bool = False, network_security_config: bool = False, target_class: str = None, use_aapt2: bool = False, gadget_config: str = None, script_source: str = None, ignore_nativelibs: bool = True, manifest: str = None, skip_signing: bool = False, only_main_classes: bool = False, fix_concurrency_to = None) -> None: """ Patches an Android APK by extracting, patching SMALI, repackaging and signing a new APK. :param source: :param architecture: :param pause: :param skip_cleanup: :param enable_debug: :param gadget_version: :param skip_resources: :param network_security_config: :param target_class: :param use_aapt2: :param gadget_config: :param script_source: :param manifest: :param skip_signing: :param ignore_nativelibs: :param only_main_classes: :param fix_concurrency_to: :return: """ github = Github(gadget_version=gadget_version) android_gadget = AndroidGadget(github) # without an architecture set, attempt to determine one using adb if not architecture: click.secho('No architecture specified. Determining it using `adb`...', dim=True) o = delegator.run('adb shell getprop ro.product.cpu.abi') # read the ach from the process' output architecture = o.out.strip() if len(architecture) <= 0: click.secho('Failed to determine architecture. Is the device connected and authorized?', fg='red', bold=True) return click.secho('Detected target device architecture as: {0}'.format(architecture), fg='green', bold=True) # set the architecture we are interested in android_gadget.set_architecture(architecture) # check the gadget config flags if script_source and not gadget_config: click.secho('A script source was specified but no gadget configuration was set.', fg='red', bold=True) return # check if a gadget version was specified. if not, get the latest one. if gadget_version is not None: github_version = gadget_version click.secho('Using manually specified version: {0}'.format(gadget_version), fg='green', bold=True) else: github_version = github.get_latest_version() click.secho('Using latest Github gadget version: {0}'.format(github_version), fg='green', bold=True) # get local version of the stored gadget local_version = android_gadget.get_local_version('android_' + architecture) # check if the local version needs updating. this can be either because # the version is outdated or we simply don't have the gadget yet, or, we want # a very specific version if Version(github_version) != Version(local_version) or not android_gadget.gadget_exists(): # download! click.secho('Remote FridaGadget version is v{0}, local is v{1}. Downloading...'.format( github_version, local_version), fg='green') # download, unpack, update local version and cleanup the temp files. android_gadget.download() \ .unpack() \ .set_local_version('android_' + architecture, github_version) \ .cleanup() click.secho('Patcher will be using Gadget version: {0}'.format(github_version), fg='green') patcher = AndroidPatcher(skip_cleanup=skip_cleanup, skip_resources=skip_resources, manifest=manifest, only_main_classes=only_main_classes) # ensure that we have all of the commandline requirements if not patcher.are_requirements_met(): return # ensure we have the latest apk-tool and run the if not patcher.is_apktool_ready(): click.secho('apktool is not ready for use', fg='red', bold=True) return # work on patching the APK patcher.set_apk_source(source=source) patcher.unpack_apk(fix_concurrency_to=fix_concurrency_to) patcher.inject_internet_permission() if not ignore_nativelibs: patcher.extract_native_libs_patch() if enable_debug: patcher.flip_debug_flag_to_true() if network_security_config: patcher.add_network_security_config() patcher.inject_load_library(target_class=target_class) patcher.add_gadget_to_apk(architecture, android_gadget.get_frida_library_path(), gadget_config) if script_source: click.secho('Copying over a custom script to use with the gadget config.', fg='green') shutil.copyfile(script_source, os.path.join(patcher.apk_temp_directory, 'lib', architecture, 'libfrida-gadget.script.so')) # if we are required to pause, do that. if pause: click.secho(('Patching paused. The next step is to rebuild the APK. ' 'If you require any manual fixes, the current temp ' 'directory is:'), bold=True) click.secho('{0}'.format(patcher.get_temp_working_directory()), fg='green', bold=True) input('Press ENTER to continue...') patcher.build_new_apk(use_aapt2=use_aapt2, fix_concurrency_to=fix_concurrency_to) patcher.zipalign_apk() if not skip_signing: patcher.sign_apk() # woohoo, get the APK! destination = source.replace('.apk', '.objection.apk') click.secho( 'Copying final apk from {0} to {1} in current directory...'.format(patcher.get_patched_apk_path(), destination)) shutil.copyfile(patcher.get_patched_apk_path(), os.path.join(os.path.abspath('.'), destination)) def sign_android_apk(source: str, skip_cleanup: bool = True) -> None: """ Zipaligns and signs an Android APK with the objection key. :param source: :param skip_cleanup: :return: """ patcher = AndroidPatcher(skip_cleanup=skip_cleanup) # ensure that we have all of the commandline requirements if not patcher.are_requirements_met(): return patcher.set_apk_source(source=source) patcher.zipalign_apk() patcher.sign_apk() # woohoo, get the APK! destination = source.replace('.apk', '.objection.apk') click.secho( 'Copying final apk from {0} to {1} in current directory...'.format(patcher.get_patched_apk_path(), destination)) shutil.copyfile(patcher.get_patched_apk_path(), os.path.join(os.path.abspath('.'), destination)) ================================================ FILE: objection/commands/plugin_manager.py ================================================ import importlib.util import os import traceback import uuid import click from ..utils.plugin import Plugin as PluginType def load_plugin(args: list = None) -> None: """ Loads an objection plugin. :param args: :return: """ if len(args) <= 0: click.secho('Usage: plugin load ()', bold=True) return path = os.path.abspath(args[0]) if os.path.isdir(path): path = os.path.join(path, '__init__.py') if not os.path.exists(path): click.secho('[plugin] {0} does not appear to be a valid plugin. Missing __init__.py'.format( os.path.dirname(path)), fg='red', dim=True) return spec = importlib.util.spec_from_file_location(str(uuid.uuid4())[:8], path) plugin = importlib.util.module_from_spec(spec) spec.loader.exec_module(plugin) namespace = plugin.namespace if len(args) >= 2: namespace = args[1] plugin.__name__ = namespace # try and load the plugin (aka: run its __init__) try: instance = plugin.plugin(namespace) assert isinstance(instance, PluginType) except AssertionError: click.secho('Failed to load plugin \'{0}\'. Invalid plugin type.'.format(namespace), fg='red', bold=True) return except Exception as e: click.secho('Failed to load plugin \'{0}\' with error: {1}'.format(namespace, str(e)), fg='red', bold=True) click.secho('{0}'.format(traceback.format_exc()), dim=True) return from ..console import commands commands.COMMANDS['plugin']['commands'][instance.namespace] = instance.implementation click.secho('Loaded plugin: {0}'.format(plugin.__name__), bold=True) ================================================ FILE: objection/commands/sqlite.py ================================================ import binascii import os import tempfile import click import litecli from litecli.main import LiteCli from ..commands.filemanager import download, upload, pwd, path_exists def modify_config(rc): """ Monkey patches the LiteCLI config to toggle settings that make more sense for us. :param rc: :return: """ c = real_get_config(rc) c['main']['less_chatty'] = 'True' c['main']['enable_pager'] = 'False' return c real_get_config = litecli.main.get_config litecli.main.get_config = modify_config def cleanup(p) -> None: """ Remove a cached SQLite db :param p: :return: """ os.remove(p) def _should_sync_once_done(args: list) -> bool: """ Checks if --sync flag was provided. :param args: :return: """ return '--sync' in args def connect(args: list) -> None: """ Connects to a SQLite database by downloading a copy of the database from the device and storing it locally in a temporary directory. :param args: :return: """ if len(args) <= 0: click.secho('Usage: sqlite connect (optional: --sync)', bold=True) return db_location = args[0] _, local_path = tempfile.mkstemp('.sqlite') use_shm = False # does Shared Memory temp file exist ? use_wal = False # does Write-Ahead-Log temp file exist ? use_jnl = False # does Journal temp file exist ? write_back_tmp_sqlite = False # if enabled temporary DB files are re-uploaded, this has not been testes # update the full remote path for future syncs full_remote_file = db_location \ if os.path.isabs(db_location) else os.path.join(pwd(), db_location) click.secho('Caching local copy of database file...', fg='green') download([db_location, local_path]) if path_exists(full_remote_file + '-shm'): click.secho('... caching local copy of database "shm" file...', fg='green') download([db_location + '-shm', local_path + '-shm']) use_shm = True if path_exists(full_remote_file + '-wal'): click.secho('... caching local copy of database "wal" file...', fg='green') download([db_location + '-wal', local_path + '-wal']) use_wal = True if path_exists(full_remote_file + '-journal'): click.secho('... caching local copy of database "journal" file...', fg='green') download([db_location + '-journal', local_path + '-journal']) use_jnl = True click.secho('Validating SQLite database format', dim=True) with open(local_path, 'rb') as f: header = f.read(16) header = binascii.hexlify(header) if header != b'53514c69746520666f726d6174203300': click.secho('File does not appear to be a SQLite3 db. Try downloading and manually inspecting this one.', fg='red') cleanup(local_path) return click.secho('Connected to SQLite database at: {0}'.format(db_location), fg='green') # boot the litecli prompt lite = LiteCli(prompt='SQLite @ {} > '.format(db_location)) lite.connect(local_path) lite.run_cli() if _should_sync_once_done(args): click.secho('Synchronizing changes back...', dim=True) upload([local_path, full_remote_file]) # re-uploading temp sqlite files has not been tested and thus is disabled by default if write_back_tmp_sqlite: if use_shm: upload([local_path + '-shm', full_remote_file + '-shm']) if use_wal: upload([local_path + '-wal', full_remote_file + '-wal']) if use_jnl: upload([local_path + '-journal', full_remote_file + '-journal']) else: click.secho('NOT synchronizing changes back to device. Use --sync if you want that.', fg='green') # maak skoon cleanup(local_path) if use_shm: cleanup(local_path + '-shm') if use_wal: cleanup(local_path + '-wal') if use_jnl: cleanup(local_path + '-journal') ================================================ FILE: objection/commands/ui.py ================================================ import click from objection.state.connection import state_connection from ..state.device import device_state, Ios, Android def alert(args: list = None) -> None: """ Displays an alert message via a popup or a Toast message on the mobile device. :param args: :return: """ if len(args) <= 0: message = 'objection!' else: message = args[0] if device_state.platform == Ios: _alert_ios(message) if device_state.platform == Android: pass def _alert_ios(message: str): """ Display an alert on iOS :param message: :return: """ api = state_connection.get_api() api.ios_ui_alert(message) def ios_screenshot(args: list = None) -> None: """ Take an iOS screenshot. :param args: :return: """ if len(args) <= 0: click.secho('Usage: ios ui screenshot ', bold=True) return destination = args[0] if not destination.endswith('.png'): destination = destination + '.png' api = state_connection.get_api() png = api.ios_ui_screenshot() with open(destination, 'wb') as f: f.write(png) click.secho('Screenshot saved to: {0}'.format(destination), fg='green') def dump_ios_ui(args: list = None) -> None: """ Dumps the current iOS user interface in a serialized form. :param args: :return: """ api = state_connection.get_api() ui = api.ios_ui_window_dump() click.secho(ui) def bypass_touchid(args: list = None) -> None: """ Starts a new objection job that hooks into the iOS TouchID classes, replacing the verification logic to always pass. :param args: :return: """ api = state_connection.get_api() api.ios_ui_biometrics_bypass() def android_screenshot(args: list = None) -> None: """ Take an Android screenshot. :param args: :return: """ if len(args) <= 0: click.secho('Usage: android ui screenshot ', bold=True) return # add the .png extension if it does not already exist destination = args[0] if args[0].endswith('.png') else args[0] + '.png' # download the file api = state_connection.get_api() data = api.android_ui_screenshot() image = bytearray(map(lambda x: x % 256, data)) with open(destination, 'wb') as f: f.write(image) click.secho('Screenshot saved to: {0}'.format(destination), fg='green') def android_flag_secure(args: list = None) -> None: """ Control FLAG_SECURE of the current Activity, allowing or disallowing the use of hardware key combinations and screencap to take screenshots. :param args: :return: """ if len(args) <= 0 or args[0] not in ('true', 'false'): click.secho('Usage: android ui FLAG_SECURE ', bold=True) return api = state_connection.get_api() api.android_ui_set_flag_secure(args[0]) ================================================ FILE: objection/console/__init__.py ================================================ ================================================ FILE: objection/console/cli.py ================================================ import threading import time from pathlib import Path import click from frida import ServerNotRunningError from objection.commands.plugin_manager import load_plugin from objection.utils.agent import Agent, AgentConfig from objection.utils.helpers import debug_print, warn_about_older_operating_systems from .repl import Repl from ..__init__ import __version__ from ..commands.mobile_packages import patch_ios_ipa, patch_android_apk, sign_android_apk from ..state.api import api_state from ..state.app import app_state from ..state.connection import state_connection def get_agent() -> Agent: """ get_agent bootstraps an agent instance """ agent = Agent(AgentConfig( name=state_connection.name, host=state_connection.host, port=state_connection.port, device_type=state_connection.device_type, device_id=state_connection.device_id, spawn=state_connection.spawn, foremost=state_connection.foremost, debugger=state_connection.debugger, pause=not state_connection.no_pause, uid=state_connection.uid )) try: agent.run() except ServerNotRunningError: click.secho('Frida server or gadget is not running on the target!', fg='red') exit(1) return agent # Start the Click command group @click.group() @click.option('--network', '-N', is_flag=True, help='Connect using a network connection instead of USB.', show_default=True) @click.option('--host', '-h', default='127.0.0.1', show_default=True) @click.option('--port', '-P', required=False, default=27042, show_default=True) @click.option('--api-host', '-ah', default='127.0.0.1', show_default=True) @click.option('--api-port', '-ap', required=False, default=8888, show_default=True) @click.option('--name', '-n', required=False, help='Name or bundle identifier to attach to.', show_default=True) @click.option('--gadget', '-g', is_eager=True, hidden=True, deprecated="Please use '-n' or '--name' instead") @click.option('--serial', '-S', required=False, default=None, help='A device serial to connect to.') @click.option('--debug', '-d', required=False, default=False, is_flag=True, help='Enable debug mode with verbose output.') @click.option('--spawn', '-s', required=False, is_flag=True, help='Spawn the target.') @click.option('--no-pause', '-p', required=False, is_flag=True, help='Resume the target immediately.') @click.option('--foremost', '-f', required=False, is_flag=True, help='Use the current foremost application.') @click.option('--debugger', required=False, default=False, is_flag=True, help='Enable the Chrome debug port.') @click.option('--uid', required=False, default=None, help='Specify the uid to run as (Android only).') def cli(network: bool, host: str, port: int, api_host: str, api_port: int, name: str, gadget: str, serial: str, debug: bool, spawn: bool, no_pause: bool, foremost: bool, debugger: bool, uid: int) -> None: """ \b _ _ _ _ ___| |_|_|___ ___| |_|_|___ ___ | . | . | | -_| _| _| | . | | |___|___| |___|___|_| |_|___|_|_| |___|(object)inject(ion) \b Runtime Mobile Exploration by: @leonjza from @sensepost """ if debug: app_state.debug = debug if network: state_connection.use_network() state_connection.host = host state_connection.port = port if serial: state_connection.device_id = serial # set api parameters app_state.api_host = api_host app_state.api_port = api_port # Backwards compatibility if gadget is not None: name = gadget state_connection.name = name state_connection.spawn = spawn state_connection.no_pause = no_pause state_connection.foremost = foremost state_connection.debugger = debugger state_connection.uid = uid @cli.command() def api(): """ Start the objection API server in headless mode. """ agent = get_agent() state_connection.set_agent(agent=agent) click.secho(f'Starting API server on {app_state.api_host}:{app_state.api_port}', fg='yellow', bold=True) api_state.start(app_state.api_host, app_state.api_port, app_state.debug) @cli.command() @click.option('--plugin-folder', '-P', required=False, default=None, help='The folder to load plugins from.') @click.option('--quiet', '-q', required=False, default=False, is_flag=True) @click.option('--startup-command', '-s', required=False, multiple=True, help='A command to run before the repl polls the device for information.') @click.option('--file-commands', '-c', required=False, type=click.File('r'), help=('A file containing objection commands, separated by a ' 'newline, that will run before the repl polls the device for information.')) @click.option('--startup-script', '-S', required=False, type=click.File('r'), help='A script to import and run before the repl polls the device for information.') @click.option('--enable-api', '-a', required=False, default=False, is_flag=True, help='Start the objection API server.') def start(plugin_folder: str, quiet: bool, startup_command: str, file_commands, startup_script: click.File, enable_api: bool) -> None: """ Start a new session """ agent = get_agent() state_connection.set_agent(agent) # load plugins if plugin_folder: folder = Path(plugin_folder).resolve() debug_print(f'[plugin] Plugins path is: {folder}') for p in folder.iterdir(): if p.is_file() or p.name.startswith('.'): debug_print(f'[plugin] Skipping {p.name}') continue debug_print(f'[plugin] Attempting to load plugin at {p}') load_plugin([p]) repl = Repl() if startup_script: click.secho(f'Importing and running startup script at: {startup_script}', dim=True) script_name = f'startup_script<{startup_script.name}>' agent.attach_script(script_name, startup_script.read()) if startup_command: for command in startup_command: click.secho(f'Running a startup command... {command}', dim=True) repl.run_command(command) if file_commands: click.secho('Running commands from file...', bold=True) for command in file_commands.readlines(): command = command.strip() if command == '': continue # run the command using the instantiated repl click.secho(f'Running: \'{command}\':\n', dim=True) repl.run_command(command) warn_about_older_operating_systems() # start the api server if enable_api: def api_thread(): """ Small method to run Flask non-blocking """ api_state.start(app_state.api_host, app_state.api_port) click.secho(f'Starting API server on {app_state.api_host}:{app_state.api_port}', fg='yellow', bold=True) thread = threading.Thread(target=api_thread) thread.daemon = True thread.start() time.sleep(2) # drop into the repl repl.run(quiet=quiet) # Some ugly backwards compatibility @cli.command(deprecated="Use 'objection start' instead of 'objection explore'", hidden=True) @click.option('--plugin-folder', '-P', required=False, default=None, help='The folder to load plugins from.') @click.option('--quiet', '-q', required=False, default=False, is_flag=True) @click.option('--startup-command', '-s', required=False, multiple=True, help='A command to run before the repl polls the device for information.') @click.option('--file-commands', '-c', required=False, type=click.File('r'), help=('A file containing objection commands, separated by a ' 'newline, that will run before the repl polls the device for information.')) @click.option('--startup-script', '-S', required=False, type=click.File('r'), help='A script to import and run before the repl polls the device for information.') @click.option('--enable-api', '-a', required=False, default=False, is_flag=True, help='Start the objection API server.') def explore(plugin_folder: str, quiet: bool, startup_command: str, file_commands, startup_script: click.File, enable_api: bool) -> None: """ Deprecated: Use 'start' instead. """ # Call the start command's callback directly ctx = click.get_current_context() ctx.invoke(start, plugin_folder=plugin_folder, quiet=quiet, startup_command=startup_command, file_commands=file_commands, startup_script=startup_script, enable_api=enable_api) @cli.command() @click.option('--hook-debug', '-d', required=False, default=False, is_flag=True, help='Print compiled hooks as they are run to the screen and logfile.') @click.argument('command', nargs=-1) def run(hook_debug: bool, command: tuple) -> None: """ Run a single objection command. """ if len(command) <= 0: click.secho('Please specify a command to run', fg='red') return # specify if hooks should be debugged app_state.debug_hooks = hook_debug agent = get_agent() state_connection.set_agent(agent=agent) command = ' '.join(command) # use the methods in the main REPL to run the command click.secho('Running command... `{0}`'.format(command), dim=True) Repl().run_command(command) @cli.command() def version() -> None: """ Prints the current version and exits. """ click.secho('objection: {0}'.format(__version__)) @cli.command() @click.option('--source', '-s', help='The source IPA to patch', required=True) @click.option('--gadget-version', '-V', help=('The gadget version to use. If not ' 'specified, the latest version will be used.'), default=None) @click.option('--codesign-signature', '-c', help='Codesigning Identity to use. Get it with: `security find-identity -p codesigning -v`', required=True) @click.option('--provision-file', '-P', help='The .mobileprovision file to use in the patched .ipa') @click.option('--binary-name', '-b', help='Name of the Mach-O binary in the IPA (used to patch with Frida)') @click.option('--skip-cleanup', '-k', is_flag=True, help='Do not clean temporary files once finished.', show_default=True) @click.option('--pause', '-p', is_flag=True, help='Pause the patcher before rebuilding the IPA.', show_default=True) @click.option('--unzip-unicode', '-z', is_flag=True, help='Unzip IPA containing Unicode characters.') @click.option('--gadget-config', '-C', default=None, help=( 'The gadget configuration file to use. ' 'Refer to https://frida.re/docs/gadget/ for more information.'), show_default=False) @click.option('--script-source', '-l', default=None, help=( 'A script file to use with the the "path" config type. ' 'Remember that use the name of this file in your "path". It will be next to the config.'), show_default=False) @click.option('--bundle-id', '-b', default=None, help='The bundleid to set when codesigning the IPA') def patchipa(source: str, gadget_version: str, codesign_signature: str, provision_file: str, binary_name: str, skip_cleanup: bool, pause: bool, unzip_unicode: bool, gadget_config: str, script_source: str, bundle_id: str) -> None: """ Patch an IPA with the FridaGadget dylib. """ patch_ios_ipa(**locals()) @cli.command() @click.option('--source', '-s', help='The source APK to patch', required=True) @click.option('--architecture', '-a', help=('The architecture of the device the patched APK will run on. ' 'This can be determined with `adb shell getprop ro.product.cpu.abi`. ' 'If it is not specified, this command will try and determine it automatically.'), required=False) @click.option('--gadget-version', '-V', help=('The gadget version to use. If not ' 'specified, the latest version will be used.'), default=None) @click.option('--pause', '-p', is_flag=True, help='Pause the patcher before rebuilding the APK.', show_default=True) @click.option('--skip-cleanup', '-k', is_flag=True, help='Do not clean temporary files once finished.', show_default=True) @click.option('--enable-debug', '-d', is_flag=True, help='Set the android:debuggable flag to true in the application manifest.', show_default=True) @click.option('--network-security-config', '-N', is_flag=True, default=False, help='Include a network_security_config.xml file allowing for user added CA\'s to be trusted on ' 'Android 7 and up. This option can not be used with the --skip-resources flag.') @click.option('--skip-resources', '-D', is_flag=True, default=False, help='Skip resource decoding as part of the apktool processing.', show_default=False) @click.option('--skip-signing', '-C', is_flag=True, default=False, help='Skip signing the apk file.', show_default=False) @click.option('--target-class', '-t', help='The target class to patch.', default=None) @click.option('--use-aapt2', '-2', is_flag=True, default=False, help='Use the aapt2 binary instead of aapt as part of the apktool processing.', show_default=False) @click.option('--gadget-config', '-c', default=None, help=( 'The gadget configuration file to use. ' 'Refer to https://frida.re/docs/gadget/ for more information.'), show_default=False) @click.option('--script-source', '-l', default=None, help=('A script file to use with the the "path" config type. ' 'Specify "libfrida-gadget.script.so" as the "path" in your config.'), show_default=False) @click.option('--ignore-nativelibs', '-n', is_flag=True, default=False, help='Do not change the extractNativeLibs flag in the AndroidManifest.xml.', show_default=False) @click.option('--manifest', '-m', help='A decoded AndroidManifest.xml file to read.', default=None) @click.option('--only-main-classes', help="Only patch classes that are in the main dex file.", is_flag=True, default=False) @click.option('--fix-concurrency-to', '-j', help="Only use N threads for repackaging - set to 1 if running into OOM errors.", default=None) def patchapk(source: str, architecture: str, gadget_version: str, pause: bool, skip_cleanup: bool, enable_debug: bool, skip_resources: bool, network_security_config: bool, target_class: str, use_aapt2: bool, gadget_config: str, script_source: str, ignore_nativelibs: bool, manifest: str, skip_signing: bool, only_main_classes:bool = False, fix_concurrency_to = None) -> None: """ Patch an APK with the frida-gadget.so. """ # ensure we decode resources if we have the network-security-config flag. if network_security_config and skip_resources: click.secho('The --network-security-config flag is incompatible with the --skip-resources flag.', fg='red') return # ensure we decode resources if we have the enable-debug flag. if enable_debug and skip_resources: click.secho('The --enable-debug flag is incompatible with the --skip-resources flag.', fg='red') return # ensure we decode resources if we do not have the --ignore-nativelibs flag. if not ignore_nativelibs and skip_resources: click.secho('The --ignore-nativelibs flag is required with the --skip-resources flag.', fg='red') return patch_android_apk(**locals()) @cli.command() @click.argument('sources', nargs=-1, type=click.Path(exists=True), required=True) @click.option('--skip-cleanup', '-k', is_flag=True, help='Do not clean temporary files once finished.', show_default=True) def signapk(sources, skip_cleanup: bool) -> None: """ Zipalign and sign an APK with the objection key. """ for source in sources: sign_android_apk(source, skip_cleanup) if __name__ == '__main__': cli() ================================================ FILE: objection/console/commands.py ================================================ from objection.commands import http from ..commands import command_history from ..commands import custom from ..commands import device from ..commands import filemanager from ..commands import frida_commands from ..commands import jobs from ..commands import memory from ..commands import plugin_manager from ..commands import sqlite from ..commands import ui from ..commands.android import clipboard from ..commands.android import command from ..commands.android import general from ..commands.android import generate as android_generate from ..commands.android import heap as android_heap from ..commands.android import hooking as android_hooking from ..commands.android import intents from ..commands.android import keystore from ..commands.android import pinning as android_pinning from ..commands.android import proxy as android_proxy from ..commands.android import root from ..commands.ios import binary from ..commands.ios import bundles from ..commands.ios import cookies from ..commands.ios import generate as ios_generate from ..commands.ios import heap as ios_heap from ..commands.ios import hooking as ios_hooking from ..commands.ios import jailbreak from ..commands.ios import keychain from ..commands.ios import monitor as ios_crypto from ..commands.ios import nsurlcredentialstorage from ..commands.ios import nsuserdefaults from ..commands.ios import pasteboard from ..commands.ios import pinning as ios_pinning from ..commands.ios import plist # commands are defined with their name being the key, then optionally # have a meta, dynamic and commands key. # meta: A small one-liner containing information about the command itself # dynamic: A method to execute that would return completions to populate in the prompt # exec: The *actual* method to execute when the command is issued. # commands help is stored in the help files directory as a txt file. COMMANDS = { 'plugin': { 'meta': 'Work with plugins', 'commands': { 'load': { 'meta': 'Load a plugin', 'exec': plugin_manager.load_plugin } } }, '!': { 'meta': 'Execute an Operating System command', 'exec': None, # handled in the Repl class itself }, 'reconnect': { 'meta': 'Reconnect to the current app', 'exec': None, # handled in the Repl class itself }, 'reconnect_spawn': { 'meta': 'Respawn the current app', 'exec': None, # handled in the Repl class itself }, 'resume': { 'meta': 'Resume the attached process', 'exec': None }, 'import': { 'meta': 'Import fridascript from a full path and run it', 'exec': frida_commands.load_background }, 'ping': { 'meta': 'Ping the injected agent', 'exec': frida_commands.ping }, # file manager commands 'cd': { 'meta': 'Change the current working directory', 'dynamic': filemanager.list_folders_in_current_fm_directory, 'exec': filemanager.cd }, 'commands': { 'meta': 'Work with commands run in the current session', 'commands': { 'history': { 'meta': 'List all unique commands that have run for this session', 'exec': command_history.history, }, 'save': { 'meta': 'Save all unique commands that have run in this session to a file', 'exec': command_history.save }, 'clear': { 'meta': 'Clear the current sessions command history', 'exec': command_history.clear } } }, 'ls': { 'meta': 'List files in the current working directory', 'dynamic': filemanager.list_folders_in_current_fm_directory, 'exec': filemanager.ls, }, 'pwd': { 'meta': 'Print the current working directory on the device', 'exec': filemanager.pwd_print, }, 'filesystem': { 'meta': 'Work with files on the remote filesystem', 'commands': { 'cat': { 'meta': 'Print a files contents', 'dynamic': filemanager.list_files_in_current_fm_directory, 'exec': filemanager.cat }, 'upload': { 'meta': 'Upload a file', 'exec': filemanager.upload }, 'download': { 'meta': 'Download a file or folder', 'flags': ['--folder'], 'dynamic': filemanager.list_content_in_current_fm_directory, 'exec': filemanager.download }, # http file server # 'http': { # 'meta': 'Work with an on device HTTP file server', # 'commands': { # 'start': { # 'meta': 'Start\'s an HTTP server in the current working directory', # 'exec': http.start # }, # 'status': { # 'meta': 'Get the status of the HTTP server', # 'exec': http.status # }, # 'stop': { # 'meta': 'Stop\'s a running HTTP server', # 'exec': http.stop # } # } # }, } }, 'rm': { 'meta': 'Delete files from the remote filesystem', 'dynamic': filemanager.list_files_in_current_fm_directory, 'exec': filemanager.rm }, # device and env info commands 'env': { 'meta': 'Print information about the environment', 'exec': device.get_environment }, 'frida': { 'meta': 'Get information about the Frida environment', 'exec': frida_commands.frida_environment }, 'evaluate': { 'meta': 'Evaluate JavaScript within the agent', 'exec': custom.evaluate }, # memory commands 'memory': { 'meta': 'Work with the current processes memory', 'commands': { 'dump': { 'meta': 'Commands to dump parts of the processes memory', 'commands': { 'all': { 'meta': 'Dump the entire memory of the current process', 'exec': memory.dump_all }, 'from_base': { 'meta': 'Dump (x) bytes of memory from a base address to file', 'exec': memory.dump_from_base } }, }, 'list': { 'meta': 'List memory related information about the current process', 'commands': { 'modules': { 'meta': 'List loaded modules in the current process', 'flags': ['--json'], 'exec': memory.list_modules }, 'exports': { 'meta': 'List the exports of a module', 'flags': ['--json'], 'exec': memory.list_exports } }, }, 'search': { 'meta': 'Search for pattern in the applications memory', 'flags': ['--string', '--offsets-only'], 'exec': memory.find_pattern }, 'replace': { 'meta': 'Search and replace pattern in the applications memory', 'flags': ['--string-pattern', '--string-replace'], 'exec': memory.replace_pattern }, 'write': { 'meta': 'Write raw bytes to a memory address. Use with caution!', 'flags': ['--string'], 'exec': memory.write } }, }, # sqlite commands 'sqlite': { 'meta': 'Work with SQLite databases', 'commands': { 'connect': { 'meta': 'Connect to a SQLite database file', 'flags': ['--sync'], 'dynamic': filemanager.list_files_in_current_fm_directory, 'exec': sqlite.connect }, } }, # jobs commands 'jobs': { 'meta': 'Work with objection jobs', 'commands': { 'list': { 'meta': 'List all of the current jobs', 'exec': jobs.show }, 'kill': { 'meta': 'Kill a job. This unloads the script', 'dynamic': jobs.list_current_jobs, 'exec': jobs.kill } } }, # generic ui commands 'ui': { 'meta': 'Generic user interface commands', 'commands': { 'alert': { 'meta': 'Show an alert message, optionally specifying the message to show. (Currently crashes iOS)', 'exec': ui.alert } } }, # android commands 'android': { 'meta': 'Commands specific to Android', 'commands': { 'deoptimize': { 'meta': 'Force the VM to execute everything in the interpreter', 'exec': general.deoptimise }, 'shell_exec': { 'meta': 'Execute a shell command', 'exec': command.execute }, 'hooking': { 'meta': 'Commands used for hooking methods in Android', 'commands': { 'list': { 'meta': 'Lists various bits of information', 'commands': { 'classes': { 'meta': 'List the currently loaded classes', 'exec': android_hooking.show_android_classes }, 'class_methods': { 'meta': 'List the methods available on a class', 'exec': android_hooking.show_android_class_methods }, 'class_loaders': { 'meta': 'List the registered class loaders', 'exec': android_hooking.show_android_class_loaders }, 'activities': { 'meta': 'List the registered Activities', 'exec': android_hooking.show_registered_activities }, 'receivers': { 'meta': 'List the registered BroadcastReceivers', 'exec': android_hooking.show_registered_broadcast_receivers }, 'services': { 'meta': 'List the registered Services', 'exec': android_hooking.show_registered_services }, } }, 'watch': { 'meta': 'Watch for Android Java invocations', 'exec': android_hooking.watch, 'flags': ['--dump-args', '--dump-backtrace', '--dump-return'] }, 'set': { 'meta': 'Set various values', 'commands': { 'return_value': { 'meta': 'Set a methods return value. Supports only boolean returns.', 'exec': android_hooking.set_method_return_value, 'flags': ['--dump-args', '--dump-return', '--dump-backtrace'] } } }, 'get': { 'meta': 'Get various values', 'commands': { 'current_activity': { 'meta': 'Get the currently foregrounded activity', 'exec': android_hooking.get_current_activity } } }, 'search': { 'meta': 'Search for various classes and or methods', 'exec': android_hooking.search, 'flags': ['--json', '--only-classes'] }, 'notify': { 'meta': 'Notify when a class becomes available', 'exec': android_hooking.notify, 'flags': ['--dump-args', '--dump-return', '--dump-backtrace', '--watch'] }, 'generate': { 'meta': 'Generate Frida hooks for Android', 'commands': { 'class': { 'meta': 'A generic hook manager for Classes', 'exec': android_generate.clazz }, 'simple': { 'meta': 'Simple hooks for each Class method', 'exec': android_generate.simple } } } }, }, 'heap': { 'meta': 'Commands to work with the Android Heap', 'commands': { 'search': { 'meta': 'Search for information about the current Android heap', 'commands': { 'instances': { 'meta': 'Search for live instances of a particular class', 'exec': android_heap.instances } } }, 'print': { 'meta': 'Print information about objects on the Android heap', 'commands': { 'fields': { 'meta': 'Print instance fields for a Java object handle', 'exec': android_heap.fields }, 'methods': { 'meta': 'Print instance methods for an Android handle', 'flags': ['--without-arguments'], 'exec': android_heap.methods } } }, 'execute': { 'meta': 'Execute methods on Java class handles', 'flags': ['--return-string'], 'exec': android_heap.execute }, 'evaluate': { 'meta': 'Evaluate JavaScript on Java class handle', 'exec': android_heap.evaluate } } }, 'keystore': { 'meta': 'Commands to work with the Android KeyStore', 'commands': { 'list': { 'meta': 'Lists entries in the Android KeyStore', 'exec': keystore.entries }, 'detail': { 'meta': 'Lists details for all items in the Android KeyStore', 'flags': ['--json'], 'exec': keystore.detail }, 'clear': { 'meta': 'Clears the Android KeyStore', 'exec': keystore.clear }, 'watch': { 'meta': 'Watches usage of the Android keystore', 'exec': keystore.watch } } }, 'clipboard': { 'meta': 'Work with the Android Clipboard', 'commands': { 'monitor': { 'meta': 'Monitor the Android Clipboard', 'exec': clipboard.monitor } } }, 'intent': { 'meta': 'Commands to work with Android intents', 'commands': { 'launch_activity': { 'meta': 'Launch an Activity class using an Intent', 'exec': intents.launch_activity }, 'launch_service': { 'meta': 'Launch a Service class using an Intent', 'exec': intents.launch_service }, 'implicit_intents': { 'meta': 'Analyze implicit intents', 'exec': intents.analyze_implicit_intents, 'flags': ['--dump-backtrace'] } } }, 'root': { 'meta': 'Commands to work with Android root detection', 'commands': { 'disable': { 'meta': 'Attempt to disable root detection', 'exec': root.disable }, 'simulate': { 'meta': 'Attempt to simulate a rooted environment', 'exec': root.simulate } } }, 'sslpinning': { 'meta': 'Work with Android SSL pinning', 'commands': { 'disable': { 'meta': 'Attempt to disable SSL pinning in various Java libraries/classes', 'flags': ['--quiet'], 'exec': android_pinning.android_disable } } }, 'proxy': { 'meta': 'Commands to work with a proxy for the application', 'commands': { 'set': { 'meta': 'Set a proxy for the application', 'exec': android_proxy.android_proxy_set } } }, 'ui': { 'meta': 'Android user interface commands', 'commands': { 'screenshot': { 'meta': 'Screenshot the current Activity', 'exec': ui.android_screenshot }, 'FLAG_SECURE': { 'meta': 'Control FLAG_SECURE of the current Activity', 'exec': ui.android_flag_secure }, } }, }, }, # ios commands 'ios': { 'meta': 'Commands specific to iOS', 'commands': { 'info': { 'meta': 'Get iOS and application related information', 'commands': { 'binary': { 'meta': 'Get information about application binaries and dylibs', 'exec': binary.info } } }, 'keychain': { 'meta': 'Work with the iOS keychain', 'commands': { 'dump': { 'meta': 'Dump the keychain for the current app\'s entitlement group', 'flags': ['--json', '--smart'], 'exec': keychain.dump }, 'dump_raw': { 'meta': 'Dump raw, unprocessed keychain entries (advanced)', 'exec': keychain.dump_raw }, 'clear': { 'meta': 'Delete all keychain entries for the current app\'s entitlement group', 'exec': keychain.clear }, 'remove': { 'meta': 'Remove an entry from the iOS keychain', 'flags': ['--account', '--service'], 'exec': keychain.remove }, 'update': { 'meta': 'Update an entry from the iOS keychain', 'flags': ['--account', '--service', '--newData'], 'exec': keychain.update }, 'add': { 'meta': 'Add an entry to the iOS keychain', 'flags': ['--account', '--service', '--data'], 'exec': keychain.add } } }, 'plist': { 'meta': 'Work with iOS Plists', 'commands': { 'cat': { 'meta': 'Cat a plist', 'dynamic': filemanager.list_files_in_current_fm_directory, 'exec': plist.cat } } }, 'bundles': { 'meta': 'Work with iOS Bundles', 'commands': { 'list_frameworks': { 'meta': 'Lists all of the application\'s bundles that represent frameworks', 'flags': ['--include-apple-frameworks', '--full-path'], 'exec': bundles.show_frameworks }, 'list_bundles': { 'meta': 'Lists all of the application\'s non framework bundles', 'flags': ['--full-path'], 'exec': bundles.show_bundles } } }, 'nsuserdefaults': { 'meta': 'Work with NSUserDefaults', 'commands': { 'get': { 'meta': 'Get all of the entries', 'exec': nsuserdefaults.get } } }, 'nsurlcredentialstorage': { 'meta': 'Work with the shared NSURLCredentialStorage', 'commands': { 'dump': { 'meta': 'Dump all of the credentials in the shared NSURLCredentialStorage', 'exec': nsurlcredentialstorage.dump } } }, 'cookies': { 'meta': 'Work with shared cookies', 'commands': { 'get': { 'meta': 'Get the current apps shared cookies', 'flags': ['--json'], 'exec': cookies.get } } }, 'ui': { 'meta': 'iOS user interface commands', 'commands': { 'alert': { 'meta': ('Show an alert message, optionally specifying the message to' 'show. (Currently crashes iOS)'), 'exec': ui.alert }, 'dump': { 'meta': 'Dump the serialized UI', 'exec': ui.dump_ios_ui }, 'screenshot': { 'meta': 'Screenshot the current UIView', 'exec': ui.ios_screenshot }, 'biometrics_bypass': { 'meta': 'Hook the iOS Biometrics LAContext and respond with successful auth', 'exec': ui.bypass_touchid } } }, 'heap': { 'meta': 'Commands to work with the iOS heap', 'commands': { 'print': { 'meta': 'Print information about objects on the iOS heap', 'commands': { 'ivars': { 'meta': 'Print instance variables for an Objective-C object', 'flags': ['--to-utf8'], 'exec': ios_heap.ivars }, 'methods': { 'meta': 'Print instance methods for an Objective-C object', 'flags': ['--without-arguments'], 'exec': ios_heap.methods } } }, 'search': { 'meta': 'Search for information about the current iOS heap', 'commands': { 'instances': { 'meta': 'Search for live instances of a particular class', 'exec': ios_heap.instances } } }, 'execute': { 'meta': 'Execute methods on objects on the iOS heap', 'flags': ['--return-string'], 'exec': ios_heap.execute }, 'evaluate': { 'meta': 'Evaluate JavaScript on objects on the iOS heap', 'flags': ['--inline'], 'exec': ios_heap.evaluate } } }, 'hooking': { 'meta': 'Commands used for hooking methods in iOS', 'commands': { 'list': { 'meta': 'Lists various bits of information', 'commands': { 'classes': { 'meta': 'List classes available in the current application', 'exec': ios_hooking.show_ios_classes }, 'class_methods': { 'meta': 'List the methods in a class', 'flags': ['--include-parents'], 'exec': ios_hooking.show_ios_class_methods } } }, 'watch': { 'meta': 'Watch invocations of classes and methods', 'exec': ios_hooking.watch, 'flags': ['--dump-args', '--dump-backtrace', '--dump-return'], }, 'set': { 'meta': 'Set various values', 'commands': { 'return_value': { 'meta': 'Set a methods return value. Supports only boolean returns', 'exec': ios_hooking.set_method_return_value } } }, 'search': { 'meta': 'Search for various classes and or methods', 'exec': ios_hooking.search, 'flags': ['--json', '--only-classes'] }, 'generate': { 'meta': 'Generate Frida hooks for iOS', 'commands': { 'class': { 'meta': 'A generic hook manager for Classes', 'exec': ios_generate.clazz }, 'simple': { 'meta': 'Simple hooks for each Class method', 'exec': ios_generate.simple } }, } } }, 'pasteboard': { 'meta': 'Work with the iOS pasteboard', 'commands': { 'monitor': { 'meta': 'Monitor the iOS pasteboard', 'exec': pasteboard.monitor } } }, 'sslpinning': { 'meta': 'Work with iOS SSL pinning', 'commands': { 'disable': { 'meta': 'Attempt to disable SSL pinning in various iOS libraries/classes', 'flags': ['--quiet'], 'exec': ios_pinning.ios_disable } } }, 'jailbreak': { 'meta': 'Work with iOS Jailbreak detection', 'commands': { 'disable': { 'meta': 'Attempt to disable Jailbreak detection', 'exec': jailbreak.disable }, 'simulate': { 'meta': 'Attempt to simulate a Jailbroken environment', 'exec': jailbreak.simulate }, } }, 'monitor': { 'meta': 'Commands to work with ios function monitoring', 'commands': { 'crypto': { 'meta': 'Monitor CommonCrypto operations', 'exec': ios_crypto.crypto_enable } }, }, } }, 'exit': { 'meta': 'Exit', }, } ================================================ FILE: objection/console/completer.py ================================================ import collections from prompt_toolkit.completion import Completer, Completion, CompleteEvent from prompt_toolkit.document import Document from .commands import COMMANDS from ..utils.helpers import get_tokens class CommandCompleter(Completer): """ The objection REPL command completer. """ def __init__(self) -> None: super(CommandCompleter, self).__init__() self.COMMANDS = COMMANDS def find_completions(self, document: Document) -> dict: """ Find tab completions from the commands repository. Completions are returned based on tokens extracted from the command text received by prompt_toolkit. A dictionary is then walked, matching a token to a nested dictionary until no more dictionaries are available. The resultant dictionary then becomes the suggestions for tab completion. Some commands may have 'dynamic' completions, such as file system related commands. They are defined with a 'dynamic' key, and the method defined as the value for this key is executed to get completions. :param document: :return: """ # extract tokens from the document similar to # how a shell invocation would have been done. # we will also cleanup flags that come in the form # of --flag so that multiples can be suggested. tokens = [token for token in get_tokens(document.text) if not token.startswith('--')] # extract the flags in the received tokens. This list # will be used to remove suggested flags from those # already present in the command. flags = [flag for flag in get_tokens(document.text) if flag.startswith('--')] # start with the current suggestions dictionary being # all commands current_suggestions = self.COMMANDS # when the tokens are extracted, we are expecting something in # the format of: # command sub_command sub_sub_command # so, lets use that and search the the COMMAND dictionary for # the last dictionary with a correct suggestion for token in tokens: candidate = token.lower() if candidate in list(current_suggestions.keys()): # if there are sub commands, grab them if 'commands' in current_suggestions[candidate]: current_suggestions = current_suggestions[candidate]['commands'] # dynamic commands change based on the current status of the # environment, so, call the method defined elif 'dynamic' in current_suggestions[candidate]: current_suggestions = current_suggestions[candidate]['dynamic']() # make --flags in the 'flags' key tab completable elif 'flags' in current_suggestions[candidate]: current_suggestions = { flag: '' for flag in current_suggestions[candidate]['flags'] if flag not in flags } # in this case, there are probably no sub commands, so return # an empty dictionary else: return {} suggestions = {} # once we have the deepest suggestions dictionary in the # current_suggestions variable, loop through and check for # 'sorta' matched versions if current_suggestions and len(current_suggestions) > 0: for k, _ in current_suggestions.items(): # fuzzy-ish matching when part of a word is in a suggestion if document.get_word_before_cursor().lower() in k.lower(): suggestions[k] = current_suggestions[k] return suggestions def get_completions(self, document: Document, complete_event: CompleteEvent) -> Completion: """ The main method that gets called by prompt-toolkit to determine which completions to show. This :param document: :param complete_event: :return: """ # get the stuff we have typed so far word_before_cursor = document.get_word_before_cursor() # if this is an os command, we can't complete anything if document.text.startswith('!'): return commands = self.find_completions(document) # if there are no commands return if len(commands) <= 0: return # sort alphabetically commands = collections.OrderedDict(sorted(list(commands.items()), key=lambda t: t[0])) # loop the commands that we have determined to be useful # based on the current text input and populate a 'meta' field # if one exists. for cmd, extra in commands.items(): meta = extra['meta'] if type(extra) is dict and 'meta' in extra else None # finally, yield the generator for completions yield Completion(cmd, -(len(word_before_cursor)), display_meta=meta) ================================================ FILE: objection/console/helpfiles/!.txt ================================================ Command: ! Usage: ! Executes operating system commands using pythons Subprocess module. Commands that have caused an error, or when there is output to display from stderr, will show in red. Commands that have output that was sent to stdout will display in white. Examples: !ls !uname -a ================================================ FILE: objection/console/helpfiles/android.clipboard.monitor.txt ================================================ Command: android clipboard monitor Usage: android clipboard monitor Gets a handle on the Android clipboard service and polls it every 5 seconds for data. If new data is found, different from the previous poll, that data will be dumped to screen. Examples: android clipboard monitor ================================================ FILE: objection/console/helpfiles/android.clipboard.txt ================================================ Contains subcommands to work with the Android Clipboard. ================================================ FILE: objection/console/helpfiles/android.heap.search.instances.txt ================================================ Command: android heap search instances Usage: android heap search instances Search for and print live instances of a specific Java class, specified by a fully qualified class name. Output is the result of an attempt at getting a string value for a discovered object which would typically contain property values for the object. Hashcodes in the list could be used for other heap interactions. Examples: android heap search instances java.net.Socket android heap search instances java.io.File ================================================ FILE: objection/console/helpfiles/android.hooking.list.activities.txt ================================================ Command: android hooking list activities Usage: android hooking list activities List all the Activities that have been specified by the AndroidManifest.xml. Activity classes found using this command could be used with the `android intent launch_activity` command to launch them. Examples: android hooking list activities ================================================ FILE: objection/console/helpfiles/android.hooking.list.class_methods.txt ================================================ Command: android hooking list class_methods Usage: android hooking list class_methods Lists the methods declared in a Java class, together with the arguments that they may required using getDeclaredMethods(). Examples: android hooking list class_methods com.example.utils.RootUtils android hooking list class_methods com.test.Helpers.Communications ================================================ FILE: objection/console/helpfiles/android.hooking.list.classes.txt ================================================ Command: android hooking list classes Usage: android hooking list classes List the classes *currently loaded*. As the target application gets used more, this command will return more classes. Examples: android hooking list classes ================================================ FILE: objection/console/helpfiles/android.hooking.list.receivers.txt ================================================ Command: android hooking list receivers Usage: android hooking list receivers List all the Broadcast Receivers that have been registered at Runtime as well as those specified by the AndroidManifest.xml. Examples: android hooking list receivers ================================================ FILE: objection/console/helpfiles/android.hooking.list.services.txt ================================================ Command: android hooking list services Usage: android hooking list services List all the Services that have been registered at Runtime as well as those specified by the AndroidManifest.xml. Examples: android hooking list services ================================================ FILE: objection/console/helpfiles/android.hooking.list.txt ================================================ Contains subcommands to list various bits of Java class information. ================================================ FILE: objection/console/helpfiles/android.hooking.search.classes.txt ================================================ Command: android hooking search classes Usage: android hooking search classes Search for classes in the current Java runtime with the search string as part of the class name. Examples: android hooking search classes jailbreak android hooking search classes sslpinning ================================================ FILE: objection/console/helpfiles/android.hooking.search.methods.txt ================================================ Command: android hooking search methods Usage: android hooking search methods (optional: package-filter) Search for class methods in the current Java runtime with the search string as part of the class name. An optional package filter may be used to limit the method search to a specific namespace. WARNING: This command may easily crash the application without a filter. Examples: android hooking search classes jailbreak com.package android hooking search classes sslpinning ================================================ FILE: objection/console/helpfiles/android.hooking.search.txt ================================================ Contains subcommands helpful when searching for classes and methods. ================================================ FILE: objection/console/helpfiles/android.hooking.set.return_value.txt ================================================ Command: android hooking set return_value Usage: android hooking set return_value "" "" Sets a methods return value to always be true / false. This could be a useful module to use in cases where generic SSL pinning or root detection / simulations are possible. If an overload is not specified, all overloads for the base methods will be modified. NOTE: This is only possible on methods that return a boolean. While methods that don't return booleans can be hooked, the results may be unpredictable. Examples: android hooking set return_value com.example.test.rootUtils.isRooted false android hooking set return_value com.example.test.rootUtils.isRooted "java.lang.String" false android hooking set return_value com.example.test.encryption.hasKey.overload("java.lang.String") true android hooking set return_value com.example.test.communication.setPinningType.overload("java.lang.String", "[B") false ================================================ FILE: objection/console/helpfiles/android.hooking.txt ================================================ Contains subcommands to hook Android Java methods. ================================================ FILE: objection/console/helpfiles/android.hooking.watch.class.txt ================================================ Command: android hooking watch class Usage: android hooking watch class Hooks a specified class' methods and reports on invocations. Examples: android hooking watch class com.example.test ================================================ FILE: objection/console/helpfiles/android.hooking.watch.class_method.txt ================================================ Command: android hooking watch class_method Usage: android hooking watch class_method (optional: --dump-args) (optional: --dump-backtrace) (optional: --dump-return) Hooks a specified class method and reports on invocations, together with the number of arguments that method was called with. This command will also hook all of the methods available overloads unless a specific overload is specified. If the --include-backtrace flag is provided, a full stack trace that lead to the methods invocation will also be dumped. This would aid in discovering who called the original method. Examples: android hooking watch class_method com.example.test.login android hooking watch class_method com.example.test.helper.executeQuery android hooking watch class_method com.example.test.helper.executeQuery "java.lang.String,java.lang.String" android hooking watch class_method com.example.test.helper.executeQuery --dump-backtrace android hooking watch class_method com.example.test.login --dump-args --dump-return ================================================ FILE: objection/console/helpfiles/android.hooking.watch.txt ================================================ Contains subcommands to watch for various bits of information on class invocations. ================================================ FILE: objection/console/helpfiles/android.intent.implicit_intents.txt ================================================ Command: android intent implicit_intents Usage: android intent implicit_intents Starts a hook to analyze implicit intents during runtime. Optionally add a backtrade by adding --dump-backtrace Examples: android intent implicit_intents ================================================ FILE: objection/console/helpfiles/android.intent.launch_activity.txt ================================================ Command: android intent launch_activity Usage: android intent launch_activity Launches an activity class by building a new Intent and running startActivity() with it as an argument. The Intent.FLAG_ACTIVITY_NEW_TASK flag is added to achieve this, with the side effect that the history stack may be reset. Examples: android intent launch_activity com.test.example.MainActivity android intent launch_activity com.test.example.SecretActivity android intent launch_activity com.example.test.Other ================================================ FILE: objection/console/helpfiles/android.intent.launch_service.txt ================================================ Command: android intent launch_service Usage: android intent launch_service Launches an exported service class by building a new Intent and running startActivity() with it as an argument. The Intent.FLAG_ACTIVITY_NEW_TASK flag is added to achieve this, with the side effect that the history stack may be reset. Examples: android intent launch_service com.test.example.PingService android intent launch_service com.test.example.utils.SyncService ================================================ FILE: objection/console/helpfiles/android.intent.txt ================================================ Contains subcommands to work with Android Intents. ================================================ FILE: objection/console/helpfiles/android.keystore.clear.txt ================================================ Command: android keystore clear Usage: android keystore clear Clears all aliases in the current applications 'AndroidKeyStore' Keystore. The KeyStore is loaded, and each alias entry found gets the deleteEntry() method on the KeyStore called. Examples: android keystore clear ================================================ FILE: objection/console/helpfiles/android.keystore.detail.txt ================================================ Command: android keystore detail Usage: android keystore detail Lists detailed 'AndroidKeyStore' items for the current application. Ref: https://developer.android.com/reference/java/security/KeyStore.html Examples: android keystore listDetails ================================================ FILE: objection/console/helpfiles/android.keystore.list.txt ================================================ Command: android keystore list Usage: android keystore list Lists aliases in the current applications 'AndroidKeyStore' KeyStore. Each alias is queried for its type which will be either a certificate or a key. Ref: https://developer.android.com/reference/java/security/KeyStore.html Examples: android keystore list ================================================ FILE: objection/console/helpfiles/android.keystore.txt ================================================ Contains subcommands to work with Android KeyStore. ================================================ FILE: objection/console/helpfiles/android.keystore.watch.txt ================================================ Command: android keystore watch Usage: android keystore watch Watches usage of the Android Keystore. Two KeyStore methods are watched at the moment. Those are: KeyStore.getKey() KeyStore.load() In both cases, a password for the keystore/key is passed along as the second parameter of the function. Ref: https://developer.android.com/reference/java/security/KeyStore.html Examples: android keystore watch ================================================ FILE: objection/console/helpfiles/android.root.disable.txt ================================================ Command: android root disable Usage: android root disable Attempts to disable root detection on Android devices. This is achieved by hooking numerous classes such as java.lang.String (for contains()), java.lang.Runtime (for exec()) and java.io.File (for exists()). These hooked methods have their properties or arguments inspected to determine if artifacts commonly checked for in root detection is used, and manipulated. If this method does not effectively disable root detection for you, keep in mind that it us very common for applications to have helper methods such as isRooted() that perform a number of checks in one class method. Using the boolean return module on one of these utility methods may achieve a similar effect. Examples: android root disable ================================================ FILE: objection/console/helpfiles/android.root.simulate.txt ================================================ Command: android root simulate Usage: android root simulate Attempts to simulate a rooted Android environment. This is achieved by responding positively to common checks that are performed within Android applications. Examples: android root simulate ================================================ FILE: objection/console/helpfiles/android.shell_exec.txt ================================================ Command: android shell_exec Usage: android shell_exec Execute a shell command on an android device. These commands are run from within the security context of the application that is being instrumented. Examples: android shell_exec id android shell_exec ls -lah / android shell_exec rm /data/data/user/0/somefile ================================================ FILE: objection/console/helpfiles/android.sslpinning.disable.txt ================================================ Command: android sslpinning disable Usage: android sslpinning disable Attempts to disable SSL Pinning on Android devices. This is achieved by creating a new TrustManager that will accept any certificate irrespective of its validity, and providing that to calls to SSLContext.init(). Additionally, to support the OkHTTP v3 library, the okhttp3.CertificatePinner.check() method is replaced with one that will not throw an exception in the case of an invalid certificate being presented. With these two implementations, the following request libraries should have its pinning checks disabled with this command: - Traditional HttpsURLConnection - OkHTTP - Retrofit (Wraps OkHTTP) - Volley (Uses a TrustManager) - Picasso (Uses a TrustManager) If this method does not disable the applications SSL pinning implementation, then it may still be possible to bypass it via 'helper' methods commonly used by developers to help when testing in development / staging environments. Be on the lookout for classes / methods that relate to pinning that may simply return a boolean value. Examples: android sslpinning disable ================================================ FILE: objection/console/helpfiles/android.sslpinning.txt ================================================ Contains subcommands to work with Android SSL pinning related calls. ================================================ FILE: objection/console/helpfiles/android.txt ================================================ Contains subcommands to work with Android specific features. These include shell commands, bypassing SSL pinning and simulating a rooted environment. ================================================ FILE: objection/console/helpfiles/android.ui.FLAG_SECURE.txt ================================================ Command: android ui FLAG_SECURE Usage: android ui FLAG_SECURE Control the value of FLAG_SECURE for the current Activity. Setting the value to false in activities where it is true by default may enable you to take screenshots using the hardware keys. NOTE: This command currently crashes the target application on 32bit devices due to an SELinux DENY. For more information see this PR: https://github.com/sensepost/objection/pull/24 Examples: android ui FLAG_SECURE false ================================================ FILE: objection/console/helpfiles/android.ui.screenshot.txt ================================================ Command: android ui screenshot Usage: android ui screenshot Screenshots the current foregrounded Activity and saves it as a PNG locally. If the `.png` extension is not used in the resultant filename, it will be automatically added. Examples: android ui screenshot application_image android ui screenshot app_screenshot.png ================================================ FILE: objection/console/helpfiles/cd.txt ================================================ Usage: cd Changes the current working directory on the device. Many commands within objection are mindful and aware of the current working directory. An example of this includes the sqlite command, that allows you to connect to a file in the current path, or to a file specified with a full path relative to root (/). For more directories that are applicable to the current app, inspect the output of the `env` command. Examples: cd Library/Caches cd Preferences cd / ================================================ FILE: objection/console/helpfiles/env.txt ================================================ Command: env Usage: env Display directory information for the current application environment. On iOS devices, this includes the location of the applications bundle, the Documents/ and Library/ directory. On Android devices, this includes the location of the files and cache directories to name a few. Examples: env ================================================ FILE: objection/console/helpfiles/exit.txt ================================================ Performs cleanups operations and quits the objection REPL. ================================================ FILE: objection/console/helpfiles/file.download.txt ================================================ Command: file download Usage: file download (optional: ) Download a file from a location on the mobile device, to a local destination. If no destination is provided, the downloaded file will be saved in the current directory with the same name Examples: file download Document/Preferences/test.sqlite foo.sqlite file download Document/Preferences/preferences.plist ================================================ FILE: objection/console/helpfiles/file.txt ================================================ Contains subcommands to work with files on the remote filesystem ================================================ FILE: objection/console/helpfiles/file.upload.txt ================================================ Command: file upload Usage: file upload (optional: ) Upload a file from the local filesystem to the remote filesystem. If a full path is not specified for the remote destination, the current working directory is assumed as the relative directory for the upload destination. If the file already exists on the remote filesystem, it will be overridden. If no remove filename is specified, the same filename of the source file will be used. Examples: file upload test.sqlite Document/Preferences/test.sqlite file upload foo.txt ================================================ FILE: objection/console/helpfiles/frida.txt ================================================ Command: frida Usage: frida Displays information about Frida. This includes the version of the Frida gadget, process architecture and platform. Examples: frida ================================================ FILE: objection/console/helpfiles/import.txt ================================================ Command: import Usage: import (optional: ) (optional: --no-exception-handler) Imports Fridascript from a file on the local filesystem and executes it as a job. To 'unload' the script, the job that was started to should be killed. You can list all of the current jobs using the `jobs list` command. If no name was specified for your job, a generic name of 'user-script' will be used for the job started as a result of the import. Scripts that are run using this command get wrapped in a global, generic JavaScript try/catch block. If this is not something that you want, the '--no-exception-handler' flag may be specified. Examples: import ~/home/myscript.js import ~/home/hooks/custom.js custom-hook-name import ~/home/hooks/custom.js custom-hook-name --no-exception-handler import ~/home/script.js --no-exception-handler ================================================ FILE: objection/console/helpfiles/ios.bundles.list_bundles.txt ================================================ Command: ios bundles list_bundles Usage: ios bundles list_bundles (optional: --full-path) Returns all the application's non-framework bundles. [1] Output includes the frameworks executable, bundle name and version. The path value is truncated by default, however, adding the --full-path flag would print the entire path to the framework. [1] https://developer.apple.com/documentation/foundation/nsbundle/1413705-allbundles?language=objc Examples: ios bundles list_bundles ios bundles list_bundles --full-path ================================================ FILE: objection/console/helpfiles/ios.bundles.list_frameworks.txt ================================================ Command: ios bundles list_frameworks Usage: ios bundles list_frameworks (optional: --include-apple-frameworks) (optional: --full-path) Returns all of the application's bundles that represent frameworks. Only frameworks with one or more Objective-C classes in them are included. [1] Output includes the frameworks executable, bundle name and version. The path value is truncated by default, however, adding the --full-path flag would print the entire path to the framework. The --include-apple-frameworks flag signals the command to also output bundles that form part of the com.apple package namespace. By default this is hidden and only external frameworks not in the com.apple namespace is returned. [1] https://developer.apple.com/documentation/foundation/nsbundle/1408056-allframeworks?language=objc Examples: ios bundles list_frameworks ios bundles list_frameworks --include-apple-frameworks ios bundles list_frameworks --include-apple-frameworks --full-path ios bundles list_frameworks --full-path ================================================ FILE: objection/console/helpfiles/ios.bundles.txt ================================================ Contains subcommands to work with iOS bundles. ================================================ FILE: objection/console/helpfiles/ios.cookies.get.txt ================================================ Command: ios cookies get Usage: ios cookies get Queries iOS's NSHTTPCookieStorage class, extracting cookie values out of the sharedHTTPCookieStorage. Various URL fetching methods use the sharedHTTPCookieStorage to store cookie data. This information may be useful to get session cookies for web services to reuse in other tools/browsers. Examples: ios cookies get ================================================ FILE: objection/console/helpfiles/ios.cookies.txt ================================================ Contains subcommands to work with iOS shared cookies. ================================================ FILE: objection/console/helpfiles/ios.hooking.list.class_methods.txt ================================================ Command: ios hooking list class_methods Usage: ios hooking list class_methods (--include-parents) Lists the methods within an Objective-C class. Adding the --include-parents flag will also list methods available due to class inheritance. Examples: ios hooking list class_methods KeychainDataManager ios hooking list class_methods KeychainDataManager --include-parents ================================================ FILE: objection/console/helpfiles/ios.hooking.list.classes.txt ================================================ Command: ios hooking list classes Usage: ios hooking list classes (optional: --ignore-native) Lists all of the classes in the current Objective-C runtime. Specifying the --ignore-native flag, filters out classes with common prefixes such as 'NS' and 'CF'. Examples: ios hooking list classes --ignore-native ================================================ FILE: objection/console/helpfiles/ios.hooking.list.txt ================================================ Contains subcommands to list various bits of information, such as Objective-C classes and class methods. ================================================ FILE: objection/console/helpfiles/ios.hooking.search.classes.txt ================================================ Command: ios hooking search classes Usage: ios hooking search classes Search for classes in the current Objective-C runtime with the search string as part of the class name. Examples: ios hooking search classes jailbreak ios hooking search classes sslpinning ================================================ FILE: objection/console/helpfiles/ios.hooking.search.methods.txt ================================================ Command: ios hooking search methods Usage: ios hooking search methods Search for methods in classes in the current Objective-C runtime with the search string as part of the method name. Examples: ios hooking search methods keychain ios hooking search methods sslpinning ================================================ FILE: objection/console/helpfiles/ios.hooking.search.txt ================================================ Contains subcommands helpful when searching for classes and methods. ================================================ FILE: objection/console/helpfiles/ios.hooking.set.return_value.txt ================================================ Command: ios hooking set return_value Usage: ios hooking set return_value "" Hooks into a specified Objective-C method and sets its return value to either True or False. This is useful in cases where simple methods are used to determine things like 'Should SSL pinning be enabled?' as an example. Examples: ios hooking set return_value "+[JailbreakDetection isJailbroken]" false ios hooking set return_value "-[SecurityHelper shouldPinSSL:]" true ================================================ FILE: objection/console/helpfiles/ios.hooking.set.txt ================================================ Sets various bits of hooking related information. ================================================ FILE: objection/console/helpfiles/ios.hooking.txt ================================================ Contains subcommands helpful when developing custom hooks. This includes discovery of Objective-C classes and methods in those classes, as well as dumping method arguments as they are called in real time. ================================================ FILE: objection/console/helpfiles/ios.hooking.watch.class.txt ================================================ Command: ios hooking watch class Usage: ios hooking watch (--include-parents) Hooks into all of the methods available in the Objective-C class specified by class_name and reports on invocations of any methods contained within. If the --include-parents flag is specified, all methods inherited from a parent class will also be hooked and reported on. Examples: ios hooking watch KeychainDataManager ios hooking watch PinnedNSURLSessionStarwarsApi --include-parents ================================================ FILE: objection/console/helpfiles/ios.hooking.watch.method.txt ================================================ Command: ios hooking watch method Usage: ios hooking method "" (optional: --dump-backtrace) (optional: --dump-args) (optional: --dump-return) Hooks into a specified Objective-C method and reports on invocations. A full class and method is expected, including whether its an instance or class method. If the --include-backtrace flag is provided, a full stack trace that lead to the methods invocation will also be dumped. Examples: ios hooking watch method "+[KeychainDataManager update:forKey:]" ios hooking watch method "-[PinnedNSURLSessionStarwarsApi getJsonResponseFrom:onSuccess:onFailure:]" --include-backtrace ios hooking watch method "+[KeychainDataManager update:forKey:]" --dump-args --dump-return ================================================ FILE: objection/console/helpfiles/ios.hooking.watch.txt ================================================ Contains subcommands to watch for method invocations on Objective-C classes. ================================================ FILE: objection/console/helpfiles/ios.jailbreak.disable.txt ================================================ Command: ios jailbreak disable Usage: ios jailbreak disable Attempts to disable Jailbreak detection on iOS devices. This is achieved by hooking the NSFileManager fileExistsAtPath method, and checking if it was called with a path to common Jailbroken path artifacts. Calls to the fork() method are also hooked and will respond with a 0, indicating that it was unsuccessful. Examples: ios jailbreak disable ================================================ FILE: objection/console/helpfiles/ios.jailbreak.simulate.txt ================================================ Command: ios jailbreak simulate Usage: ios jailbreak simulate Attempts to simulate a Jailbroken iOS environment. This is achieved by returning positive results for file existence checks from NSFileManager fileExistsAtPath as well as indicating that a fork() was successful if that is called. Examples: ios jailbreak simulate ================================================ FILE: objection/console/helpfiles/ios.jailbreak.txt ================================================ Contains subcommands to work with iOS Jailbreak detection, such as disabling it, or simulating that a device is Jailbroken. ================================================ FILE: objection/console/helpfiles/ios.keychain.add.txt ================================================ Command: ios keychain add Usage: ios keychain add --account --service --data Adds a new entry to the iOS keychain using SecItemAdd. The new keychain entry class would be kSecClassGenericPassword with no extra kSecAttrAccessControl set. Examples: ios keychain add --account token --data 1122-33344-55122-55512 ios keychain add --service foo --data bar ================================================ FILE: objection/console/helpfiles/ios.keychain.clear.txt ================================================ Command: ios keychain clear Usage: ios keychain clear Clears all the keychain items for the current application. This is achieved by iterating over the keychain type classes available in iOS and populating a search dictionary with them. This dictionary is then used as a query to SecItemDelete(), deleting the entries. Items that will be deleted include everything stored with the entitlement group used during the patching/signing process. Examples: ios keychain clear ================================================ FILE: objection/console/helpfiles/ios.keychain.dump.txt ================================================ Command: ios keychain dump Usage: ios keychain dump (optional: --json ) (optional: --smart) Extracts the keychain items for the current application. This is achieved by iterating over the keychain type classes available in iOS and populating a search dictionary with them. This dictionary is then used as a query to SecItemCopyMatching() and the results parsed. Use the --smart flag to attempt smart decoding of items in the keychain. By default, UTF8 string representations of data will be displayed. For a hex string of the data, use the --json flag which will indlude a 'dataHex' key. By default, only a small subset of each entry is displayed. For a more complete dump, use the --json flag. Items that will be accessible include everything stored with the entitlement group used during the patching/signing process. Providing a filename with the --json flag will dump all of the keychain attributes to the file specified for later inspection. Examples: ios keychain dump ios keychain dump --json keychain.json ================================================ FILE: objection/console/helpfiles/ios.keychain.txt ================================================ Contains subcommands to work with the iOS keychain. ================================================ FILE: objection/console/helpfiles/ios.monitor.crypto.txt ================================================ Command: ios monitor crypto monitor Usage: ios monitor crypto monitor Hooks CommonCrypto to output information about cryptographic operation. Works best for AES with PKCS7 Padding. Currently the following hooks are supported: - SecRandomCopyBytes - CCKeyDerivationPBKDF - CCCrypt - CCCryptorCreate - CCCryptorUpdate - CCCryptorFinal Examples: ios monitor crypto monitor ================================================ FILE: objection/console/helpfiles/ios.nsuserdefaults.get.txt ================================================ Command: ios nsuserdefaults get Usage: ios nsuserdefaults get Queries the applications NSUserDefaults class for all of the entries in the current application bundle and echoes the entries to screen. Examples: ios nsuserdefaults get ================================================ FILE: objection/console/helpfiles/ios.nsuserdefaults.txt ================================================ Contains subcommands to work with the iOS NSUserDefaults class. ================================================ FILE: objection/console/helpfiles/ios.pasteboard.monitor.txt ================================================ Command: ios pasteboard monitor Usage: ios pasteboard monitor Hooks into the iOS UIPasteboard class and polls the generalPasteboard every 5 seconds for data. If new data is found, different from the previous poll, that data will be dumped to screen. Examples: ios pasteboard monitor ================================================ FILE: objection/console/helpfiles/ios.pasteboard.txt ================================================ Contains subcommands to work with the iOS pasteboard. ================================================ FILE: objection/console/helpfiles/ios.plist.cat.txt ================================================ Command: ios plist cat Usage: ios plist cat Parses and echoes a plist file on the remote iOS device to screen. If this parsing is not sufficient, one can always `download` the plist file itself for parsing using other tools. Examples: ios plist cat Info.plist ================================================ FILE: objection/console/helpfiles/ios.plist.txt ================================================ Contains subcommands to work with iOS Plist entries. ================================================ FILE: objection/console/helpfiles/ios.sslpinning.disable.txt ================================================ Command: ios sslpinning disable Usage: ios sslpinning disable Attempts to disable SSL Pinning on iOS devices. This is achieved by hooking into methods commonly used by Frameworks and Libraries such as AFNetworking, NSURLSession and the now deprecated NSURLConnection. This command also implements the bypass techniques used in the well-known SSL-Killswitch2 app, including a new technique reportedly working in iOS10. If this method does not disable the applications SSL pinning implementation, then it may still be possible to bypass it via 'helper' methods commonly used by developers to help when testing in development / staging environments. Be on the lookout for classes / methods that relate to pinning that may simply return a BOOL value. Examples: ios sslpinning disable ================================================ FILE: objection/console/helpfiles/ios.sslpinning.txt ================================================ Contains subcommands to work with iOS SSL pinning related calls. ================================================ FILE: objection/console/helpfiles/ios.txt ================================================ Contains subcommands to work with iOS specific features. These include features such as keychain dumping, reading plists and bypassing SSL pinning. ================================================ FILE: objection/console/helpfiles/ios.ui.alert.txt ================================================ Command: ios ui alert Usage: ios ui alert (optional: "") Displays an alert popup on an iOS device. A message to display may be specified optionally. Examples: ios ui alert ios ui alert 'my message' ================================================ FILE: objection/console/helpfiles/ios.ui.dump.txt ================================================ Command: ios ui dump Usage: ios ui dump Dumps the current, serialized user interface. This is useful to see which values or classes may be attached to UI elements. Examples: ios dump ================================================ FILE: objection/console/helpfiles/ios.ui.screenshot.txt ================================================ Command: ios ui screenshot Usage: ios ui screenshot Screenshots the current foregrounded UIView and saves it as a PNG locally. Note: Does not work at the moment, may actually need a jailbroken device. Examples: ios ui screenshot screenshot.png ================================================ FILE: objection/console/helpfiles/ios.ui.touchid_bypass.txt ================================================ Command: ios ui touchid_bypass Usage: ios ui touchid_bypass Hooks into the -[LAContext evaluatePolicy:localizedReason:reply:] selector and replies with a successful message from the operating system when a touchID prompt is dismissed. This is useful in cases where the application relies solely on the operating system to tell it if a fingerprint read was successful or not. Note: This does *not* bypass cases where TouchID is needed to decrypt a keychain entry, simply because the actual data itself is not stored in the keychain but instead lives in the Secure Enclave. The keychain simply contains a token to the data itself. Examples: ios ui touchid_bypass ================================================ FILE: objection/console/helpfiles/ios.ui.txt ================================================ Contains subcommands to interact with the iOS user interface. This includes commands to dump the current view hierarchy as well as bypassing screens that require TouchID to proceed. ================================================ FILE: objection/console/helpfiles/jobs.kill.txt ================================================ Command: jobs kill Usage: jobs kill Kills a running job identified by its UUID. When a job is killed, objection will unload the Fridascript from the process' memory. Examples: jobs kill 9415c4c7-2824-46a5-8539-d2d35ba2158c ================================================ FILE: objection/console/helpfiles/jobs.list.txt ================================================ Command: jobs list Usage: jobs list List the currently running jobs. Jobs are asynchronous Fridascripts that were submitted and have not yet been unloaded from the process. Examples of such jobs include the iOS method argument dumper and pasteboard monitor. To unload a job, the `jobs kill ` command may be used. Examples: jobs list ================================================ FILE: objection/console/helpfiles/jobs.txt ================================================ Contains subcommands to work with objection jobs. This includes listing and killing them. ================================================ FILE: objection/console/helpfiles/ls.txt ================================================ Command: ls Usage: ls (optional: ) Display the contents of a directory on the mobile device. The output details the permissions of the directory in question, as well as those for each file and directory within. If no directory is specified, the current working directory is assumed and listed. Examples: ls Library/Caches ls / ls ================================================ FILE: objection/console/helpfiles/memory.dump.all.txt ================================================ Command: memory dump all Usage: memory dump all Dumps all of the current processes' memory that is marked as readable and writable (rw-) to a file specified by local destination. Examples: memory dump all process_memory.dmp ================================================ FILE: objection/console/helpfiles/memory.dump.from_base.txt ================================================ Command: memory dump all Usage: memory dump Dumps memory from within the current process from a base address, for a set number of bytes to a local file specified by local destination. For example addresses and sizes, the `memory list modules` command may be used. Specifying addresses or sizes that are outside of the current processes sandbox has a *high* chance of crashing the application. Use with caution. Examples: memory dump from_base 0x10009c000 442368 main memory dump from_base 0x10f88e000 548864 CoreAudio ================================================ FILE: objection/console/helpfiles/memory.dump.txt ================================================ Contains subcommands to dump process memory ================================================ FILE: objection/console/helpfiles/memory.list.exports.txt ================================================ Command: memory list exports Usage: memory list exports (optional: --json ) List exports in a specific loaded module. Exports found using this command could be used in Fridascripts to hook with module.findExportByName(). For a list of modules to list exports from the `memory list modules` command may be used. Examples: memory list exports libsystem_configuration.dylib memory list exports UserManagement memory list exports UserManagement --json UserManagementExports.json ================================================ FILE: objection/console/helpfiles/memory.list.modules.txt ================================================ Command: memory list modules Usage: memory list modules (optional: --json ) List all of the modules loaded in the current process, detailing their base address, size and location on disk. Providing a filename with the --json flag will output all of the module attributes to the file specified for later inspection. Examples: memory list modules memory list modules --json modules.json ================================================ FILE: objection/console/helpfiles/memory.list.txt ================================================ Contains subcommands to list modules and module exports ================================================ FILE: objection/console/helpfiles/memory.search.txt ================================================ Command: memory search Usage: memory search "" (optional: --string) (--offsets-only) Search the current processes' heap for a pattern. A pattern is represented by a byte sequence such as eb ff aa. It is also possible to specify wildcards such as eb ff ?? aa, indicating that you are looking for a pattern that starts with eb ff, has any other byte and then has aa. It is also possible to provide a raw string, which should be suffixed with the --string flag, indicating to the command that it should convert the string to bytes before executing the search. Wildcards are not supported in string searches. Output may be controlled primarily with the --offsets-only flag which indicates wether a small hexdump of matched memory regions should occur, or if only the matched offsets should be printed. Examples: memory search "41 41 41 41" memory search "41 ?? de ad" memory search "deadbeef" --string ================================================ FILE: objection/console/helpfiles/memory.txt ================================================ Contains subcommands to work with memory within the current process. Examples include commands to dump the current process memory, dump the memory of a specific loaded module, list exported modules or write raw bytes to memory addresses. ================================================ FILE: objection/console/helpfiles/memory.write.txt ================================================ Command: memory write Usage: memory write "
" "" (optional: --string) Write an arbitrary set of bytes to an address in memory. Using this command has a high chance of crashing the applications process if you attempt to write to addresses outside of the applications heap, or your bytes specified cause to go outside of some memory boundary. Examples: memory write 0x117a2e347 "ff 41 41 42" ================================================ FILE: objection/console/helpfiles/plugin.load.txt ================================================ Command: plugin load Usage: plugin load (optional: namespace) Loads an objection plugin into the current session. For more information on plugins, please refer to the project wiki at: https://github.com/sensepost/objection/wiki By default, plugin commands are nested beneath the plugin context menu and will use the plugin's built-in namespace as the subcommand to use. However, this namespace may be specified at load time, overriding the plugins buil-in name. Examples: plugin load ~/home/objection-plugins/feature plugin load ~/home/objection-plugins/feature newname ================================================ FILE: objection/console/helpfiles/plugin.txt ================================================ Contains subcommands to work with objection plugins. ================================================ FILE: objection/console/helpfiles/pwd.print.txt ================================================ Command: pwd print Usage: pwd print Display the current working directory. Examples: pwd print ================================================ FILE: objection/console/helpfiles/pwd.txt ================================================ Contains subcommands to work with the current working directory on the device. ================================================ FILE: objection/console/helpfiles/reconnect.txt ================================================ Command: reconnect Usage: reconnect Attempts to reconnect to the Frida Gadget specified with --gadget on startup. The connection mode (ie: usb / network) can not be changed unless the repl is restarted. Examples: reconnect ================================================ FILE: objection/console/helpfiles/rm.txt ================================================ Command: rm Usage: rm Delete a file on the remote operating system. Examples: rm file.txt rm /path/to/file.png ================================================ FILE: objection/console/helpfiles/sqlite.connect.txt ================================================ Command: sqlite connect Usage: sqlite connect Connect to a SQLite database on the remote device. The connection process downloads a copy of the remote database file to a local temporary directory. The file is then validated to make sure that it is a SQLite3 database file. Once considered a valid database file, the connection is considered complete. The `sqlite status` command will show details about the connection once successful. Examples: sqlite connect Preferences/settings.sqlite sqlite connect credentials.sqlite ================================================ FILE: objection/console/helpfiles/sqlite.disconnect.txt ================================================ Command: sqlite disconnect Usage: sqlite disconnect Disconnect from the currently connected SQLite database file. This command will clean the locally cached version of the database file. If you made changes you want to save, run the `sqlite sync` command before disconnecting. This command is also run if the REPL is existed. Examples: sqlite disconnect ================================================ FILE: objection/console/helpfiles/sqlite.execute.query.txt ================================================ Command: sqlite execute query Usage: sqlite execute query Execute a query against the cached copy of the connected SQLite database. If your changes need to be effective on the device, execute the `sqlite sync` command to upload the modified database back to the device. Examples: sqlite execute query select * from data sqlite execute query delete from data ================================================ FILE: objection/console/helpfiles/sqlite.execute.schema.txt ================================================ Command: sqlite execute schema Usage: sqlite execute schema Get the database schema for the currently connected SQLite database. Examples: sqlite execute schema ================================================ FILE: objection/console/helpfiles/sqlite.execute.txt ================================================ Contains subcommands to execute queries against a connected SQLite database. ================================================ FILE: objection/console/helpfiles/sqlite.status.txt ================================================ Command: sqlite status Usage: sqlite status Check the status of the SQLite connection. Outputs the the locally cached location as well as the remote source it was cached from. Examples: sqlite status ================================================ FILE: objection/console/helpfiles/sqlite.sync.txt ================================================ Command: sqlite sync Usage: sqlite sync Sync the locally cached SQLite database with the remote database. Any changes made since the last `sqlite connect` will be available on the device post-sync. Examples: sqlite sync ================================================ FILE: objection/console/helpfiles/sqlite.txt ================================================ Contains subcommands to work with SQLite databases on the remote device. Connecting to a SQLite database will result in a copy of the database from the remote device being downloaded locally. All queries that are run will be run on the locally cached database. If the changes need to be available on the remote device, the database should be `sync`'ed back. ================================================ FILE: objection/console/helpfiles/ui.alert.txt ================================================ Command: ui alert Usage: ui alert (optional: "") Displays an alert popup on iOS devices, or a Toast message on Android devices. This is useful to demonstrate that the application was successfully hooked. Providing an alert message will display that message instead of the default. Note: Currently, once the iOS alert message has been displayed, dismissing the message unfortunately crashes the application. Examples: ui alert ui alert 'custom message!' ================================================ FILE: objection/console/helpfiles/ui.txt ================================================ Contains subcommands that interact with the applications user interface. ================================================ FILE: objection/console/repl.py ================================================ import logging import os import traceback import click import delegator import frida from prompt_toolkit import PromptSession from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.completion import FuzzyCompleter from prompt_toolkit.history import FileHistory from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.styles import Style from .commands import COMMANDS from .completer import CommandCompleter from ..__init__ import __version__ from ..state.app import app_state from ..state.connection import state_connection from ..utils.helpers import get_tokens class Repl(object): """ The exploration REPL for objection """ def __init__(self) -> None: self.cli = None self.completer = FuzzyCompleter(CommandCompleter()) self.commands_repository = COMMANDS self.session = self.get_prompt_session() def get_prompt_session(self) -> PromptSession: """ Starts a new prompt session. :return: """ return PromptSession( history=FileHistory(os.path.expanduser('~/.objection/objection_history')), completer=self.completer, style=self.get_prompt_style(), auto_suggest=AutoSuggestFromHistory(), reserve_space_for_menu=4, complete_in_thread=True, ) @staticmethod def get_prompt_style() -> Style: """ Get the style to use for our prompt :return: """ return Style.from_dict({ # completions menu 'completion-menu.completion.current': 'bg:#00aaaa #000000', 'completion-menu.completion': 'bg:#008888 #ffffff', # fuzzy match outside 'completion-menu.completion fuzzymatch.outside': 'fg:#000000', # Prompt. 'applicationname': '#007cff', 'status': '#717171', 'on': '#00aa00', 'devicetype': '#00ff48', 'version': '#00ff48', 'jobs': '', # TODO 'connection': '#717171' }) @staticmethod def get_prompt_message() -> list: """ Return prompt tokens to use in the cli app. If none were set during the init of this class, it is assumed that the connection failed. :return: """ agent = state_connection.agent dev = state_connection.get_agent().device params = dev.query_system_parameters() return [ ('class:applicationname', state_connection.name), ('class:status', ' (' + ('run' if agent.resumed else 'pause') + ')'), ('class:on', ' on '), ('class:devicetype', '(' + params['os']['name'] + ': '), ('class:version', params['os']['version'] + ') '), ('class:connection', '[' + dev.type + '] # '), ] def run_command(self, document: str) -> None: """ Process a command as received by prompt_toolkit. :param document: :return: """ logging.info(document) if document.strip() == '': return # handle os commands if document.strip().startswith('!'): # strip the leading ! os_cmd = document[1:] click.secho('Running OS command: {0}\n'.format(os_cmd), dim=True) o = delegator.run(os_cmd, binary=True) # print stdout if len(o.out) > 0: click.secho(o.out.decode('utf-8', 'replace'), bold=True) # print stderr if len(o.err) > 0: click.secho(o.err.decode('utf-8', 'replace'), fg='red') return # a normal command is to be run, extract the tokens and # find which method we should be calling tokens = get_tokens(document) # check if we should be presenting help instead of executing # a command. this is indicated by the fact that the command # starts with the word 'help' if len(tokens) > 0 and 'help' == tokens[0]: # skip the 'help' entry from the tokens list so that # the following method can find the correct help tokens.remove('help') command_help = self._find_command_help(tokens) if not command_help: click.secho(('No help found for: {0}. Either the command ' 'does not exist or contains subcommands with help.' ).format(' '.join(tokens)), fg='yellow') return # output the help and leave click.secho(command_help, fg='blue', bold=True) return # find an execution method to run token_matches, exec_method = self._find_command_exec_method(tokens) if exec_method is None: click.secho('Unknown or ambiguous command: `{0}`. Try `help {0}`.'.format(document), fg='yellow') return # strip the command matching tokens and leave # the rest as arguments arguments = tokens[token_matches:] # run the method for the command itself! exec_method(arguments) app_state.add_command_to_history(command=document) def _find_command_exec_method(self, tokens: list) -> tuple: """ Attempt to find the actual python method to run for the command tokens we have. This is done by 'walking' the command dictionary, looking for the deepest 'exec' method definition. We are interested in the number of tokens walked as well, so that the calling command can know how many tokens to strip, sending the rest as arguments to the exec method. :param tokens: :return: """ # start with all of the commands we have dict_to_walk = self.commands_repository # ... and an empty method to execute exec_method = None # keep count of the number of tokens # used in this walk. this will help indicate to # the caller how many tokens should be stripped to # get to the arguments for the command walked_tokens = 0 for token in tokens: # increment the walked tokens walked_tokens += 1 # check if the token matches a command if token in dict_to_walk: # matched a dict for the token we have. we need # to have *all* of the tokens match a nested dict # so that we can extract the final 'exec' key. # if we encounter a key that does not have nested commands, # chances are we are where we need to be to exec a command. if 'commands' not in dict_to_walk[token]: if 'exec' in dict_to_walk[token]: exec_method = dict_to_walk[token]['exec'] break else: dict_to_walk = dict_to_walk[token]['commands'] # stop if there is nothing that matches else: break return walked_tokens, exec_method def _find_command_help(self, tokens: list) -> str: """ Attempt to find help for a command. Just like how the _find_command_exec_method works, this method also walks the command dictionary, searching for the deepest key. The tokens that match form part of a new list, later joined together to pickup the correct help.txt. :param tokens: :return: """ # start with all of the commands we have dict_to_walk = self.commands_repository helpfile_name = [] user_help = '' for token in tokens: # check if the token matches a command if token in dict_to_walk: # add this token to the helpfile helpfile_name.append(token) # if there are subcommands, continue with the walk if 'commands' in dict_to_walk[token]: dict_to_walk = dict_to_walk[token]['commands'] # stop if we don't have a token that matches anything else: break # once we have the help, load its .txt contents if len(helpfile_name) > 0: help_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'helpfiles', '.'.join(helpfile_name) + '.txt') # no helpfile... warn. if not os.path.exists(help_file): click.secho('Unable to find helpfile {0}'.format(' '.join(helpfile_name)), dim=True) return user_help # read the helpfile with open(help_file, 'r') as f: user_help = f.read() return user_help @staticmethod def handle_reconnect(document: str) -> bool: """ Handles a reconnection attempt to a device. A reconnection means that the current agent will be unloaded and reloaded again. :param document: :return: """ if document.strip() in ('reconnect', 'reset', 'reconnect_spawn'): try: from .cli import get_agent reconnect_spawn = document.strip() == 'reconnect_spawn' if reconnect_spawn: click.secho('Performing full-restart...', fg='yellow') state_connection.spawn = True state_connection.no_pause = True else: click.secho('Performing soft-restart...', fg='yellow') state_connection.spawn = False curr_agent = state_connection.agent # Cleanup current agent (ignore errors if already destroyed) click.secho('Unloading current agent...', dim=True) try: if curr_agent.script: curr_agent.script.unload() except (frida.InvalidOperationError, Exception): pass # Script already destroyed or detached try: if curr_agent.session: curr_agent.session.detach() except (frida.InvalidOperationError, Exception): pass # Session already detached # Need to clear because destructor will attempt to clear script/session again. curr_agent.script = None state_connection.agent = None state_connection.session = None click.secho(f'Re-attaching to {state_connection.name}...', dim=True) # Try respawn the agent. new_agent = get_agent() state_connection.agent = new_agent click.secho('Reconnection successful!', fg='green') except Exception as e: click.secho(f'Reconnection failed: {e}', fg='red') click.secho('Ensure the application is running and the device is connected.', dim=True) return True return False def run(self, quiet: bool) -> None: """ Start the objection repl. """ banner = (""" _ _ _ _ ___| |_|_|___ ___| |_|_|___ ___ | . | . | | -_| _| _| | . | | |___|___| |___|___|_| |_|___|_|_| |___|(object)inject(ion) v{0} Runtime Mobile Exploration by: @leonjza from @sensepost """).format(__version__) if not quiet: click.secho(banner, bold=True) click.secho('[tab] for command suggestions', fg='white', dim=True) # the main application loop is here, reading inputs provided by # prompt_toolkit and sending it off the needed handlers while True: try: with patch_stdout(raw=True): document = self.session.prompt(self.get_prompt_message()) # check if this is an exit command if document.strip() in ('quit', 'exit', 'bye'): click.secho('Exiting...', dim=True) break if document.strip() in ('resume', 'res'): click.secho('Resuming attached process', dim=True) state_connection.agent.resume() continue # if we got the reconnect command, handle just that if self.handle_reconnect(document): continue # dispatch to the command handler. if something goes horribly # wrong, catch it instead of crashing the REPL try: # find something to run self.run_command(document) except frida.core.RPCException as e: click.secho('A Frida agent exception has occurred.', fg='red', bold=True) click.secho('{0}'.format(e), fg='red') click.secho('\nPython stack trace: {}'.format(traceback.format_exc()), dim=True) except Exception as e: click.secho(('An unexpected internal exception has occurred. If this ' 'looks like a code related error, please file a bug report!'), fg='red', bold=True) click.secho('{0}'.format(e), fg='red') click.secho('\nPython stack trace: {}'.format(traceback.format_exc()), dim=True) except KeyboardInterrupt: pass except EOFError: click.secho('Exiting...', dim=True) break ================================================ FILE: objection/state/__init__.py ================================================ ================================================ FILE: objection/state/api.py ================================================ from ..api.app import create_app class ApiState(object): """ A class representing the state API for this app """ def __init__(self): self.core_api = create_app() self.blueprints = [] def append_api_blueprint(self, blueprint): """ Add extra blueprints to the API. This method would typically be called by the plugin loader to slot in endpoints that plugins may expose. :param blueprint: :return: """ self.blueprints.append(blueprint) def start(self, host: str, port: int, debug: bool = False): """ Starts the Flask-based API server after registering any extra blueprints that would typically have been sources from plugins. :param host: :param port: :param debug: :return: """ for bp in self.blueprints: self.core_api.register_blueprint(bp) self.core_api.run(host=host, port=port, debug=debug) api_state = ApiState() ================================================ FILE: objection/state/app.py ================================================ class AppState(object): """ A class representing generic state variable for this app """ def __init__(self): self.debug_hooks = False self.debug = False self.api_host = '127.0.0.1' self.api_port = 8888 self.successful_commands = [] def add_command_to_history(self, command: str) -> None: """ Adds a command to the list of successful commands. :param command: :return: """ if command not in self.successful_commands: self.successful_commands.append(command) def clear_command_history(self) -> None: """ Clears the list of successful commands recorded for this session. :return: """ self.successful_commands = [] def should_debug_hooks(self) -> bool: """ Returns if debugging of Frida hooks is needed. :return: """ return self.debug_hooks def should_debug(self) -> bool: """ Checks if debugging is enabled :return: """ return self.debug app_state = AppState() ================================================ FILE: objection/state/connection.py ================================================ class StateConnection(object): """ A class controlling the connection state of a device. """ def __init__(self) -> None: """ Init a new connection state, defaulting to a USB connection. """ self.network = False self.host = None self.port = None self.device_type = 'usb' self.device_id = None self.spawn = False self.no_pause = False self.foremost = False self.debugger = False self.name = None self.agent = None self.api = None self.uid = None def use_usb(self) -> None: """ Sets the values required to have a USB connection. :return: """ self.network = False self.device_type = 'usb' def use_network(self) -> None: """ Sets the values required to have a Network connection. :return: """ self.network = True self.device_type = 'remote' def get_comms_type(self) -> int: """ Returns the currently configured connection type. :return: """ def get_api(self): """ Return a Frida RPC API session :return: """ if not self.agent: raise Exception('No session available to get API') return self.agent.exports() def set_agent(self, agent): """ Sets the active agent to use for communications. :param agent: :return: """ self.agent = agent def get_agent(self): if not self.agent: raise Exception('No Agent available') return self.agent def __repr__(self) -> str: return f' str: return f'' device_state = DeviceState() ================================================ FILE: objection/state/filemanager.py ================================================ class FileManagerState(object): """ A class representing the state of the filemanager. """ def __init__(self) -> None: self.cwd = None file_manager_state = FileManagerState() ================================================ FILE: objection/state/jobs.py ================================================ import atexit from random import randint import click import frida from objection.state.connection import state_connection class Job(object): """ A class representing a REPL Job or agent Job with one or more hooks. """ def __init__(self, name, job_type, handle, uuid: int = None) -> None: """ Init a new job. This requires the job_type to know how to manage the job as well as a handle to manage and kill the job. :param name: :param job_type: :param handle: :return: """ if uuid is not None: self.uuid = int(uuid) else: self.uuid = randint(100000, 999999) self.name = name self.job_type = job_type self.handle = handle def end(self): """ Revert hooks that the job created. :return: """ if self.job_type == "script": click.secho("[job manager] Killing job {0}. Name: {1}. Type: {2}" .format(self.uuid, self.name, self.job_type), dim=True) self.handle.unload() elif self.job_type == "hook": api = state_connection.get_api() api.jobs_kill(self.uuid) else: click.secho(('[job {0}] - Unknown job type {1}'.format(self.uuid, self.job_type)), fg='red', dim=True) class JobManagerState(object): """ A class representing the current Job manager. """ def __init__(self) -> None: """ Init a new job state manager. This method will also register an atexit(), ensuring that cleanup operations are performed on jobs when this class is GC'd. """ self.jobs: dict[int, Job] = {} atexit.register(self.cleanup) def add_job(self, new_job: Job) -> None: """ Adds a job to the job state manager. :param new_job: :return: """ # avoid duplicate jobs. if new_job.uuid not in self.jobs: self.jobs[new_job.uuid] = new_job def remove_job(self, job_uuid: int): """ Removes a job from the job state manager. :param job_uuid: :return Job: """ if job_uuid not in self.jobs: click.secho(f"Error: Job with ID {job_uuid} does not exist.", fg='red') return job_to_remove = self.jobs.pop(job_uuid) job_to_remove.end() def cleanup(self) -> None: """ Clean up all the jobs in the job manager. This method is typical called when at the end of an objection session. :return: """ for uuid in list(self.jobs.keys()): try: job = self.jobs.pop(uuid) job.end() except frida.InvalidOperationError: click.secho(('[job manager] Job: {0} - An error occurred stopping job. Device may ' 'no longer be available.'.format(uuid)), fg='red', dim=True) job_manager_state = JobManagerState() ================================================ FILE: objection/utils/__init__.py ================================================ import logging import os import threading import click from .update_checker import check_version class MakeFileHandler(logging.FileHandler): """ Wrapper Class around the builtin Filehandler. """ def __init__(self, filename: str, mode: str = 'a', encoding: str = None, delay: bool = False) -> None: """ The original FileHandler's init is called, right after the directory used to store the objection logfile is created. :param filename: :param mode: :param encoding: :param delay: """ os.makedirs(os.path.dirname(filename), exist_ok=True) logging.FileHandler.__init__(self, filename, mode, encoding, delay) def new_secho(text: str, **kwargs) -> None: """ Patch the secho method from the click package so that the text that should be echoed is logged first. :param text: :param kwargs: :return: """ logging.info(text) real_secho(text, **kwargs) # Configure the logging used in objection logger = logging.getLogger() handler = MakeFileHandler(os.path.expanduser('~/.objection/objection.log')) formatter = logging.Formatter('%(asctime)s %(levelname)-8s\n%(message)s\n') handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.DEBUG) # monkey patch secho to log to file real_secho = click.secho click.secho = new_secho try: # kick off a background thread to check the version of objection threading.Thread(target=check_version).start() except Exception: pass ================================================ FILE: objection/utils/agent.py ================================================ import argparse import atexit import json import sys from dataclasses import dataclass from pathlib import Path from pprint import pprint import click import frida from objection.state.app import app_state from objection.state.connection import state_connection from objection.state.device import device_state, Ios, Android from objection.state.jobs import job_manager_state, Job from objection.utils.helpers import debug_print @dataclass class AgentConfig(object): """ Default configuration for an Agent instance """ name: str host: str = None port: int = None device_type: str = 'usb' device_id: str = None foremost: bool = False spawn: bool = False pause: bool = True debugger: bool = False uid: int = None class OutputHandlers(object): """ Output handlers for an Agent instance """ def device_output(self): pass def device_lost(self): pass @staticmethod def session_on_detached(message: str, crash): """ The callback to run for the detach signal :param message: :param crash: :return: """ try: # log the hook response if needed if app_state.should_debug(): click.secho('- [incoming message] ' + '-' * 18, dim=True) click.secho(json.dumps(message, indent=2, sort_keys=True), dim=True) click.secho('- [./incoming message] ' + '-' * 16, dim=True) # process the response if message: click.secho('(session detach message) ' + message, fg='red') # Frida 12.3 crash reporting # https://www.nowsecure.com/blog/2019/02/07/frida-12-3-debuts-new-crash-reporting-feature/ if crash: click.secho('(process crash report)', fg='red') click.secho('\n\t{0}'.format(crash.report), dim=True) except Exception as e: click.secho('Failed to process an incoming message for a session detach signal: {0}'.format(e), fg='red', bold=True) raise e @staticmethod def script_on_message(message: dict, data): """ The callback to run when a message is received from the agent. :param message: :param data: :return: """ try: # log the hook response if needed if app_state.should_debug(): click.secho('- [incoming message] ' + '-' * 18, dim=True) click.secho(json.dumps(message, indent=2, sort_keys=True), dim=True) click.secho('- [./incoming message] ' + '-' * 16, dim=True) # process the response if message and 'payload' in message: if len(message['payload']) > 0: if isinstance(message['payload'], dict): click.secho('(agent) ' + json.dumps(message['payload'])) elif isinstance(message['payload'], str): click.secho('(agent) ' + message['payload']) else: click.secho('Dumping unknown agent message', fg='yellow') pprint(message['payload']) except Exception as e: click.secho('Failed to process an incoming message from agent: {0}'.format(e), fg='red', bold=True) raise e class Agent(object): """ Class to manage the lifecycle of the objection Frida agent """ agent_path: Path = None c: AgentConfig handlers: OutputHandlers device: frida.core.Device = None session: frida.core.Session = None script: frida.core.Script = None pid: int = None resumed: bool = True def __init__(self, config: AgentConfig): """ initialises the agent class """ self.agent_path = Path(__file__).parent.parent / 'agent.js' if not self.agent_path.exists(): raise Exception(f'Unable to locate Objection agent sources at: {self.agent_path}. ' 'If this is a development install, check the wiki for more ' 'information on building the agent.') debug_print('Agent path is: {path}'.format(path=self.agent_path)) self.config = config debug_print(f'agent config: {self.config}') self.handlers = OutputHandlers() atexit.register(self.teardown) def _get_agent_source(self) -> str: """ Loads the frida-compiled agent from disk. :return: """ with open(self.agent_path, 'r', encoding='utf-8') as f: src = f.readlines() return ''.join([str(x) for x in src]) def set_device(self): """ Set's the target device to work with. :return: """ if self.config.device_id is not None: self.device = frida.get_device(self.config.device_id) elif (self.config.host is not None) or (self.config.device_type == 'remote'): if self.config.host is None: self.device = frida.get_remote_device() else: host = self.config.host port = self.config.port self.device = frida.get_device_manager() \ .add_remote_device(f'{host}:{port}' if host is not None else f'127.0.0.1:{port}') elif self.config.device_type is not None: for dev in frida.enumerate_devices(): if dev.type == self.config.device_type: self.device = dev else: self.device = frida.get_local_device() # surely we have a device by now? if self.device is None: raise Exception('Unable to find a device') self.device.on('output', self.handlers.device_output) self.device.on('lost', self.handlers.device_lost) debug_print(f'device determined as: {self.device}') def set_target_pid(self): """ Set's the PID we should attach to. This method will spawn the target if needed. The resumed value is also toggled here. Defaults: resumed: bool = True :return: """ if (self.config.name is None) and (not self.config.foremost): click.secho('Need a target name to spawn/attach to', fg='red') sys.exit(1) if self.config.foremost: try: app = self.device.get_frontmost_application() except Exception as e: click.secho(f'Could not get foremost application on {self.device.name}: {e}', fg='red') sys.exit(1) if app is None: click.secho(f'No foremost application on {self.device.name}', fg='red') sys.exit(1) self.pid = app.pid # update the global state for the prompt etc. state_connection.name = app.identifier elif self.config.spawn: if self.config.uid is not None: if self.device.query_system_parameters()['os']['id'] != 'android': raise Exception('--uid flag can only be used on Android.') self.pid = self.device.spawn(self.config.name, uid=int(self.config.uid)) else: try: self.pid = self.device.spawn(self.config.name) except frida.InvalidArgumentError: pass # Maybe we have an app name and not identifier app_list = self.device.enumerate_applications() app_name_lc = self.config.name.lower() matching_app = [app for app in app_list if app.name.lower() == app_name_lc] # Don't care about matching_app[0].pid not in (0, None), if already running we restart anyway. if len(matching_app) == 1: debug_print("Found app by name instead of package, spawning.") self.pid = self.device.spawn(matching_app[0].identifier) self.resumed = False else: # check if the name is actually an integer. this way we can # assume we got the target PID already try: self.pid = int(self.config.name) except ValueError: pass # maybe we have a process name if self.pid is None: try: self.pid = self.device.get_process(self.config.name).pid except frida.ProcessNotFoundError: pass if self.pid is None: # maybe we have an app identifier/package name app_list = self.device.enumerate_applications() app_name_lc = self.config.name.lower() matching_app = [app for app in app_list if app.identifier.lower() == app_name_lc] if len(matching_app) == 1 and matching_app[0].pid not in (0, None): debug_print("Found app by package name.") self.pid = matching_app[0].pid elif len(matching_app) > 1: app_list_str = ', '.join([f"{app.identifier}: {app.pid}" for app in matching_app]) click.secho("Ambiguous identifier. Applications with the same identifier found: "+ app_list_str, fg='red') sys.exit(1) if self.pid is None: click.secho("Unable to find target application.", fg='red', bold=True) sys.exit(1) debug_print(f'process PID determined as {self.pid}') def attach(self): """ Attaches to an enumerated PID, injecting the objection agent. :return: """ if self.pid is None: raise Exception('A PID needs to be set before attach()') if self.config.uid is None: debug_print(f'Attaching to PID: {self.pid}') self.session = self.device.attach(self.pid) else: self.session = self.device.attach(self.pid) self.session.on('detached', self.handlers.session_on_detached) if self.config.debugger: click.secho('debugger enabled and runtime set to v8. visit chrome://inspect', bold=True) self.script = self.session.create_script(source=self._get_agent_source(), runtime='v8') self.script.enable_debugger() else: self.script = self.session.create_script(source=self._get_agent_source()) self.script.on('message', self.handlers.script_on_message) self.script.load() def attach_script(self, job_name, source): """ Attaches an arbitrary script session. :param job_name: :param source: :return: """ session: frida.core.Session = self.device.attach(self.pid) script: frida.core.Script = session.create_script(source=source) script.on('message', self.handlers.script_on_message) script.load() script_job = Job(job_name, 'script', script) job_manager_state.add_job(script_job) def update_device_state(self): """ Updates the device_state. Useful in other parts where we need platform specific decisions. :return: """ params = self.device.query_system_parameters() # set os platform if params['os']['id'] == 'ios': device_state.set_platform(Ios) elif params['os']['id'] == 'android': device_state.set_platform(Android) # set os version device_state.set_version(params['os']['version']) def resume(self): """ Resume the target pid. :return: """ if self.resumed: return if not self.pid: raise Exception('Cannot resume without self.pid') self.device.resume(self.pid) self.resumed = True def exports(self): """ Returns the RPC exports exposed by the Frida agent :return: """ if not self.script: raise Exception('Need a script created before reading exports()') return self.script.exports_sync def run(self): """ Run the Agent by getting a device, setting the target pid and attaching. If we should skip pausing, also resume the process. :return: """ self.set_device() self.set_target_pid() self.attach() # internal state self.update_device_state() if not self.config.pause: debug_print('asked to run without pausing, so resuming in run()') self.resume() def teardown(self): try: if self.script: click.secho('Asking jobs to stop...', dim=True) job_manager_state.cleanup() click.secho('Unloading objection agent...', dim=True) self.script.unload() except frida.InvalidOperationError as e: click.secho(f'Unable to run cleanups: {e}', fg='yellow', dim=True) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('name', help='target app to attach/spawn. needs to be bundle ' 'identifier for spawn') parser.add_argument('--no-spawn', dest='no_spawn', default=True, action='store_false', help='do not try and spawn the target app') parser.add_argument('--no-pause', dest='no_pause', default=True, action='store_false', help='resume the app after spawning') parser.add_argument('--debug', default=False, action='store_true', help='print debug logging') args = parser.parse_args() if args.name is None: print('error: need a target app to attach/spawn') sys.exit(1) if args.debug: app_state.debug = True c = AgentConfig(name=args.name, spawn=args.no_spawn, pause=args.no_pause) a = Agent(config=c) a.run() print(a.exports().env_frida()) ================================================ FILE: objection/utils/assets/javahookmanager.js ================================================ // Frida Java hooking helper class. // // Edit the example below the HookManager class to suit your // needs and then run with: // frida -U "App Name" -l objchookmanager.js // // Generated using objection: // https://github.com/sensepost/objection class JavaHookManager { // create a new Hook for clazzName, specifying if we // want verbose logging of this class' internals. constructor(clazzName, verbose = false) { this.printVerbose(`Booting JavaHookManager for ${clazzName}...`); this.target = Java.use(clazzName); // store hooked methods as { method: x, replacements: [y1, y2] } this.hooking = []; this.available_methods = []; this.verbose = verbose; this.populateAvailableMethods(clazzName); } printVerbose(message) { if (!this.verbose) { return; } this.print(`[v] ${message}`); } print(message) { console.log(message); } // basically from: // https://github.com/sensepost/objection/blob/fa6a8b8f9b68d6be41b51acb512e6d08754a2f1e/agent/src/android/hooking.ts#L43 populateAvailableMethods(clazz) { this.printVerbose(`Populating available methods...`); this.available_methods = this.target.class.getDeclaredMethods().map((method) => { var m = method.toGenericString(); // Remove generics from the method while (m.includes("<")) { m = m.replace(/<.*?>/g, ""); } // remove any "Throws" the method may have if (m.indexOf(" throws ") !== -1) { m = m.substring(0, m.indexOf(" throws ")); } // remove scope and return type declarations (aka: first two words) // remove the class name // remove the signature and return m = m.slice(m.lastIndexOf(" ")); m = m.replace(` ${clazz}.`, ""); return m.split("(")[0]; }).filter((value, index, self) => { return self.indexOf(value) === index; }); this.printVerbose(`Have ${this.available_methods.length} methods...`); } validMethod(method) { if (!this.available_methods.includes(method)) { return false; } return true; } isHookingMethod(method) { if (this.hooking.map(element => { if (element.method == method) { return true; } return false; }).includes(true)) { return true; } else { return false; }; } hook(m, f = null) { if (!this.validMethod(m)) { this.print(`Method ${m} is not valid for this class.`); return; } if (this.isHookingMethod(m)) { this.print(`Already hooking ${m}. Bailing`); return; } this.printVerbose(`Hookig ${m} and all overloads...`); var r = []; this.target[m].overloads.forEach(overload => { if (f == null) { overload.replacement = function () { return overload.apply(this, arguments); } } else { overload.implementation = function () { var ret = overload.apply(this, arguments); return f(arguments, ret); } } r.push(overload); }); this.hooking.push({ method: m, replacements: r }); } unhook(method) { if (!this.validMethod(method)) { this.print(`Method ${method} is not valid for this class.`); return; } if (!this.isHookingMethod(method)) { this.print(`Not hooking ${method}. Bailing`); return; } const hooking = this.hooking.filter(element => { if (element.method == method) { this.printVerbose(`Reverting replacement hook from ${method}`); element.replacements.forEach(r => { r.implementation = null; }); return; // effectively removing it } return element; }); this.hooking = hooking; } } // SAMPLE Usage: // var replace = function(args, ret) { // // be sure to check the args, you may have an overloaded method // console.log('Hello from our new function body!'); // console.log(JSON.stringify(args)); // console.log(ret); // return ret; // } // Java.perform(function () { // const hook = new JavaHookManager('okhttp3.Request'); // hook.hook('header', replace); // // hook.unhook('header'); // }); ================================================ FILE: objection/utils/assets/network_security_config.xml ================================================ ================================================ FILE: objection/utils/assets/objchookmanager.js ================================================ // Frida Objective-C hooking helper class. // // Edit the example below the HookManager class to suit your // needs and then run with: // frida -U "App Name" -l objchookmanager.js // // Generated using objection: // https://github.com/sensepost/objection class ObjCHookManager { // create a new Hook for clazzName, specifying if we // want verbose logging of this class' internals. constructor(clazzName, verbose = false) { this.printVerbose(`Booting ObjCHookManager for ${clazzName}...`); this.target = ObjC.classes[clazzName]; // store hooked methods as { method: x, listener: y } this.hooking = []; this.available_methods = []; this.verbose = verbose; this.populateAvailableMethods(clazzName); } printVerbose(message) { if (!this.verbose) { return; } this.print(`[v] ${message}`); } print(message) { console.log(message); } populateAvailableMethods(clazz) { this.printVerbose(`Populating available methods...`); this.available_methods = ObjC.classes[clazz].$ownMethods; this.printVerbose(`Have ${this.available_methods.length} methods...`); } validMethod(method) { if (!this.available_methods.includes(method)) { return false; } return true; } isHookingMethod(method) { if (this.hooking.map(element => { if (element.method == method) { return true; } return false; }).includes(true)) { return true; } else { return false; }; } hook(m, enter = null, leave = null) { if (!this.validMethod(m)) { this.print(`Method ${m} is not valid for this class.`); return; } if (this.isHookingMethod(m)) { this.print(`Already hooking ${m}. Bailing`); return; } this.printVerbose(`Hookig ${m}...`); const l = Interceptor.attach(this.target[m].implementation, { onEnter: function (args) { if (enter != null) { enter(args); } }, onLeave: function (retval) { if (leave != null) { leave(retval); } }, }); this.hooking.push({ method: m, listener: l }); } unhook(method) { if (!this.validMethod(method)) { this.print(`Method ${method} is not valid for this class.`); return; } if (!this.isHookingMethod(method)) { this.print(`Not hooking ${method}. Bailing`); return; } const hooking = this.hooking.filter(element => { if (element.method == method) { this.printVerbose(`Detaching hook from ${method}`); element.listener.detach(); return; // effectively removing it } return element; }); this.hooking = hooking; } } // SAMPLE Usage: // const hook = new ObjCHookManager('NSURLSession'); // // Define the logic to use when entering / leaving // // the target method. // const enter = function(args) { // console.log(`Entered method.`); // } // const leave = function(retval) { // console.log(`Method done. Retval was ${retval}`); // } // hook.hook('- downloadTaskWithRequest:completionHandler:', enter, leave); // hook.unhook('- downloadTaskWithRequest:completionHandler:'); ================================================ FILE: objection/utils/helpers.py ================================================ import re import shlex import click from packaging.version import Version from ..state.app import app_state from ..state.device import device_state, Ios, Android def debug_print(message: str) -> None: """ Prints a message if the application is running with debugging enabled. :param message: :return: """ if app_state.should_debug(): click.secho('[debug] {message}'.format(message=message), dim=True) def pretty_concat(data: str, at_most: int = 75, left: bool = False) -> str: """ Limits a string to the maximum value of 'at_most', ending it off with 3 '.'s. If true is specified for the left parameter, the end of the string will be used with 3 '.'s prefixed. :param data: :param at_most: :param left: :return: """ # do nothing if we are below the max length if len(data) <= at_most: return data if left: return '...' + data[len(data) - at_most:] return data[:at_most] + '...' def sizeof_fmt(num: float, suffix: str = 'B') -> str: """ Pretty print bytes """ for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: if abs(num) < 1024.0: return '%3.1f %s%s' % (num, unit, suffix) num /= 1024.0 return '%.1f %s%s' % (num, 'Yi', suffix) def get_tokens(text: str) -> list: """ Split the text line, shell-style. Sometimes we will have strings that don't have the last quotes added yet. In those cases, we can just ignore shlex errors. :) :param text: :return: """ try: tokens = shlex.split(text) except ValueError: # return a response that wont match a next command tokens = ['lajfhlaksjdfhlaskjfhafsdlkjh'] return tokens def clean_argument_flags(args: list) -> list: """ Returns a list of arguments with flags removed. Items are considered flags when they are prefixed with two dashes. :param args: :return: """ return [x for x in args if not x.startswith('--')] def to_snake_case(w: str) -> str: """ https://stackoverflow.com/a/1176023 :param w: :return: """ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', w) return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() def print_frida_connection_help() -> None: """ Prints help information about connecting to devices and processes. :return: """ click.secho('If you are using a rooted/jailbroken device, specify a process with ' 'the --gadget flag. Eg: objection --gadget "Calendar" explore', fg='red') click.secho('If you are using a non rooted/jailbroken device, ensure that your patched application ' 'is running and in the foreground.', fg='red') click.secho('') click.secho('If you have multiple devices, specify the target device with --serial. A list ' 'of attached device serials can be found with the frida-ls-devices command.', fg='yellow') click.secho('') click.secho('For more information, please refer to the objection wiki at: ' 'https://github.com/sensepost/objection/wiki', fg='green') def warn_about_older_operating_systems() -> None: """ Prints a warning to the console about the recommended Android and iOS versions to use with objection. :return: """ android_supported = '5' ios_supported = '9' # android & ios version warnings if device_state.platform == Android and ( Version(device_state.version) < Version(android_supported)): click.secho('Warning: You appear to be running Android {0} which may result in ' 'some hooks failing.\nIt is recommended to use at least an Android ' 'version {1} device with objection.'.format(device_state.version, android_supported), fg='yellow') # android & ios version warnings if device_state.platform == Ios and ( Version(device_state.version) < Version(ios_supported)): click.secho('Warning: You appear to be running iOS {0} which may result in ' 'some hooks failing.\nIt is recommended to use at least an iOS ' 'version {1} device with objection.'.format(device_state.version, ios_supported), fg='yellow') ================================================ FILE: objection/utils/patchers/__init__.py ================================================ ================================================ FILE: objection/utils/patchers/android.py ================================================ import contextlib import lzma import os import re import shutil import tempfile import xml.etree.ElementTree as ElementTree import click import delegator import requests import semver from .base import BasePlatformGadget, BasePlatformPatcher, objection_path from .github import Github from ..helpers import debug_print class AndroidGadget(BasePlatformGadget): """ Class used to download Android Frida libraries """ android_library_path = os.path.join(objection_path, 'android') # Lists the supported architectures. Key matches Android support # https://developer.android.com/ndk/guides/abis.html#sa # Value matches library arch for frida. architectures = { 'armeabi': 'arm', 'armeabi-v7a': 'arm', 'arm64': 'arm64', 'arm64-v8a': 'arm64', 'x86': 'x86', 'x86_64': 'x86_64', } def __init__(self, github: Github) -> None: """ Build a new instance, ensuring that the paths needed are available. :param github: """ super(AndroidGadget, self).__init__(github) self.architecture = None # prep paths. if they dont exist, create them for path in self.architectures.keys(): d = os.path.join(self.android_library_path, path) if not os.path.exists(d): os.makedirs(d) def set_architecture(self, architecture: str): """ Set the CPU architecture we will work with. :param architecture: :return: """ if architecture not in self.architectures.keys(): raise Exception('Invalid architecture `{0}` set. Valid options are: {1}'.format( architecture, ', '.join(self.architectures))) self.architecture = architecture return self def get_architecture(self) -> str: """ Get the architecture we are working with. :return: """ return self.architecture def get_frida_library_path(self, packed: bool = False) -> str: """ Get the path to a frida-library, both in the packed and :param packed: :return: """ if not self.architecture: raise Exception('Unable to determine path without architecture') return os.path.join(self.android_library_path, self.architecture, 'libfrida-gadget.so' + ('.xz' if packed else '')) def gadget_exists(self) -> bool: """ Determines of a frida-gadget library exists. :return: """ if not self.architecture: raise Exception('Unable to determine path without architecture') return os.path.exists(self.get_frida_library_path()) def download(self): """ Downloads the latest Android gadget for this architecture. :return: """ download_url = self._get_download_url() click.secho('Downloading from: {0}'.format(download_url), dim=True) # stream the download using requests library = requests.get(download_url, stream=True) library_destination = self.get_frida_library_path(packed=True) # save the requests stream to file with open(library_destination, 'wb') as f: click.secho('Downloading {0} library to {1}...'.format(self.architecture, library_destination), fg='green', dim=True) shutil.copyfileobj(library.raw, f) return self def _get_download_url(self) -> str: """ Determines the download URL to use for the Android gadget. :return: """ url = '' # url should contain 'frida-gadget-{version}-android-{arch}.so.xz url_start = 'frida-gadget-' url_end = 'android-' + self.architectures[self.architecture] + '.so.xz' for asset in self.github.get_assets(): if asset['name'].startswith(url_start) and asset['name'].endswith(url_end): url = asset['browser_download_url'] if not url: click.secho('Unable to determine URL to download the library', fg='red') raise Exception('Unable to determine URL for Android gadget download.') return url def unpack(self): """ Unpacks a downloaded .xz gadget. :return: """ click.secho('Unpacking {0}...'.format(self.get_frida_library_path(packed=True)), dim=True) with lzma.open(self.get_frida_library_path(packed=True)) as f: with open(self.get_frida_library_path(), 'wb') as g: g.write(f.read()) return self def cleanup(self): """ Cleans up a downloaded iOS .xz gadget. :return: """ click.secho('Cleaning up downloaded archives...', dim=True) os.remove(self.get_frida_library_path(packed=True)) class AndroidPatcher(BasePlatformPatcher): """ Class used to patch Android APK's""" required_commands = { 'aapt': { 'installation': 'apt install aapt (Kali Linux)' }, 'adb': { 'installation': 'apt install adb (Kali Linux); brew install adb (macOS)' }, 'apksigner': { 'installation': 'apt install apksigner (Kali Linux)' }, 'apktool': { 'installation': 'Install from https://apktool.org/docs/install' }, 'zipalign': { 'installation': 'apt install zipalign (Kali Linux)' } } def __init__(self, skip_cleanup: bool = False, skip_resources: bool = False, manifest: str = None, only_main_classes: bool = False): super(AndroidPatcher, self).__init__() self.apk_source = None self.apk_temp_directory = tempfile.mkdtemp(suffix='.apktemp') self.apk_temp_frida_patched = self.apk_temp_directory + '.objection.apk' self.apk_temp_frida_patched_aligned = self.apk_temp_directory + '.aligned.objection.apk' self.aapt = None self.skip_cleanup = skip_cleanup self.skip_resources = skip_resources self.manifest = manifest self.keystore = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../assets', 'objection.jks') self.netsec_config = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../assets', 'network_security_config.xml') self.only_main_classes = only_main_classes def is_apktool_ready(self) -> bool: """ Check if apktool is ready for use. :return:bool """ min_version = '2.6.0' # the version of apktool we require o = delegator.run(self.list2cmdline([ self.required_commands['apktool']['location'], '-version', ]), timeout=self.command_run_timeout).out.strip() # On windows we get this 'Press any key to continue' thing, # localized to the the current language. Assume that the version # string we want is always the first line. if len(o.split('\n')) > 1: o = o.split('\n')[0] # Apktool v2.12.0 has changed the syntax `apktool -version, this grabs the version from the usage screen output # instead of re-running as `apktool v`. if len(o.split(' ')) > 1: o = o.split(' ')[1] if len(o) == 0: click.secho('Unable to determine apktool version. Is it installed') return False click.secho('Detected apktool version as: ' + o, dim=True) # ensure we have at least apktool MIN_VERSION if semver.compare(o, min_version) < 0: click.secho('apktool version should be at least ' + min_version, fg='red', bold=True) click.secho('Please see the following URL for more information: ' 'https://github.com/sensepost/objection/wiki/Apktool-Upgrades', fg='yellow') return False # run clean-frameworks-dir click.secho('Running apktool empty-framework-dir...', dim=True) o = delegator.run(self.list2cmdline([ self.required_commands['apktool']['location'], 'empty-framework-dir', ]), timeout=self.command_run_timeout).out.strip() if len(o) > 0: click.secho(o, fg='yellow', dim=True) return True def set_apk_source(self, source: str): """ Set the source APK to work with. :param source: :return: """ if not os.path.exists(source): raise Exception('Source {0} not found.'.format(source)) self.apk_source = source return self def _get_android_manifest(self) -> ElementTree: """ Get the AndroidManifest as a parsed ElementTree :return: """ # error if --skip-resources was used because the manifest is encoded if self.skip_resources is True and self.manifest is None: click.secho('Cannot manually parse the AndroidManifest.xml when --skip-resources ' 'is set, remove this and try again, or manually specify a manifest with --manifest.', fg='red') raise Exception('Cannot --skip-resources when trying to manually parse the AndroidManifest.xml') # use the android namespace ElementTree.register_namespace('android', 'http://schemas.android.com/apk/res/android') if self.manifest is not None: return ElementTree.parse(self.manifest) else: return ElementTree.parse(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml')) def _get_appt_output(self): """ Get the output of `aapt dump badging`. :return: """ if not self.aapt: o = delegator.run(self.list2cmdline([ self.required_commands['aapt']['location'], 'dump', 'badging', self.apk_source ]), timeout=self.command_run_timeout) if len(o.err) > 0: click.secho('An error may have occurred while running aapt.', fg='red') click.secho(o.err, fg='red') self.aapt = o.out return self.aapt def _get_launchable_activity(self) -> str: """ Determines the class name for the activity that is launched on application startup. This is done by first trying to parse the output of aapt dump badging, then falling back to manually parsing the AndroidManifest for activity-alias tags. :return: """ activities = (match.groups()[0] for match in re.finditer(r"^launchable-activity: name='([^']+)'", self._get_appt_output(), re.MULTILINE)) activity = next(activities, None) # If we got the activity using aapt, great, return that if activity is not None: return activity # if we dont have the activity yet, check out activity aliases click.secho(('Unable to determine the launchable activity using aapt, trying ' 'to manually parse the AndroidManifest for activity aliases...'), dim=True, fg='yellow') # Try and parse the manifest manually manifest = self._get_android_manifest() root = manifest.getroot() # grab all of the activity-alias tags for alias in root.findall('./application/activity-alias'): # Take not of the current activity current_activity = alias.get('{http://schemas.android.com/apk/res/android}targetActivity') categories = alias.findall('./intent-filter/category') # make sure we have categories for this alias if categories is None: continue for category in categories: # check if the name of this category is that of LAUNCHER # its possible to have multiples, but once we determine one # that fits we can just return and move on category_name = category.get('{http://schemas.android.com/apk/res/android}name') if category_name == 'android.intent.category.LAUNCHER': return current_activity # getting here means we were unable to determine what the launchable # activity is click.secho('Unable to determine the launchable activity for this app.', fg='red') raise Exception('Unable to determine launchable activity') def get_patched_apk_path(self) -> str: """ Returns the path of the patched, aligned APK. :return: """ return self.apk_temp_frida_patched_aligned def get_temp_working_directory(self) -> str: """ Returns the temporary working directory used by this patcher. :return: """ return self.apk_temp_directory def unpack_apk(self, fix_concurrency_to = None): """ Unpack an APK with apktool. :return: """ click.secho('Unpacking {0}'.format(self.apk_source), dim=True) o = delegator.run(self.list2cmdline([ self.required_commands['apktool']['location'], 'decode', '-f', ] + (['-r'] if self.skip_resources else []) + (['--only-main-classes'] if self.only_main_classes else []) + [ '-o', self.apk_temp_directory, self.apk_source ] + ([] if fix_concurrency_to is None else ['-j', fix_concurrency_to])), timeout=self.command_run_timeout) debug_print("Command:" + o.cmd) if len(o.err) > 0: click.secho('An error may have occurred while extracting the APK.', fg='red') click.secho(o.err, fg='red') def inject_internet_permission(self): """ Checks the status of the source APK to see if it has the INTERNET permission. If not, the manifest file is parsed and the permission injected. :return: """ internet_permission = 'android.permission.INTERNET' # if the app already has the internet permission, easy mode :D if internet_permission in self._get_appt_output(): click.secho('App already has android.permission.INTERNET', fg='green') return # if not, we need to inject an element with it click.secho('App does not have android.permission.INTERNET, attempting to patch the AndroidManifest.xml...', dim=True, fg='yellow') xml = self._get_android_manifest() root = xml.getroot() click.secho('Injecting permission: {0}'.format(internet_permission), fg='green') # prepare a new 'uses-permission' tag child = ElementTree.Element('uses-permission') child.set('android:name', internet_permission) root.append(child) click.secho('Writing new Android manifest...', dim=True) xml.write(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml'), encoding='utf-8', xml_declaration=True) def extract_native_libs_patch(self): """ Check the AndroidManifest.xml file for extractNativeLibs="false" if it exists, change it to extractNativeLibs="true". Since AndroidStudio 2.1 this flag is set as false by default. This breaks it when installing the .apk to the device. :return: """ xml = self._get_android_manifest() root = xml.getroot() application_tag = root.findall('application') # ensure that we got the application tag if len(application_tag) <= 0: message = 'Could not find the application tag in the AndroidManifest.xml' click.secho(message, fg='red', bold=True) raise Exception(message) application_tag = application_tag[0] # Check if the flag is present and set to false if '{http://schemas.android.com/apk/res/android}extractNativeLibs' in application_tag.attrib \ and application_tag.attrib['{http://schemas.android.com/apk/res/android}extractNativeLibs'] == 'false': # Set the flag to true application_tag.attrib['{http://schemas.android.com/apk/res/android}extractNativeLibs'] = 'true' click.secho('Setting extractNativeLibs to true...', dim=True) xml.write(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml'), encoding='utf-8', xml_declaration=True) return def flip_debug_flag_to_true(self): """ Set the android:debuggable flag to true in the AndroidManifest. :return: """ xml = self._get_android_manifest() root = xml.getroot() click.secho('Setting debug flag to true', fg='green') application_tag = root.findall('application') # ensure that we got the application tag if len(application_tag) <= 0: message = 'Could not find the application tag in the AndroidManifest.xml' click.secho(message, fg='red', bold=True) raise Exception(message) application_tag = application_tag[0] if '{http://schemas.android.com/apk/res/android}debuggable' in application_tag.attrib \ and application_tag.attrib['{http://schemas.android.com/apk/res/android}debuggable'] == 'true': click.secho('Application already has the android:debuggable flag set to True') return # set the debuggable flag application_tag.attrib['{http://schemas.android.com/apk/res/android}debuggable'] = 'true' click.secho('Writing new Android manifest...', dim=True) xml.write(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml'), encoding='utf-8', xml_declaration=True) def add_network_security_config(self): """ Add a network_security_config.xml to the AndroidManifest.xml for Android 7+. Refs: https://serializethoughts.com/2016/09/10/905/ https://warroom.securestate.com/android-7-intercepting-app-traffic/ https://www.nowsecure.com/blog/2017/06/15/certificate-pinning-for-android-and-ios-mobile-man-in-the-middle-attack-prevention/ https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2017/november/bypassing-androids-network-security-configuration/ https://sensepost.com/blog/2018/tip-toeing-past-android-7s-network-security-configuration/ return: """ xml = self._get_android_manifest() root = xml.getroot() application_tag = root.findall('application') # ensure that we got the application tag if len(application_tag) <= 0: message = 'Could not find the application tag in the AndroidManifest.xml' click.secho(message, fg='red', bold=True) raise Exception(message) application_tag = application_tag[0] click.secho('Checking for an existing networkSecurityConfig tag', dim=True) if '{http://schemas.android.com/apk/res/android}networkSecurityConfig' in application_tag.attrib: if not click.prompt('An existing network security config was found. Do you want to replace it?', type=bool, default=True): return # copy our network security configuration to res/xml/network_security_config.xml sec_config_path = os.path.join(self.apk_temp_directory, 'res', 'xml') # check if the config path exists if not os.path.exists(sec_config_path): click.secho('Creating XML res path: {0}'.format(sec_config_path), dim=True) os.makedirs(sec_config_path) click.secho('Copying network_security_config.xml...', fg='green', dim=True) shutil.copyfile(self.netsec_config, os.path.join(sec_config_path, 'network_security_config.xml')) # set the networkSecurityConfig xml location # this is in res/xml/network_security_config.xml application_tag.attrib[ '{http://schemas.android.com/apk/res/android}networkSecurityConfig'] = '@xml/network_security_config' click.secho('Writing new Android manifest...', dim=True) xml.write(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml'), encoding='utf-8', xml_declaration=True) def _determine_smali_path_for_class(self, target_class) -> str: """ Attempts to determine the local path for a target class' smali :param target_class: :return: """ # convert to a filesystem path, just like how it would be on disk # from the apktool dump target_class = target_class.replace('.', '/') activity_path = os.path.join(self.apk_temp_directory, 'smali', target_class) + '.smali' # check if the activity path exists. If not, try and see if this may have been # a multidex setup if not os.path.exists(activity_path): click.secho('Smali not found in smali directory. This might be a multidex APK. Searching...', dim=True) # apk tool will dump the dex classes to a smali directory. in multidex setups # we have folders such as smali_classes2, smali_classes3 etc. we will search # those paths for the launch activity we detected. for x in range(2, 100): smali_path = os.path.join(self.apk_temp_directory, 'smali_classes{0}'.format(x)) # stop if the smali_classes directory does not exist. if not os.path.exists(smali_path): break # determine the path to the launchable activity again activity_path = os.path.join(smali_path, target_class) + '.smali' # if we found the activity, stop the loop if os.path.exists(activity_path): click.secho('Found smali at: {0}'.format(activity_path), dim=True) break # one final check to ensure we have the target .smali file if not os.path.exists(activity_path): raise Exception('Unable to find smali to patch!') return activity_path @staticmethod def _determine_end_of_smali_method_from_line(smali: list, start: int) -> int: """ Determines where the .end method line is. This method is also aware of a methods that 'returns' and will return the line before that too. :param smali: :param start: :return: """ # enumerate all of # the lines in the original smali sources and mark the offsets of the # lines that contain '.end method'. the search starts right after the # original inject marker so that we can pick the top most .end method # when we are done searching. this is also why the # represented in the # inject marker is added to the calculated marker in the list of end methods. end_methods = [(i + start) for i, x in enumerate(smali[start:]) if '.end method' in x] # ensure that we found at least one .end method if len(end_methods) <= 0: raise Exception('Unable to find the end of the existing constructor') # set the last line of the constructors method to the one # just before the .end method line end_of_method = end_methods[0] - 1 # check if the constructor has a return type call. if it does, # move up one line again to inject our loadLibrary before the return if 'return' in smali[end_of_method]: end_of_method -= 1 return end_of_method @staticmethod def _determine_first_inject_point_of_smali_method_from_line(smali: list, start: int) -> int: """ Determines the first line in a smali method where we can inject code. This is the line after any .locals or .annotations This method is also aware of a methods that 'returns' and will return the line before that too. :param smali: :param start: :return: """ pos = start in_annotation = False while pos + 1 < len(smali): pos = pos + 1 line = smali[pos].strip() # skip empty lines if not line: continue # skip locals if line.startswith(".locals "): continue # skip annotations if in_annotation or line.startswith(".annotation "): in_annotation = True continue if line.startswith(".end annotation"): in_annotation = False continue return pos - 1 def _patch_smali_with_load_library(self, smali_lines: list, inject_marker: int) -> list: """ Patches a list of smali lines with the appropriate loadLibrary call based on wether a constructor already exists or not. :param smali_lines: :param inject_marker: :return: """ # raw smali to inject. # ref: https://koz.io/using-frida-on-android-without-root/ # if no constructor is present, the full_load_library is used full_load_library = ('.method static constructor ()V\n' ' .locals 0\n' # _revalue_locals_count() will ++ this '\n' ' .prologue\n' ' const-string v0, "frida-gadget"\n' '\n' ' invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\n' '\n' ' return-void\n' '.end method\n') # if an existing constructor is present, this partial_load_library # will be used instead partial_load_library = ('\n const-string v0, "frida-gadget"\n' '\n' ' invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\n') # Check if there is an existing clinit here. If there is, then we need # to determine where the constructor ends and inject a simple loadLibrary # just before the end if 'clinit' in smali_lines[inject_marker]: click.secho('Injecting into an existing constructor', fg='yellow') inject_point = self._determine_first_inject_point_of_smali_method_from_line(smali_lines, inject_marker) click.secho('Injecting loadLibrary call at line: {0}'.format(inject_point), dim=True, fg='green') patched_smali = \ smali_lines[:inject_point] + partial_load_library.splitlines(keepends=True) + \ smali_lines[inject_point:] else: # if there is no constructor, we can simply inject a fresh constructor click.secho('Injecting loadLibrary call at line: {0}'.format(inject_marker), dim=True, fg='green') # inject the load_library code between patched_smali = \ smali_lines[:inject_marker] + full_load_library.splitlines(keepends=True) + smali_lines[inject_marker:] return patched_smali def _revalue_locals_count(self, patched_smali: list, inject_marker: int): """ Attempt to ++ the first .locals declaration in a list of smali lines confined to the same method. :param patched_smali: :param inject_marker: :return: """ def _h(): click.secho('Could not update .locals value. Sometimes this may break things,' 'but not always. If the applications crashes after patching, try ' 'and add the --pause flag, fixing the patched smali manually.', fg='yellow') # next, update the .locals count (if its defined) # if this step fails, its not really a big deal as many times its not # fatal. however, if it does fail, warn about it. click.secho('Attempting to fix the constructors .locals count', dim=True) end_of_method = self._determine_end_of_smali_method_from_line(patched_smali, inject_marker) # check if we have a .locals declaration right after the start of our # already matched constructor defined_locals = [i for i, x in enumerate(patched_smali[inject_marker:end_of_method]) if '.locals' in x] if len(defined_locals) <= 0: click.secho('Unable to determine any .locals for the target constructor', fg='yellow') _h() return patched_smali # determine the offset for the first matched .locals definition locals_smali_offset = defined_locals[0] + inject_marker try: defined_local_value = patched_smali[locals_smali_offset].split(' ')[-1] defined_local_value_as_int = int(defined_local_value, 10) new_locals_value = defined_local_value_as_int + 1 except ValueError as e: click.secho( 'Unable to parse .locals value for the injected constructor with error: {0}'.format(str(e)), fg='yellow') _h() return patched_smali click.secho('Current locals value is {0}, updating to {1}:'.format( defined_local_value_as_int, new_locals_value), dim=True) # simply search / replace the integer values we already calculated on the relevant line patched_smali[locals_smali_offset] = patched_smali[locals_smali_offset].replace( str(defined_local_value_as_int), str(new_locals_value)) return patched_smali def inject_load_library(self, target_class: str = None): """ Injects a loadLibrary call into a class. If a target class is not specified, we will make an attempt at searching for a launchable activity in the target APK. Most of the idea for this comes from: https://koz.io/using-frida-on-android-without-root/ :return: """ # determine the path to the smali we should inject the load_library # call into. a user may specify a specific class to target, otherwise # we get a class name from the internal launchable activity method # of this class. if target_class: click.secho('Using target class: {0} for patch'.format(target_class), fg='green', bold=True) else: click.secho('Target class not specified, searching for launchable activity instead...', fg='green', bold=True) activity_path = self._determine_smali_path_for_class( target_class if target_class else self._get_launchable_activity()) click.secho('Reading smali from: {0}'.format(activity_path), dim=True) # apktool d smali will have a comment line line: '# direct methods' with open(activity_path, 'r') as f: smali_lines = f.readlines() # search for the line starting with '# direct methods' in it inject_marker = [i for i, x in enumerate(smali_lines) if '# direct methods' in x] # ensure we got a marker if len(inject_marker) <= 0: raise Exception('Unable to determine position to inject a loadLibrary call') # pick the first position for the inject. add one line as we # want to inject right below the comment we matched inject_marker = inject_marker[0] + 1 patched_smali = self._patch_smali_with_load_library(smali_lines, inject_marker) patched_smali = self._revalue_locals_count(patched_smali, inject_marker) click.secho('Writing patched smali back to: {0}'.format(activity_path), dim=True) with open(activity_path, 'w') as f: f.write(''.join(patched_smali)) def add_gadget_to_apk(self, architecture: str, gadget_source: str, gadget_config: str): """ Copies a frida gadget for a specific architecture to an extracted APK's lib path. :param architecture: :param gadget_source: :param gadget_config: :return: """ libs_path = os.path.join(self.apk_temp_directory, 'lib', architecture) # check if the libs path exists if not os.path.exists(libs_path): click.secho('Creating library path: {0}'.format(libs_path), dim=True) os.makedirs(libs_path) click.secho('Copying Frida gadget to libs path...', fg='green', dim=True) shutil.copyfile(gadget_source, os.path.join(libs_path, 'libfrida-gadget.so')) if gadget_config: click.secho('Adding a gadget configuration file...', fg='green') shutil.copyfile(gadget_config, os.path.join(libs_path, 'libfrida-gadget.config.so')) def build_new_apk(self, use_aapt2: bool = False, fix_concurrency_to = None): """ Build a new .apk with the frida-gadget patched in. :return: """ click.secho('Rebuilding the APK with the frida-gadget loaded...', fg='green', dim=True) o = delegator.run( self.list2cmdline([self.required_commands['apktool']['location'], 'build', self.apk_temp_directory, ] + (['--use-aapt2'] if use_aapt2 else []) + [ '-o', self.apk_temp_frida_patched ]+ ([] if fix_concurrency_to is None else ['-j', fix_concurrency_to])) , timeout=self.command_run_timeout) if len(o.err) > 0: click.secho(('Rebuilding the APK may have failed. Read the following ' 'output to determine if apktool actually had an error: \n'), fg='red') click.secho(o.err, fg='red') click.secho('Built new APK with injected loadLibrary and frida-gadget', fg='green') def zipalign_apk(self): """ Performs the zipalign command on an APK. :return: """ click.secho('Performing zipalign', dim=True) o = delegator.run(self.list2cmdline([ self.required_commands['zipalign']['location'], '-p', '4', self.apk_temp_frida_patched if os.path.exists(self.apk_temp_frida_patched) else self.apk_source, self.apk_temp_frida_patched_aligned ])) if len(o.err) > 0: click.secho(('Zipaligning the APK may have failed. Read the following ' 'output to determine if zipalign actually had an error: \n'), fg='red') click.secho(o.err, fg='red') click.secho('Zipalign completed', fg='green') def sign_apk(self): """ Signs an APK with the objection key. The keystore itself was created with: keytool -genkey -v -keystore objection.jks -alias objection -keyalg RSA -keysize 2048 -validity 3650 pass: basil-joule-bug :return: """ click.secho('Signing new APK.', dim=True) o = delegator.run(self.list2cmdline([ self.required_commands['apksigner']['location'], 'sign', '--ks', self.keystore, '--ks-pass', 'pass:basil-joule-bug', '--ks-key-alias', 'objection', self.apk_temp_frida_patched_aligned ])) if len(o.err) > 0: click.secho('Signing the new APK may have failed.', fg='red') click.secho(o.out, fg='yellow') click.secho(o.err, fg='red') click.secho('Signed the new APK', fg='green') def __del__(self): """ Cleanup after ourselves. :return: """ if self.skip_cleanup: click.secho('Not cleaning up temporary files', dim=True) return click.secho('Cleaning up temp files...', dim=True) try: shutil.rmtree(self.apk_temp_directory, ignore_errors=True) with contextlib.suppress(FileNotFoundError): os.remove(self.apk_temp_frida_patched) with contextlib.suppress(FileNotFoundError): os.remove(self.apk_temp_frida_patched_aligned) except Exception as err: click.secho('Failed to cleanup with error: {0}'.format(err), fg='red', dim=True) ================================================ FILE: objection/utils/patchers/base.py ================================================ import json import os import shlex import shutil from subprocess import list2cmdline import click from .github import Github # default paths objection_path = os.path.join(os.path.expanduser('~'), '.objection') gadget_versions = os.path.join(objection_path, 'gadget_versions') def list2posix_cmdline(seq): """ Translate a sequence of arguments into a command line string. Implemented using shlex.quote because subprocess.list2cmdline doesn't work with POSIX """ return ' '.join(map(shlex.quote, seq)) class BasePlatformGadget(object): """ Class with base methods for any platforms Gadget downloaded """ def __init__(self, github: Github) -> None: """ Build a new instance with an existing Github instance. :param github: """ self.github = github @staticmethod def get_local_version(gadget_type: str) -> str: """ Check and return the local version of the FridaGadget type we have. :return: """ if not os.path.exists(gadget_versions): return '0' with open(gadget_versions, 'r') as f: versions = f.read() # load the json. try: versions = json.loads(versions) except json.decoder.JSONDecodeError: return '0' if gadget_type in versions: return versions[gadget_type] return '0' def set_local_version(self, gadget_type: str, version: str): """ Writes the version number to file, recording it as the current local version. :param gadget_type: :param version: :return: """ # read the current versions if it exists, else start # a new dictionary for it if os.path.exists(gadget_versions): # load the json from disk try: with open(gadget_versions, 'r') as f: versions = json.loads(f.read()) except json.decoder.JSONDecodeError: versions = {} else: versions = {} # add the new version versions[gadget_type] = version # and write it to file with open(gadget_versions, 'w') as f: f.write(json.dumps(versions)) return self class BasePlatformPatcher(object): """ Base class with methods used by any platform patcher. """ # extended classes should fill this property required_commands = {} def __init__(self): # check dependencies self.have_all_commands = self._check_commands() self.command_run_timeout = 60 * 5 if os.name == 'nt': self.list2cmdline = list2cmdline else: self.list2cmdline = list2posix_cmdline def _check_commands(self) -> bool: """ Check if the shell commands in required_commands are available. :return: """ for cmd, attributes in self.required_commands.items(): location = shutil.which(cmd) if location is None: click.secho('Unable to find {0}. Install it with: {1} before continuing.'.format( cmd, attributes['installation']), fg='red', bold=True) return False self.required_commands[cmd]['location'] = location return True def are_requirements_met(self): """ Checks if the command requirements have all been met. :return: """ return self.have_all_commands ================================================ FILE: objection/utils/patchers/github.py ================================================ import requests class Github(object): """ Interact with Github """ GITHUB_LATEST_RELEASE = 'https://api.github.com/repos/frida/frida/releases/latest' GITHUB_TAGGED_RELEASE = 'https://api.github.com/repos/frida/frida/releases/tags/{tag}' # the 'context' of this Github instance gadget_version = None def __init__(self, gadget_version: str = None): """ Init a new instance of Github """ if gadget_version: self.gadget_version = gadget_version self.request_cache = {} def _call(self, endpoint: str) -> dict: """ Make a call to Github and cache the response. :param endpoint: :return: """ # return a cached response if possible if endpoint in self.request_cache: return self.request_cache[endpoint] # get a new response results = requests.get(endpoint).json() # cache it self.request_cache[endpoint] = results # and return it return results def get_latest_version(self) -> str: """ Call Github and get the tag_name of the latest release. :return: """ self.gadget_version = self._call(self.GITHUB_LATEST_RELEASE)['tag_name'] return self.gadget_version def get_assets(self) -> dict: """ Gets the assets for the currently selected gadget_version. :return: """ assets = self._call(self.GITHUB_TAGGED_RELEASE.format(tag=self.gadget_version)) if 'assets' not in assets: raise Exception(('Unable to determine assets for gadget version \'{0}\'. ' 'Are you sure this version is available on Github?').format(self.gadget_version)) return assets['assets'] ================================================ FILE: objection/utils/patchers/ios.py ================================================ import datetime import lzma import os import plistlib import shutil import tempfile import zipfile import click import delegator import requests from .base import BasePlatformGadget, BasePlatformPatcher, objection_path from .github import Github class IosGadget(BasePlatformGadget): """ Class used to work with the iOS Frida Gadget """ ios_dylib_path = os.path.join(objection_path, 'ios') ios_dylib_gadget_path = os.path.join(ios_dylib_path, 'FridaGadget.dylib') ios_dylib_gadget_archive_path = os.path.join(ios_dylib_path, 'FridaGadget.dylib.xz') def __init__(self, github: Github) -> None: """ Build a new instance, ensuring that the paths needed are available. :param github: """ super(IosGadget, self).__init__(github) # ensure we have the ios gadget path available if not os.path.exists(self.ios_dylib_path): os.makedirs(self.ios_dylib_path) def get_gadget_path(self) -> str: """ Returns the path on disk where the iOS FridaGadget can be found. :return: """ return self.ios_dylib_gadget_path def gadget_exists(self): """ Checks if the iOS gadget exists on disk. :return: """ return os.path.exists(self.ios_dylib_gadget_path) def download(self): """ Downloads the latest iOS gadget. :return: """ download_url = self._get_download_url() click.secho('Downloading from: {0}'.format(download_url), dim=True) # stream the download using requests dylib = requests.get(download_url, stream=True) # save the requests stream to file try: with open(self.ios_dylib_gadget_archive_path, 'wb') as f: click.secho('Downloading iOS dylib to {0}...'.format(self.ios_dylib_gadget_archive_path), fg='green', dim=True) shutil.copyfileobj(dylib.raw, f) except Exception: if os.path.isfile(self.ios_dylib_gadget_archive_path): os.remove(self.ios_dylib_gadget_archive_path) return self def _get_download_url(self) -> str: """ Determines the download URL to use for the iOS gadget. :return: """ url = '' for asset in self.github.get_assets(): if 'ios-universal.dylib.xz' in asset['name']: url = asset['browser_download_url'] if not url: click.secho('Unable to determine URL to download the dylib', fg='red') raise Exception('Unable to determine URL for iOS gadget download.') return url def unpack(self): """ Unpacks a downloaded .xz gadget. :return: """ click.secho('Unpacking {0}...'.format(self.ios_dylib_gadget_archive_path), dim=True) try: with lzma.open(self.ios_dylib_gadget_archive_path) as f: with open(self.ios_dylib_gadget_path, 'wb') as g: g.write(f.read()) except Exception: if os.path.isfile(self.ios_dylib_gadget_archive_path): os.remove(self.ios_dylib_gadget_archive_path) if os.path.isfile(self.ios_dylib_gadget_path): os.remove(self.ios_dylib_gadget_path) return self def cleanup(self): """ Cleans up a downloaded iOS .xz gadget. :return: """ click.secho('Cleaning up downloaded archives...', dim=True) os.remove(self.ios_dylib_gadget_archive_path) class IosPatcher(BasePlatformPatcher): """ Class used to Patch iOS applications """ required_commands = { 'xcodebuild': { 'installation': 'Install XCode on macOS via the Appstore' }, 'applesign': { 'installation': 'npm install -g applesign' }, 'insert_dylib': { 'installation': ('git clone https://github.com/Tyilo/insert_dylib && cd insert_dylib &&' 'xcodebuild && cp build/Release/insert_dylib /usr/local/bin/insert_dylib') }, 'codesign': { 'installation': 'Part of XCode' }, 'security': { 'installation': 'macOS builtin command' }, 'zip': { 'installation': 'macOS builtin command' }, 'unzip': { 'installation': 'macOS builtin command' }, 'plutil': { 'installation': 'macOS builtin command' }, } def __init__(self, skip_cleanup: bool = False): """ Init a new instance of the IosPatcher class. """ super(IosPatcher, self).__init__() self.provision_file = None self.payload_directory = None self.app_folder = None self.app_binary = None self.patched_ipa_path = None self.patched_codesigned_ipa_path = None self.skip_cleanup = skip_cleanup self.bundle_id = None # temp_file to copy an IPA to _, self.temp_file = tempfile.mkstemp(suffix='.ipa') # a working directory to extract the IPA to self.temp_directory = os.path.dirname(self.temp_file) # cleanup the temp_directory to work with self._cleanup_extracted_data() def set_provsioning_profile(self, provision_file: str = None, bundle_id: str = None) -> None: """ Sets the provision file to use during patching. :param bundle_id: :param provision_file: :return: """ # have provision file? set it and be done if provision_file: self.provision_file = provision_file if bundle_id: click.secho('Setting bundleid to specified value: {}'.format(bundle_id), dim=True) self.bundle_id = bundle_id else: self._set_bundle_id_from_profile() return click.secho('No provision file specified, searching for one...', bold=True) # locate a valid mobile provision on disk in: ~/Library/Developer/Xcode/DerivedData/ possible_provisions = [os.path.join(dp, f) for dp, dn, fn in os.walk(os.path.expanduser('~/Library/Developer/Xcode/DerivedData/')) for f in fn if 'embedded.mobileprovision' in f] if len(possible_provisions) <= 0: message = 'No provisioning files found. Please specify one or generate one by building an app.' click.secho(message, fg='red') raise Exception(message) # we have some provisioning profiles, lets find the one # with the most days left current_time = datetime.datetime.now() expirations = {} for pf in possible_provisions: _, decoded_location = tempfile.mkstemp('decoded_provision') # Decode the mobile provision using macOS's security cms tool delegator.run(self.list2cmdline([ self.required_commands['security']['location'], 'cms', '-D', '-i', pf, '-o', decoded_location ]), timeout=self.command_run_timeout) # read the expiration date from the profile with open(decoded_location, 'rb') as f: parsed_data = plistlib.load(f) if parsed_data['ExpirationDate'] > current_time: expirations[pf] = parsed_data['ExpirationDate'] - current_time click.secho('Found provision file {0} expiring in {1}'.format(pf, expirations[pf]), dim=True) # cleanup the temp path os.remove(decoded_location) # ensure that we got some valid mobileprovisions to work with if len(expirations) <= 0: message = 'Could not find a non-expired provisioning file. Please specify or generate one.' click.secho(message, fg='red') raise Exception(message) # sort the results so that the mobileprovision with the most time is at # the top of the list click.secho('Found a valid provisioning profile', fg='green', bold=True) self.provision_file = sorted(expirations, key=expirations.get, reverse=True)[0] if bundle_id: click.secho('Setting bundleid to specified value: {}'.format(bundle_id), dim=True) self.bundle_id = bundle_id else: self._set_bundle_id_from_profile() def extract_ipa(self, unzip_unicode, ipa_source: str) -> None: """ Extracts a source IPA into the temporary directories. :param ipa_source: :param unzip_unicode: :return: """ # copy the original ipa to the temp directory. shutil.copyfile(ipa_source, self.temp_file) if unzip_unicode: with zipfile.ZipFile(self.temp_file, 'r') as ipa: for info in ipa.infolist(): info.filename = info.filename.encode('cp437').decode('utf-8') ipa.extract(info, self.temp_directory) else: # extract the IPA this should result in a 'Payload' directory ipa = zipfile.ZipFile(self.temp_file, 'r') ipa.extractall(self.temp_directory) ipa.close() # check what is in the Payload directory self.payload_directory = os.listdir(os.path.join(self.temp_directory, 'Payload')) if len(self.payload_directory) > 1: click.secho('Warning: Payload folder has more than one file, this is unexpected.', fg='yellow') # get the folder that ends with .app. This is where we will be patching # the executable with FridaGadget app_name = ''.join([x for x in self.payload_directory if x.endswith('.app')]) click.secho('Working with app: {0}'.format(app_name)) self.app_folder = os.path.join(self.temp_directory, 'Payload', app_name) def set_application_binary(self, binary: str = None) -> None: """ Sets the binary that will be patched. If a binary is not defined, the applications Info.plist is parsed and the CFBundleIdentifier key read. :param binary: :return: """ if binary is not None: click.secho('Using user provided binary name of: {0}'.format(binary)) self.app_binary = os.path.join(self.app_folder, binary) return with open(os.path.join(self.app_folder, 'Info.plist'), 'rb') as f: info_plist = plistlib.load(f) # print the bundle identifier click.secho('Bundle identifier is: {0}'.format(info_plist['CFBundleIdentifier']), fg='green', bold=True) self.app_binary = os.path.join(self.app_folder, info_plist['CFBundleExecutable']) def patch_and_codesign_binary(self, frida_gadget: str, codesign_signature: str, gadget_config: str) -> None: """ Patches an iOS binary to load a Frida gadget on startup. Any other dylibs within the application will also be code signed with the same signature used for the FridaGadget itself. :param frida_gadget: :param codesign_signature: :param gadget_config: :return: """ if not self.app_binary: raise Exception('The applications binary should be set first.') if not self.app_folder: raise Exception('The application should be extracted first.') # create a Frameworks directory if it does not already exist if not os.path.exists(os.path.join(self.app_folder, 'Frameworks')): click.secho('Creating Frameworks directory for FridaGadget...', fg='green') os.mkdir(os.path.join(self.app_folder, 'Frameworks')) # copy the frida gadget to the applications Frameworks directory shutil.copyfile(frida_gadget, os.path.join(self.app_folder, 'Frameworks', 'FridaGadget.dylib')) if gadget_config: click.secho('Copying Gadget Config to Frameworks path...', fg='green', dim=True) shutil.copyfile(gadget_config, os.path.join(self.app_folder, 'Frameworks', 'FridaGadget.config')) # patch the app binary load_library_output = delegator.run(self.list2cmdline([ self.required_commands['insert_dylib']['location'], '--strip-codesig', '--inplace', '@executable_path/Frameworks/FridaGadget.dylib', self.app_binary ]), timeout=self.command_run_timeout) # check if the insert_dylib call may have failed if 'Added LC_LOAD_DYLIB' not in load_library_output.out: click.secho('Injecting the load library to {0} might have failed.'.format(self.app_binary), fg='yellow') click.secho(load_library_output.out, fg='red', dim=True) click.secho(load_library_output.err, fg='red') # get the paths of all of the .dylib files in this applications # bundle. we will have to codesign all of them and not just the # frida gadget dylibs_to_sign = [ os.path.join(dp, f) for dp, dn, fn in os.walk(self.app_folder) for f in fn if f.endswith('.dylib')] # codesign the dylibs in this bundle click.secho('Codesigning {0} .dylib\'s with signature {1}'.format(len(dylibs_to_sign), codesign_signature), fg='green') for dylib in dylibs_to_sign: click.secho('Code signing: {0}'.format(os.path.basename(dylib)), dim=True) delegator.run(self.list2cmdline([ self.required_commands['codesign']['location'], '-f', '-v', '-s', codesign_signature, dylib])) def archive_and_codesign(self, original_name: str, codesign_signature: str) -> None: """ Creates a new archive of the patched IPA. :param original_name: :param codesign_signature: :return: """ click.secho('Creating new archive with patched contents...', dim=True) self.patched_ipa_path = os.path.join( self.temp_directory, os.path.basename( '{0}-frida.ipa'.format(os.path.splitext(original_name)[0]))) def zipdir(path, ziph): # ziph is a zipfile handle for root, dirs, files in os.walk(path): for fi in files: ziph.write(os.path.join(root, fi), os.path.relpath(os.path.join(root, fi), os.path.join(path, '..'))) zipf = zipfile.ZipFile(self.patched_ipa_path, 'w') zipdir(os.path.join(self.temp_directory, 'Payload'), zipf) zipf.close() # codesign the new ipa click.secho('Codesigning patched IPA...', fg='green') self.patched_codesigned_ipa_path = os.path.join(self.temp_directory, os.path.basename( '{0}-frida-codesigned.ipa'.format(os.path.splitext(original_name)[0]))) ipa_codesign = delegator.run(self.list2cmdline([ self.required_commands['applesign']['location'], '--identity', codesign_signature, '--mobileprovision', self.provision_file, '--bundleid', self.bundle_id, '--clone-entitlements', '--output', self.patched_codesigned_ipa_path, self.patched_ipa_path ]), timeout=self.command_run_timeout) click.secho(ipa_codesign.err, dim=True) def get_patched_ipa_path(self) -> str: """ Returns the path where the final patched IPA would be. :return: """ return self.patched_codesigned_ipa_path def _set_bundle_id_from_profile(self): """ Extracts and sets a bundle id from a decoded mobileprovision :return: """ if not self.provision_file: click.secho('Provisioning profile not set. Skipping bundleid extraction', dim=True) return _, decoded_location = tempfile.mkstemp('decoded_provision') # Decode the mobile provision using macOS's security cms tool delegator.run(self.list2cmdline([ self.required_commands['security']['location'], 'cms', '-D', '-i', self.provision_file, '-o', decoded_location ]), timeout=self.command_run_timeout) # https://stackoverflow.com/a/66820375 # security cms -D -i your.mobileprovision | plutil -extract # Entitlements.application-identifier xml1 -o - - | grep string | # sed 's/^[^\.]*\.\(.*\)<\/string>$/\1/g' c = delegator.run(self.list2cmdline([ 'cat', decoded_location ]), timeout=self.command_run_timeout).pipe(self.list2cmdline([ self.required_commands['plutil']['location'], '-extract', 'Entitlements.application-identifier', 'xml1', '-o', '-', '-' ]), timeout=self.command_run_timeout).pipe(self.list2cmdline([ 'grep', 'string' ]), timeout=self.command_run_timeout).pipe(self.list2cmdline([ 'sed', r's/^[^\.]*\.\(.*\)<\/string>$/\1/g' ]), timeout=self.command_run_timeout) if len(c.out) > 0: self.bundle_id = c.out.strip() click.secho('Mobile provision bundle identifier is: {}'.format(self.bundle_id), dim=True) # cleanup the temp path os.remove(decoded_location) def _cleanup_extracted_data(self) -> None: """ Small helper method to cleanup temporary files created when an older IPA was extracted. :return: """ p = os.path.join(self.temp_directory, 'Payload') shutil.rmtree(p, ignore_errors=True) def __del__(self): """ Cleanup after ourselves. :return: """ if self.skip_cleanup: click.secho('Not cleaning up temporary files', dim=True) return click.secho('Cleaning up temp files...', dim=True) try: self._cleanup_extracted_data() os.remove(self.temp_file) os.remove(self.patched_ipa_path) os.remove(self.patched_codesigned_ipa_path) except Exception as err: click.secho('Failed to cleanup with error: {0}'.format(err), fg='red', dim=True) ================================================ FILE: objection/utils/plugin.py ================================================ import os from abc import ABC from objection.state.connection import state_connection from objection.utils.helpers import debug_print from ..state.api import api_state class Plugin(ABC): """ Plugin object to extend for development of custom functionality """ def __init__(self, plugin_file: str, namespace: str, implementation: dict): """ Start a new plugin instance. :param plugin_file: :param namespace: :param implementation: """ self.namespace = namespace self.implementation = implementation self.plugin_file = plugin_file # plugin properties if not hasattr(self, 'script_src'): self.script_src = None if not hasattr(self, 'script_path'): self.script_path = None if not hasattr(self, 'on_message_handler'): self.on_message_handler = None self.agent = None self.session = None self.script = None self.api = None self._prepare_source() self._append_to_api() def _prepare_source(self): """ Prepares the self.script_src attribute based on a few rules. If the script source is already set, simply return as there is nothing for us to do. If the script path is set, read that and populate the script_src attribute. If neither script_src not script_path is set, attempt to read the index.js that lives next to the plugin file. If all of the above fail, simply return, writing a debug warning no script source could be found. :return: """ if self.script_src: return if self.script_path: self.script_path = os.path.abspath(self.script_path) with open(self.script_path, 'r', encoding='utf-8') as f: self.script_src = '\n'.join(f.readlines()) return possible_src = os.path.abspath(os.path.join( os.path.abspath(os.path.dirname(self.plugin_file)), 'index.js')) if os.path.exists(possible_src): self.script_path = possible_src with open(self.script_path, 'r', encoding='utf-8') as f: self.script_src = '\n'.join(f.readlines()) return debug_print('[warning] No Fridascript could be found for plugin {0}'.format(self.namespace)) def inject(self) -> None: """ Injects the script sources in a new Frida session. :return: """ if not self.script_src: raise Exception('Unable to discover Frida script source to inject') if not self.agent: self.agent = state_connection.get_agent() self.session = self.agent.device.attach(self.agent.pid) self.script = self.session.create_script(source=self.script_src) # check for a custom message handler, otherwise fallback # to the default objection handler self.script.on('message', self.on_message_handler if self.on_message_handler else self.agent.handlers.script_on_message) self.script.load() self.api = self.script.exports def _append_to_api(self): """ If the http_api() function is defined in the child class, take it's return (it should always return a flask.Blueprint) and append it to the existing blueprints in objections core API. The ApiState class will handle the loading and starting of the API with them included. :return: """ if not hasattr(self, 'http_api'): return if not callable(getattr(self, 'http_api')): raise Exception('The http_api property must be a function returning a Flask Blueprint') api_state.append_api_blueprint(getattr(self, 'http_api')()) ================================================ FILE: objection/utils/update_checker.py ================================================ import json import os from datetime import datetime, timedelta import click import requests from packaging.version import Version from ..__init__ import __version__ objection_path = os.path.join(os.path.expanduser('~'), '.objection') version_file = os.path.join(objection_path, 'version_info') version_data = { 'remote_version': '0.0.0', 'last_check': datetime.now() - timedelta(days=7) } date_fmt = '%d%m%y %H:%M:%S' def cached_version_data() -> version_data: """ Reads the local version file and returns the version data structure :return:version_data """ if not os.path.exists(version_file): return version_data with open(version_file, 'r') as f: data = json.load(f) data['last_check'] = datetime.strptime(data['last_check'], date_fmt) return data def update_version_cache(version: str) -> None: """ Store version information. :param version: :return: """ version_data['remote_version'] = version version_data['last_check'] = datetime.now().strftime(date_fmt) with open(version_file, 'w') as f: json.dump(version_data, f) def notify_newer_version() -> None: """ Print a notification message about the newer version that is available. :return: """ cache_version = cached_version_data()['remote_version'] if Version(cache_version) > Version(__version__): click.secho('\n\nA newer version of objection is available!', fg='green') click.secho('You have v{0} and v{1} is ready for download.\n'.format( __version__, cache_version), fg='green') click.secho('Upgrade with: pip3 install objection --upgrade', fg='green', bold=True) click.secho('For more information, please see: ' 'https://github.com/sensepost/objection/wiki/Updating\n', dim=True) def check_version() -> None: """ Checks if the current version of objection is up to date. :return: """ # if we have not checked for a new version today if not (cached_version_data()['last_check'] > datetime.now() - timedelta(hours=23)): click.secho('Checking for a newer version of objection...', dim=True) # noinspection PyBroadException try: r = requests.get('https://api.github.com/repos/sensepost/objection/releases/latest').json() update_version_cache(r['tag_name']) # Just be quiet about any exceptions here. If this method fails # it really doesn't matter. except Exception: # there is good chance an installation does not have internet, so cache # the current version as the latest to not be annoying about updates update_version_cache(__version__) notify_newer_version() ================================================ FILE: plugins/README.md ================================================ # plugins `objection` has the ability to sideload external plugins. This directory contains a few sample plugins that you could use to kickstart developing your own! ================================================ FILE: plugins/api/__init__.py ================================================ from flask import Blueprint from objection.utils.plugin import Plugin class ApiLoader(Plugin): """ ApiLoader is a plugin that includes an API. This is just an example plugin to demonstrate how you could extend the objection API add your own endpoints. Since this plugins namespace is called api, the urls in our http_api method will therefore be: http://localhost/api/ping http://localhost/api/pong For more information on Flask blueprints, check out the documentation here: https://flask.palletsprojects.com/en/1.1.x/blueprints/ """ def __init__(self, ns): """ Creates a new instance of the plugin :param ns: """ implementation = {} super().__init__(__file__, ns, implementation) self.inject() def http_api(self) -> Blueprint: """ The API endpoints for this plugin. :return: """ # sets the uri path to /api in this case bp = Blueprint(self.namespace, __name__, url_prefix='/' + self.namespace) # the endpoint with this function as the logic will be # /api/ping. # that's because the url_prefix is the namespace name, # and the endpoint is /ping @bp.route('/ping', methods=('GET', 'POST')) def ping(): return 'pong' @bp.route('/version', methods=('GET', 'POST')) def version(): # call getVersion via the Frida RPC for this plugins # agent, defined in index.js return self.api.get_version() return bp namespace = 'api' plugin = ApiLoader ================================================ FILE: plugins/api/index.js ================================================ rpc.exports = { getVersion: function () { return Frida.version; } } ================================================ FILE: plugins/flex/README.md ================================================ # objection Flex plugin This plugin should sideload Flex[1], loaded as a plugin in objection. Flex itself should be a shared library (with your target's architecture as either a thin/fat Mach-o). The source code for a shared library called libFlex is included in this gist as .h and .m files. You need to copy the `Classes/` directory from the official Flex project[1] into your project and compile that as a shared library. [1] [https://github.com/Flipboard/FLEX](https://github.com/Flipboard/FLEX) ================================================ FILE: plugins/flex/__init__.py ================================================ import os import click from objection.utils.plugin import Plugin from objection.commands.filemanager import _path_exists_ios, _upload_ios from objection.commands.device import _get_ios_environment from objection.state.connection import state_connection class FlexLoader(Plugin): """ FlexLoader loads Flex """ def __init__(self, ns): """ Creates a new instance of the plugin :param ns: """ implementation = { 'meta': 'Work with Flex', 'commands': { 'load': { 'meta': 'Load flex', 'exec': self.load_flex } } } super().__init__(__file__, ns, implementation) self.inject() self.flex_dylib = 'libFlex.arm64.dylib' def load_flex(self, args: list): """ Loads flex. :param args: :return: """ agent = state_connection.get_api() device_dylib_path = os.path.join(agent.env_ios_paths()['DocumentDirectory'], self.flex_dylib) if not _path_exists_ios(device_dylib_path): print('Flex not uploaded, uploading...') if not self._upload_flex(device_dylib_path): return click.secho('Asking flex to load...', dim=True) self.api.init_flex(self.flex_dylib) click.secho('Flex should be up!', fg='green') def _upload_flex(self, location: str) -> bool: """ Uploads Flex to the remote filesystem. :return: """ local_flex = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.flex_dylib) if not os.path.exists(local_flex): click.secho('{0} not available next to plugin file. Please build it!'.format(self.flex_dylib), fg='red') return False _upload_ios(local_flex, location) return True namespace = 'flex' plugin = FlexLoader ================================================ FILE: plugins/flex/index.js ================================================ rpc.exports = { initFlex: function (dlib) { const NSDocumentDirectory = 9; const NSUserDomainMask = 1 const p = ObjC.classes.NSFileManager.defaultManager() .URLsForDirectory_inDomains_(NSDocumentDirectory, NSUserDomainMask).lastObject().path(); ObjC.schedule(ObjC.mainQueue, function () { const libFlexModule = Module.load(p + '/' + dlib); const libFlexPtr = libFlexModule.findExportByName("OBJC_CLASS_$_libFlex"); const libFlex = new ObjC.Object(libFlexPtr); libFlex.alloc().init().flexUp(); }); } } ================================================ FILE: plugins/flex/libFlex.h ================================================ #import @interface libFlex : NSObject - (id)init; - (void)logSomething:(NSString *)something; - (void)flexUp; @end ================================================ FILE: plugins/flex/libFlex.m ================================================ #import "libFlex.h" #import "FlexManager.h" @implementation libFlex - (id)init { self = [super init]; return self; } - (void)logSomething:(NSString *)something { NSLog(@"%@", something); } - (void)flexUp { [[FLEXManager sharedManager] showExplorer]; } @end static void __attribute__((constructor)) initialize(void){ NSLog(@"==== Booted ===="); } ================================================ FILE: plugins/mettle/README.md ================================================ # objection Mettle plugin This plugin should sideload [Mettle](https://github.com/rapid7/mettle), loaded as a plugin in objection. Mettle itself should be a shared library available in this directory. ## installation Getting Mettle is super simple. 1. Clone the respistory with `git clone https://github.com/rapid7/mettle.git`. 2. Build Mettle for your target architecture. Eg: `make TARGET=aarch64-iphone-darwin`. 3. Codesign the new dylib in the build directory with `codesign -f -s mettle.dylib` 4. Copy the codesigned dylib into this plugin folder. Running `plugin mettle load` will grab the new dylib and upload it to the device. ================================================ FILE: plugins/mettle/__init__.py ================================================ import os import click from objection.commands.filemanager import _path_exists_ios, _upload_ios from objection.state.connection import state_connection from objection.utils.plugin import Plugin class MettleLoader(Plugin): """ MettleLoader loads Mettle """ def __init__(self, ns): """ Creates a new instance of the plugin :param ns: """ implementation = { 'meta': 'Work with Mettle', 'commands': { 'load': { 'meta': 'Load mettle', 'exec': self.load_mettle }, 'connect': { 'meta': 'Connect mettle', 'exec': self.connect_mettle } } } super().__init__(__file__, ns, implementation) self.inject() self.mettle_dylib = 'mettle.dylib' def load_mettle(self, args: list): """ Loads mettle. :param args: :return: """ agent = state_connection.get_api() device_dylib_path = os.path.join(agent.env_ios_paths()['DocumentDirectory'], self.mettle_dylib) if not _path_exists_ios(device_dylib_path): print('Mettle not uploaded, uploading...') if not self._upload_mettle(device_dylib_path): return click.secho('Loading dylib...', dim=True) self.api.init_mettle(self.mettle_dylib) click.secho('Mettle should be loaded! You can now issue the connect command.', fg='green') def _upload_mettle(self, location: str) -> bool: """ Uploads Mettle to the remote filesystem. :return: """ local_mettle = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.mettle_dylib) if not os.path.exists(local_mettle): click.secho('{0} not available next to plugin file. Please build it and copy it there!'.format( self.mettle_dylib), fg='red') return False _upload_ios(local_mettle, location) return True def connect_mettle(self, args: list): if len(args) < 2: click.secho("Usage: plugin mettle connect ") return ip = args[0] port = args[1] click.secho("Connecting to {}:{}".format(ip, port), dim=True) self.api.connect_mettle(self.mettle_dylib, ip, port) namespace = 'mettle' plugin = MettleLoader ================================================ FILE: plugins/mettle/index.js ================================================ rpc.exports = { initMettle: function (dlib) { const NSDocumentDirectory = 9; const NSUserDomainMask = 1 const p = ObjC.classes.NSFileManager.defaultManager() .URLsForDirectory_inDomains_(NSDocumentDirectory, NSUserDomainMask).lastObject().path(); ObjC.schedule(ObjC.mainQueue, function () { Module.load(p + '/' + dlib); }); }, connectMettle: function(dlib, ip, port) { var source = "#include " + "char **getargs() {" + " char **argv = g_malloc(3 * sizeof(char*));" + " argv[0] = \"mettle\";" + " argv[1] = \"-u\";" + " argv[2] = \"tcp://{ip}:{port}\";" + " return argv;" + "}"; // update with the target ip:port source = source.replace("{ip}", ip); source = source.replace("{port}", port); const cm = new CModule(source); const argv = new NativeFunction(cm.getargs, 'pointer', []); const mettle = Process.getModuleByName(dlib); const mettleMainPtr = mettle.findExportByName('main'); console.log('Found mettle::main @ ' + mettleMainPtr); const mettleMain = new NativeFunction(mettleMainPtr, 'void', ['int', 'pointer']); // don't block the ui ObjC.schedule(ObjC.mainQueue, function () { console.log('Calling mettleMain()'); mettleMain(3, argv()); }); } } ================================================ FILE: plugins/stetho/README.md ================================================ # objection stetho plugin This plugin should sideload Facebook's Stetho [1], loaded as a plugin in objection. [1] [http://facebook.github.io/stetho/](http://facebook.github.io/stetho/) ================================================ FILE: plugins/stetho/__init__.py ================================================ import os import click from objection.utils.plugin import Plugin from objection.commands.filemanager import _path_exists_android, _upload_android from objection.commands.device import _get_android_environment from objection.state.connection import state_connection class StethoLoader(Plugin): """ StethoLoader loads Facebook's stetho """ def __init__(self, ns): """ Creates a new instance of the plugin :param ns: """ implementation = { 'meta': 'Work with Facebook\'s stetho', 'commands': { 'load': { 'meta': 'Load stetho', 'exec': self.load_stetho } } } super().__init__(__file__, ns, implementation) self.inject() self.stetho_jar = 'stetho.apk' def load_stetho(self, args: list): """ Loads stetho. :param args: :return: """ agent = state_connection.get_api() device_jar_path = os.path.join(agent.env_android_paths()['cacheDirectory'], self.stetho_jar) if not _path_exists_android(device_jar_path): print('Stetho not uploaded, uploading...') if not self._upload_stetho(device_jar_path): return click.secho('Asking stetho to load...', dim=True) self.api.init_stetho() def _upload_stetho(self, location: str) -> bool: """ Uploads Stetho to the remote filesystem. :return: """ local_stetho = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.stetho_jar) if not os.path.exists(local_stetho): click.secho('{0} not available next to plugin file. Please download Stetho and convert first!'.format(self.stetho_jar), fg='red') click.secho(' curl -sL https://github.com/facebook/stetho/releases/download/v1.5.1/stetho-1.5.1-fatjar.jar -O', dim=True) click.secho(' dx --dex --output="stetho.apk" stetho-1.5.1.jar', dim=True) return False _upload_android(local_stetho, location) return True namespace = 'stetho' plugin = StethoLoader ================================================ FILE: plugins/stetho/index.js ================================================ rpc.exports = { initStetho: function () { Java.perform(function () { const stethoClassName = 'com.facebook.stetho.Stetho'; const stethoJar = 'stetho.apk'; const pathClassLoader = Java.use('dalvik.system.PathClassLoader'); const javaFile = Java.use('java.io.File'); const activityThread = Java.use('android.app.ActivityThread'); const currentApplication = activityThread.currentApplication(); const context = currentApplication.getApplicationContext(); // Check if stetho is here already. console.log('Searching for stetho...'); const stethoCheck = Java.enumerateLoadedClassesSync().filter(function (e) { return e.includes('com.facebook.stetho.Stetho'); }); if (stethoCheck.length > 0) { console.log('Stetho class already loaded!'); } else { console.log('Stetho class not found, running classloader'); const packageFilesDir = context.getCacheDir().getAbsolutePath().toString(); const stethoJarDir = packageFilesDir + '/' + stethoJar; const javaStethoJarDir = javaFile.$new(stethoJarDir); if (!javaStethoJarDir.exists()) { console.log('Stetho jar is not available in cachedir at: ' + packageFilesDir); console.log('Stetho NOT successfully loaded'); return; } // https://developer.android.com/reference/dalvik/system/PathClassLoader#PathClassLoader(java.lang.String,%20java.lang.String,%20java.lang.ClassLoader) const loader = pathClassLoader.$new(javaStethoJarDir.getAbsolutePath(), null, currentApplication.getClassLoader()); console.log('Loading class ' + stethoClassName + ' using new classloader'); loader.loadClass(stethoClassName); } // Attempt to use the new class. First, search for a specific classloader to use. try { console.log('Searching for the new stetho classloader...'); const classLoaders = Java.enumerateClassLoadersSync().filter(function (l) { return l.toString().includes('stetho'); }); if (classLoaders.length != 1) { throw "No valid Stetho classloader found"; } Java.classFactory.loader = classLoaders[0]; console.log('Using the class: ' + stethoClassName); const stetho = Java.use(stethoClassName); console.log('Calling initializeWithDefaults'); stetho.initializeWithDefaults(context); } catch (err) { console.log('Failed to load by specifying the classloader with: ' + err.toString()); console.log('Trying plan B...'); try { const stetho = Java.use(stethoClassName); console.log('Calling initializeWithDefaults'); stetho.initializeWithDefaults(context); } catch (err) { console.log('Could not find stetho without specifying a classloader either (plan B). Err: ' + err.toString()); console.log('Stetho NOT successfully loaded'); return; } } console.log('\nStetho up!'); }); } } ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=70.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "objection" version = "1.12.3" description = "Instrumented Mobile Pentest Framework" readme = "README.md" requires-python = ">=3.10" license = "GPL-3.0-or-later" authors = [{name = "Leon Jacobs", email = "leon@sensepost.com"}] keywords = ["mobile", "instrumentation", "pentest", "frida", "hook"] classifiers = [ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: JavaScript", ] dependencies = [ "click>=8.2.0", "delegator-py>=0.1.1", "flask>=3.0.0", "frida>=16.0.0", "frida-tools>=10.0.0", "litecli>=1.3.0", "packaging>=23.0", "prompt-toolkit>=3.0.30,<4.0.0", "pygments>=2.0.0", "requests>=2.32.0", "semver>=2", "setuptools>=70.0.0", "tabulate>=0.9.0", ] [project.urls] Homepage = "https://github.com/sensepost/objection" Repository = "https://github.com/sensepost/objection" "Bug Tracker" = "https://github.com/sensepost/objection/issues" [project.scripts] objection = "objection.console.cli:cli" [tool.setuptools] package-dir = {"" = "."} [tool.setuptools.packages.find] include = ["objection", "objection.*"] [tool.setuptools.package-data] objection = [ "console/helpfiles/*.txt", "utils/assets/*.jks", "utils/assets/*.js", "utils/assets/*.xml", "agent.js", ] ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/commands/__init__.py ================================================ ================================================ FILE: tests/commands/android/__init__.py ================================================ ================================================ FILE: tests/commands/android/test_clipboard.py ================================================ import unittest from unittest import mock from objection.commands.android.clipboard import monitor class TestClipboard(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_monitor(self, mock_api): monitor([]) self.assertTrue(mock_api.return_value.android_monitor_clipboard.called) ================================================ FILE: tests/commands/android/test_command.py ================================================ import unittest from unittest import mock from objection.commands.android.command import execute from ...helpers import capture class TestCommand(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_execute_prints_output(self, mock_api): mock_api.return_value.android_shell_exec.return_value = { 'command': 'foo bar baz', 'stdErr': 'bazfoo', 'stdOut': 'foobar\n' } with capture(execute, ['foo', 'bar', 'baz']) as o: output = o expected_output = """Running shell command: foo bar baz foobar bazfoo """ self.assertEqual(output, expected_output) ================================================ FILE: tests/commands/android/test_heap.py ================================================ import unittest from unittest import mock from objection.commands.android.heap import instances from tests.helpers import capture class TestHeap(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_print_live_instances_validates_command(self, mock_api): with capture(instances, []) as o: output = o self.assertEqual('Usage: android heap print_instances (eg: com.example.test)\n', output) self.assertFalse(mock_api.return_value.android_heap_get_live_class_instances.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_print_live_instances_validates_command(self, mock_api): instances(['java.io.File']) self.assertTrue(mock_api.return_value.android_heap_get_live_class_instances.called) ================================================ FILE: tests/commands/android/test_hooking.py ================================================ import unittest from unittest import mock from objection.commands.android.hooking import _string_is_true, _should_dump_backtrace, _should_dump_args, \ _should_dump_return_value, show_android_classes, show_android_class_methods, \ show_registered_broadcast_receivers, show_registered_services, show_registered_activities, \ set_method_return_value, get_current_activity from ...helpers import capture class TestHooking(unittest.TestCase): def test_checks_if_string_value_is_python_boolean_true(self): result = _string_is_true('true') self.assertTrue(result) def test_checks_if_string_value_is_python_boolean_false(self): result = _string_is_true('false') self.assertFalse(result) def test_argument_includes_backtrace_flag(self): result = _should_dump_backtrace([ '--test', '--dump-backtrace' ]) self.assertTrue(result) def test_argument_dump_args_returns_true(self): result = _should_dump_args([ '--foo', '--dump-args' ]) self.assertTrue(result) def test_argument_dump_args_returns_false(self): result = _should_dump_args([ '--foo', ]) self.assertFalse(result) def test_argument_dump_return_returns_true(self): result = _should_dump_return_value([ '--foo', '--dump-return' ]) self.assertTrue(result) def test_argument_dump_return_returns_false(self): result = _should_dump_return_value([ '--foo', ]) self.assertFalse(result) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_android_classes(self, mock_api): mock_api.return_value.android_hooking_get_classes.return_value = [ 'foo', 'bar', 'baz' ] with capture(show_android_classes, []) as o: output = o expected_output = """bar baz foo Found 3 classes """ self.assertEqual(output, expected_output) def test_show_android_class_methods_validates_arguments(self): with capture(show_android_class_methods, []) as o: output = o self.assertEqual(output, 'Usage: android hooking list class_methods \n') @mock.patch('objection.state.connection.state_connection.get_api') def test_show_android_class_methods(self, mock_api): mock_api.return_value.android_hooking_get_class_methods.return_value = [ 'foo', 'bar', 'baz' ] with capture(show_android_class_methods, ['com.foo.bar']) as o: output = o expected_output = """bar baz foo Found 3 method(s) """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_registered_broadcast_receivers_handles_empty_data(self, mock_api): mock_api.return_value.android_hooking_list_broadcast_receivers.return_value = [] with capture(show_registered_broadcast_receivers, []) as o: output = o self.assertEqual(output, '\nFound 0 classes\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_show_registered_broadcast_receivers(self, mock_api): mock_api.return_value.android_hooking_list_broadcast_receivers.return_value = [ 'foo', 'bar', 'baz' ] with capture(show_registered_broadcast_receivers, []) as o: output = o expected_output = """bar baz foo Found 3 classes """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_registered_services_handles_empty_data(self, mock_api): mock_api.return_value.android_hooking_list_services.return_value = [] with capture(show_registered_services, []) as o: output = o self.assertEqual(output, '\nFound 0 classes\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_show_services(self, mock_api): mock_api.return_value.android_hooking_list_services.return_value = [ 'foo', 'bar', 'baz' ] with capture(show_registered_services, []) as o: output = o expected_output = """bar baz foo Found 3 classes """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_registered_activities_handles_empty_data(self, mock_api): mock_api.return_value.android_hooking_list_activities.return_value = [] with capture(show_registered_activities, []) as o: output = o self.assertEqual(output, '\nFound 0 classes\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_show_registered_activities(self, mock_api): mock_api.return_value.android_hooking_list_activities.return_value = [ 'foo', 'bar', 'baz' ] with capture(show_registered_activities, []) as o: output = o expected_output = """bar baz foo Found 3 classes """ self.assertEqual(output, expected_output) def test_set_method_return_value_validates_arguments(self): with capture(set_method_return_value, ['com.foo.bar']) as o: output = o self.assertEqual(output, 'Usage: android hooking set return_value ' '"" "" (eg: "com.example.test.doLogin") \n') @mock.patch('objection.state.connection.state_connection.get_api') def test_set_method_return_value(self, mock_api): set_method_return_value(['com.foo.bar', 'isValid.overload(\'bar\')', 'false']) self.assertTrue(mock_api.return_value.android_hooking_set_method_return.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_get_current_activity_and_fragment(self, mock_api): mock_api.return_value.android_hooking_get_current_activity.return_value = { 'activity': 'foo', 'fragment': 'bar', } with capture(get_current_activity, []) as o: output = o expected_output = """Activity: foo Fragment: bar """ self.assertEqual(output, expected_output) ================================================ FILE: tests/commands/android/test_intents.py ================================================ import unittest from unittest import mock from objection.commands.android.intents import launch_activity, launch_service, analyze_implicit_intents from ...helpers import capture class TestIntents(unittest.TestCase): def test_launch_activity_validates_arguments(self): with capture(launch_activity, []) as o: output = o self.assertEqual(output, 'Usage: android intent launch_activity \n') @mock.patch('objection.state.connection.state_connection.get_api') def test_launch_activity(self, mock_api): launch_activity(['com.foo.bar']) self.assertTrue(mock_api.return_value.android_intent_start_activity.called) def test_launch_service_validates_arguments(self): with capture(launch_service, []) as o: output = o self.assertEqual(output, 'Usage: android intent launch_service \n') @mock.patch('objection.state.connection.state_connection.get_api') def test_launch_service(self, mock_api): launch_service(['com.foo.bar']) self.assertTrue(mock_api.return_value.android_intent_start_service.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_analyze_implicit_intents(self, mock_api): analyze_implicit_intents([]) self.assertTrue(mock_api.return_value.android_intent_analyze.called) ================================================ FILE: tests/commands/android/test_keystore.py ================================================ import unittest from unittest import mock from objection.commands.android.keystore import entries, clear from ...helpers import capture class TestKeystore(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_entries_handles_empty_data(self, mock_api): mock_api.return_value.android_keystore_list.return_value = [] with capture(entries, []) as o: output = o expected_output = """Alias Key Certificate ------- ----- ------------- """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_entries_handles(self, mock_api): mock_api.return_value.android_keystore_list.return_value = [{ 'alias': 'test', 'is_key': True, 'is_certificate': True }] with capture(entries, []) as o: output = o expected_output = """Alias Key Certificate ------- ----- ------------- test True True """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.android.keystore.click.confirm') def test_clear(self, mock_confirm, mock_api): mock_confirm.return_value = True clear() self.assertTrue(mock_api.return_value.android_keystore_clear.called) ================================================ FILE: tests/commands/android/test_pinning.py ================================================ import unittest from unittest import mock from objection.commands.android.pinning import android_disable class TestPinning(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_pinning_disable(self, mock_api): android_disable([]) self.assertTrue(mock_api.return_value.android_ssl_pinning_disable.called) ================================================ FILE: tests/commands/android/test_root.py ================================================ import unittest from unittest import mock from objection.commands.android.root import disable, simulate class TestRoot(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_disable(self, mock_api): disable([]) self.assertTrue(mock_api.return_value.android_root_detection_disable.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_simulate(self, mock_api): simulate([]) self.assertTrue(mock_api.return_value.android_root_detection_enable.called) ================================================ FILE: tests/commands/ios/__init__.py ================================================ ================================================ FILE: tests/commands/ios/test_bundles.py ================================================ import unittest from unittest import mock from objection.commands.ios.bundles import show_frameworks, _should_include_apple_bundles, _should_print_full_path, \ _is_apple_bundle, show_bundles from ...helpers import capture class TestBundles(unittest.TestCase): def setUp(self) -> None: self.bundle_data = [ { 'bundle': 'com.apple.AppleIDSSOAuthentication', 'executable': 'AppleIDSSOAuthentication', 'path': '/AppleIDSSOAuthentication', 'version': '1.0' }, { 'bundle': 'com.apple.LinguisticData', 'executable': 'LinguisticData', 'path': '/LinguisticData/LinguisticDataLinguisticDataLinguisticDataLinguisticData', 'version': '1.0' }, { 'bundle': 'net.hockeyapp.sdk.ios', 'executable': 'hockeyapp', 'path': '/hockeyapp', 'version': '1.0' }, { 'bundle': 'za.apple.MapKit', 'executable': 'MapKit', 'path': '/MapKit', 'version': '1.0' } ] def test_should_include_apple_bundles_helper_is_true(self): data = ['foo', 'bar', '--include-apple-frameworks'] self.assertTrue(_should_include_apple_bundles(data)) def test_should_include_apple_bundles_helper_is_false(self): data = ['foo', 'bar'] self.assertFalse(_should_include_apple_bundles(data)) def test_should_print_full_path_helper_is_true(self): data = ['foo', 'bar', '--full-path'] self.assertTrue(_should_print_full_path(data)) def test_should_print_full_path_helper_is_false(self): data = ['foo', 'bar'] self.assertFalse(_should_print_full_path(data)) def test_is_apple_bunlde_returns_false_on_none(self): self.assertFalse(_is_apple_bundle(None)) def test_is_apple_bunlde_returns_true_for_apple_bundle(self): self.assertTrue(_is_apple_bundle('com.apple.PhoneNumbers')) def test_is_apple_bunlde_returns_false_for_string_not_starting_with_com_apple(self): self.assertFalse(_is_apple_bundle('za.com.apple.PhoneNumbers')) def test_is_apple_bunlde_returns_false_for_non_apple_bundle(self): self.assertFalse(_is_apple_bundle('net.hockeyapp.sdk.ios')) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_frameworks_prints_without_apple_bundles(self, mock_api): mock_api.return_value.ios_bundles_get_frameworks.return_value = self.bundle_data with capture(show_frameworks, []) as o: output = o expected = """Executable Bundle Version Path ------------ --------------------- --------- ---------- hockeyapp net.hockeyapp.sdk.ios 1 /hockeyapp MapKit za.apple.MapKit 1 /MapKit """ self.assertEqual(output, expected) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_frameworks_prints_with_apple_bundles(self, mock_api): mock_api.return_value.ios_bundles_get_frameworks.return_value = self.bundle_data with capture(show_frameworks, ['--include-apple-frameworks']) as o: output = o expected = """Executable Bundle Version Path ------------------------ ---------------------------------- --------- ------------------------------------------- AppleIDSSOAuthentication com.apple.AppleIDSSOAuthentication 1 /AppleIDSSOAuthentication LinguisticData com.apple.LinguisticData 1 ...nguisticDataLinguisticDataLinguisticData hockeyapp net.hockeyapp.sdk.ios 1 /hockeyapp MapKit za.apple.MapKit 1 /MapKit """ self.assertEqual(output, expected) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_frameworks_prints_with_apple_bundles_and_full_paths(self, mock_api): mock_api.return_value.ios_bundles_get_frameworks.return_value = self.bundle_data with capture(show_frameworks, ['--include-apple-frameworks', '--full-path']) as o: output = o expected = """Executable Bundle Version Path ------------------------ ---------------------------------- --------- ------------------------------------------------------------------------ AppleIDSSOAuthentication com.apple.AppleIDSSOAuthentication 1 /AppleIDSSOAuthentication LinguisticData com.apple.LinguisticData 1 /LinguisticData/LinguisticDataLinguisticDataLinguisticDataLinguisticData hockeyapp net.hockeyapp.sdk.ios 1 /hockeyapp MapKit za.apple.MapKit 1 /MapKit """ self.assertEqual(output, expected) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_bundles_prints_bundles(self, mock_api): mock_api.return_value.ios_bundles_get_bundles.return_value = self.bundle_data with capture(show_bundles, []) as o: output = o expected = """Executable Bundle Version Path ------------------------ ---------------------------------- --------- ------------------------------------------- AppleIDSSOAuthentication com.apple.AppleIDSSOAuthentication 1 /AppleIDSSOAuthentication LinguisticData com.apple.LinguisticData 1 ...nguisticDataLinguisticDataLinguisticData hockeyapp net.hockeyapp.sdk.ios 1 /hockeyapp MapKit za.apple.MapKit 1 /MapKit """ self.assertEqual(output, expected) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_bundles_prints_bundles(self, mock_api): mock_api.return_value.ios_bundles_get_bundles.return_value = self.bundle_data with capture(show_bundles, ['--full-path']) as o: output = o expected = """Executable Bundle Version Path ------------------------ ---------------------------------- --------- ------------------------------------------------------------------------ AppleIDSSOAuthentication com.apple.AppleIDSSOAuthentication 1 /AppleIDSSOAuthentication LinguisticData com.apple.LinguisticData 1 /LinguisticData/LinguisticDataLinguisticDataLinguisticDataLinguisticData hockeyapp net.hockeyapp.sdk.ios 1 /hockeyapp MapKit za.apple.MapKit 1 /MapKit """ self.assertEqual(output, expected) ================================================ FILE: tests/commands/ios/test_cookies.py ================================================ import unittest from unittest import mock from objection.commands.ios.cookies import get from ...helpers import capture class TestCookies(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_get_handles_empty_data(self, mock_api): mock_api.return_value.ios_cookies_get.return_value = [] with capture(get, []) as o: output = o self.assertEqual(output, 'No cookies found\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_get(self, mock_api): mock_api.return_value.ios_cookies_get.return_value = [{ 'name': 'foo', 'value': 'bar', 'expiresDate': '01-01-1970 00:00:00 +0000', 'domain': 'foo.com', 'path': '/', 'isSecure': 'false', 'isHTTPOnly': 'true' }] with capture(get, []) as o: output = o expected_output = """Name Value Expires Domain Path Secure HTTPOnly ------ ------- ------------------------- -------- ------ -------- ---------- foo bar 01-01-1970 00:00:00 +0000 foo.com / false true """ self.assertEqual(output, expected_output) ================================================ FILE: tests/commands/ios/test_hooking.py ================================================ import unittest from unittest import mock from objection.commands.ios.hooking import _should_ignore_native_classes, _should_include_parent_methods, \ _class_is_prefixed_with_native, _string_is_true, show_ios_class_methods, set_method_return_value from ...helpers import capture class TestHooking(unittest.TestCase): def test_should_ignore_native_classes_returns_true(self): result = _should_ignore_native_classes([ '--test', '--ignore-native' ]) self.assertTrue(result) def test_should_ignore_native_classes_returns_false(self): result = _should_ignore_native_classes([ '--test', ]) self.assertFalse(result) def test_should_include_parents_includes_returns_true(self): result = _should_include_parent_methods([ '--test', '--include-parents' ]) self.assertTrue(result) def test_should_include_parents_includes_returns_false(self): result = _should_include_parent_methods([ '--test', ]) self.assertFalse(result) def test_class_is_prefixed_with_native_returns_true(self): result = _class_is_prefixed_with_native('ACFoo') self.assertTrue(result) def test_class_is_prefixed_with_native_returns_false(self): result = _class_is_prefixed_with_native('FooBar') self.assertFalse(result) def test_string_is_true_returns_true(self): result = _string_is_true('true') self.assertTrue(result) def test_string_is_true_returns_false(self): result = _string_is_true('foo') self.assertFalse(result) def test_show_ios_class_methods_validates_arguments(self): with capture(show_ios_class_methods, []) as o: output = o self.assertEqual(output, 'Usage: ios hooking list class_methods (--include-parents)\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_show_ios_class_methods(self, mock_api): mock_api.return_value.ios_hooking_get_class_methods.return_value = ['foo', 'bar'] with capture(show_ios_class_methods, ['TEKeychainManager']) as o: output = o expected_output = """foo bar Found 2 methods """ self.assertEqual(output, expected_output) def test_set_method_return_value_validates_arguments(self): with capture(set_method_return_value, []) as o: output = o self.assertEqual(output, 'Usage: ios hooking set_method_return "" ' '(eg: "-[ClassName methodName:]") \n') @mock.patch('objection.state.connection.state_connection.get_api') def test_set_method_return_value(self, mock_api): set_method_return_value(['-[TEKeychainManager forData:]', 'true']) self.assertTrue(mock_api.return_value.ios_hooking_set_return_value.called) ================================================ FILE: tests/commands/ios/test_jailbreak.py ================================================ import unittest from unittest import mock from objection.commands.ios.jailbreak import disable, simulate class TestJailbreak(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_disable(self, mock_api): disable([]) self.assertTrue(mock_api.return_value.ios_jailbreak_disable.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_simulate(self, mock_api): simulate([]) self.assertTrue(mock_api.return_value.ios_jailbreak_enable.called) ================================================ FILE: tests/commands/ios/test_keychain.py ================================================ import unittest from unittest import mock from objection.commands.ios.keychain import _should_output_json, dump, dump_raw, clear, add, \ _data_flag_has_identifier, _get_flag_value, _should_do_smart_decode from ...helpers import capture class TestKeychain(unittest.TestCase): def test_should_output_json_in_arguments_returns_true(self): result = _should_output_json([ '--test', '--json' ]) self.assertTrue(result) def test_should_output_json_in_arguments_returns_false(self): result = _should_output_json([ '--test', ]) self.assertFalse(result) def test_dump_validates_arguments_if_json_output_is_wanted(self): with capture(dump, ['--json']) as o: output = o self.assertEqual(output, 'Usage: ios keychain dump (--json )\n') def test_data_flag_check_ignored_without_data_flag(self): result = _data_flag_has_identifier(['--key', 'test_key']) self.assertTrue(result) def test_data_flag_is_checked_when_flag_is_specified(self): result = _data_flag_has_identifier(['--key', 'test_key', '--data', 'foo']) self.assertFalse(result) def test_data_flag_is_checked_when_only_data_flag_is_specified_without_key(self): result = _data_flag_has_identifier(['--data', 'foo']) self.assertFalse(result) def test_should_do_smart_decode_returns_true(self): result = _should_do_smart_decode(['--json', '--smart']) self.assertTrue(result) def test_should_do_smart_decode_returns_false(self): result = _should_do_smart_decode(['--json']) self.assertFalse(result) def test_get_flag_value_gets_value_of_flag(self): result = _get_flag_value(['--key', 'test_value'], '--key') self.assertEqual(result, 'test_value') @mock.patch('objection.state.connection.state_connection.get_api') def test_dump_to_screen_handles_empty_data(self, mock_api): mock_api.return_value.keychain_list.return_value = [] with capture(dump, []) as o: output = o expected_output = """Note: You may be asked to authenticate using the devices passcode or TouchID Save the output by adding `--json keychain.json` to this command Dumping the iOS keychain... Created Accessible ACL Type Account Service Data --------- ------------ ----- ------ --------- --------- ------ """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_dump_to_screen(self, mock_api): mock_api.return_value.ios_keychain_list.return_value = [ {'account': 'foo', 'create_date': 'now', 'accessible_attribute': 'None', 'access_control': 'None', 'item_class': 'kSecClassGeneric', 'service': 'foo', 'data': 'bar'} ] with capture(dump, []) as o: output = o expected_output = """Note: You may be asked to authenticate using the devices passcode or TouchID Save the output by adding `--json keychain.json` to this command Dumping the iOS keychain... Created Accessible ACL Type Account Service Data --------- ------------ ----- ------ --------- --------- ------ now None None foo foo bar """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_dump_raw(self, mock_api): mock_api.return_value.ios_keychain_list_raw.return_value = [] with capture(dump_raw, []) as o: _ = o self.assertTrue(mock_api.return_value.ios_keychain_list_raw.called) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.ios.keychain.open', create=True) def test_dump_to_json(self, mock_open, mock_api): mock_api.return_value.ios_keychain_list.return_value = [ {'access_control': '', 'account': '', 'alias': '', 'comment': '', 'create_date': '2018-07-21 18:11:15 +0000', 'creator': '', 'custom_icon': '', 'data': 'bar', 'description': '', 'entitlement_group': '8AH3PS2AS7.za.sensepost.ipewpew', 'generic': '', 'invisible': '', 'item_class': 'genp', 'label': '', 'modification_date': '2018-07-21 18:11:15 +0000', 'negative': '', 'protected': '', 'script_code': '', 'service': 'foos', 'type': ''}] with capture(dump, ['--json', 'foo.json']) as o: output = o expected_output = """Note: You may be asked to authenticate using the devices passcode or TouchID Dumping the iOS keychain... Writing keychain as json to foo.json... Dumped keychain to: foo.json """ self.assertEqual(output, expected_output) self.assertTrue(mock_open.called) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.ios.keychain.click.confirm') def test_clear(self, mock_confirm, mock_api): mock_confirm.return_value = True with capture(clear, []) as o: output = o self.assertEqual(output, 'Clearing the keychain...\nKeychain cleared\n') self.assertTrue(mock_api.return_value.ios_keychain_empty.called) def test_adds_item_validates_data_key_to_need_identifier(self): with capture(add, ['--data', 'test_data']) as o: output = o self.assertEqual(output, 'When specifying the --data flag, either --account or --server should also be added\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_adds_item_successfully(self, mock_api): mock_api.return_value.keychain_add.return_value = True with capture(add, ['--account', 'test_key', '--data', 'test_data']) as o: output = o self.assertEqual(output, """Adding a new entry to the iOS keychain... Account: test_key Service: None Data: test_data Successfully added the keychain item\n""") @mock.patch('objection.state.connection.state_connection.get_api') def test_adds_item_with_failure(self, mock_api): mock_api.return_value.ios_keychain_add.return_value = False with capture(add, ['--service', 'test_key', '--data', 'test_data']) as o: output = o self.assertEqual(output, """Adding a new entry to the iOS keychain... Account: None Service: test_key Data: test_data Failed to add the keychain item\n""") ================================================ FILE: tests/commands/ios/test_nsurlcredentialstorage.py ================================================ import unittest from unittest import mock from objection.commands.ios.nsurlcredentialstorage import dump from ...helpers import capture class TestNsusercredentialstorage(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_dump(self, mock_api): mock_api.return_value.ios_credential_storage.return_value = [{ 'protocol': 'https', 'host': 'foo.bar', 'port': '80', 'authMethod': 'NSURLAuthenticationMethodDefault', 'user': 'foo', 'password': 'bar', }] with capture(dump, []) as o: output = o expected_output = """Protocol Host Port Authentication Method User Password ---------- ------- ------ ----------------------- ------ ---------- https foo.bar 80 Default foo bar """ self.assertEqual(output, expected_output) ================================================ FILE: tests/commands/ios/test_nsuserdefaults.py ================================================ import unittest from unittest import mock from objection.commands.ios.nsuserdefaults import get from ...helpers import capture class TestNsuserdefaults(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_get(self, mock_api): mock_api.return_value.ios_nsuser_defaults_get.return_value = 'foo' with capture(get, []) as o: output = o self.assertEqual(output, 'foo\n') ================================================ FILE: tests/commands/ios/test_pasteboard.py ================================================ import unittest from unittest import mock from objection.commands.ios.pasteboard import monitor class TestPasteboard(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_monitor(self, mock_api): monitor([]) self.assertTrue(mock_api.return_value.ios_monitor_pasteboard.called) ================================================ FILE: tests/commands/ios/test_pinning.py ================================================ import unittest from unittest import mock from objection.commands.ios.pinning import ios_disable, _should_be_quiet class TestPinning(unittest.TestCase): @mock.patch('objection.state.connection.state_connection.get_api') def test_disable(self, mock_api): ios_disable([]) self.assertTrue(mock_api.return_value.ios_pinning_disable.called) def test_should_be_quiet_returns_true(self): result = _should_be_quiet(['test', '--quiet']) self.assertTrue(result) def test_should_be_quiet_returns_false(self): result = _should_be_quiet(['test']) self.assertFalse(result) ================================================ FILE: tests/commands/ios/test_plist.py ================================================ import unittest from unittest import mock from objection.commands.ios.plist import cat from objection.state.device import device_state, Ios from ...helpers import capture class TestPlist(unittest.TestCase): def test_cat_validates_arguments(self): with capture(cat, []) as o: output = o self.assertEqual(output, 'Usage: ios plist cat \n') @mock.patch('objection.state.connection.state_connection.get_api') def test_cat_with_full_path(self, mock_api): mock_api.return_value.ios_plist_read.return_value = 'foo' with capture(cat, ['/foo']) as o: output = o self.assertEqual(output, 'foo\n') @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.ios.plist.filemanager') def test_cat_with_relative(self, mock_file_manager, mock_api): mock_file_manager.pwd.return_value = '/baz' mock_api.return_value.ios_plist_read.return_value = 'foobar' device_state.platform = Ios with capture(cat, ['foo']) as o: output = o self.assertEqual(output, 'foobar\n') ================================================ FILE: tests/commands/test_command_history.py ================================================ import unittest from unittest import mock from objection.commands.command_history import history, save, clear from objection.state.app import app_state from ..helpers import capture class TestCommandHistory(unittest.TestCase): def setUp(self): app_state.successful_commands = ['foo', 'bar'] def tearDown(self): app_state.successful_commands = [] def test_prints_command_history(self): with capture(history, []) as o: output = o expected_output = """Unique commands run in current session: foo bar """ self.assertEqual(output, expected_output) def test_save_validates_arguments(self): with capture(save, []) as o: output = o self.assertEqual(output, 'Usage: commands save \n') @mock.patch('objection.commands.command_history.open', create=True) def test_save_saves_to_file(self, mock_open): with capture(save, ['foo.rc']) as o: output = o self.assertEqual(output, 'Saved commands to: foo.rc\n') self.assertTrue(mock_open.called) def test_clear_clears_command_history(self): with capture(clear, []) as o: output = o self.assertEqual(output, 'Command history cleared.\n') self.assertEqual(len(app_state.successful_commands), 0) ================================================ FILE: tests/commands/test_device.py ================================================ import unittest from unittest import mock from objection.commands.device import get_environment, _get_ios_environment, _get_android_environment from objection.state.device import Android, Ios from ..helpers import capture class TestDevice(unittest.TestCase): @mock.patch('objection.commands.device._get_ios_environment') @mock.patch('objection.commands.device.device_state') def test_gets_environment_and_calls_ios_platform_specific_method(self, mock_device_state, mock_ios_environment): type(mock_device_state).platform = mock.PropertyMock(return_value=Ios) get_environment() self.assertTrue(mock_ios_environment.called) @mock.patch('objection.commands.device._get_android_environment') @mock.patch('objection.commands.device.device_state') def test_gets_environment_and_calls_android_platform_specific_method(self, mock_device_state, mock_android_environment): type(mock_device_state).platform = mock.PropertyMock(return_value=Android) get_environment() self.assertTrue(mock_android_environment.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_prints_ios_environment_via_platform_helpers(self, mock_api): mock_api.return_value.env_ios_paths.return_value = { 'LibraryDirectory': '/var/mobile/Containers/Data/Application/C1D04553/Library'} with capture(_get_ios_environment) as o: output = o expected_output = """ Name Path ---------------- -------------------------------------------------------- LibraryDirectory /var/mobile/Containers/Data/Application/C1D04553/Library """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_prints_android_environment_via_platform_helpers(self, mock_api): mock_api.return_value.env_android_paths.return_value = { 'packageCodePath': '/data/app/com.sensepost.apewpew-1/base.apk'} with capture(_get_android_environment) as o: output = o expected_output = """ Name Path --------------- ------------------------------------------ packageCodePath /data/app/com.sensepost.apewpew-1/base.apk """ self.assertEqual(output, expected_output) ================================================ FILE: tests/commands/test_filemanager.py ================================================ import unittest from unittest import mock from objection.commands.filemanager import cd, _path_exists_ios, _path_exists_android, pwd, pwd_print, _pwd_ios, \ _pwd_android, ls, _ls_ios, _ls_android, download, _download_ios, _download_android, upload, rm, _rm_android from objection.state.device import device_state, Ios, Android from objection.state.filemanager import file_manager_state from ..helpers import capture class TestFileManager(unittest.TestCase): def tearDown(self): file_manager_state.cwd = None def test_cd_argument_validation(self): with capture(cd, []) as o: output = o self.assertEqual(output, 'Usage: cd \n') def test_cd_to_dot_directory_does_nothing(self): file_manager_state.cwd = '/foo' cd(['.']) self.assertEqual(file_manager_state.cwd, '/foo') def test_cd_to_double_dot_moves_up_one_directory(self): file_manager_state.cwd = '/foo/bar' with capture(cd, ['..']) as o: output = o self.assertEqual(output, '/foo\n') self.assertEqual(file_manager_state.cwd, '/foo') def test_cd_to_double_dot_moves_stays_in_current_directory_if_already_root(self): file_manager_state.cwd = '/' with capture(cd, ['..']) as o: output = o self.assertEqual(output, '/\n') self.assertEqual(file_manager_state.cwd, '/') @mock.patch('objection.commands.filemanager._path_exists_ios') def test_cd_to_absoluate_ios_path(self, mock_path_exists_ios): mock_path_exists_ios.return_value = True file_manager_state.cwd = '/foo' device_state.platform = Ios with capture(cd, ['/foo/bar/baz']) as o: output = o self.assertEqual(output, '/foo/bar/baz\n') self.assertEqual(file_manager_state.cwd, '/foo/bar/baz') @mock.patch('objection.commands.filemanager._path_exists_android') def test_cd_to_absoluate_android_path(self, mock_path_exists_android): mock_path_exists_android.return_value = True file_manager_state.cwd = '/foo' device_state.platform = Android with capture(cd, ['/foo/bar/baz']) as o: output = o self.assertEqual(output, '/foo/bar/baz\n') self.assertEqual(file_manager_state.cwd, '/foo/bar/baz') @mock.patch('objection.commands.filemanager._path_exists_ios') def test_cd_to_absoluate_ios_path_that_does_not_exist(self, mock_path_exists_ios): mock_path_exists_ios.return_value = False file_manager_state.cwd = '/foo' device_state.platform = Ios with capture(cd, ['/foo/bar/baz']) as o: output = o self.assertEqual(output, 'Invalid path: `/foo/bar/baz`\n') self.assertEqual(file_manager_state.cwd, '/foo') @mock.patch('objection.commands.filemanager._path_exists_ios') def test_cd_to_relative_path_ios(self, mock_path_exists_ios): mock_path_exists_ios.return_value = True file_manager_state.cwd = '/foo' device_state.platform = Ios with capture(cd, ['bar']) as o: output = o self.assertEqual(output, '/foo/bar\n') self.assertEqual(file_manager_state.cwd, '/foo/bar') @mock.patch('objection.commands.filemanager._path_exists_android') def test_cd_to_relative_path_android(self, mock_path_exists_android): mock_path_exists_android.return_value = True file_manager_state.cwd = '/foo' device_state.platform = Android with capture(cd, ['bar']) as o: output = o self.assertEqual(output, '/foo/bar\n') self.assertEqual(file_manager_state.cwd, '/foo/bar') @mock.patch('objection.commands.filemanager._path_exists_ios') def test_cd_to_relative_path_ios_that_does_not_exist(self, mock_path_exists_ios): mock_path_exists_ios.return_value = False file_manager_state.cwd = '/foo' device_state.platform = Ios with capture(cd, ['bar']) as o: output = o self.assertEqual(output, 'Invalid path: `/foo/bar`\n') self.assertEqual(file_manager_state.cwd, '/foo') @mock.patch('objection.state.connection.state_connection.get_api') def test_ios_path_exists_helper(self, mock_api): mock_api.return_value.ios_file_exists.return_value = True self.assertTrue(_path_exists_ios('/foo/bar')) def test_rm_dispatcher_validates_arguments(self): with capture(rm, []) as o: output = o expected = 'Usage: rm \n' self.assertEqual(output, expected) @mock.patch('objection.commands.filemanager.click.confirm') @mock.patch('objection.commands.filemanager._rm_android') def test_rm_dispatcher_confirms_before_delete(self, mock_android_rm, mock_confirm): device_state.platform = Android file_manager_state.cwd = '/foo' mock_confirm.return_value = False with capture(rm, ['poo']) as o: output = o expected = 'Not deleting /foo/poo\n' self.assertEqual(output, expected) self.assertFalse(mock_android_rm.called) @mock.patch('objection.commands.filemanager.click.confirm') @mock.patch('objection.commands.filemanager._rm_android') def test_rm_dispatcher_calls_android_rm_helper(self, mock_android_rm, mock_confirm): device_state.platform = Android mock_android_rm.return_value = True mock_confirm.return_value = True rm('/poo') self.assertTrue(mock_android_rm.called) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.filemanager._path_exists_android') def test_rm_android_helper_file_exists(self, mock_exists, mock_api): mock_exists.return_value = True mock_api.return_value.android_file_delete.return_value = True with capture(_rm_android, '/poo') as o: output = o expected = '/poo successfully deleted\n' self.assertTrue(output, expected) @mock.patch('objection.state.connection.state_connection.get_api') def test_android_path_exists_helper(self, mock_api): mock_api.return_value.android_file_exists.return_value = True self.assertTrue(_path_exists_android('/foo/bar')) def test_returns_current_directory_via_helper_when_already_set(self): file_manager_state.cwd = '/foo' self.assertEqual(pwd(), '/foo') @mock.patch('objection.commands.filemanager._pwd_ios') def test_returns_current_directory_via_helper_for_ios(self, mock_pwd_ios): mock_pwd_ios.return_value = '/foo/bar' device_state.platform = Ios self.assertEqual(pwd(), '/foo/bar') self.assertTrue(mock_pwd_ios.called) @mock.patch('objection.commands.filemanager._pwd_android') def test_returns_current_directory_via_helper_for_android(self, mock_pwd_android): mock_pwd_android.return_value = '/foo/bar' device_state.platform = Android self.assertEqual(pwd(), '/foo/bar') self.assertTrue(mock_pwd_android.called) def test_prints_the_current_working_directory(self): file_manager_state.cwd = '/foo/bar/baz' with capture(pwd_print) as o: output = o self.assertEqual(output, 'Current directory: /foo/bar/baz\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_get_ios_pwd_via_helper(self, mock_api): mock_api.return_value.ios_file_cwd.return_value = '/foo/bar' self.assertEqual(_pwd_ios(), '/foo/bar') self.assertEqual(file_manager_state.cwd, '/foo/bar') @mock.patch('objection.state.connection.state_connection.get_api') def test_get_android_pwd_via_helper(self, mock_api): mock_api.return_value.android_file_cwd.return_value = '/foo/baz' self.assertEqual(_pwd_android(), '/foo/baz') self.assertEqual(file_manager_state.cwd, '/foo/baz') @mock.patch('objection.commands.filemanager.pwd') @mock.patch('objection.commands.filemanager._ls_ios') def test_ls_gets_pwd_from_helper_with_no_argument(self, _, mock_pwd): device_state.platform = Ios ls([]) self.assertTrue(mock_pwd.called) @mock.patch('objection.commands.filemanager._ls_ios') def test_ls_calls_ios_helper_method(self, mock_ls_ios): device_state.platform = Ios ls(['/foo/bar']) self.assertTrue(mock_ls_ios.called) @mock.patch('objection.commands.filemanager._ls_android') def test_ls_calls_android_helper_method(self, mock_ls_android): device_state.platform = Android ls(['/foo/bar']) self.assertTrue(mock_ls_android.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_lists_readable_ios_directory_using_helper_method(self, mock_api): mock_api.return_value.ios_file_ls.return_value = { 'path': '/foo/bar', 'readable': True, 'writable': False, 'files': { 'test': { 'fileName': 'test', 'readable': True, 'writable': False, 'attributes': { 'NSFileType': 'A', 'NSFilePosixPermissions': 'B', 'NSFileProtectionKey': 'C', 'NSFileOwnerAccountName': 'D', 'NSFileOwnerAccountID': 'E', 'NSFileGroupOwnerAccountName': 'F', 'NSFileGroupOwnerAccountID': 'G', 'NSFileSize': 123918204914, 'NSFileCreationDate': 'H' } } } } with capture(_ls_ios, ['/foo/bar']) as o: output = o expected_outut = """NSFileType Perms NSFileProtection Read Write Owner Group Size Creation Name ------------ ------- ------------------ ------ ------- ------- ------- --------- ---------- ------ A B C True False D (E) F (G) 115.4 GiB H test Readable: True Writable: False """ self.assertEqual(output, expected_outut) @mock.patch('objection.state.connection.state_connection.get_api') def test_lists_readable_ios_directory_using_helper_method_no_attributes(self, mock_api): mock_api.return_value.ios_file_ls.return_value = { 'path': '/foo/bar', 'readable': True, 'writable': True, 'files': { 'test': { 'fileName': 'test', 'readable': True, 'writable': True, 'attributes': {} } } } with capture(_ls_ios, ['/foo/bar']) as o: output = o expected_outut = """NSFileType Perms NSFileProtection Read Write Owner Group Size Creation Name ------------ ------- ------------------ ------ ------- --------- --------- ------ ---------- ------ n/a n/a n/a True True n/a (n/a) n/a (n/a) n/a n/a test Readable: True Writable: True """ self.assertEqual(output, expected_outut) @mock.patch('objection.state.connection.state_connection.get_api') def test_lists_unreadable_ios_directory_using_helper_method(self, mock_api): mock_api.return_value.ios_file_ls.return_value = { 'path': '/foo/bar', 'readable': False, 'writable': False, 'files': {} } with capture(_ls_ios, ['/foo/bar']) as o: output = o self.assertEqual(output, '\nReadable: False Writable: False\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_lists_readable_android_directory_using_helper_method(self, mock_api): mock_api.return_value.android_file_ls.return_value = { 'path': '/foo/bar', 'readable': True, 'writable': True, 'files': { 'test': { 'fileName': 'test', 'readable': True, 'writable': True, 'attributes': { 'isDirectory': False, 'isFile': True, 'isHidden': False, 'lastModified': 1507189001000, 'size': 249, } } } } with capture(_ls_android, ['/foo/bar']) as o: output = o expected_outut = """Type Last Modified Read Write Hidden Size Name ------ ----------------------- ------ ------- -------- ------- ------ File 2017-10-05 07:36:41 GMT True True False 249.0 B test Readable: True Writable: True """ self.assertEqual(output, expected_outut) @mock.patch('objection.state.connection.state_connection.get_api') def test_lists_unreadable_android_directory_using_helper_method(self, mock_api): mock_api.return_value.android_file_ls.return_value = { 'path': '/foo/bar', 'readable': False, 'writable': False, 'files': {} } with capture(_ls_android, ['/foo/bar']) as o: output = o self.assertEqual(output, '\nReadable: False Writable: False\n') def test_download_platform_proxy_validates_arguments(self): with capture(download, []) as o: output = o self.assertEqual(output, 'Usage: filesystem download (optional: )\n') @mock.patch('objection.commands.filemanager._download_ios') def test_download_platform_proxy_calls_ios_method(self, mock_download_ios): device_state.platform = Ios download(['/foo', '/bar']) self.assertTrue(mock_download_ios.called) @mock.patch('objection.commands.filemanager._download_android') def test_download_platform_proxy_calls_android_method(self, mock_download_android): device_state.platform = Android download(['/foo', '/bar']) self.assertTrue(mock_download_android.called) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.filemanager.open', create=True) def test_downloads_file_with_ios_helper(self, mock_open, mock_api): mock_api.return_value.ios_file_readable.return_value = True mock_api.return_value.ios_file_path_is_file.return_value = True mock_api.return_value.ios_file_download.return_value = {'data': b'\x00'} file_manager_state.cwd = '/foo' with capture(_download_ios, '/foo', '/bar') as o: output = o expected_output = """Downloading /foo to /bar Streaming file from device... Writing bytes to destination... Successfully downloaded /foo to /bar """ self.assertTrue(mock_open.called) self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_downloads_file_but_fails_on_unreadable_with_ios_helper(self, mock_api): mock_api.return_value.ios_file_readable.return_value = False with capture(_download_ios, '/foo', '/bar') as o: output = o self.assertEqual(output, 'Downloading /foo to /bar\nUnable to download file. File is not readable.\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_downloads_file_but_fails_on_file_type_with_ios_helper(self, mock_api): mock_api.return_value.ios_file_readable.return_value = True mock_api.return_value.ios_file_path_is_file.return_value = False with capture(_download_ios, '/foo', '/bar') as o: output = o self.assertEqual(output, 'Downloading /foo to /bar\nUnable to download file. Target path is not a file.\n') @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.filemanager.open', create=True) def test_downloads_file_with_android_helper(self, mock_open, mock_api): mock_api.return_value.android_file_readable.return_value = True mock_api.return_value.android_file_path_is_file.return_value = True mock_api.return_value.android_file_download.return_value = {'data': b'\x00'} file_manager_state.cwd = '/foo' with capture(_download_android, '/foo', '/bar') as o: output = o expected = """Downloading /foo to /bar Streaming file from device... Writing bytes to destination... Successfully downloaded /foo to /bar """ self.assertTrue(mock_open.called) self.assertEqual(output, expected) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.filemanager.open', create=True) def test_downloads_file_but_fails_on_unreadable_with_android_helper(self, mock_open, mock_api): mock_api.return_value.android_file_readable.return_value = False file_manager_state.cwd = '/foo' with capture(_download_android, '/foo', '/bar') as o: output = o self.assertFalse(mock_open.called) self.assertEqual(output, 'Downloading /foo to /bar\nUnable to download file. Target path is not readable.\n') @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.filemanager.open', create=True) def test_downloads_file_but_fails_on_file_type_with_android_helper(self, mock_open, mock_api): mock_api.return_value.android_file_readable.return_value = True mock_api.return_value.android_file_path_is_file.return_value = False file_manager_state.cwd = '/foo' with capture(_download_android, '/foo', '/bar') as o: output = o self.assertFalse(mock_open.called) self.assertEqual(output, 'Downloading /foo to /bar\nUnable to download file. Target path is not a file.\n') def test_file_upload_method_proxy_validates_arguments(self): with capture(upload, []) as o: output = o self.assertEqual(output, 'Usage: filesystem upload (optional: )\n') @mock.patch('objection.commands.filemanager._upload_ios') def test_file_upload_method_proxy_calls_ios_helper_method(self, mock_upload_ios): device_state.platform = Ios upload(['/foo', '/bar']) self.assertTrue(mock_upload_ios.called) @mock.patch('objection.commands.filemanager._upload_android') def test_file_upload_method_proxy_calls_android_helper_method(self, mock_upload_android): device_state.platform = Android upload(['/foo', '/bar']) self.assertTrue(mock_upload_android.called) ================================================ FILE: tests/commands/test_frida_commands.py ================================================ import unittest from unittest import mock from objection.commands.frida_commands import _should_disable_exception_handler, frida_environment from ..helpers import capture class TestFridaCommands(unittest.TestCase): def test_detects_no_exception_handler_argument(self): result = _should_disable_exception_handler([ '--test', '--no-exception-handler' ]) self.assertTrue(result) @mock.patch('objection.state.connection.state_connection.get_api') def test_gets_frida_environment(self, mock_api): mock_api.return_value.env_frida.return_value = { 'arch': 'x64', 'debugger': True, 'heap': 6988464, 'platform': 'darwin', 'version': '12.0.3', 'runtime': 'DUK', } with capture(frida_environment, []) as o: output = o expected_output = """-------------------- ------- Frida Version 12.0.3 Process Architecture x64 Process Platform darwin Debugger Attached True Script Runtime DUK Frida Heap Size 6.7 MiB -------------------- ------- """ self.assertEqual(output, expected_output) ================================================ FILE: tests/commands/test_jobs.py ================================================ import unittest from unittest import mock from objection.commands.jobs import show, kill from objection.state.jobs import job_manager_state from ..helpers import capture class MockJob: """ A mock job for testing purposes """ def __init__(self): self.id = '3c3c65c7-67d2-4617-8fba-b96b6d2130d7' self.started = '2017-10-14 09:21:01' self.name = 'test' self.args = ['--foo', 'bar'] def end(self): pass class TestJobs(unittest.TestCase): def setUp(self): self.mock_job = MockJob() def tearDown(self): job_manager_state.jobs = [] @mock.patch('objection.state.connection.state_connection.get_api') def test_displays_empty_jobs_message(self, mock_api): mock_api.return_value.jobs_get.return_value = [] with capture(show) as o: output = o expected_output = """Job ID Hooks Type -------- ------- ------ """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_displays_list_of_jobs(self, mock_api): mock_api.return_value.jobs_get.return_value = [ {'identifier': 'rdcjq16g8xi', 'invocations': [{}], 'type': 'ios-jailbreak-disable'}] with capture(show, []) as o: output = o expected_outut = """Job ID Hooks Type ----------- ------- --------------------- rdcjq16g8xi 1 ios-jailbreak-disable """ self.assertEqual(output, expected_outut) def test_kill_validates_arguments(self): with capture(kill, []) as o: output = o self.assertEqual(output, 'Usage: jobs kill \n') @mock.patch('objection.state.connection.state_connection.get_api') def test_cant_find_job_by_uuid(self, mock_api): kill('foo') self.assertTrue(mock_api.return_value.jobs_kill.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_kills_job_by_uuid(self, mock_api): kill('foo') self.assertTrue(mock_api.return_value.jobs_kill.called) ================================================ FILE: tests/commands/test_memory.py ================================================ import unittest from unittest import mock from objection.commands.memory import _is_string_input, dump_all, dump_from_base, list_modules, list_exports, \ find_pattern, replace_pattern from ..helpers import capture class MockRange: """ Mock Memory Rage """ def __init__(self): self.size = 100 self.base_address = 0x00008000 class TestMemory(unittest.TestCase): def test_parses_is_string_input_flag_from_arguments(self): result = _is_string_input([ '--test', '--string' ]) self.assertTrue(result) def test_dump_all_validates_arguments(self): with capture(dump_all, []) as o: output = o self.assertEqual(output, 'Usage: memory dump all \n') @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.memory.open', create=True) def test_dump_all(self, mock_open, mock_api): mock_api.return_value.memory_list_ranges.return_value = [ {'size': 100, 'base': '0x7fff90800000'} ] mock_api.return_value.memory_dump.return_value = b'\x00' with capture(dump_all, ['/foo']) as o: output = o expected_output = """Will dump 1 rw- images, totalling 100.0 B Memory dumped to file: /foo """ self.assertEqual(output, expected_output) self.assertTrue(mock_open.called) def test_dump_from_base_validates_arguments(self): with capture(dump_from_base, []) as o: output = o self.assertEqual(output, 'Usage: memory dump from_base ' ' \n') @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.memory.open', create=True) def test_dump_from_base(self, mock_open, mock_api): mock_api.return_value.memory_dump.return_value = b'\x00' with capture(dump_from_base, ['0x00008000', '200', '/foo']) as o: output = o expected_output = """Dumping 200.0 B from 0x00008000 to /foo Memory dumped to file: /foo """ self.assertEqual(output, expected_output) self.assertTrue(mock_open.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_list_modules_without_errors_without_json_flag(self, mock_api): mock_api.return_value.memory_list_modules.return_value = [{ 'name': 'test', 'base': 0x00008000, 'size': 200, 'path': '/foo' }] with capture(list_modules, []) as o: output = o expected_outut = """Save the output by adding `--json modules.json` to this command Name Base Size Path ------ ------ ------------- ------ test 32768 200 (200.0 B) /foo """ self.assertEqual(output, expected_outut) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.memory.open', create=True) def test_list_modules_without_errors_with_json_flag(self, mock_open, mock_api): mock_api.return_value.memory_list_modules.return_value = [{ 'name': 'test', 'base': 0x00008000, 'size': 200, 'path': '/foo' }] with capture(list_modules, ['--json', 'foo']) as o: output = o expected_outut = """Writing modules as json to foo... Wrote modules to: foo """ self.assertEqual(output, expected_outut) self.assertTrue(mock_open.called) def test_dump_exports_validates_arguments_without_json_flag(self): with capture(list_exports, []) as o: output = o expected = """Save the output by adding `--json exports.json` to this command Usage: memory list exports """ self.assertEqual(output, expected) def test_dump_exports_validates_arguments_with_json_flag(self): with capture(list_exports, ['--json']) as o: output = o self.assertEqual(output, 'Usage: memory list exports (--json )\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_dump_exports_without_error(self, mock_api): mock_api.return_value.memory_list_exports.return_value = [{ 'name': 'test', 'address': 0x00008000, 'type': 'function' }] with capture(list_exports, ['foo']) as o: output = o expected_outut = """Save the output by adding `--json exports.json` to this command Type Name Address -------- ------ --------- function test 32768 """ self.assertEqual(output, expected_outut) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.memory.open', create=True) def test_dump_exports_without_error_as_json(self, mock_open, mock_api): mock_api.return_value.memory_list_exports.return_value = [{ 'name': 'test', 'address': 0x00008000, 'type': 'function' }] with capture(list_exports, ['foo', '--json', 'foo']) as o: output = o expected_outut = """Writing exports as json to foo... Wrote exports to: foo """ self.assertEqual(output, expected_outut) self.assertTrue(mock_open.called) def test_find_pattern_validates_arguments(self): with capture(find_pattern, []) as o: output = o self.assertEqual(output, 'Usage: memory search "" (--string) (--offsets-only)\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_find_pattern_without_string_argument(self, mock_api): mock_api.return_value.memory_search.return_value = ['0x08000000'] with capture(find_pattern, ['41 41 41']) as o: output = o expected_output = """Searching for: 41 41 41 Pattern matched at 1 addresses """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_find_pattern_with_string_argument(self, mock_api): mock_api.return_value.memory_search.return_value = ['0x08000000'] with capture(find_pattern, ['foo-bar-baz', '--string']) as o: output = o expected_output = """Searching for: 66 6f 6f 2d 62 61 72 2d 62 61 7a Pattern matched at 1 addresses """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_find_pattern_without_string_argument_with_offets_only(self, mock_api): mock_api.return_value.memory_search.return_value = ['0x08000000'] with capture(find_pattern, ['41 41 41', '--offsets-only']) as o: output = o expected_output = """Searching for: 41 41 41 Pattern matched at 1 addresses 0x08000000 """ self.assertEqual(output, expected_output) def test_replace_pattern_validates_arguments(self): with capture(replace_pattern, []) as o: output = o self.assertEqual(output, 'Usage: memory replace "" "" (--string-pattern) (--string-replace)\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_replace_pattern_without_string_argument(self, mock_api): mock_api.return_value.memory_replace.return_value = ['0x08000000'] with capture(replace_pattern, ['41 41 41','41 42']) as o: output = o expected_output = """Searching for: 41 41 41, replacing with: 41 42 Pattern replaced at 1 addresses 0x08000000 """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_replace_pattern_with_string_argument(self, mock_api): mock_api.return_value.memory_replace.return_value = ['0x08000000'] with capture(replace_pattern, ['foo-bar-baz', '41 41', '--string-pattern']) as o: output = o expected_output = """Searching for: 66 6f 6f 2d 62 61 72 2d 62 61 7a, replacing with: 41 41 Pattern replaced at 1 addresses 0x08000000 """ self.assertEqual(output, expected_output) @mock.patch('objection.state.connection.state_connection.get_api') def test_replace_pattern_without_string_argument_with_offets_only(self, mock_api): mock_api.return_value.memory_replace.return_value = ['0x08000000'] with capture(replace_pattern, ['41 41 41', 'ABC', '--string-replace']) as o: output = o expected_output = """Searching for: 41 41 41, replacing with: ABC Pattern replaced at 1 addresses 0x08000000 """ self.assertEqual(output, expected_output) ================================================ FILE: tests/commands/test_mobile_packages.py ================================================ import unittest from unittest import mock from objection.commands.mobile_packages import patch_ios_ipa, patch_android_apk from ..helpers import capture class TestMobilePackages(unittest.TestCase): @mock.patch('objection.commands.mobile_packages.Github') @mock.patch('objection.commands.mobile_packages.IosGadget') @mock.patch('objection.commands.mobile_packages.IosPatcher') @mock.patch('objection.commands.mobile_packages.shutil') @mock.patch('objection.commands.mobile_packages.os') def test_patching_ios_ipa(self, mock_os, mock_shutil, mock_iospatcher, mock_iosgadget, mock_github): mock_github.return_value.get_latest_version.return_value = '1.0' mock_iosgadget.return_value.get_local_version.return_value = '0.9' mock_iospatcher.return_value.are_requirements_met.return_value = True mock_iospatcher.return_value.get_patched_ipa_path.return_value = '/foo/ipa' with capture(patch_ios_ipa, 'test.ipa', '00-11', '/foo', '', False, False) as o: output = o expected_output = """Using latest Github gadget version: 1.0 Remote FridaGadget version is v1.0, local is v0.9. Downloading... Patcher will be using Gadget version: 1.0 Copying final ipa from /foo/ipa to current directory... """ self.assertEqual(output, expected_output) self.assertTrue(mock_shutil.copyfile.called) self.assertTrue(mock_os.path.join.called) self.assertTrue(mock_os.path.abspath.called) self.assertTrue(mock_os.path.basename.called) @mock.patch('objection.commands.mobile_packages.Github') @mock.patch('objection.commands.mobile_packages.AndroidGadget') @mock.patch('objection.commands.mobile_packages.AndroidPatcher') @mock.patch('objection.commands.mobile_packages.shutil') @mock.patch('objection.commands.mobile_packages.os') @mock.patch('objection.commands.mobile_packages.delegator') @mock.patch('objection.commands.mobile_packages.input', create=True) def test_patching_android_apk(self, mock_input, mock_delegator, mock_os, mock_shutil, mock_androidpatcher, mock_androidgadget, mock_github): mock_github.return_value.get_latest_version.return_value = '1.0' mock_androidgadget.return_value.get_local_version.return_value = '0.9' mock_androidpatcher.return_value.are_requirements_met.return_value = True mock_androidpatcher.return_value.get_temp_working_directory.return_value = '/foo/apk' mock_androidpatcher.return_value.get_patched_apk_path.return_value = '/foo/bar/apk' mock_delegator_output = mock.Mock() type(mock_delegator_output).out = 'x86' mock_delegator.run.return_value = mock_delegator_output mock_input.return_value = '' with capture(patch_android_apk, 'test.apk', '', True, False) as o: output = o expected_output = """No architecture specified. Determining it using `adb`... Detected target device architecture as: x86 Using latest Github gadget version: 1.0 Remote FridaGadget version is v1.0, local is v0.9. Downloading... Patcher will be using Gadget version: 1.0 Patching paused. The next step is to rebuild the APK. If you require any manual fixes, the current temp directory is: /foo/apk Copying final apk from /foo/bar/apk to test.objection.apk in current directory... """ self.assertEqual(output, expected_output) self.assertTrue(mock_shutil.copyfile.called) self.assertTrue(mock_os.path.join.called) self.assertTrue(mock_os.path.abspath.called) ================================================ FILE: tests/commands/test_plugin_manager.py ================================================ import os import unittest from unittest import mock from objection.commands.plugin_manager import load_plugin from ..helpers import capture class TestPluginManager(unittest.TestCase): def setUp(self): self.plugin_path = os.path.abspath(os.path.dirname(__file__) + '/../data/plugin') def test_load_plugin_validates_arguments(self): with capture(load_plugin, []) as o: output = o expected_output = 'Usage: plugin load ()\n' self.assertEqual(output, expected_output) @mock.patch('objection.commands.plugin_manager.os.path.exists') def test_load_plugin_validates_plugin_init_exists(self, mock_exists): mock_exists.return_value = False with capture(load_plugin, [self.plugin_path]) as o: output = o self.assertTrue('tests/data/plugin does not appear to be a valid plugin. Missing __init__.py' in output) @mock.patch('objection.utils.plugin.state_connection') def test_load_plugin_loads_plugin(self, mock_state_connection): with capture(load_plugin, [self.plugin_path]) as o: output = o from objection.console import commands self.assertTrue(commands.COMMANDS['plugin']['commands']['version']['commands']['info'] ['meta'] == 'Get the current Frida version') self.assertEqual('Loaded plugin: version\n', output) self.assertTrue(mock_state_connection.get_agent.called) ================================================ FILE: tests/commands/test_ui.py ================================================ import unittest from unittest import mock from objection.commands.ui import alert, _alert_ios, ios_screenshot, dump_ios_ui, bypass_touchid, android_screenshot, \ android_flag_secure from objection.state.device import device_state, Ios from ..helpers import capture class TestUI(unittest.TestCase): def tearDown(self): device_state.platform = None @mock.patch('objection.commands.ui._alert_ios') def test_alert_helper_method_proxy_calls_ios(self, mock_alert_ios): device_state.platform = Ios() alert([]) self.assertTrue(mock_alert_ios.called_with('objection!')) @mock.patch('objection.commands.ui._alert_ios') def test_alert_helper_method_proxy_calls_ios_custom_message(self, mock_alert_ios): device_state.platform = Ios() alert(['foo']) self.assertTrue(mock_alert_ios.called_with('foo')) @mock.patch('objection.state.connection.state_connection.get_api') def test_alert_ios_helper_method(self, mock_api): _alert_ios('foo') self.assertTrue(mock_api.return_value.ios_ui_alert.called) def test_ios_screenshot_validates_arguments(self): with capture(ios_screenshot, []) as o: output = o self.assertTrue(output, 'Usage: ios ui screenshot \n') @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.ui.open', create=True) def test_ios_screenshot(self, mock_open, mock_api): mock_api.return_value.ios_ui_screenshot.return_value = b'\x00' with capture(ios_screenshot, ['foo']) as o: output = o self.assertTrue(output, 'Screenshot saved to: foo.png\n') self.assertTrue(mock_open.called) @mock.patch('objection.state.connection.state_connection.get_api') def test_dump_ios_ui(self, mock_api): mock_api.return_value.ios_ui_window_dump.return_value = 'test_ui' with capture(dump_ios_ui, []) as o: output = o self.assertTrue(output, 'test_ui\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_bypass_touchid(self, mock_api): bypass_touchid() self.assertTrue(mock_api.return_value.ios_ui_biometrics_bypass.called) def test_android_screenshot_validates_arguments(self): with capture(android_screenshot, []) as o: output = o self.assertEqual(output, 'Usage: android ui screenshot \n') @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.ui.open', create=True) def test_android_screenshot(self, mock_open, mock_api): mock_api.return_value.android_ui_screenshot.return_value = b'\x00' with capture(android_screenshot, ['foo']) as o: output = o self.assertTrue(output, 'Screenshot saved to: foo.png\n') self.assertTrue(mock_open.called) def test_android_flag_secure_validates_argument_as_boolean_string(self): with capture(android_flag_secure, ['foo']) as o: output = o self.assertEqual(output, 'Usage: android ui FLAG_SECURE \n') def test_android_flag_secure_validates_argument_is_present(self): with capture(android_flag_secure, []) as o: output = o self.assertEqual(output, 'Usage: android ui FLAG_SECURE \n') @mock.patch('objection.state.connection.state_connection.get_api') def test_android_flag_secure(self, mock_api): android_flag_secure(['true']) self.assertTrue(mock_api.return_value.android_ui_set_flag_secure.called) ================================================ FILE: tests/console/__init__.py ================================================ ================================================ FILE: tests/console/test_cli.py ================================================ import unittest from unittest import mock from click.testing import CliRunner from objection.__init__ import __version__ from objection.console.cli import version, patchipa, patchapk class TestsCommandLineInteractions(unittest.TestCase): def test_version(self): runner = CliRunner() result = runner.invoke(version) self.assertIsNone(result.exception) self.assertEqual(result.exit_code, 0) self.assertEqual(result.output, 'objection: ' + __version__ + '\n') @mock.patch('objection.console.cli.patch_android_apk') def test_patchapk_runs_with_minimal_cli_arguments(self, _): runner = CliRunner() result = runner.invoke(patchapk, ['--source', 'foo.apk']) self.assertIsNone(result.exception) self.assertEqual(result.exit_code, 0) @mock.patch('objection.console.cli.patch_android_apk') def test_patchapk_runs_with_all_cli_arguments(self, _): runner = CliRunner() result = runner.invoke(patchapk, [ '--source', 'foo.apk', '--architecture', 'x86', '--pause', '--skip-resources', '--network-security-config', '--skip-cleanup', '--enable-debug', ]) self.assertIsNone(result.exception) self.assertEqual(result.exit_code, 0) def test_patchapk_fails_and_wants_source(self): runner = CliRunner() result = runner.invoke(patchapk) self.assertIsNotNone(result.exception) self.assertEqual(result.exit_code, 2) @mock.patch('objection.console.cli.patch_ios_ipa') def test_patchipa_runs_with_source_and_codesignature(self, _): runner = CliRunner() result = runner.invoke(patchipa, ['--source', 'foo.ipa', '--codesign-signature', 'bar']) self.assertIsNone(result.exception) self.assertEqual(result.exit_code, 0) @mock.patch('objection.console.cli.patch_ios_ipa') def test_patchipa_runs_with_all_cli_arguments(self, _): runner = CliRunner() result = runner.invoke(patchipa, [ '--source', 'foo.ipa', '--codesign-signature', 'bar', '--provision-file', 'baz.mobileprovision', '--binary-name', 'zet', '--skip-cleanup' ]) self.assertIsNone(result.exception) self.assertEqual(result.exit_code, 0) def test_patchipa_fails_and_wants_source(self): runner = CliRunner() result = runner.invoke(patchipa, ['--codesign-signature', 'foo']) self.assertIsNotNone(result.exception) self.assertEqual(result.exit_code, 2) def test_patchipa_fails_and_wants_codesign_signature(self): runner = CliRunner() result = runner.invoke(patchipa, ['--source', 'foo']) self.assertIsNotNone(result.exception) self.assertEqual(result.exit_code, 2) ================================================ FILE: tests/console/test_completer.py ================================================ import unittest from prompt_toolkit.document import Document from objection.console.completer import CommandCompleter class TestConsoleCommandCompletion(unittest.TestCase): def setUp(self): self.command_completer = CommandCompleter() def test_can_find_command_completion(self): document = Document('android hooking list ', 21) completions = self.command_completer.find_completions(document) self.assertEqual(type(completions), dict) self.assertEqual(completions['activities']['meta'], 'List the registered Activities') def test_will_have_empty_dict_for_invalid_command(self): document = Document('android hooking list fruitcakes ', 30) completions = self.command_completer.find_completions(document) self.assertEqual(type(completions), dict) self.assertEqual(len(completions), 0) ================================================ FILE: tests/console/test_repl.py ================================================ import unittest from unittest import mock from objection.commands.android.hooking import show_registered_activities from objection.console.repl import Repl from ..helpers import capture class TestRepl(unittest.TestCase): def setUp(self): self.repl = Repl() def test_does_nothing_when_empty_command_is_passed(self): with capture(self.repl.run_command, '') as output: self.assertEqual('', output) def test_does_nothing_when_only_spaces_as_command_is_passed(self): with capture(self.repl.run_command, ' ') as o: output = o self.assertEqual(output, '') @mock.patch('objection.console.repl.delegator.run') def test_runs_os_command_when_prefixed_with_excalmation_mark(self, patched_delegator): patched_delegator.return_value = mock.MagicMock(out=b'out_test', err=b'err_test') with capture(self.repl.run_command, '!id') as o: output = o expected_output = ('Running OS command: id\n' '\n' 'out_test\n' 'err_test\n') self.assertEqual(output, expected_output) def test_finds_help_when_prefixed_with_help_command(self): with capture(self.repl.run_command, 'help android') as o: output = o expected_output = ('Contains subcommands to work with Android specific features. These include\n' 'shell commands, bypassing SSL pinning and simulating a rooted environment.\n' '\n') self.assertEqual(output, expected_output) def test_fails_to_find_help_for_invalid_command(self): with capture(self.repl.run_command, 'help what') as o: output = o expected_output = ('No help found for: what. Either the command ' 'does not exist or contains subcommands with help.\n') self.assertEqual(output, expected_output) def test_fails_when_invalid_command_is_passed(self): with capture(self.repl.run_command, 'android wont do this') as o: output = o expected_outut = 'Unknown or ambiguous command: `android wont do this`. Try `help android wont do this`.\n' self.assertEqual(output, expected_outut) def test_is_able_to_find_an_executable_method_to_run_with_tokens_passed(self): walk_count, method = self.repl._find_command_exec_method(['android', 'hooking', 'list', 'activities']) self.assertEqual(walk_count, 4) self.assertEqual(method, show_registered_activities) def test_will_fail_to_find_exec_method_with_invalid_tokens(self): walk_count, method = self.repl._find_command_exec_method(['android', 'hooking', 'list', 'invalid']) self.assertEqual(walk_count, 4) self.assertIsNone(method) def test_is_able_to_locate_nested_helpfile_contents(self): help_file = self.repl._find_command_help(['ios', 'keychain', 'clear']) expected_output = ('Command: ios keychain clear\n' '\n' 'Usage: ios keychain clear\n' '\n' 'Clears all the keychain items for the current application. This is achieved by\n' 'iterating over the keychain type classes available in iOS and populating a search\n' 'dictionary with them. This dictionary is then used as a query to SecItemDelete(),\n' 'deleting the entries.\n' 'Items that will be deleted include everything stored with the entitlement group used\n' 'during the patching/signing process.\n' '\n' 'Examples:\n' ' ios keychain clear\n') self.assertEqual(help_file, expected_output) @mock.patch('objection.console.repl.PromptSession') @mock.patch('objection.console.repl.Repl.run_command') def test_runs_commands_and_catches_exceptions(self, prompt, run_command): prompt.return_value.prompt.return_value = 'ios keychain clear' run_command.side_effect = TypeError() self.assertRaises(TypeError) ================================================ FILE: tests/data/plugin/__init__.py ================================================ __description__ = "An example plugin, also used in a UnitTest" from objection.utils.plugin import Plugin s = """ rpc.exports = { getInformation: function() { console.log('hello from Frida'); // direct output send('Incoming message'); // output via send for 'message' signal return Frida.version; // return type } } """ class VersionInfo(Plugin): """ VersionInfo is a sample plugin to get Frida version information """ def __init__(self, ns): """ Creates a new instance of the plugin :param ns: """ self.script_src = s # self.script_path = os.path.join(os.path.dirname(__file__), "script.js") implementation = { 'meta': 'Work with Frida version information', 'commands': { 'info': { 'meta': 'Get the current Frida version', 'exec': self.version } } } super().__init__(__file__, ns, implementation) self.inject() def version(self, args: list): """ Tests a plugin by calling an RPC export method called getInformation, and printing the result. :param args: :return: """ v = self.api.get_information() print('Frida version: {0}'.format(v)) namespace = 'version' plugin = VersionInfo ================================================ FILE: tests/helpers.py ================================================ import sys from contextlib import contextmanager from io import StringIO # http://schinckel.net/2013/04/15/capture-and-test-sys.stdout-sys.stderr-in-unittest.testcase/ @contextmanager def capture(command, *args, **kwargs): out, sys.stdout = sys.stdout, StringIO() try: command(*args, **kwargs) sys.stdout.seek(0) yield sys.stdout.read() finally: sys.stdout = out ================================================ FILE: tests/state/__init__.py ================================================ ================================================ FILE: tests/state/test_app.py ================================================ import unittest from objection.state.app import app_state class TestApp(unittest.TestCase): def tearDown(self): app_state.debug_hooks = False app_state.successful_commands = [] def test_app_should_not_debug_hooks_by_default(self): self.assertFalse(app_state.should_debug_hooks()) def test_app_should_debug_hooks_if_true(self): app_state.debug_hooks = True self.assertTrue(app_state.should_debug_hooks()) def test_adds_command_to_history(self): app_state.add_command_to_history('foo') self.assertEqual(len(app_state.successful_commands), 1) self.assertEqual(app_state.successful_commands[0], 'foo') def test_clears_command_history(self): app_state.successful_commands = ['foo', 'bar'] app_state.clear_command_history() self.assertEqual(len(app_state.successful_commands), 0) ================================================ FILE: tests/state/test_jobs.py ================================================ import unittest from objection.state.jobs import job_manager_state class TestJobManager(unittest.TestCase): def tearDown(self): job_manager_state.jobs = [] def test_job_manager_starts_with_empty_jobs(self): self.assertEqual(len(job_manager_state.jobs), 0) def test_adds_jobs(self): job_manager_state.add_job('foo') self.assertEqual(len(job_manager_state.jobs), 1) def test_removes_jobs(self): job_manager_state.add_job('foo') job_manager_state.add_job('bar') job_manager_state.remove_job('foo') job_manager_state.remove_job('bar') self.assertEqual(len(job_manager_state.jobs), 0) ================================================ FILE: tests/utils/__init__.py ================================================ ================================================ FILE: tests/utils/patchers/__init__.py ================================================ ================================================ FILE: tests/utils/patchers/test_android.py ================================================ import os import unittest from unittest import mock from objection.utils.patchers.android import AndroidGadget, AndroidPatcher class TestAndroidGadget(unittest.TestCase): @mock.patch('objection.utils.patchers.android.Github') @mock.patch('objection.utils.patchers.android.os') def setUp(self, github, mock_os): mock_os.path.exists.return_value = True self.android_gadget = AndroidGadget(github) self.github_get_assets_sample = [ { "url": "https://api.github.com/repos/frida/frida/releases/assets/5005221", "id": 5005221, "name": "frida-gadget-10.6.8-android-x86.so.xz", "label": "", "uploader": { "id": 735197, }, "state": "uploaded", "size": 12912624, "download_count": 1, "created_at": "2017-10-07T00:01:10Z", "updated_at": "2017-10-07T00:01:17Z", "browser_download_url": "https://github.com/frida/frida/releases/download/" "10.6.8/frida-gadget-10.6.8-android-x86.so.xz" } ] def test_sets_architecture(self): self.android_gadget.set_architecture('x86') self.assertEqual(self.android_gadget.architecture, 'x86') def test_raises_exception_with_invalid_architecture(self): with self.assertRaises(Exception) as _: self.android_gadget.set_architecture('foo') def test_sets_architecture_and_returns_context(self): result = self.android_gadget.set_architecture('x86') self.assertEqual(type(result), AndroidGadget) def test_gets_architecture_when_set(self): self.android_gadget.set_architecture('x86') architecture = self.android_gadget.get_architecture() self.assertEqual(architecture, 'x86') def test_gets_frida_library_path(self): self.android_gadget.set_architecture('x86') frida_path = self.android_gadget.get_frida_library_path() self.assertTrue('.objection/android/x86/libfrida-gadget.so' in frida_path) def test_fails_to_get_frida_library_path_without_architecture(self): with self.assertRaises(Exception) as _: self.android_gadget.get_frida_library_path() @mock.patch('objection.utils.patchers.android.os') def test_checks_if_gadget_exists_if_it_really_exists(self, mock_os): mock_os.path.exists.return_value = True self.android_gadget.set_architecture('x86') status = self.android_gadget.gadget_exists() self.assertTrue(status) @mock.patch('objection.utils.patchers.android.os') def test_checks_if_gadget_exists_if_it_really_does_not_exist(self, mock_os): mock_os.path.exists.return_value = False self.android_gadget.set_architecture('x86') status = self.android_gadget.gadget_exists() self.assertFalse(status) def test_check_if_gadget_exists_fails_without_architecture(self): with self.assertRaises(Exception) as _: self.android_gadget.gadget_exists() def test_can_find_download_url_for_gadget(self): mock_github = mock.MagicMock() mock_github.get_assets.return_value = self.github_get_assets_sample self.android_gadget.github = mock_github self.android_gadget.architecture = 'x86' # the method we actually testing here! url = self.android_gadget._get_download_url() self.assertEqual(url, 'https://github.com/frida/frida/releases/download/' '10.6.8/frida-gadget-10.6.8-android-x86.so.xz') def test_throws_exception_when_download_url_could_not_be_determined(self): mock_github = mock.MagicMock() mock_github.get_assets.return_value = self.github_get_assets_sample self.android_gadget.github = mock_github self.android_gadget.architecture = 'arm' # the method we actually testing here! with self.assertRaises(Exception) as _: self.android_gadget._get_download_url() class TestAndroidPatcher(unittest.TestCase): @mock.patch('objection.utils.patchers.android.BasePlatformPatcher.__init__', mock.Mock(return_value=None)) @mock.patch('objection.utils.patchers.android.AndroidPatcher.__del__', mock.Mock(return_value=None)) @mock.patch('objection.utils.patchers.android.tempfile') def test_inits_patcher(self, tempfile): tempfile.mkdtemp.return_value = '/tmp/test' patcher = AndroidPatcher() self.assertIsNone(patcher.apk_source) self.assertEqual(patcher.apk_temp_directory, '/tmp/test') self.assertEqual(patcher.apk_temp_frida_patched, '/tmp/test.objection.apk') self.assertFalse(patcher.skip_cleanup) self.assertTrue('objection/utils/patchers/../assets/objection.jks' in patcher.keystore) self.assertTrue(os.path.exists(patcher.keystore)) @mock.patch('objection.utils.patchers.android.AndroidPatcher.__init__', mock.Mock(return_value=None)) @mock.patch('objection.utils.patchers.android.AndroidPatcher.__del__', mock.Mock(return_value=None)) @mock.patch('objection.utils.patchers.android.tempfile') @mock.patch('objection.utils.patchers.android.os') def test_set_android_apk_source(self, _, mock_os): mock_os.path.exists.return_value = True patcher = AndroidPatcher() source = patcher.set_apk_source('foo.apk') self.assertEqual(type(source), AndroidPatcher) self.assertEqual(patcher.apk_source, 'foo.apk') ================================================ FILE: tests/utils/patchers/test_base.py ================================================ import unittest from unittest import mock from objection.utils.patchers.base import BasePlatformGadget, BasePlatformPatcher from ...helpers import capture class TestBasePlatformGadget(unittest.TestCase): @mock.patch('objection.utils.patchers.base.Github') def setUp(self, mock_github): self.gadget = BasePlatformGadget(github=mock_github) @mock.patch('objection.utils.patchers.base.os') def test_sets_version_to_zero_if_no_local_record_is_found(self, mock_os): mock_os.path.exists.return_value = False version = self.gadget.get_local_version('test') self.assertEqual(version, '0') class TestBasePlatformPatcher(unittest.TestCase): def setUp(self): pass @mock.patch('objection.utils.patchers.base.BasePlatformPatcher._check_commands', mock.Mock(return_value=True)) def test_inits_base_patcher(self): base_patcher = BasePlatformPatcher() self.assertTrue(base_patcher.have_all_commands) self.assertEqual(base_patcher.command_run_timeout, 300) @mock.patch('objection.utils.patchers.base.BasePlatformPatcher._check_commands', mock.Mock(return_value=True)) def test_are_requirements_met_returns_true_if_met(self): base_patcher = BasePlatformPatcher() self.assertTrue(base_patcher.are_requirements_met()) @mock.patch('objection.utils.patchers.base.BasePlatformPatcher._check_commands', mock.Mock(return_value=False)) def test_are_requirements_met_returns_false_if_not_met(self): base_patcher = BasePlatformPatcher() self.assertFalse(base_patcher.are_requirements_met()) @mock.patch('objection.utils.patchers.base.BasePlatformPatcher.__init__', mock.Mock(return_value=None)) @mock.patch('objection.utils.patchers.base.shutil') def test_check_commands_finds_commands_and_sets_location(self, mock_shutil): mock_shutil.which.return_value = '/bin/test' base_patcher = BasePlatformPatcher() base_patcher.required_commands = { 'aapt': { 'installation': 'apt install aapt (Kali Linux)' } } check_result = base_patcher._check_commands() self.assertTrue(check_result) self.assertEqual(base_patcher.required_commands['aapt']['location'], '/bin/test') @mock.patch('objection.utils.patchers.base.BasePlatformPatcher.__init__', mock.Mock(return_value=None)) @mock.patch('objection.utils.patchers.base.shutil') def test_check_commands_fails_to_find_command_and_displays_error(self, mock_shutil): mock_shutil.which.return_value = None base_patcher = BasePlatformPatcher() base_patcher.required_commands = { 'aapt': { 'installation': 'apt install aapt (Kali Linux)' } } with capture(base_patcher._check_commands) as o: output = o self.assertEqual(output, 'Unable to find aapt. Install it with:' ' apt install aapt (Kali Linux) before continuing.\n') ================================================ FILE: tests/utils/patchers/test_github.py ================================================ import unittest from unittest import mock from objection.utils.patchers.github import Github class TestGithub(unittest.TestCase): def setUp(self): self.github = Github() self.mock_response = { "tag_name": "10.6.9", "target_commitish": "master", "name": "Frida 10.6.9", "created_at": "2017-10-09T23:52:02Z", "published_at": "2017-10-10T00:02:48Z", "assets": [ { "url": "https://api.github.com/repos/frida/frida/releases/assets/5024320", "name": "frida-core-devkit-10.6.9-android-arm.tar.xz", "label": "", "updated_at": "2017-10-10T00:13:36Z", "browser_download_url": "https://github.com/frida/frida/releases/download/" "10.6.9/frida-core-devkit-10.6.9-android-arm.tar.xz" }, ], "tarball_url": "https://api.github.com/repos/frida/frida/tarball/10.6.9", "zipball_url": "https://api.github.com/repos/frida/frida/zipball/10.6.9", "body": "See http://www.frida.re/news/ for details." } @mock.patch('objection.utils.patchers.github.requests') def test_makes_call_and_stores_result_in_cache(self, mock_requests): mock_response = mock.Mock() mock_response.status_code = 200 mock_response.json.return_value = self.mock_response mock_requests.get.return_value = mock_response result = self.github._call('/test') self.assertEqual(result, self.mock_response) self.assertEqual(len(self.github.request_cache), 1) @mock.patch('objection.utils.patchers.github.requests') def test_makes_call_and_stores_result_in_cache_and_fetches_next_from_cache(self, mock_requests): mock_response = mock.Mock() mock_response.status_code = 200 mock_response.json.return_value = self.mock_response mock_requests.get.return_value = mock_response self.github._call('/test') # entry is now stored in cache, update the next response object # and make the request again. mock_response = mock.Mock() mock_response.status_code = 200 mock_response.json.return_value = {'other'} mock_requests.get.return_value = mock_response result = self.github._call('/test') self.assertEqual(result, self.mock_response) @mock.patch('objection.utils.patchers.github.requests') def test_makes_call_and_gets_latest_version(self, mock_requests): mock_response = mock.Mock() mock_response.status_code = 200 mock_response.json.return_value = self.mock_response mock_requests.get.return_value = mock_response result = self.github.get_latest_version() self.assertEqual(result, self.mock_response['tag_name']) @mock.patch('objection.utils.patchers.github.requests') def test_makes_call_and_fails_to_get_assets(self, mock_requests): mock_response = mock.Mock() mock_response.status_code = 404 mock_response.json.return_value = {} mock_requests.get.return_value = mock_response with self.assertRaises(Exception) as _: self.github.get_assets() @mock.patch('objection.utils.patchers.github.requests') def test_makes_call_and_gets_assets(self, mock_requests): mock_response = mock.Mock() mock_response.status_code = 200 mock_response.json.return_value = self.mock_response mock_requests.get.return_value = mock_response result = self.github.get_assets() self.assertEqual(result, self.mock_response['assets']) ================================================ FILE: tests/utils/patchers/test_ios.py ================================================ import unittest from unittest import mock from objection.utils.patchers.ios import IosGadget, IosPatcher class TestIosGadget(unittest.TestCase): @mock.patch('objection.utils.patchers.ios.Github') @mock.patch('objection.utils.patchers.android.os') def setUp(self, mock_github, mock_os): mock_os.path.exists.return_value = True self.ios_gadget = IosGadget(github=mock_github) self.github_get_assets_sample = [ { "url": "https://api.github.com/repos/frida/frida/releases/assets/5005221", "id": 5005221, "name": "frida-gadget-10.6.8-ios-universal.dylib.xz", "label": "", "uploader": { "id": 735197, }, "state": "uploaded", "size": 12912624, "download_count": 1, "created_at": "2017-10-07T00:01:10Z", "updated_at": "2017-10-07T00:01:17Z", "browser_download_url": "https://github.com/frida/frida/releases/download/" "frida-gadget-10.6.8-ios-universal.dylib.xz" } ] def test_gets_gadget_path(self): self.ios_gadget.ios_dylib_gadget_path = '/tmp/foo' result = self.ios_gadget.get_gadget_path() self.assertEqual(result, '/tmp/foo') @mock.patch('objection.utils.patchers.ios.os') def test_checks_if_gadget_exists(self, mock_os): mock_os.path.exists.return_value = True result = self.ios_gadget.gadget_exists() self.assertTrue(result) def test_can_find_asset_download_url(self): mock_github = mock.MagicMock() mock_github.get_assets.return_value = self.github_get_assets_sample self.ios_gadget.github = mock_github result = self.ios_gadget._get_download_url() self.assertEqual(result, 'https://github.com/frida/frida/releases/download/' 'frida-gadget-10.6.8-ios-universal.dylib.xz') class TestIosPatcher(unittest.TestCase): @mock.patch('objection.utils.patchers.ios.IosPatcher.__init__', mock.Mock(return_value=None)) @mock.patch('objection.utils.patchers.ios.IosPatcher.__del__', mock.Mock(return_value=None)) @mock.patch('objection.utils.patchers.ios.click.secho', mock.Mock(return_value=None)) def test_sets_provisioning_profile(self): patcher = IosPatcher() patcher.set_provsioning_profile('profile.mobileprovision', 'com.foo.bar') self.assertEqual(patcher.provision_file, 'profile.mobileprovision') ================================================ FILE: tests/utils/test_helpers.py ================================================ import unittest from objection.state.device import device_state, Ios from objection.utils.helpers import clean_argument_flags from objection.utils.helpers import get_tokens from objection.utils.helpers import pretty_concat from objection.utils.helpers import print_frida_connection_help from objection.utils.helpers import sizeof_fmt from objection.utils.helpers import warn_about_older_operating_systems from ..helpers import capture class TestHelpers(unittest.TestCase): def test_pretty_concat_with_less_than_seventy_five_chars(self): result = pretty_concat('test') self.assertEqual(result, 'test') def test_pretty_concat_with_more_than_max_chars(self): result = pretty_concat('testing', 5) self.assertEqual(result, 'testi...') def test_pretty_concat_with_more_than_max_chars_to_the_left(self): result = pretty_concat('testing', 5, left=True) self.assertEqual(result, '...sting') def test_sizeof_formats_values(self): result = sizeof_fmt(3000) self.assertEqual(result, '2.9 KiB') def test_gets_tokens_without_quotes(self): result = get_tokens('this is a test') self.assertEqual(result, ['this', 'is', 'a', 'test']) def test_gets_tokens_with_quotes(self): result = get_tokens('this is "a test"') self.assertEqual(result, ['this', 'is', 'a test']) def test_gets_tokens_and_handles_missing_quotes(self): result = get_tokens('this is "a test') self.assertEqual(result, ['lajfhlaksjdfhlaskjfhafsdlkjh']) def test_cleans_argument_lists_with_flags(self): result = clean_argument_flags(['foo', '--bar']) self.assertEqual(result, ['foo']) def test_prints_frida_connection_help(self): with capture(print_frida_connection_help) as o: output = o expected_output = """If you are using a rooted/jailbroken device, specify a process with the --gadget flag. Eg: objection --gadget "Calendar" explore If you are using a non rooted/jailbroken device, ensure that your patched application is running and in the foreground. If you have multiple devices, specify the target device with --serial. A list of attached device serials can be found with the frida-ls-devices command. For more information, please refer to the objection wiki at: https://github.com/sensepost/objection/wiki """ self.assertEqual(output, expected_output)