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.
[](https://twitter.com/leonjza)
[](https://pypi.python.org/pypi/objection)
[](https://www.blackhat.com/eu-17/arsenal-overview.html)
[](https://www.blackhat.com/us-19/arsenal-overview.html)
- 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