├── tests ├── __init__.py ├── state │ ├── __init__.py │ ├── test_jobs.py │ └── test_app.py ├── utils │ ├── __init__.py │ ├── patchers │ │ ├── __init__.py │ │ └── test_ios.py │ └── test_helpers.py ├── commands │ ├── __init__.py │ ├── ios │ │ ├── __init__.py │ │ ├── test_pasteboard.py │ │ ├── test_nsuserdefaults.py │ │ ├── test_jailbreak.py │ │ ├── test_pinning.py │ │ ├── test_nsurlcredentialstorage.py │ │ ├── test_plist.py │ │ └── test_cookies.py │ ├── android │ │ ├── __init__.py │ │ ├── test_clipboard.py │ │ ├── test_pinning.py │ │ ├── test_root.py │ │ ├── test_command.py │ │ ├── test_heap.py │ │ ├── test_intents.py │ │ └── test_keystore.py │ ├── test_frida_commands.py │ ├── test_command_history.py │ ├── test_plugin_manager.py │ ├── test_jobs.py │ └── test_device.py ├── console │ ├── __init__.py │ └── test_completer.py ├── helpers.py └── data │ └── plugin │ └── __init__.py ├── .python-version ├── objection ├── api │ ├── __init__.py │ ├── app.py │ ├── script.py │ └── rpc.py ├── state │ ├── __init__.py │ ├── filemanager.py │ ├── device.py │ ├── api.py │ ├── app.py │ └── connection.py ├── commands │ ├── __init__.py │ ├── android │ │ ├── __init__.py │ │ ├── clipboard.py │ │ ├── general.py │ │ ├── proxy.py │ │ ├── monitor.py │ │ ├── root.py │ │ ├── pinning.py │ │ ├── command.py │ │ ├── generate.py │ │ ├── intents.py │ │ └── keystore.py │ ├── ios │ │ ├── __init__.py │ │ ├── monitor.py │ │ ├── pasteboard.py │ │ ├── nsuserdefaults.py │ │ ├── jailbreak.py │ │ ├── pinning.py │ │ ├── nsurlcredentialstorage.py │ │ ├── plist.py │ │ ├── binary.py │ │ ├── cookies.py │ │ ├── generate.py │ │ └── bundles.py │ ├── http.py │ ├── command_history.py │ ├── device.py │ ├── custom.py │ ├── plugin_manager.py │ ├── jobs.py │ └── frida_commands.py ├── console │ ├── __init__.py │ └── helpfiles │ │ ├── memory.dump.txt │ │ ├── ios.bundles.txt │ │ ├── ios.plist.txt │ │ ├── plugin.txt │ │ ├── android.hooking.txt │ │ ├── android.intent.txt │ │ ├── exit.txt │ │ ├── ios.cookies.txt │ │ ├── ios.hooking.set.txt │ │ ├── ios.keychain.txt │ │ ├── memory.list.txt │ │ ├── android.keystore.txt │ │ ├── file.txt │ │ ├── ios.pasteboard.txt │ │ ├── android.clipboard.txt │ │ ├── ui.txt │ │ ├── ios.nsuserdefaults.txt │ │ ├── ios.sslpinning.txt │ │ ├── android.sslpinning.txt │ │ ├── ios.hooking.search.txt │ │ ├── pwd.txt │ │ ├── android.hooking.list.txt │ │ ├── android.hooking.search.txt │ │ ├── sqlite.execute.txt │ │ ├── ios.hooking.watch.txt │ │ ├── jobs.txt │ │ ├── android.hooking.watch.txt │ │ ├── ios.hooking.list.txt │ │ ├── ios.jailbreak.txt │ │ ├── pwd.print.txt │ │ ├── ios.txt │ │ ├── android.txt │ │ ├── rm.txt │ │ ├── ios.ui.txt │ │ ├── frida.txt │ │ ├── sqlite.execute.schema.txt │ │ ├── ios.hooking.txt │ │ ├── ios.ui.dump.txt │ │ ├── android.hooking.watch.class.txt │ │ ├── memory.txt │ │ ├── sqlite.status.txt │ │ ├── jobs.kill.txt │ │ ├── ios.ui.alert.txt │ │ ├── reconnect.txt │ │ ├── sqlite.sync.txt │ │ ├── android.intent.implicit_intents.txt │ │ ├── ios.nsuserdefaults.get.txt │ │ ├── android.hooking.list.classes.txt │ │ ├── android.hooking.list.services.txt │ │ ├── memory.dump.all.txt │ │ ├── android.hooking.list.receivers.txt │ │ ├── android.keystore.detail.txt │ │ ├── android.root.simulate.txt │ │ ├── ios.ui.screenshot.txt │ │ ├── android.keystore.clear.txt │ │ ├── ios.plist.cat.txt │ │ ├── ios.hooking.search.classes.txt │ │ ├── android.clipboard.monitor.txt │ │ ├── ios.pasteboard.monitor.txt │ │ ├── !.txt │ │ ├── android.hooking.search.classes.txt │ │ ├── ios.hooking.search.methods.txt │ │ ├── sqlite.txt │ │ ├── ios.hooking.list.classes.txt │ │ ├── android.keystore.list.txt │ │ ├── env.txt │ │ ├── ios.jailbreak.simulate.txt │ │ ├── android.hooking.list.activities.txt │ │ ├── jobs.list.txt │ │ ├── android.shell_exec.txt │ │ ├── android.ui.screenshot.txt │ │ ├── sqlite.disconnect.txt │ │ ├── ios.cookies.get.txt │ │ ├── ls.txt │ │ ├── android.hooking.list.class_methods.txt │ │ ├── sqlite.execute.query.txt │ │ ├── ios.hooking.list.class_methods.txt │ │ ├── ios.keychain.add.txt │ │ ├── file.download.txt │ │ ├── ios.monitor.crypto.txt │ │ ├── memory.write.txt │ │ ├── memory.list.modules.txt │ │ ├── ios.jailbreak.disable.txt │ │ ├── android.keystore.watch.txt │ │ ├── ui.alert.txt │ │ ├── android.intent.launch_service.txt │ │ ├── ios.keychain.clear.txt │ │ ├── ios.hooking.watch.class.txt │ │ ├── ios.hooking.set.return_value.txt │ │ ├── cd.txt │ │ ├── android.ui.FLAG_SECURE.txt │ │ ├── memory.list.exports.txt │ │ ├── android.hooking.search.methods.txt │ │ ├── android.heap.search.instances.txt │ │ ├── android.intent.launch_activity.txt │ │ ├── ios.bundles.list_bundles.txt │ │ ├── sqlite.connect.txt │ │ ├── file.upload.txt │ │ ├── memory.dump.from_base.txt │ │ ├── plugin.load.txt │ │ ├── ios.ui.touchid_bypass.txt │ │ ├── ios.hooking.watch.method.txt │ │ ├── android.root.disable.txt │ │ ├── ios.sslpinning.disable.txt │ │ ├── import.txt │ │ ├── memory.search.txt │ │ ├── android.hooking.set.return_value.txt │ │ ├── ios.bundles.list_frameworks.txt │ │ ├── ios.keychain.dump.txt │ │ ├── android.hooking.watch.class_method.txt │ │ └── android.sslpinning.disable.txt ├── utils │ ├── patchers │ │ ├── __init__.py │ │ └── github.py │ ├── assets │ │ ├── objection.jks │ │ └── network_security_config.xml │ └── __init__.py └── __init__.py ├── agent ├── .gitignore ├── src │ ├── generic │ │ ├── ping.ts │ │ ├── custom.ts │ │ └── memory.ts │ ├── lib │ │ ├── constants.ts │ │ ├── interfaces.ts │ │ ├── color.ts │ │ └── helpers.ts │ ├── rpc │ │ ├── jobs.ts │ │ ├── other.ts │ │ ├── environment.ts │ │ └── memory.ts │ ├── android │ │ ├── general.ts │ │ ├── monitor.ts │ │ ├── proxy.ts │ │ ├── lib │ │ │ ├── types.ts │ │ │ ├── libjava.ts │ │ │ ├── interfaces.ts │ │ │ └── intentUtils.ts │ │ ├── clipboard.ts │ │ └── shell.ts │ ├── index.ts │ └── ios │ │ ├── nsuserdefaults.ts │ │ ├── plist.ts │ │ ├── lib │ │ ├── types.ts │ │ └── interfaces.ts │ │ ├── pasteboard.ts │ │ ├── binarycookies.ts │ │ ├── binary.ts │ │ ├── bundles.ts │ │ └── credentialstorage.ts ├── tslint.json ├── tsconfig.json ├── README.md └── package.json ├── MANIFEST.in ├── .vscode └── settings.json ├── images ├── api.png ├── ios_ls.png ├── android_ls.png ├── frida_logo.png ├── objection.png ├── ios_keychain.png ├── sqlite_example.png ├── ios_ssl_pinning_bypass.png └── android_ssl_pinning_bypass.png ├── plugins ├── api │ ├── index.js │ └── __init__.py ├── README.md ├── flex │ ├── libFlex.h │ ├── libFlex.m │ ├── README.md │ ├── index.js │ └── __init__.py ├── stetho │ ├── README.md │ └── __init__.py └── mettle │ ├── README.md │ ├── index.js │ └── __init__.py ├── Makefile ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── pypi.yml │ └── codeql-analysis.yml ├── .gitignore ├── pyproject.toml ├── README.md └── CONTRIBUTING.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/state/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.14 2 | -------------------------------------------------------------------------------- /objection/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /objection/state/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/console/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /objection/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /objection/console/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/commands/ios/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/utils/patchers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /objection/commands/android/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /objection/commands/ios/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /objection/utils/patchers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/commands/android/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } 4 | -------------------------------------------------------------------------------- /agent/src/generic/ping.ts: -------------------------------------------------------------------------------- 1 | export const ping = (): boolean => true; 2 | -------------------------------------------------------------------------------- /images/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/images/api.png -------------------------------------------------------------------------------- /images/ios_ls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/images/ios_ls.png -------------------------------------------------------------------------------- /objection/console/helpfiles/memory.dump.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to dump process memory 2 | -------------------------------------------------------------------------------- /images/android_ls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/images/android_ls.png -------------------------------------------------------------------------------- /images/frida_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/images/frida_logo.png -------------------------------------------------------------------------------- /images/objection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/images/objection.png -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.bundles.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with iOS bundles. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.plist.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with iOS Plist entries. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/plugin.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with objection plugins. 2 | -------------------------------------------------------------------------------- /images/ios_keychain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/images/ios_keychain.png -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to hook Android Java methods. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.intent.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with Android Intents. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/exit.txt: -------------------------------------------------------------------------------- 1 | Performs cleanups operations and quits the objection REPL. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.cookies.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with iOS shared cookies. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.set.txt: -------------------------------------------------------------------------------- 1 | Sets various bits of hooking related information. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.keychain.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with the iOS keychain. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/memory.list.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to list modules and module exports 2 | -------------------------------------------------------------------------------- /images/sqlite_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/images/sqlite_example.png -------------------------------------------------------------------------------- /objection/console/helpfiles/android.keystore.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with Android KeyStore. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/file.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with files on the remote filesystem 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.pasteboard.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with the iOS pasteboard. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.clipboard.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with the Android Clipboard. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ui.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands that interact with the applications user interface. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.nsuserdefaults.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with the iOS NSUserDefaults class. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.sslpinning.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with iOS SSL pinning related calls. 2 | -------------------------------------------------------------------------------- /plugins/api/index.js: -------------------------------------------------------------------------------- 1 | rpc.exports = { 2 | getVersion: function () { 3 | return Frida.version; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /images/ios_ssl_pinning_bypass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/images/ios_ssl_pinning_bypass.png -------------------------------------------------------------------------------- /objection/console/helpfiles/android.sslpinning.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with Android SSL pinning related calls. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.search.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands helpful when searching for classes and methods. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/pwd.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with the current working directory 2 | on the device. 3 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.list.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to list various bits of Java class information. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.search.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands helpful when searching for classes and methods. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/sqlite.execute.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to execute queries against a connected SQLite database. 2 | -------------------------------------------------------------------------------- /objection/utils/assets/objection.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/objection/utils/assets/objection.jks -------------------------------------------------------------------------------- /images/android_ssl_pinning_bypass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensepost/objection/HEAD/images/android_ssl_pinning_bypass.png -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.watch.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to watch for method invocations on Objective-C classes. 2 | -------------------------------------------------------------------------------- /objection/console/helpfiles/jobs.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with objection jobs. This includes listing and killing them. 2 | -------------------------------------------------------------------------------- /agent/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export enum DeviceType { 2 | IOS = "ios", 3 | ANDROID = "android", 4 | UNKNOWN = "unknown", 5 | } 6 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.watch.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to watch for various bits of information on class invocations. 2 | -------------------------------------------------------------------------------- /agent/src/generic/custom.ts: -------------------------------------------------------------------------------- 1 | export const evaluate = (js: string): void => { 2 | // tslint:disable-next-line:no-eval 3 | eval(js); 4 | }; 5 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.list.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to list various bits of information, such as 2 | Objective-C classes and class methods. 3 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.jailbreak.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with iOS Jailbreak detection, such as disabling 2 | it, or simulating that a device is Jailbroken. 3 | -------------------------------------------------------------------------------- /objection/console/helpfiles/pwd.print.txt: -------------------------------------------------------------------------------- 1 | Command: pwd print 2 | 3 | Usage: pwd print 4 | 5 | Display the current working directory. 6 | 7 | Examples: 8 | pwd print 9 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with iOS specific features. These include features 2 | such as keychain dumping, reading plists and bypassing SSL pinning. 3 | -------------------------------------------------------------------------------- /agent/src/rpc/jobs.ts: -------------------------------------------------------------------------------- 1 | import * as j from "../lib/jobs.js"; 2 | 3 | export const jobs = { 4 | // jobs 5 | jobsGet: () => j.all(), 6 | jobsKill: (ident: number) => j.kill(ident), 7 | }; 8 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with Android specific features. These include 2 | shell commands, bypassing SSL pinning and simulating a rooted environment. 3 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # plugins 2 | 3 | `objection` has the ability to sideload external plugins. This directory contains a few sample plugins that you could use to kickstart developing your own! 4 | -------------------------------------------------------------------------------- /plugins/flex/libFlex.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface libFlex : NSObject 4 | 5 | - (id)init; 6 | - (void)logSomething:(NSString *)something; 7 | - (void)flexUp; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /objection/console/helpfiles/rm.txt: -------------------------------------------------------------------------------- 1 | Command: rm 2 | 3 | Usage: rm 4 | 5 | Delete a file on the remote operating system. 6 | 7 | Examples: 8 | rm file.txt 9 | rm /path/to/file.png 10 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.ui.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to interact with the iOS user interface. This includes commands 2 | to dump the current view hierarchy as well as bypassing screens that require TouchID 3 | to proceed. 4 | -------------------------------------------------------------------------------- /plugins/stetho/README.md: -------------------------------------------------------------------------------- 1 | # objection stetho plugin 2 | 3 | This plugin should sideload Facebook's Stetho [1], loaded as a plugin in objection. 4 | 5 | [1] [http://facebook.github.io/stetho/](http://facebook.github.io/stetho/) 6 | -------------------------------------------------------------------------------- /objection/console/helpfiles/frida.txt: -------------------------------------------------------------------------------- 1 | Command: frida 2 | 3 | Usage: frida 4 | 5 | Displays information about Frida. This includes the version of the Frida gadget, 6 | process architecture and platform. 7 | 8 | Examples: 9 | frida 10 | -------------------------------------------------------------------------------- /objection/console/helpfiles/sqlite.execute.schema.txt: -------------------------------------------------------------------------------- 1 | Command: sqlite execute schema 2 | 3 | Usage: sqlite execute schema 4 | 5 | Get the database schema for the currently connected SQLite database. 6 | 7 | Examples: 8 | sqlite execute schema 9 | -------------------------------------------------------------------------------- /objection/state/filemanager.py: -------------------------------------------------------------------------------- 1 | class FileManagerState(object): 2 | """ A class representing the state of the filemanager. """ 3 | 4 | def __init__(self) -> None: 5 | self.cwd = None 6 | 7 | 8 | file_manager_state = FileManagerState() 9 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands helpful when developing custom hooks. This includes discovery 2 | of Objective-C classes and methods in those classes, as well as dumping method 3 | arguments as they are called in real time. 4 | -------------------------------------------------------------------------------- /agent/src/android/general.ts: -------------------------------------------------------------------------------- 1 | import { 2 | wrapJavaPerform, 3 | Java 4 | } from "./lib/libjava.js"; 5 | 6 | export const deoptimize = (): Promise => { 7 | return wrapJavaPerform(() => { 8 | Java.deoptimizeEverything(); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /agent/src/android/monitor.ts: -------------------------------------------------------------------------------- 1 | import { wrapJavaPerform } from "./lib/libjava.js"; 2 | 3 | export namespace monitor { 4 | export const stringCanary = (can: string): Promise => { 5 | return wrapJavaPerform(() => { 6 | 7 | }); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.ui.dump.txt: -------------------------------------------------------------------------------- 1 | Command: ios ui dump 2 | 3 | Usage: ios ui dump 4 | 5 | Dumps the current, serialized user interface. This is useful to see which values 6 | or classes may be attached to UI elements. 7 | 8 | Examples: 9 | ios dump 10 | -------------------------------------------------------------------------------- /agent/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "no-namespace": false 9 | }, 10 | "rulesDirectory": [], 11 | "alwaysShowStatus": true 12 | } -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.watch.class.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking watch class 2 | 3 | Usage: android hooking watch class 4 | Hooks a specified class' methods and reports on invocations. 5 | 6 | Examples: 7 | android hooking watch class com.example.test 8 | -------------------------------------------------------------------------------- /objection/console/helpfiles/memory.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with memory within the current process. 2 | Examples include commands to dump the current process memory, dump 3 | the memory of a specific loaded module, list exported modules or 4 | write raw bytes to memory addresses. 5 | -------------------------------------------------------------------------------- /objection/console/helpfiles/sqlite.status.txt: -------------------------------------------------------------------------------- 1 | Command: sqlite status 2 | 3 | Usage: sqlite status 4 | 5 | Check the status of the SQLite connection. Outputs the the locally cached 6 | location as well as the remote source it was cached from. 7 | 8 | Examples: 9 | sqlite status 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DIST_DIR := dist 2 | 3 | default: clean frida-agent sdist 4 | 5 | clean: 6 | $(RM) $(DIST_DIR)/* 7 | 8 | frida-agent: 9 | cd agent && npm run build 10 | 11 | sdist: 12 | uv build 13 | 14 | testupload: 15 | uv publish --index testpypi 16 | 17 | upload: 18 | uv publish 19 | -------------------------------------------------------------------------------- /objection/console/helpfiles/jobs.kill.txt: -------------------------------------------------------------------------------- 1 | Command: jobs kill 2 | 3 | Usage: jobs kill 4 | 5 | Kills a running job identified by its UUID. When a job is killed, objection will 6 | unload the Fridascript from the process' memory. 7 | Examples: 8 | jobs kill 9415c4c7-2824-46a5-8539-d2d35ba2158c 9 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.ui.alert.txt: -------------------------------------------------------------------------------- 1 | Command: ios ui alert 2 | 3 | Usage: ios ui alert (optional: "") 4 | 5 | Displays an alert popup on an iOS device. A message to display may be specified 6 | optionally. 7 | 8 | Examples: 9 | ios ui alert 10 | ios ui alert 'my message' 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/reconnect.txt: -------------------------------------------------------------------------------- 1 | Command: reconnect 2 | 3 | Usage: reconnect 4 | 5 | Attempts to reconnect to the Frida Gadget specified with --gadget on startup. 6 | The connection mode (ie: usb / network) can not be changed unless the repl 7 | is restarted. 8 | 9 | Examples: 10 | reconnect 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/sqlite.sync.txt: -------------------------------------------------------------------------------- 1 | Command: sqlite sync 2 | 3 | Usage: sqlite sync 4 | 5 | Sync the locally cached SQLite database with the remote database. 6 | Any changes made since the last `sqlite connect` will be available on the 7 | device post-sync. 8 | 9 | Examples: 10 | sqlite sync 11 | -------------------------------------------------------------------------------- /agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["es2020"], 5 | "allowJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "module": "Node16", 9 | "esModuleInterop": true, 10 | "noImplicitAny": false, 11 | "strictNullChecks": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.intent.implicit_intents.txt: -------------------------------------------------------------------------------- 1 | Command: android intent implicit_intents 2 | 3 | Usage: android intent implicit_intents 4 | 5 | Starts a hook to analyze implicit intents during runtime. Optionally add a backtrade by adding --dump-backtrace 6 | 7 | Examples: 8 | android intent implicit_intents 9 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.nsuserdefaults.get.txt: -------------------------------------------------------------------------------- 1 | Command: ios nsuserdefaults get 2 | 3 | Usage: ios nsuserdefaults get 4 | 5 | Queries the applications NSUserDefaults class for all of the entries in 6 | the current application bundle and echoes the entries to screen. 7 | 8 | Examples: 9 | ios nsuserdefaults get 10 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.list.classes.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking list classes 2 | 3 | Usage: android hooking list classes 4 | 5 | List the classes *currently loaded*. As the target application gets used 6 | more, this command will return more classes. 7 | 8 | Examples: 9 | android hooking list classes 10 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.list.services.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking list services 2 | 3 | Usage: android hooking list services 4 | 5 | List all the Services that have been registered at Runtime as well 6 | as those specified by the AndroidManifest.xml. 7 | 8 | Examples: 9 | android hooking list services 10 | -------------------------------------------------------------------------------- /objection/console/helpfiles/memory.dump.all.txt: -------------------------------------------------------------------------------- 1 | Command: memory dump all 2 | 3 | Usage: memory dump all 4 | 5 | Dumps all of the current processes' memory that is marked as readable and 6 | writable (rw-) to a file specified by local destination. 7 | 8 | Examples: 9 | memory dump all process_memory.dmp 10 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.list.receivers.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking list receivers 2 | 3 | Usage: android hooking list receivers 4 | 5 | List all the Broadcast Receivers that have been registered at Runtime 6 | as well as those specified by the AndroidManifest.xml. 7 | 8 | Examples: 9 | android hooking list receivers 10 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.keystore.detail.txt: -------------------------------------------------------------------------------- 1 | Command: android keystore detail 2 | 3 | Usage: android keystore detail 4 | 5 | Lists detailed 'AndroidKeyStore' items for the current application. 6 | 7 | Ref: https://developer.android.com/reference/java/security/KeyStore.html 8 | 9 | Examples: 10 | android keystore listDetails 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.root.simulate.txt: -------------------------------------------------------------------------------- 1 | Command: android root simulate 2 | 3 | Usage: android root simulate 4 | 5 | Attempts to simulate a rooted Android environment. This is achieved by 6 | responding positively to common checks that are performed within Android 7 | applications. 8 | 9 | Examples: 10 | android root simulate 11 | 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.ui.screenshot.txt: -------------------------------------------------------------------------------- 1 | Command: ios ui screenshot 2 | 3 | Usage: ios ui screenshot 4 | 5 | Screenshots the current foregrounded UIView and saves it as a PNG locally. 6 | Note: Does not work at the moment, may actually need a jailbroken device. 7 | 8 | Examples: 9 | ios ui screenshot screenshot.png 10 | -------------------------------------------------------------------------------- /objection/commands/ios/monitor.py: -------------------------------------------------------------------------------- 1 | from objection.state.connection import state_connection 2 | 3 | 4 | def crypto_enable(args: list = None) -> None: 5 | """ 6 | Attempts to enable ios crypto monitoring. 7 | 8 | :param args: 9 | :return: 10 | """ 11 | 12 | api = state_connection.get_api() 13 | api.ios_monitor_crypto_enable() 14 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.keystore.clear.txt: -------------------------------------------------------------------------------- 1 | Command: android keystore clear 2 | 3 | Usage: android keystore clear 4 | 5 | Clears all aliases in the current applications 'AndroidKeyStore' Keystore. The 6 | KeyStore is loaded, and each alias entry found gets the deleteEntry() method 7 | on the KeyStore called. 8 | 9 | Examples: 10 | android keystore clear 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.plist.cat.txt: -------------------------------------------------------------------------------- 1 | Command: ios plist cat 2 | 3 | Usage: ios plist cat 4 | 5 | Parses and echoes a plist file on the remote iOS device to screen. If this 6 | parsing is not sufficient, one can always `download` the plist file itself 7 | for parsing using other tools. 8 | 9 | Examples: 10 | ios plist cat Info.plist 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.search.classes.txt: -------------------------------------------------------------------------------- 1 | Command: ios hooking search classes 2 | 3 | Usage: ios hooking search classes 4 | 5 | Search for classes in the current Objective-C runtime with the search string 6 | as part of the class name. 7 | 8 | Examples: 9 | ios hooking search classes jailbreak 10 | ios hooking search classes sslpinning 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.clipboard.monitor.txt: -------------------------------------------------------------------------------- 1 | Command: android clipboard monitor 2 | 3 | Usage: android clipboard monitor 4 | 5 | Gets a handle on the Android clipboard service and polls it every 5 seconds 6 | for data. If new data is found, different from the previous poll, that data 7 | will be dumped to screen. 8 | 9 | Examples: 10 | android clipboard monitor 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.pasteboard.monitor.txt: -------------------------------------------------------------------------------- 1 | Command: ios pasteboard monitor 2 | 3 | Usage: ios pasteboard monitor 4 | 5 | Hooks into the iOS UIPasteboard class and polls the generalPasteboard every 6 | 5 seconds for data. If new data is found, different from the previous poll, 7 | that data will be dumped to screen. 8 | 9 | Examples: 10 | ios pasteboard monitor 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/!.txt: -------------------------------------------------------------------------------- 1 | Command: ! 2 | 3 | Usage: ! 4 | 5 | Executes operating system commands using pythons Subprocess module. 6 | Commands that have caused an error, or when there is output to 7 | display from stderr, will show in red. Commands that have output 8 | that was sent to stdout will display in white. 9 | 10 | Examples: 11 | !ls 12 | !uname -a 13 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.search.classes.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking search classes 2 | 3 | Usage: android hooking search classes 4 | 5 | Search for classes in the current Java runtime with the search string 6 | as part of the class name. 7 | 8 | Examples: 9 | android hooking search classes jailbreak 10 | android hooking search classes sslpinning 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.search.methods.txt: -------------------------------------------------------------------------------- 1 | Command: ios hooking search methods 2 | 3 | Usage: ios hooking search methods 4 | 5 | Search for methods in classes in the current Objective-C runtime with the 6 | search string as part of the method name. 7 | 8 | Examples: 9 | ios hooking search methods keychain 10 | ios hooking search methods sslpinning 11 | -------------------------------------------------------------------------------- /objection/api/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from . import rpc 4 | from . import script 5 | 6 | 7 | def create_app() -> Flask: 8 | """ 9 | Creates a new Flask instance for the objection API 10 | 11 | :return: 12 | """ 13 | 14 | app = Flask(__name__) 15 | app.register_blueprint(rpc.bp) 16 | app.register_blueprint(script.bp) 17 | 18 | return app 19 | -------------------------------------------------------------------------------- /objection/console/helpfiles/sqlite.txt: -------------------------------------------------------------------------------- 1 | Contains subcommands to work with SQLite databases on the remote device. 2 | Connecting to a SQLite database will result in a copy of the database from 3 | the remote device being downloaded locally. All queries that are run will 4 | be run on the locally cached database. If the changes need to be available 5 | on the remote device, the database should be `sync`'ed back. 6 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.list.classes.txt: -------------------------------------------------------------------------------- 1 | Command: ios hooking list classes 2 | 3 | Usage: ios hooking list classes (optional: --ignore-native) 4 | 5 | Lists all of the classes in the current Objective-C runtime. Specifying 6 | the --ignore-native flag, filters out classes with common prefixes such as 7 | 'NS' and 'CF'. 8 | 9 | Examples: 10 | ios hooking list classes --ignore-native 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.keystore.list.txt: -------------------------------------------------------------------------------- 1 | Command: android keystore list 2 | 3 | Usage: android keystore list 4 | 5 | Lists aliases in the current applications 'AndroidKeyStore' KeyStore. Each alias 6 | is queried for its type which will be either a certificate or a key. 7 | 8 | Ref: https://developer.android.com/reference/java/security/KeyStore.html 9 | 10 | Examples: 11 | android keystore list 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/env.txt: -------------------------------------------------------------------------------- 1 | Command: env 2 | 3 | Usage: env 4 | 5 | Display directory information for the current application environment. 6 | 7 | On iOS devices, this includes the location of the applications bundle, 8 | the Documents/ and Library/ directory. 9 | 10 | On Android devices, this includes the location of the files and cache 11 | directories to name a few. 12 | 13 | Examples: 14 | env 15 | -------------------------------------------------------------------------------- /agent/src/rpc/other.ts: -------------------------------------------------------------------------------- 1 | import * as custom from "../generic/custom.js"; 2 | import * as http from "../generic/http.js"; 3 | 4 | export const other = { 5 | evaluate: (js: string): void => custom.evaluate(js), 6 | 7 | // http server 8 | httpServerStart: (p: string, port: number): void => http.start(p, port), 9 | httpServerStatus: (): void => http.status(), 10 | httpServerStop: (): void => http.stop(), 11 | }; 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.jailbreak.simulate.txt: -------------------------------------------------------------------------------- 1 | Command: ios jailbreak simulate 2 | 3 | Usage: ios jailbreak simulate 4 | 5 | Attempts to simulate a Jailbroken iOS environment. This is achieved by returning 6 | positive results for file existence checks from NSFileManager fileExistsAtPath 7 | as well as indicating that a fork() was successful if that is called. 8 | 9 | Examples: 10 | ios jailbreak simulate 11 | -------------------------------------------------------------------------------- /objection/commands/ios/pasteboard.py: -------------------------------------------------------------------------------- 1 | from objection.state.connection import state_connection 2 | 3 | 4 | def monitor(args: list = None) -> None: 5 | """ 6 | Starts a new objection job that monitors the iOS pasteboard 7 | and reports on new strings found. 8 | 9 | :param args: 10 | :return: 11 | """ 12 | 13 | api = state_connection.get_api() 14 | api.ios_monitor_pasteboard() 15 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.list.activities.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking list activities 2 | 3 | Usage: android hooking list activities 4 | 5 | List all the Activities that have been specified by the AndroidManifest.xml. 6 | Activity classes found using this command could be used with the 7 | `android intent launch_activity` command to launch them. 8 | 9 | Examples: 10 | android hooking list activities 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/jobs.list.txt: -------------------------------------------------------------------------------- 1 | Command: jobs list 2 | 3 | Usage: jobs list 4 | 5 | List the currently running jobs. Jobs are asynchronous Fridascripts that were 6 | submitted and have not yet been unloaded from the process. Examples of such 7 | jobs include the iOS method argument dumper and pasteboard monitor. To unload 8 | a job, the `jobs kill ` command may be used. 9 | 10 | Examples: 11 | jobs list 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.shell_exec.txt: -------------------------------------------------------------------------------- 1 | Command: android shell_exec 2 | 3 | Usage: android shell_exec 4 | 5 | Execute a shell command on an android device. These commands are run from within 6 | the security context of the application that is being instrumented. 7 | 8 | Examples: 9 | android shell_exec id 10 | android shell_exec ls -lah / 11 | android shell_exec rm /data/data/user/0/somefile 12 | -------------------------------------------------------------------------------- /objection/utils/assets/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/commands/ios/test_pasteboard.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.ios.pasteboard import monitor 5 | 6 | 7 | class TestPasteboard(unittest.TestCase): 8 | @mock.patch('objection.state.connection.state_connection.get_api') 9 | def test_monitor(self, mock_api): 10 | monitor([]) 11 | 12 | self.assertTrue(mock_api.return_value.ios_monitor_pasteboard.called) 13 | -------------------------------------------------------------------------------- /objection/commands/android/clipboard.py: -------------------------------------------------------------------------------- 1 | from objection.state.connection import state_connection 2 | 3 | 4 | def monitor(args: list = None) -> None: 5 | """ 6 | Starts a new objection job that monitors the Android clipboard 7 | and reports on new strings found. 8 | 9 | :param args: 10 | :return: 11 | """ 12 | 13 | api = state_connection.get_api() 14 | api.android_monitor_clipboard() 15 | -------------------------------------------------------------------------------- /tests/commands/android/test_clipboard.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.android.clipboard import monitor 5 | 6 | 7 | class TestClipboard(unittest.TestCase): 8 | @mock.patch('objection.state.connection.state_connection.get_api') 9 | def test_monitor(self, mock_api): 10 | monitor([]) 11 | 12 | self.assertTrue(mock_api.return_value.android_monitor_clipboard.called) 13 | -------------------------------------------------------------------------------- /agent/README.md: -------------------------------------------------------------------------------- 1 | # objection agent 2 | 3 | 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. 4 | 5 | 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). 6 | -------------------------------------------------------------------------------- /agent/src/rpc/environment.ts: -------------------------------------------------------------------------------- 1 | import * as environment from "../generic/environment.js"; 2 | 3 | export const env = { 4 | // environment 5 | envAndroid: () => environment.androidPackage(), 6 | envAndroidPaths: () => environment.androidPaths(), 7 | envFrida: () => environment.frida(), 8 | envIos: () => environment.iosPackage(), 9 | envIosPaths: () => environment.iosPaths(), 10 | envRuntime: () => environment.runtime(), 11 | }; 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.ui.screenshot.txt: -------------------------------------------------------------------------------- 1 | Command: android ui screenshot 2 | 3 | Usage: android ui screenshot 4 | 5 | Screenshots the current foregrounded Activity and saves it as a PNG locally. 6 | If the `.png` extension is not used in the resultant filename, it will be automatically 7 | added. 8 | 9 | Examples: 10 | android ui screenshot application_image 11 | android ui screenshot app_screenshot.png 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/sqlite.disconnect.txt: -------------------------------------------------------------------------------- 1 | Command: sqlite disconnect 2 | 3 | Usage: sqlite disconnect 4 | 5 | Disconnect from the currently connected SQLite database file. This command will clean 6 | the locally cached version of the database file. If you made changes you want to save, 7 | run the `sqlite sync` command before disconnecting. This command is also run if the 8 | REPL is existed. 9 | 10 | Examples: 11 | sqlite disconnect 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.cookies.get.txt: -------------------------------------------------------------------------------- 1 | Command: ios cookies get 2 | 3 | Usage: ios cookies get 4 | 5 | Queries iOS's NSHTTPCookieStorage class, extracting cookie values out of the 6 | sharedHTTPCookieStorage. Various URL fetching methods use the 7 | sharedHTTPCookieStorage to store cookie data. This information may be useful 8 | to get session cookies for web services to reuse in other tools/browsers. 9 | 10 | Examples: 11 | ios cookies get 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ls.txt: -------------------------------------------------------------------------------- 1 | Command: ls 2 | 3 | Usage: ls (optional: ) 4 | 5 | Display the contents of a directory on the mobile device. The output details 6 | the permissions of the directory in question, as well as those for each file 7 | and directory within. If no directory is specified, the current working 8 | directory is assumed and listed. 9 | 10 | Examples: 11 | ls Library/Caches 12 | ls / 13 | ls 14 | -------------------------------------------------------------------------------- /tests/commands/android/test_pinning.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.android.pinning import android_disable 5 | 6 | 7 | class TestPinning(unittest.TestCase): 8 | @mock.patch('objection.state.connection.state_connection.get_api') 9 | def test_pinning_disable(self, mock_api): 10 | android_disable([]) 11 | 12 | self.assertTrue(mock_api.return_value.android_ssl_pinning_disable.called) 13 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.list.class_methods.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking list class_methods 2 | 3 | Usage: android hooking list class_methods 4 | 5 | Lists the methods declared in a Java class, together with the arguments 6 | that they may required using getDeclaredMethods(). 7 | 8 | Examples: 9 | android hooking list class_methods com.example.utils.RootUtils 10 | android hooking list class_methods com.test.Helpers.Communications 11 | -------------------------------------------------------------------------------- /objection/console/helpfiles/sqlite.execute.query.txt: -------------------------------------------------------------------------------- 1 | Command: sqlite execute query 2 | 3 | Usage: sqlite execute query 4 | 5 | Execute a query against the cached copy of the connected SQLite database. 6 | If your changes need to be effective on the device, execute the `sqlite sync` 7 | command to upload the modified database back to the device. 8 | 9 | Examples: 10 | sqlite execute query select * from data 11 | sqlite execute query delete from data 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.list.class_methods.txt: -------------------------------------------------------------------------------- 1 | Command: ios hooking list class_methods 2 | 3 | Usage: ios hooking list class_methods (--include-parents) 4 | 5 | Lists the methods within an Objective-C class. Adding the --include-parents 6 | flag will also list methods available due to class inheritance. 7 | 8 | Examples: 9 | ios hooking list class_methods KeychainDataManager 10 | ios hooking list class_methods KeychainDataManager --include-parents 11 | -------------------------------------------------------------------------------- /objection/commands/ios/nsuserdefaults.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from objection.state.connection import state_connection 4 | 5 | 6 | def get(args: list = None) -> None: 7 | """ 8 | Gets all of the values stored in NSUserDefaults and prints 9 | them to screen. 10 | 11 | :param args: 12 | :return: 13 | """ 14 | 15 | api = state_connection.get_api() 16 | defaults = api.ios_nsuser_defaults_get() 17 | 18 | click.secho(defaults, bold=True) 19 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.keychain.add.txt: -------------------------------------------------------------------------------- 1 | Command: ios keychain add 2 | 3 | Usage: ios keychain add --account --service --data 4 | 5 | Adds a new entry to the iOS keychain using SecItemAdd. 6 | 7 | The new keychain entry class would be kSecClassGenericPassword with no extra 8 | kSecAttrAccessControl set. 9 | 10 | Examples: 11 | ios keychain add --account token --data 1122-33344-55122-55512 12 | ios keychain add --service foo --data bar 13 | -------------------------------------------------------------------------------- /agent/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ping } from "./generic/ping.js"; 2 | import { android } from "./rpc/android.js"; 3 | import { env } from "./rpc/environment.js"; 4 | import { ios } from "./rpc/ios.js"; 5 | import { jobs } from "./rpc/jobs.js"; 6 | import { memory } from "./rpc/memory.js"; 7 | import { other } from "./rpc/other.js"; 8 | 9 | rpc.exports = { 10 | ...android, 11 | ...ios, 12 | ...env, 13 | ...jobs, 14 | ...memory, 15 | ...other, 16 | ping: (): boolean => ping(), 17 | }; 18 | -------------------------------------------------------------------------------- /objection/console/helpfiles/file.download.txt: -------------------------------------------------------------------------------- 1 | Command: file download 2 | 3 | Usage: file download (optional: ) 4 | 5 | Download a file from a location on the mobile device, to a local destination. 6 | If no destination is provided, the downloaded file will be saved in the 7 | current directory with the same name 8 | 9 | Examples: 10 | file download Document/Preferences/test.sqlite foo.sqlite 11 | file download Document/Preferences/preferences.plist 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.monitor.crypto.txt: -------------------------------------------------------------------------------- 1 | Command: ios monitor crypto monitor 2 | 3 | Usage: ios monitor crypto monitor 4 | 5 | Hooks CommonCrypto to output information about cryptographic operation. Works best for AES with PKCS7 Padding. 6 | Currently the following hooks are supported: 7 | - SecRandomCopyBytes 8 | - CCKeyDerivationPBKDF 9 | - CCCrypt 10 | - CCCryptorCreate 11 | - CCCryptorUpdate 12 | - CCCryptorFinal 13 | 14 | Examples: 15 | ios monitor crypto monitor 16 | -------------------------------------------------------------------------------- /objection/console/helpfiles/memory.write.txt: -------------------------------------------------------------------------------- 1 | Command: memory write 2 | 3 | Usage: memory write "
" "" (optional: --string) 4 | 5 | Write an arbitrary set of bytes to an address in memory. Using this command has a high 6 | chance of crashing the applications process if you attempt to write to addresses outside 7 | of the applications heap, or your bytes specified cause to go outside of some memory 8 | boundary. 9 | 10 | Examples: 11 | memory write 0x117a2e347 "ff 41 41 42" 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/memory.list.modules.txt: -------------------------------------------------------------------------------- 1 | Command: memory list modules 2 | 3 | Usage: memory list modules (optional: --json ) 4 | 5 | List all of the modules loaded in the current process, detailing their base 6 | address, size and location on disk. 7 | Providing a filename with the --json flag will output all of the module 8 | attributes to the file specified for later inspection. 9 | 10 | Examples: 11 | memory list modules 12 | memory list modules --json modules.json 13 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.jailbreak.disable.txt: -------------------------------------------------------------------------------- 1 | Command: ios jailbreak disable 2 | 3 | Usage: ios jailbreak disable 4 | 5 | Attempts to disable Jailbreak detection on iOS devices. This is achieved by 6 | hooking the NSFileManager fileExistsAtPath method, and checking if it was 7 | called with a path to common Jailbroken path artifacts. Calls to the fork() 8 | method are also hooked and will respond with a 0, indicating that it was 9 | unsuccessful. 10 | 11 | Examples: 12 | ios jailbreak disable 13 | -------------------------------------------------------------------------------- /plugins/flex/libFlex.m: -------------------------------------------------------------------------------- 1 | #import "libFlex.h" 2 | #import "FlexManager.h" 3 | 4 | @implementation libFlex 5 | 6 | - (id)init 7 | { 8 | self = [super init]; 9 | return self; 10 | } 11 | 12 | - (void)logSomething:(NSString *)something 13 | { 14 | NSLog(@"%@", something); 15 | } 16 | 17 | - (void)flexUp { 18 | 19 | [[FLEXManager sharedManager] showExplorer]; 20 | } 21 | 22 | @end 23 | 24 | static void __attribute__((constructor)) initialize(void){ 25 | NSLog(@"==== Booted ===="); 26 | } 27 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from contextlib import contextmanager 3 | from io import StringIO 4 | 5 | 6 | # http://schinckel.net/2013/04/15/capture-and-test-sys.stdout-sys.stderr-in-unittest.testcase/ 7 | @contextmanager 8 | def capture(command, *args, **kwargs): 9 | out, sys.stdout = sys.stdout, StringIO() 10 | 11 | try: 12 | 13 | command(*args, **kwargs) 14 | sys.stdout.seek(0) 15 | yield sys.stdout.read() 16 | 17 | finally: 18 | 19 | sys.stdout = out 20 | -------------------------------------------------------------------------------- /objection/commands/android/general.py: -------------------------------------------------------------------------------- 1 | from objection.state.connection import state_connection 2 | 3 | 4 | def deoptimise(args: list) -> None: 5 | """ 6 | Forces the VM to execute everything with its interpreter. 7 | Necessary to prevent optimizations from bypassing method hooks in some cases. 8 | 9 | Ref: https://frida.re/docs/javascript-api/ 10 | 11 | :param args: 12 | :return: 13 | """ 14 | 15 | api = state_connection.get_api() 16 | api.android_deoptimize() 17 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.keystore.watch.txt: -------------------------------------------------------------------------------- 1 | Command: android keystore watch 2 | 3 | Usage: android keystore watch 4 | 5 | Watches usage of the Android Keystore. Two KeyStore methods are watched at the 6 | moment. Those are: 7 | 8 | KeyStore.getKey() 9 | KeyStore.load() 10 | 11 | In both cases, a password for the keystore/key is passed along as the 12 | second parameter of the function. 13 | 14 | Ref: https://developer.android.com/reference/java/security/KeyStore.html 15 | 16 | Examples: 17 | android keystore watch 18 | -------------------------------------------------------------------------------- /plugins/flex/README.md: -------------------------------------------------------------------------------- 1 | # objection Flex plugin 2 | 3 | This plugin should sideload Flex[1], loaded as a plugin in objection. 4 | Flex itself should be a shared library (with your target's architecture as either a thin/fat Mach-o). 5 | 6 | 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. 7 | 8 | [1] [https://github.com/Flipboard/FLEX](https://github.com/Flipboard/FLEX) 9 | -------------------------------------------------------------------------------- /objection/commands/android/proxy.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from objection.state.connection import state_connection 4 | 5 | 6 | def android_proxy_set(args: list = None) -> None: 7 | """ 8 | Sets a proxy specifically within the application. 9 | 10 | :param args: 11 | :return: 12 | """ 13 | 14 | if len(args) != 2: 15 | click.secho('Usage: android proxy set ', bold=True) 16 | return 17 | 18 | api = state_connection.get_api() 19 | api.android_proxy_set(args[0], args[1]) 20 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ui.alert.txt: -------------------------------------------------------------------------------- 1 | Command: ui alert 2 | 3 | Usage: ui alert (optional: "") 4 | 5 | Displays an alert popup on iOS devices, or a Toast message on Android devices. 6 | This is useful to demonstrate that the application was successfully hooked. Providing 7 | an alert message will display that message instead of the default. 8 | Note: Currently, once the iOS alert message has been displayed, dismissing the message 9 | unfortunately crashes the application. 10 | 11 | Examples: 12 | ui alert 13 | ui alert 'custom message!' 14 | -------------------------------------------------------------------------------- /tests/commands/ios/test_nsuserdefaults.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.ios.nsuserdefaults import get 5 | from ...helpers import capture 6 | 7 | 8 | class TestNsuserdefaults(unittest.TestCase): 9 | @mock.patch('objection.state.connection.state_connection.get_api') 10 | def test_get(self, mock_api): 11 | mock_api.return_value.ios_nsuser_defaults_get.return_value = 'foo' 12 | 13 | with capture(get, []) as o: 14 | output = o 15 | 16 | self.assertEqual(output, 'foo\n') 17 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.intent.launch_service.txt: -------------------------------------------------------------------------------- 1 | Command: android intent launch_service 2 | 3 | Usage: android intent launch_service 4 | 5 | Launches an exported service class by building a new Intent and running startActivity() 6 | with it as an argument. The Intent.FLAG_ACTIVITY_NEW_TASK flag is added to achieve 7 | this, with the side effect that the history stack may be reset. 8 | 9 | Examples: 10 | android intent launch_service com.test.example.PingService 11 | android intent launch_service com.test.example.utils.SyncService 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.keychain.clear.txt: -------------------------------------------------------------------------------- 1 | Command: ios keychain clear 2 | 3 | Usage: ios keychain clear 4 | 5 | Clears all the keychain items for the current application. This is achieved by 6 | iterating over the keychain type classes available in iOS and populating a search 7 | dictionary with them. This dictionary is then used as a query to SecItemDelete(), 8 | deleting the entries. 9 | Items that will be deleted include everything stored with the entitlement group used 10 | during the patching/signing process. 11 | 12 | Examples: 13 | ios keychain clear 14 | -------------------------------------------------------------------------------- /agent/src/ios/nsuserdefaults.ts: -------------------------------------------------------------------------------- 1 | import { ObjC } from "../ios/lib/libobjc.js"; 2 | import { 3 | NSDictionary, 4 | NSUserDefaults 5 | } from "./lib/types.js"; 6 | 7 | 8 | export const get = (): NSUserDefaults | any => { 9 | // -- Sample Objective-C 10 | // 11 | // NSUserDefaults *d = [[NSUserDefaults alloc] init]; 12 | // NSLog(@"%@", [d dictionaryRepresentation]); 13 | 14 | const defaults: NSUserDefaults = ObjC.classes.NSUserDefaults; 15 | const data: NSDictionary = defaults.alloc().init().dictionaryRepresentation(); 16 | 17 | return data.toString(); 18 | }; 19 | -------------------------------------------------------------------------------- /agent/src/ios/plist.ts: -------------------------------------------------------------------------------- 1 | import { ObjC } from "../ios/lib/libobjc.js"; 2 | import { NSMutableDictionary } from "./lib/types.js"; 3 | 4 | 5 | export const read = (path: string): string => { 6 | // -- Sample Objective-C 7 | // 8 | // NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithContentsOfFile:path]; 9 | 10 | const dictionary: NSMutableDictionary = ObjC.classes.NSMutableDictionary; 11 | return dictionary.alloc().initWithContentsOfFile_(path).toString(); 12 | }; 13 | 14 | export const write = (path: string, data: any): void => { 15 | // TODO 16 | }; 17 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.watch.class.txt: -------------------------------------------------------------------------------- 1 | Command: ios hooking watch class 2 | 3 | Usage: ios hooking watch (--include-parents) 4 | 5 | Hooks into all of the methods available in the Objective-C class specified 6 | by class_name and reports on invocations of any methods contained within. 7 | If the --include-parents flag is specified, all methods inherited from a 8 | parent class will also be hooked and reported on. 9 | 10 | Examples: 11 | ios hooking watch KeychainDataManager 12 | ios hooking watch PinnedNSURLSessionStarwarsApi --include-parents 13 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.set.return_value.txt: -------------------------------------------------------------------------------- 1 | Command: ios hooking set return_value 2 | 3 | Usage: ios hooking set return_value "" 4 | 5 | Hooks into a specified Objective-C method and sets its return value to 6 | either True or False. This is useful in cases where simple methods are used 7 | to determine things like 'Should SSL pinning be enabled?' as an example. 8 | 9 | Examples: 10 | ios hooking set return_value "+[JailbreakDetection isJailbroken]" false 11 | ios hooking set return_value "-[SecurityHelper shouldPinSSL:]" true 12 | -------------------------------------------------------------------------------- /objection/console/helpfiles/cd.txt: -------------------------------------------------------------------------------- 1 | Usage: cd 2 | 3 | Changes the current working directory on the device. 4 | Many commands within objection are mindful and aware of the current 5 | working directory. An example of this includes the sqlite command, that 6 | allows you to connect to a file in the current path, or to a file specified 7 | with a full path relative to root (/). 8 | For more directories that are applicable to the current app, inspect the 9 | output of the `env` command. 10 | 11 | Examples: 12 | cd Library/Caches 13 | cd Preferences 14 | cd / 15 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.ui.FLAG_SECURE.txt: -------------------------------------------------------------------------------- 1 | Command: android ui FLAG_SECURE 2 | 3 | Usage: android ui FLAG_SECURE 4 | 5 | Control the value of FLAG_SECURE for the current Activity. Setting the value 6 | to false in activities where it is true by default may enable you to take 7 | screenshots using the hardware keys. 8 | 9 | NOTE: This command currently crashes the target application on 32bit devices due to 10 | an SELinux DENY. For more information see this PR: 11 | https://github.com/sensepost/objection/pull/24 12 | 13 | Examples: 14 | android ui FLAG_SECURE false 15 | -------------------------------------------------------------------------------- /objection/console/helpfiles/memory.list.exports.txt: -------------------------------------------------------------------------------- 1 | Command: memory list exports 2 | 3 | Usage: memory list exports (optional: --json ) 4 | 5 | List exports in a specific loaded module. Exports found using this command 6 | could be used in Fridascripts to hook with module.findExportByName(). 7 | For a list of modules to list exports from the `memory list modules` command 8 | may be used. 9 | 10 | Examples: 11 | memory list exports libsystem_configuration.dylib 12 | memory list exports UserManagement 13 | memory list exports UserManagement --json UserManagementExports.json 14 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.search.methods.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking search methods 2 | 3 | Usage: android hooking search methods (optional: package-filter) 4 | 5 | Search for class methods in the current Java runtime with the search string 6 | as part of the class name. An optional package filter may be used to limit 7 | the method search to a specific namespace. 8 | 9 | WARNING: This command may easily crash the application without a filter. 10 | 11 | Examples: 12 | android hooking search classes jailbreak com.package 13 | android hooking search classes sslpinning 14 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.heap.search.instances.txt: -------------------------------------------------------------------------------- 1 | Command: android heap search instances 2 | 3 | Usage: android heap search instances 4 | 5 | Search for and print live instances of a specific Java class, specified by 6 | a fully qualified class name. Output is the result of an attempt at getting 7 | a string value for a discovered object which would typically contain 8 | property values for the object. Hashcodes in the list could be used for 9 | other heap interactions. 10 | 11 | Examples: 12 | android heap search instances java.net.Socket 13 | android heap search instances java.io.File 14 | 15 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.intent.launch_activity.txt: -------------------------------------------------------------------------------- 1 | Command: android intent launch_activity 2 | 3 | Usage: android intent launch_activity 4 | 5 | Launches an activity class by building a new Intent and running startActivity() 6 | with it as an argument. The Intent.FLAG_ACTIVITY_NEW_TASK flag is added to achieve 7 | this, with the side effect that the history stack may be reset. 8 | 9 | Examples: 10 | android intent launch_activity com.test.example.MainActivity 11 | android intent launch_activity com.test.example.SecretActivity 12 | android intent launch_activity com.example.test.Other 13 | -------------------------------------------------------------------------------- /objection/commands/android/monitor.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from objection.state.connection import state_connection 4 | 5 | 6 | def string_canary(args: list) -> None: 7 | """ 8 | Monitors for a string canary argument and reports when 9 | it is found. 10 | 11 | :param args: 12 | :return: 13 | """ 14 | 15 | if len(args) < 1: 16 | click.secho('Usage: android monitor canary (optional: )', bold=True) 17 | return 18 | 19 | target_class = args[0] 20 | 21 | api = state_connection.get_api() 22 | api.android_live_print_class_instances(target_class) 23 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.bundles.list_bundles.txt: -------------------------------------------------------------------------------- 1 | Command: ios bundles list_bundles 2 | 3 | Usage: ios bundles list_bundles (optional: --full-path) 4 | 5 | Returns all the application's non-framework bundles. [1] 6 | 7 | Output includes the frameworks executable, bundle name and version. The path 8 | value is truncated by default, however, adding the --full-path flag would 9 | print the entire path to the framework. 10 | 11 | [1] https://developer.apple.com/documentation/foundation/nsbundle/1413705-allbundles?language=objc 12 | 13 | Examples: 14 | ios bundles list_bundles 15 | ios bundles list_bundles --full-path 16 | -------------------------------------------------------------------------------- /objection/commands/android/root.py: -------------------------------------------------------------------------------- 1 | from objection.state.connection import state_connection 2 | 3 | 4 | def disable(args: list = None) -> None: 5 | """ 6 | Performs a generic anti root detection. 7 | 8 | :param args: 9 | :return: 10 | """ 11 | 12 | api = state_connection.get_api() 13 | api.android_root_detection_disable() 14 | 15 | 16 | def simulate(args: list = None) -> None: 17 | """ 18 | Simulate a rooted environment. 19 | 20 | :param args: 21 | :return: 22 | """ 23 | 24 | api = state_connection.get_api() 25 | api.android_root_detection_enable() 26 | -------------------------------------------------------------------------------- /objection/commands/ios/jailbreak.py: -------------------------------------------------------------------------------- 1 | from objection.state.connection import state_connection 2 | 3 | 4 | def disable(args: list = None) -> None: 5 | """ 6 | Attempts to disable jailbreak detection. 7 | 8 | :param args: 9 | :return: 10 | """ 11 | 12 | api = state_connection.get_api() 13 | api.ios_jailbreak_disable() 14 | 15 | 16 | def simulate(args: list = None) -> None: 17 | """ 18 | Attempts to simulate a Jailbroken environment 19 | 20 | :param args: 21 | :return: 22 | """ 23 | 24 | api = state_connection.get_api() 25 | api.ios_jailbreak_enable() 26 | -------------------------------------------------------------------------------- /plugins/flex/index.js: -------------------------------------------------------------------------------- 1 | rpc.exports = { 2 | initFlex: function (dlib) { 3 | 4 | const NSDocumentDirectory = 9; 5 | const NSUserDomainMask = 1 6 | const p = ObjC.classes.NSFileManager.defaultManager() 7 | .URLsForDirectory_inDomains_(NSDocumentDirectory, NSUserDomainMask).lastObject().path(); 8 | 9 | ObjC.schedule(ObjC.mainQueue, function () { 10 | const libFlexModule = Module.load(p + '/' + dlib); 11 | const libFlexPtr = libFlexModule.findExportByName("OBJC_CLASS_$_libFlex"); 12 | const libFlex = new ObjC.Object(libFlexPtr); 13 | 14 | libFlex.alloc().init().flexUp(); 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /objection/console/helpfiles/sqlite.connect.txt: -------------------------------------------------------------------------------- 1 | Command: sqlite connect 2 | 3 | Usage: sqlite connect 4 | 5 | Connect to a SQLite database on the remote device. The connection process downloads 6 | a copy of the remote database file to a local temporary directory. The file is then 7 | validated to make sure that it is a SQLite3 database file. Once considered a valid 8 | database file, the connection is considered complete. 9 | The `sqlite status` command will show details about the connection once successful. 10 | 11 | Examples: 12 | sqlite connect Preferences/settings.sqlite 13 | sqlite connect credentials.sqlite 14 | -------------------------------------------------------------------------------- /objection/console/helpfiles/file.upload.txt: -------------------------------------------------------------------------------- 1 | Command: file upload 2 | 3 | Usage: file upload (optional: ) 4 | 5 | Upload a file from the local filesystem to the remote filesystem. 6 | If a full path is not specified for the remote destination, the current 7 | working directory is assumed as the relative directory for the upload 8 | destination. If the file already exists on the remote filesystem, it 9 | will be overridden. If no remove filename is specified, the same filename 10 | of the source file will be used. 11 | 12 | Examples: 13 | file upload test.sqlite Document/Preferences/test.sqlite 14 | file upload foo.txt 15 | -------------------------------------------------------------------------------- /tests/commands/ios/test_jailbreak.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.ios.jailbreak import disable, simulate 5 | 6 | 7 | class TestJailbreak(unittest.TestCase): 8 | @mock.patch('objection.state.connection.state_connection.get_api') 9 | def test_disable(self, mock_api): 10 | disable([]) 11 | 12 | self.assertTrue(mock_api.return_value.ios_jailbreak_disable.called) 13 | 14 | @mock.patch('objection.state.connection.state_connection.get_api') 15 | def test_simulate(self, mock_api): 16 | simulate([]) 17 | 18 | self.assertTrue(mock_api.return_value.ios_jailbreak_enable.called) 19 | -------------------------------------------------------------------------------- /objection/console/helpfiles/memory.dump.from_base.txt: -------------------------------------------------------------------------------- 1 | Command: memory dump all 2 | 3 | Usage: memory dump 4 | 5 | Dumps memory from within the current process from a base address, for a set number 6 | of bytes to a local file specified by local destination. For example addresses and 7 | sizes, the `memory list modules` command may be used. 8 | Specifying addresses or sizes that are outside of the current processes sandbox 9 | has a *high* chance of crashing the application. Use with caution. 10 | 11 | Examples: 12 | memory dump from_base 0x10009c000 442368 main 13 | memory dump from_base 0x10f88e000 548864 CoreAudio 14 | -------------------------------------------------------------------------------- /tests/commands/android/test_root.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.android.root import disable, simulate 5 | 6 | 7 | class TestRoot(unittest.TestCase): 8 | @mock.patch('objection.state.connection.state_connection.get_api') 9 | def test_disable(self, mock_api): 10 | disable([]) 11 | 12 | self.assertTrue(mock_api.return_value.android_root_detection_disable.called) 13 | 14 | @mock.patch('objection.state.connection.state_connection.get_api') 15 | def test_simulate(self, mock_api): 16 | simulate([]) 17 | 18 | self.assertTrue(mock_api.return_value.android_root_detection_enable.called) 19 | -------------------------------------------------------------------------------- /objection/console/helpfiles/plugin.load.txt: -------------------------------------------------------------------------------- 1 | Command: plugin load 2 | 3 | Usage: plugin load (optional: namespace) 4 | 5 | Loads an objection plugin into the current session. For more information on 6 | plugins, please refer to the project wiki at: 7 | https://github.com/sensepost/objection/wiki 8 | 9 | By default, plugin commands are nested beneath the plugin context menu and 10 | will use the plugin's built-in namespace as the subcommand to use. However, 11 | this namespace may be specified at load time, overriding the plugins buil-in 12 | name. 13 | 14 | Examples: 15 | plugin load ~/home/objection-plugins/feature 16 | plugin load ~/home/objection-plugins/feature newname 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /agent/src/rpc/memory.ts: -------------------------------------------------------------------------------- 1 | import * as m from "../generic/memory.js"; 2 | 3 | export const memory = { 4 | 5 | memoryDump: (address: string, size: number) => m.dump(address, size), 6 | memoryListExports: (name: string): ModuleExportDetails[] => m.listExports(name), 7 | memoryListModules: (): Module[] => m.listModules(), 8 | memoryListRanges: (protection: string): RangeDetails[] => m.listRanges(protection), 9 | memorySearch: (pattern: string, onlyOffsets: boolean): string[] => m.search(pattern, onlyOffsets), 10 | memoryReplace: (pattern: string, replace: number[]): string[] => m.replace(pattern, replace), 11 | memoryWrite: (address: string, value: number[]): void => m.write(address, value), 12 | 13 | }; 14 | -------------------------------------------------------------------------------- /plugins/mettle/README.md: -------------------------------------------------------------------------------- 1 | # objection Mettle plugin 2 | 3 | This plugin should sideload [Mettle](https://github.com/rapid7/mettle), loaded as a plugin in objection. 4 | Mettle itself should be a shared library available in this directory. 5 | 6 | ## installation 7 | 8 | Getting Mettle is super simple. 9 | 10 | 1. Clone the respistory with `git clone https://github.com/rapid7/mettle.git`. 11 | 2. Build Mettle for your target architecture. Eg: `make TARGET=aarch64-iphone-darwin`. 12 | 3. Codesign the new dylib in the build directory with `codesign -f -s mettle.dylib` 13 | 4. Copy the codesigned dylib into this plugin folder. 14 | 15 | Running `plugin mettle load` will grab the new dylib and upload it to the device. 16 | -------------------------------------------------------------------------------- /agent/src/ios/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { default as ObjCTypes } from "frida-objc-bridge"; 2 | 3 | export type NSDictionary = ObjCTypes.Object | any; 4 | export type NSMutableDictionary = ObjCTypes.Object | any; 5 | export type NSString = ObjCTypes.Object | any; 6 | export type NSFileManager = ObjCTypes.Object | any; 7 | export type NSBundle = ObjCTypes.Object | any; 8 | export type NSUserDefaults = ObjCTypes.Object | any; 9 | export type NSHTTPCookieStorage = ObjCTypes.Object | any; 10 | export type NSURLCredentialStorage = ObjCTypes.Object | any; 11 | export type NSArray = ObjCTypes.Object | any; 12 | export type NSData = ObjCTypes.Object | any; 13 | 14 | export type CFDictionaryRef = any; 15 | export type CFTypeRef = any; 16 | -------------------------------------------------------------------------------- /objection/commands/ios/pinning.py: -------------------------------------------------------------------------------- 1 | from objection.state.connection import state_connection 2 | 3 | 4 | def _should_be_quiet(args: list) -> bool: 5 | """ 6 | Checks if --quiet is part of the 7 | commands arguments. 8 | 9 | :param args: 10 | :return: 11 | """ 12 | 13 | return '--quiet' in args 14 | 15 | 16 | def ios_disable(args: list = None) -> None: 17 | """ 18 | Starts a new objection job that hooks common classes and functions, 19 | applying new logic in an attempt to bypass SSL pinning. 20 | 21 | :param args: 22 | :return: 23 | """ 24 | 25 | api = state_connection.get_api() 26 | api.ios_pinning_disable(_should_be_quiet(args)) 27 | -------------------------------------------------------------------------------- /objection/commands/android/pinning.py: -------------------------------------------------------------------------------- 1 | from objection.state.connection import state_connection 2 | 3 | 4 | def _should_be_quiet(args: list) -> bool: 5 | """ 6 | Checks if --quiet is part of the 7 | commands arguments. 8 | 9 | :param args: 10 | :return: 11 | """ 12 | 13 | return '--quiet' in args 14 | 15 | 16 | def android_disable(args: list = None) -> None: 17 | """ 18 | Starts a new objection job that hooks common classes and functions, 19 | applying new logic in an attempt to bypass SSL pinning. 20 | 21 | :param args: 22 | :return: 23 | """ 24 | 25 | api = state_connection.get_api() 26 | api.android_ssl_pinning_disable(_should_be_quiet(args)) 27 | -------------------------------------------------------------------------------- /tests/commands/ios/test_pinning.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.ios.pinning import ios_disable, _should_be_quiet 5 | 6 | 7 | class TestPinning(unittest.TestCase): 8 | @mock.patch('objection.state.connection.state_connection.get_api') 9 | def test_disable(self, mock_api): 10 | ios_disable([]) 11 | 12 | self.assertTrue(mock_api.return_value.ios_pinning_disable.called) 13 | 14 | def test_should_be_quiet_returns_true(self): 15 | result = _should_be_quiet(['test', '--quiet']) 16 | self.assertTrue(result) 17 | 18 | def test_should_be_quiet_returns_false(self): 19 | result = _should_be_quiet(['test']) 20 | self.assertFalse(result) 21 | -------------------------------------------------------------------------------- /objection/commands/android/command.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from objection.state.connection import state_connection 4 | 5 | 6 | def execute(args: list) -> None: 7 | """ 8 | Runs a shell command on an Android device. 9 | 10 | :param args: 11 | :return: 12 | """ 13 | 14 | command = ' '.join(args) 15 | click.secho('Running shell command: {0}\n'.format(command), dim=True) 16 | 17 | api = state_connection.get_api() 18 | response = api.android_shell_exec(command) 19 | 20 | if 'stdOut' in response and len(response['stdOut']) > 0: 21 | click.secho(response['stdOut'], bold=True) 22 | 23 | if 'stdErr' in response and len(response['stdErr']) > 0: 24 | click.secho(response['stdErr'], bold=True, fg='red') 25 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.ui.touchid_bypass.txt: -------------------------------------------------------------------------------- 1 | Command: ios ui touchid_bypass 2 | 3 | Usage: ios ui touchid_bypass 4 | 5 | Hooks into the -[LAContext evaluatePolicy:localizedReason:reply:] selector and 6 | replies with a successful message from the operating system when a touchID prompt 7 | is dismissed. This is useful in cases where the application relies solely on the 8 | operating system to tell it if a fingerprint read was successful or not. 9 | Note: This does *not* bypass cases where TouchID is needed to decrypt a keychain 10 | entry, simply because the actual data itself is not stored in the keychain but 11 | instead lives in the Secure Enclave. The keychain simply contains a token to the 12 | data itself. 13 | 14 | Examples: 15 | ios ui touchid_bypass 16 | -------------------------------------------------------------------------------- /tests/state/test_jobs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from objection.state.jobs import job_manager_state 4 | 5 | 6 | class TestJobManager(unittest.TestCase): 7 | def tearDown(self): 8 | job_manager_state.jobs = [] 9 | 10 | def test_job_manager_starts_with_empty_jobs(self): 11 | self.assertEqual(len(job_manager_state.jobs), 0) 12 | 13 | def test_adds_jobs(self): 14 | job_manager_state.add_job('foo') 15 | 16 | self.assertEqual(len(job_manager_state.jobs), 1) 17 | 18 | def test_removes_jobs(self): 19 | job_manager_state.add_job('foo') 20 | job_manager_state.add_job('bar') 21 | 22 | job_manager_state.remove_job('foo') 23 | job_manager_state.remove_job('bar') 24 | 25 | self.assertEqual(len(job_manager_state.jobs), 0) 26 | -------------------------------------------------------------------------------- /tests/commands/android/test_command.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.android.command import execute 5 | from ...helpers import capture 6 | 7 | 8 | class TestCommand(unittest.TestCase): 9 | @mock.patch('objection.state.connection.state_connection.get_api') 10 | def test_execute_prints_output(self, mock_api): 11 | mock_api.return_value.android_shell_exec.return_value = { 12 | 'command': 'foo bar baz', 'stdErr': 'bazfoo', 'stdOut': 'foobar\n' 13 | } 14 | 15 | with capture(execute, ['foo', 'bar', 'baz']) as o: 16 | output = o 17 | 18 | expected_output = """Running shell command: foo bar baz 19 | 20 | foobar 21 | 22 | bazfoo 23 | """ 24 | 25 | self.assertEqual(output, expected_output) 26 | -------------------------------------------------------------------------------- /agent/src/android/proxy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | wrapJavaPerform, 3 | Java 4 | } from "./lib/libjava.js"; 5 | import { colors as c } from "../lib/color.js"; 6 | 7 | export const set = (host: string, port: string): Promise => { 8 | return wrapJavaPerform(() => { 9 | var proxyHost = host; 10 | var proxyPort = port; 11 | 12 | var System = Java.use("java.lang.System"); 13 | 14 | if (System != undefined) { 15 | send(c.green(`Setting properties for a proxy`)); 16 | System.setProperty("http.proxyHost", proxyHost); 17 | System.setProperty("http.proxyPort", proxyPort); 18 | 19 | System.setProperty("https.proxyHost", proxyHost); 20 | System.setProperty("https.proxyPort", proxyPort); 21 | 22 | send(`${c.green(`Proxy configured to ` + proxyHost + ` ` + proxyPort)}`); 23 | } 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.hooking.watch.method.txt: -------------------------------------------------------------------------------- 1 | Command: ios hooking watch method 2 | 3 | Usage: ios hooking method "" (optional: --dump-backtrace) 4 | (optional: --dump-args) (optional: --dump-return) 5 | 6 | Hooks into a specified Objective-C method and reports on invocations. 7 | A full class and method is expected, including whether its an instance 8 | or class method. 9 | If the --include-backtrace flag is provided, a full stack trace that 10 | lead to the methods invocation will also be dumped. 11 | 12 | Examples: 13 | ios hooking watch method "+[KeychainDataManager update:forKey:]" 14 | ios hooking watch method "-[PinnedNSURLSessionStarwarsApi getJsonResponseFrom:onSuccess:onFailure:]" --include-backtrace 15 | ios hooking watch method "+[KeychainDataManager update:forKey:]" --dump-args --dump-return 16 | -------------------------------------------------------------------------------- /agent/src/lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IFridaInfo { 2 | arch: string; 3 | debugger: boolean; 4 | heap: number; 5 | platform: string; 6 | runtime: string; 7 | version: string; 8 | } 9 | 10 | export interface IIosPackage { 11 | applicationName: string; 12 | deviceName: string; 13 | identifierForVendor: string; 14 | model: string; 15 | systemName: string; 16 | systemVersion: string; 17 | } 18 | 19 | export interface IAndroidPackage { 20 | application_name: string; 21 | board: string; 22 | brand: string; 23 | device: string; 24 | host: string; 25 | id: string; 26 | model: string; 27 | product: string; 28 | user: string; 29 | version: string; 30 | } 31 | 32 | export interface IIosBundlePaths { 33 | BundlePath: string; 34 | CachesDirectory: string; 35 | DocumentDirectory: string; 36 | LibraryDirectory: string; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.root.disable.txt: -------------------------------------------------------------------------------- 1 | Command: android root disable 2 | 3 | Usage: android root disable 4 | 5 | Attempts to disable root detection on Android devices. This is achieved by 6 | hooking numerous classes such as java.lang.String (for contains()), 7 | java.lang.Runtime (for exec()) and java.io.File (for exists()). These hooked 8 | methods have their properties or arguments inspected to determine if artifacts 9 | commonly checked for in root detection is used, and manipulated. 10 | 11 | If this method does not effectively disable root detection for you, keep in 12 | mind that it us very common for applications to have helper methods such as 13 | isRooted() that perform a number of checks in one class method. Using the 14 | boolean return module on one of these utility methods may achieve a similar 15 | effect. 16 | 17 | Examples: 18 | android root disable 19 | 20 | -------------------------------------------------------------------------------- /objection/commands/ios/nsurlcredentialstorage.py: -------------------------------------------------------------------------------- 1 | import click 2 | from tabulate import tabulate 3 | 4 | from objection.state.connection import state_connection 5 | 6 | 7 | def dump(args: list = None) -> None: 8 | """ 9 | Dumps credentials stored in NSURLCredentialStorage 10 | 11 | :param args: 12 | :return: 13 | """ 14 | 15 | api = state_connection.get_api() 16 | cookies = api.ios_credential_storage() 17 | 18 | click.secho(tabulate( 19 | [[ 20 | entry['protocol'], 21 | entry['host'], 22 | entry['port'], 23 | entry['authMethod'].replace('NSURLAuthenticationMethod', ''), 24 | entry['user'], 25 | entry['password'], 26 | ] for entry in cookies], headers=[ 27 | 'Protocol', 'Host', 'Port', 'Authentication Method', 'User', 'Password' 28 | ], 29 | )) 30 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.sslpinning.disable.txt: -------------------------------------------------------------------------------- 1 | Command: ios sslpinning disable 2 | 3 | Usage: ios sslpinning disable 4 | 5 | Attempts to disable SSL Pinning on iOS devices. This is achieved by hooking 6 | into methods commonly used by Frameworks and Libraries such as AFNetworking, 7 | NSURLSession and the now deprecated NSURLConnection. 8 | This command also implements the bypass techniques used in the well-known 9 | SSL-Killswitch2 app, including a new technique reportedly working in iOS10. 10 | 11 | If this method does not disable the applications SSL pinning implementation, 12 | then it may still be possible to bypass it via 'helper' methods commonly 13 | used by developers to help when testing in development / staging environments. 14 | Be on the lookout for classes / methods that relate to pinning that may simply 15 | return a BOOL value. 16 | 17 | Examples: 18 | ios sslpinning disable 19 | -------------------------------------------------------------------------------- /objection/commands/ios/plist.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from objection.commands import filemanager 6 | from objection.state.connection import state_connection 7 | from objection.state.device import device_state 8 | 9 | 10 | def cat(args: list = None) -> None: 11 | """ 12 | Parses a plist on an iOS device and echoes it in a more human 13 | readable way. 14 | 15 | :param args: 16 | :return: 17 | """ 18 | 19 | if len(args) <= 0: 20 | click.secho('Usage: ios plist cat ', bold=True) 21 | return 22 | 23 | plist = args[0] 24 | 25 | if not os.path.isabs(plist): 26 | pwd = filemanager.pwd() 27 | plist = device_state.platform.path_separator.join([pwd, plist]) 28 | 29 | api = state_connection.get_api() 30 | plist_data = api.ios_plist_read(plist) 31 | 32 | click.secho(plist_data, bold=True) 33 | -------------------------------------------------------------------------------- /objection/commands/ios/binary.py: -------------------------------------------------------------------------------- 1 | import click 2 | from tabulate import tabulate 3 | 4 | from objection.state.connection import state_connection 5 | 6 | 7 | def info(args: list) -> None: 8 | """ 9 | Gets information about binaries and frameworks. 10 | 11 | :param args: 12 | :return: 13 | """ 14 | 15 | api = state_connection.get_api() 16 | binary_info = api.ios_binary_info() 17 | 18 | click.secho(tabulate( 19 | [[ 20 | name, 21 | information['type'], 22 | information['encrypted'], 23 | information['pie'], 24 | information['arc'], 25 | information['canary'], 26 | information['stackExec'], 27 | information['rootSafe'] 28 | ] for name, information in binary_info.items()], 29 | headers=['Name', 'Type', 'Encrypted', 'PIE', 'ARC', 'Canary', 'Stack Exec', 'RootSafe'], 30 | )) 31 | -------------------------------------------------------------------------------- /tests/commands/android/test_heap.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.android.heap import instances 5 | from tests.helpers import capture 6 | 7 | 8 | class TestHeap(unittest.TestCase): 9 | @mock.patch('objection.state.connection.state_connection.get_api') 10 | def test_print_live_instances_validates_command(self, mock_api): 11 | with capture(instances, []) as o: 12 | output = o 13 | 14 | self.assertEqual('Usage: android heap print_instances (eg: com.example.test)\n', output) 15 | self.assertFalse(mock_api.return_value.android_heap_get_live_class_instances.called) 16 | 17 | @mock.patch('objection.state.connection.state_connection.get_api') 18 | def test_print_live_instances_validates_command(self, mock_api): 19 | instances(['java.io.File']) 20 | 21 | self.assertTrue(mock_api.return_value.android_heap_get_live_class_instances.called) 22 | -------------------------------------------------------------------------------- /objection/console/helpfiles/import.txt: -------------------------------------------------------------------------------- 1 | Command: import 2 | 3 | Usage: import (optional: ) (optional: --no-exception-handler) 4 | 5 | Imports Fridascript from a file on the local filesystem and executes it as a job. 6 | To 'unload' the script, the job that was started to should be killed. 7 | You can list all of the current jobs using the `jobs list` command. If no name was 8 | specified for your job, a generic name of 'user-script' will be used for the 9 | job started as a result of the import. 10 | 11 | Scripts that are run using this command get wrapped in a global, generic JavaScript try/catch 12 | block. If this is not something that you want, the '--no-exception-handler' flag may be specified. 13 | 14 | Examples: 15 | import ~/home/myscript.js 16 | import ~/home/hooks/custom.js custom-hook-name 17 | import ~/home/hooks/custom.js custom-hook-name --no-exception-handler 18 | import ~/home/script.js --no-exception-handler 19 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Release to PyPi 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | pypi: 10 | name: Publish to PyPI 11 | runs-on: ubuntu-latest 12 | environment: 13 | name: pypi 14 | permissions: 15 | id-token: write 16 | contents: read 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v5 20 | - name: Ensure node for agent 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: '24' 24 | cache: npm 25 | cache-dependency-path: agent/package-lock.json 26 | - name: Install uv 27 | uses: astral-sh/setup-uv@v6 28 | - name: Set tagged version 29 | run: | 30 | VERSION="$GITHUB_REF_NAME" 31 | uv version "$VERSION" 32 | - name: Build Agent 33 | run: cd agent && npm ci 34 | - name: Build 35 | run: uv build 36 | - name: Publish 37 | run: uv publish 38 | -------------------------------------------------------------------------------- /tests/console/test_completer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from prompt_toolkit.document import Document 4 | 5 | from objection.console.completer import CommandCompleter 6 | 7 | 8 | class TestConsoleCommandCompletion(unittest.TestCase): 9 | def setUp(self): 10 | self.command_completer = CommandCompleter() 11 | 12 | def test_can_find_command_completion(self): 13 | document = Document('android hooking list ', 21) 14 | 15 | completions = self.command_completer.find_completions(document) 16 | 17 | self.assertEqual(type(completions), dict) 18 | self.assertEqual(completions['activities']['meta'], 'List the registered Activities') 19 | 20 | def test_will_have_empty_dict_for_invalid_command(self): 21 | document = Document('android hooking list fruitcakes ', 30) 22 | 23 | completions = self.command_completer.find_completions(document) 24 | 25 | self.assertEqual(type(completions), dict) 26 | self.assertEqual(len(completions), 0) 27 | -------------------------------------------------------------------------------- /agent/src/ios/pasteboard.ts: -------------------------------------------------------------------------------- 1 | import { ObjC } from "../ios/lib/libobjc.js"; 2 | import { colors as c } from "../lib/color.js"; 3 | 4 | 5 | export const monitor = (): void => { 6 | // -- Sample Objective-C 7 | // 8 | // UIPasteboard *pb = [UIPasteboard generalPasteboard]; 9 | // NSLog(@"%@", [pb string]); 10 | // NSLog(@"%@", [pb image]); 11 | 12 | const UIPasteboard = ObjC.classes.UIPasteboard; 13 | const Pasteboard = UIPasteboard.generalPasteboard(); 14 | let data: string = ""; 15 | 16 | setInterval(() => { 17 | const currentString = Pasteboard.string().toString(); 18 | 19 | // do nothing if the strings are the same as the last one 20 | // we know about 21 | if (currentString === data) { return; } 22 | 23 | // update the string_data with the new string 24 | data = currentString; 25 | 26 | // ... and send the update along 27 | send(`${c.blackBright(`[pasteboard-monitor]`)} Data: ${c.greenBright(data.toString())}`); 28 | 29 | // 5 second poll 30 | }, 1000 * 5); 31 | }; 32 | -------------------------------------------------------------------------------- /tests/state/test_app.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from objection.state.app import app_state 4 | 5 | 6 | class TestApp(unittest.TestCase): 7 | def tearDown(self): 8 | app_state.debug_hooks = False 9 | app_state.successful_commands = [] 10 | 11 | def test_app_should_not_debug_hooks_by_default(self): 12 | self.assertFalse(app_state.should_debug_hooks()) 13 | 14 | def test_app_should_debug_hooks_if_true(self): 15 | app_state.debug_hooks = True 16 | 17 | self.assertTrue(app_state.should_debug_hooks()) 18 | 19 | def test_adds_command_to_history(self): 20 | app_state.add_command_to_history('foo') 21 | 22 | self.assertEqual(len(app_state.successful_commands), 1) 23 | self.assertEqual(app_state.successful_commands[0], 'foo') 24 | 25 | def test_clears_command_history(self): 26 | app_state.successful_commands = ['foo', 'bar'] 27 | app_state.clear_command_history() 28 | 29 | self.assertEqual(len(app_state.successful_commands), 0) 30 | -------------------------------------------------------------------------------- /objection/console/helpfiles/memory.search.txt: -------------------------------------------------------------------------------- 1 | Command: memory search 2 | 3 | Usage: memory search "" (optional: --string) (--offsets-only) 4 | 5 | Search the current processes' heap for a pattern. A pattern is represented by a 6 | byte sequence such as eb ff aa. It is also possible to specify wildcards such as 7 | eb ff ?? aa, indicating that you are looking for a pattern that starts with eb ff, 8 | has any other byte and then has aa. 9 | It is also possible to provide a raw string, which should be suffixed with the 10 | --string flag, indicating to the command that it should convert the string to 11 | bytes before executing the search. Wildcards are not supported in string searches. 12 | 13 | Output may be controlled primarily with the --offsets-only flag which indicates 14 | wether a small hexdump of matched memory regions should occur, or if only the 15 | matched offsets should be printed. 16 | 17 | Examples: 18 | memory search "41 41 41 41" 19 | memory search "41 ?? de ad" 20 | memory search "deadbeef" --string 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | 24 | # PyInstaller 25 | # Usually these files are written by a python script from a template 26 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 27 | *.manifest 28 | *.spec 29 | 30 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | 34 | # Unit test / coverage reports 35 | htmlcov/ 36 | .tox/ 37 | .coverage 38 | .coverage.* 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | *,cover 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | 57 | # extra 58 | settings.ini 59 | python/ 60 | .idea/ 61 | .DS_Store 62 | 63 | # Ignore the compiled agent. 64 | objection/agent.js 65 | -------------------------------------------------------------------------------- /tests/commands/ios/test_nsurlcredentialstorage.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.ios.nsurlcredentialstorage import dump 5 | from ...helpers import capture 6 | 7 | 8 | class TestNsusercredentialstorage(unittest.TestCase): 9 | @mock.patch('objection.state.connection.state_connection.get_api') 10 | def test_dump(self, mock_api): 11 | mock_api.return_value.ios_credential_storage.return_value = [{ 12 | 'protocol': 'https', 13 | 'host': 'foo.bar', 14 | 'port': '80', 15 | 'authMethod': 'NSURLAuthenticationMethodDefault', 16 | 'user': 'foo', 17 | 'password': 'bar', 18 | }] 19 | 20 | with capture(dump, []) as o: 21 | output = o 22 | 23 | expected_output = """Protocol Host Port Authentication Method User Password 24 | ---------- ------- ------ ----------------------- ------ ---------- 25 | https foo.bar 80 Default foo bar 26 | """ 27 | 28 | self.assertEqual(output, expected_output) 29 | -------------------------------------------------------------------------------- /objection/commands/http.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from ..commands.filemanager import pwd 4 | from ..state.connection import state_connection 5 | 6 | 7 | def start(args: list) -> None: 8 | """ 9 | Start's an http server, exposing the mobile devices filesystem. 10 | 11 | :param args: 12 | :return: 13 | """ 14 | 15 | port = 9000 16 | 17 | if len(args) > 0: 18 | port = int(args[0]) 19 | 20 | click.secho('Starting server on port {port}...'.format(port=port), dim=True) 21 | 22 | api = state_connection.get_api() 23 | api.http_server_start(pwd(), port) 24 | 25 | 26 | def stop(args: list) -> None: 27 | """ 28 | Stops the on device HTTP server 29 | 30 | :param args: 31 | :return: 32 | """ 33 | 34 | api = state_connection.get_api() 35 | api.http_server_stop() 36 | 37 | 38 | def status(args: list) -> None: 39 | """ 40 | Get the status of the HTTP server 41 | 42 | :param args: 43 | :return: 44 | """ 45 | 46 | api = state_connection.get_api() 47 | api.http_server_status() 48 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.set.return_value.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking set return_value 2 | 3 | Usage: android hooking set return_value "" "" 4 | 5 | Sets a methods return value to always be true / false. This could be 6 | a useful module to use in cases where generic SSL pinning or root 7 | detection / simulations are possible. 8 | 9 | If an overload is not specified, all overloads for the base methods 10 | will be modified. 11 | 12 | NOTE: This is only possible on methods that return a boolean. While 13 | methods that don't return booleans can be hooked, the results may be 14 | unpredictable. 15 | 16 | Examples: 17 | android hooking set return_value com.example.test.rootUtils.isRooted false 18 | android hooking set return_value com.example.test.rootUtils.isRooted "java.lang.String" false 19 | android hooking set return_value com.example.test.encryption.hasKey.overload("java.lang.String") true 20 | android hooking set return_value com.example.test.communication.setPinningType.overload("java.lang.String", "[B") false 21 | -------------------------------------------------------------------------------- /objection/api/script.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request, abort 2 | 3 | from objection.state.connection import state_connection 4 | 5 | bp = Blueprint('script', __name__, url_prefix='/script') 6 | 7 | 8 | @bp.route('/runonce', methods=('POST',)) 9 | def runonce(): 10 | """ 11 | Run an arbitrary script in the connected frida 12 | enabled device. 13 | 14 | Responses are JSON encoded by default, but can be raw by adding 15 | ?json=false as a query string parameter. 16 | 17 | :return: 18 | """ 19 | 20 | source = request.data.decode('utf-8') 21 | 22 | if len(source) <= 0: 23 | return abort(jsonify(message='Missing or empty script received')) 24 | 25 | try: 26 | 27 | # run the script 28 | response = state_connection.get_agent().single(source) 29 | 30 | if 'json' in request.args and request.args.get('json').lower() == 'false': 31 | return response 32 | 33 | except Exception as e: 34 | return abort(jsonify(message='Script failed to run: {e}'.format(e=str(e)))) 35 | 36 | return jsonify(response) 37 | -------------------------------------------------------------------------------- /objection/state/device.py: -------------------------------------------------------------------------------- 1 | class Device(object): 2 | """ Represents a mobile device """ 3 | pass 4 | 5 | 6 | class Android(Device): 7 | """ Represents Android specific configurations. """ 8 | 9 | name = 'android' 10 | path_separator = '/' 11 | 12 | 13 | class Ios(Device): 14 | """ Represents iOS specific configurations. """ 15 | 16 | name = 'ios' 17 | path_separator = '/' 18 | 19 | 20 | class DeviceState(object): 21 | """ A class representing the state of a device and its runtime. """ 22 | 23 | platform: Device 24 | version: str 25 | 26 | def set_version(self, v: str): 27 | """ 28 | Set the running OS version 29 | 30 | :param v: 31 | :return: 32 | """ 33 | 34 | self.version = v 35 | 36 | def set_platform(self, t: Device): 37 | """ 38 | Set's the device type 39 | 40 | :param t: 41 | :return: 42 | """ 43 | 44 | self.platform = t 45 | 46 | def __repr__(self) -> str: 47 | return f'' 48 | 49 | 50 | device_state = DeviceState() 51 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.bundles.list_frameworks.txt: -------------------------------------------------------------------------------- 1 | Command: ios bundles list_frameworks 2 | 3 | Usage: ios bundles list_frameworks (optional: --include-apple-frameworks) (optional: --full-path) 4 | 5 | Returns all of the application's bundles that represent frameworks. Only 6 | frameworks with one or more Objective-C classes in them are included. [1] 7 | 8 | Output includes the frameworks executable, bundle name and version. The path 9 | value is truncated by default, however, adding the --full-path flag would 10 | print the entire path to the framework. 11 | 12 | The --include-apple-frameworks flag signals the command to also output 13 | bundles that form part of the com.apple package namespace. By default this 14 | is hidden and only external frameworks not in the com.apple namespace is 15 | returned. 16 | 17 | [1] https://developer.apple.com/documentation/foundation/nsbundle/1408056-allframeworks?language=objc 18 | 19 | Examples: 20 | ios bundles list_frameworks 21 | ios bundles list_frameworks --include-apple-frameworks 22 | ios bundles list_frameworks --include-apple-frameworks --full-path 23 | ios bundles list_frameworks --full-path 24 | -------------------------------------------------------------------------------- /objection/console/helpfiles/ios.keychain.dump.txt: -------------------------------------------------------------------------------- 1 | Command: ios keychain dump 2 | 3 | Usage: ios keychain dump (optional: --json ) (optional: --smart) 4 | 5 | Extracts the keychain items for the current application. This is achieved by iterating 6 | over the keychain type classes available in iOS and populating a search dictionary 7 | with them. This dictionary is then used as a query to SecItemCopyMatching() and the 8 | results parsed. 9 | 10 | Use the --smart flag to attempt smart decoding of items in the keychain. By default, 11 | UTF8 string representations of data will be displayed. For a hex string of the data, 12 | use the --json flag which will indlude a 'dataHex' key. 13 | 14 | By default, only a small subset of each entry is displayed. For a more complete dump, 15 | use the --json flag. 16 | 17 | Items that will be accessible include everything stored with the entitlement group used 18 | during the patching/signing process. Providing a filename with the --json flag will dump 19 | all of the keychain attributes to the file specified for later inspection. 20 | 21 | Examples: 22 | ios keychain dump 23 | ios keychain dump --json keychain.json 24 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.hooking.watch.class_method.txt: -------------------------------------------------------------------------------- 1 | Command: android hooking watch class_method 2 | 3 | Usage: android hooking watch class_method 4 | (optional: --dump-args) (optional: --dump-backtrace) 5 | (optional: --dump-return) 6 | 7 | Hooks a specified class method and reports on invocations, together with 8 | the number of arguments that method was called with. This command will 9 | also hook all of the methods available overloads unless a specific 10 | overload is specified. 11 | 12 | If the --include-backtrace flag is provided, a full stack trace that 13 | lead to the methods invocation will also be dumped. This would aid in 14 | discovering who called the original method. 15 | 16 | Examples: 17 | android hooking watch class_method com.example.test.login 18 | android hooking watch class_method com.example.test.helper.executeQuery 19 | android hooking watch class_method com.example.test.helper.executeQuery "java.lang.String,java.lang.String" 20 | android hooking watch class_method com.example.test.helper.executeQuery --dump-backtrace 21 | android hooking watch class_method com.example.test.login --dump-args --dump-return 22 | -------------------------------------------------------------------------------- /objection/console/helpfiles/android.sslpinning.disable.txt: -------------------------------------------------------------------------------- 1 | Command: android sslpinning disable 2 | 3 | Usage: android sslpinning disable 4 | 5 | Attempts to disable SSL Pinning on Android devices. This is achieved by creating 6 | a new TrustManager that will accept any certificate irrespective of its validity, 7 | and providing that to calls to SSLContext.init(). Additionally, to support the 8 | OkHTTP v3 library, the okhttp3.CertificatePinner.check() method is replaced with 9 | one that will not throw an exception in the case of an invalid certificate being 10 | presented. 11 | 12 | With these two implementations, the following request libraries should have its 13 | pinning checks disabled with this command: 14 | 15 | - Traditional HttpsURLConnection 16 | - OkHTTP 17 | - Retrofit (Wraps OkHTTP) 18 | - Volley (Uses a TrustManager) 19 | - Picasso (Uses a TrustManager) 20 | 21 | If this method does not disable the applications SSL pinning implementation, 22 | then it may still be possible to bypass it via 'helper' methods commonly 23 | used by developers to help when testing in development / staging environments. 24 | Be on the lookout for classes / methods that relate to pinning that may simply 25 | return a boolean value. 26 | 27 | Examples: 28 | android sslpinning disable 29 | -------------------------------------------------------------------------------- /objection/state/api.py: -------------------------------------------------------------------------------- 1 | from ..api.app import create_app 2 | 3 | 4 | class ApiState(object): 5 | """ A class representing the state API for this app """ 6 | 7 | def __init__(self): 8 | self.core_api = create_app() 9 | self.blueprints = [] 10 | 11 | def append_api_blueprint(self, blueprint): 12 | """ 13 | Add extra blueprints to the API. 14 | 15 | This method would typically be called by the 16 | plugin loader to slot in endpoints that plugins 17 | may expose. 18 | 19 | :param blueprint: 20 | :return: 21 | """ 22 | 23 | self.blueprints.append(blueprint) 24 | 25 | def start(self, host: str, port: int, debug: bool = False): 26 | """ 27 | Starts the Flask-based API server after 28 | registering any extra blueprints that would 29 | typically have been sources from plugins. 30 | 31 | :param host: 32 | :param port: 33 | :param debug: 34 | :return: 35 | """ 36 | 37 | for bp in self.blueprints: 38 | self.core_api.register_blueprint(bp) 39 | 40 | self.core_api.run(host=host, port=port, debug=debug) 41 | 42 | 43 | api_state = ApiState() 44 | -------------------------------------------------------------------------------- /agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "objection", 3 | "version": "0.0.0", 4 | "description": "Runtime Mobile Exploration", 5 | "private": true, 6 | "type": "module", 7 | "main": "src/index.ts", 8 | "scripts": { 9 | "prepare": "npm run build", 10 | "build": "frida-compile src/index.ts -o ../objection/agent.js -T none", 11 | "watch": "frida-compile src/index.ts -o ../objection/agent.js -w", 12 | "lint": "tslint -c tslint.json 'src/**/*.ts'" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/sensepost/objection.git" 17 | }, 18 | "keywords": [ 19 | "frida", 20 | "runtime", 21 | "mobile", 22 | "security", 23 | "objection" 24 | ], 25 | "author": "Leon Jacobs", 26 | "license": "GPL-3.0", 27 | "bugs": { 28 | "url": "https://github.com/sensepost/objection/issues" 29 | }, 30 | "homepage": "https://github.com/sensepost/objection#readme", 31 | "dependencies": { 32 | "frida-fs": "^7.0.0", 33 | "frida-java-bridge": "^7", 34 | "frida-objc-bridge": "^8", 35 | "frida-screenshot": "^6", 36 | "macho-ts": "^0.1.0" 37 | }, 38 | "devDependencies": { 39 | "@types/frida-gum": "^19", 40 | "@types/node": "^24", 41 | "frida-compile": "^19", 42 | "tslint": "^6" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/commands/ios/test_plist.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.ios.plist import cat 5 | from objection.state.device import device_state, Ios 6 | from ...helpers import capture 7 | 8 | 9 | class TestPlist(unittest.TestCase): 10 | def test_cat_validates_arguments(self): 11 | with capture(cat, []) as o: 12 | output = o 13 | 14 | self.assertEqual(output, 'Usage: ios plist cat \n') 15 | 16 | @mock.patch('objection.state.connection.state_connection.get_api') 17 | def test_cat_with_full_path(self, mock_api): 18 | mock_api.return_value.ios_plist_read.return_value = 'foo' 19 | 20 | with capture(cat, ['/foo']) as o: 21 | output = o 22 | 23 | self.assertEqual(output, 'foo\n') 24 | 25 | @mock.patch('objection.state.connection.state_connection.get_api') 26 | @mock.patch('objection.commands.ios.plist.filemanager') 27 | def test_cat_with_relative(self, mock_file_manager, mock_api): 28 | mock_file_manager.pwd.return_value = '/baz' 29 | mock_api.return_value.ios_plist_read.return_value = 'foobar' 30 | 31 | device_state.platform = Ios 32 | 33 | with capture(cat, ['foo']) as o: 34 | output = o 35 | 36 | self.assertEqual(output, 'foobar\n') 37 | -------------------------------------------------------------------------------- /objection/commands/ios/cookies.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import click 4 | from tabulate import tabulate 5 | 6 | from objection.state.connection import state_connection 7 | 8 | 9 | def _should_dump_json(args: list) -> bool: 10 | """ 11 | Check if --json is part of the arguments. 12 | 13 | :param args: 14 | :return: 15 | """ 16 | 17 | return '--json' in args 18 | 19 | 20 | def get(args: list) -> None: 21 | """ 22 | Gets cookies using the iOS NSHTTPCookieStorage sharedHTTPCookieStorage 23 | and prints them to the screen. 24 | 25 | :param args: 26 | :return: 27 | """ 28 | 29 | api = state_connection.get_api() 30 | cookies = api.ios_cookies_get() 31 | 32 | if _should_dump_json(args): 33 | print(json.dumps(cookies, indent=4)) 34 | return 35 | 36 | if len(cookies) <= 0: 37 | click.secho('No cookies found') 38 | return 39 | 40 | click.secho(tabulate( 41 | [[ 42 | cookie['name'], 43 | cookie['value'], 44 | cookie['expiresDate'], 45 | cookie['domain'], 46 | cookie['path'], 47 | cookie['isSecure'], 48 | cookie['isHTTPOnly'] 49 | ] for cookie in cookies], headers=['Name', 'Value', 'Expires', 'Domain', 'Path', 'Secure', 'HTTPOnly'], 50 | )) 51 | -------------------------------------------------------------------------------- /tests/commands/test_frida_commands.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.frida_commands import _should_disable_exception_handler, frida_environment 5 | from ..helpers import capture 6 | 7 | 8 | class TestFridaCommands(unittest.TestCase): 9 | def test_detects_no_exception_handler_argument(self): 10 | result = _should_disable_exception_handler([ 11 | '--test', 12 | '--no-exception-handler' 13 | ]) 14 | 15 | self.assertTrue(result) 16 | 17 | @mock.patch('objection.state.connection.state_connection.get_api') 18 | def test_gets_frida_environment(self, mock_api): 19 | mock_api.return_value.env_frida.return_value = { 20 | 'arch': 'x64', 21 | 'debugger': True, 22 | 'heap': 6988464, 23 | 'platform': 'darwin', 24 | 'version': '12.0.3', 25 | 'runtime': 'DUK', 26 | } 27 | 28 | with capture(frida_environment, []) as o: 29 | output = o 30 | 31 | expected_output = """-------------------- ------- 32 | Frida Version 12.0.3 33 | Process Architecture x64 34 | Process Platform darwin 35 | Debugger Attached True 36 | Script Runtime DUK 37 | Frida Heap Size 6.7 MiB 38 | -------------------- ------- 39 | """ 40 | 41 | self.assertEqual(output, expected_output) 42 | -------------------------------------------------------------------------------- /objection/state/app.py: -------------------------------------------------------------------------------- 1 | class AppState(object): 2 | """ A class representing generic state variable for this app """ 3 | 4 | def __init__(self): 5 | self.debug_hooks = False 6 | self.debug = False 7 | self.api_host = '127.0.0.1' 8 | self.api_port = 8888 9 | self.successful_commands = [] 10 | 11 | def add_command_to_history(self, command: str) -> None: 12 | """ 13 | Adds a command to the list of successful commands. 14 | 15 | :param command: 16 | :return: 17 | """ 18 | 19 | if command not in self.successful_commands: 20 | self.successful_commands.append(command) 21 | 22 | def clear_command_history(self) -> None: 23 | """ 24 | Clears the list of successful commands recorded 25 | for this session. 26 | 27 | :return: 28 | """ 29 | 30 | self.successful_commands = [] 31 | 32 | def should_debug_hooks(self) -> bool: 33 | """ 34 | Returns if debugging of Frida hooks is needed. 35 | 36 | :return: 37 | """ 38 | 39 | return self.debug_hooks 40 | 41 | def should_debug(self) -> bool: 42 | """ 43 | 44 | Checks if debugging is enabled 45 | 46 | :return: 47 | """ 48 | 49 | return self.debug 50 | 51 | 52 | app_state = AppState() 53 | -------------------------------------------------------------------------------- /objection/commands/command_history.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from ..state.app import app_state 6 | 7 | 8 | def history(args: list) -> None: 9 | """ 10 | Lists the commands that have been run in the current session. 11 | 12 | :param args: 13 | :return: 14 | """ 15 | 16 | click.secho('Unique commands run in current session:', dim=True) 17 | 18 | for command in app_state.successful_commands: 19 | click.secho(command) 20 | 21 | 22 | def save(args: list) -> None: 23 | """ 24 | Save the current sessions command history to a file. 25 | 26 | :param args: 27 | :return: 28 | """ 29 | 30 | if len(args) <= 0: 31 | click.secho('Usage: commands save ', bold=True) 32 | return 33 | 34 | destination = os.path.expanduser(args[0]) if args[0].startswith('~') else args[0] 35 | 36 | with open(destination, 'w') as f: 37 | for command in app_state.successful_commands: 38 | f.write('{0}\n'.format(command)) 39 | 40 | click.secho('Saved commands to: {0}'.format(destination), fg='green') 41 | 42 | 43 | def clear(args: list) -> None: 44 | """ 45 | Clears the current sessions command history. 46 | 47 | :param args: 48 | :return: 49 | """ 50 | 51 | app_state.clear_command_history() 52 | click.secho('Command history cleared.', fg='green') 53 | -------------------------------------------------------------------------------- /tests/commands/ios/test_cookies.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.ios.cookies import get 5 | from ...helpers import capture 6 | 7 | 8 | class TestCookies(unittest.TestCase): 9 | @mock.patch('objection.state.connection.state_connection.get_api') 10 | def test_get_handles_empty_data(self, mock_api): 11 | mock_api.return_value.ios_cookies_get.return_value = [] 12 | 13 | with capture(get, []) as o: 14 | output = o 15 | 16 | self.assertEqual(output, 'No cookies found\n') 17 | 18 | @mock.patch('objection.state.connection.state_connection.get_api') 19 | def test_get(self, mock_api): 20 | mock_api.return_value.ios_cookies_get.return_value = [{ 21 | 'name': 'foo', 22 | 'value': 'bar', 23 | 'expiresDate': '01-01-1970 00:00:00 +0000', 24 | 'domain': 'foo.com', 25 | 'path': '/', 26 | 'isSecure': 'false', 27 | 'isHTTPOnly': 'true' 28 | }] 29 | 30 | with capture(get, []) as o: 31 | output = o 32 | 33 | expected_output = """Name Value Expires Domain Path Secure HTTPOnly 34 | ------ ------- ------------------------- -------- ------ -------- ---------- 35 | foo bar 01-01-1970 00:00:00 +0000 foo.com / false true 36 | """ 37 | 38 | self.assertEqual(output, expected_output) 39 | -------------------------------------------------------------------------------- /plugins/mettle/index.js: -------------------------------------------------------------------------------- 1 | rpc.exports = { 2 | initMettle: function (dlib) { 3 | const NSDocumentDirectory = 9; 4 | const NSUserDomainMask = 1 5 | const p = ObjC.classes.NSFileManager.defaultManager() 6 | .URLsForDirectory_inDomains_(NSDocumentDirectory, NSUserDomainMask).lastObject().path(); 7 | 8 | ObjC.schedule(ObjC.mainQueue, function () { 9 | Module.load(p + '/' + dlib); 10 | }); 11 | }, 12 | connectMettle: function(dlib, ip, port) { 13 | var source = "#include " + 14 | "char **getargs() {" + 15 | " char **argv = g_malloc(3 * sizeof(char*));" + 16 | " argv[0] = \"mettle\";" + 17 | " argv[1] = \"-u\";" + 18 | " argv[2] = \"tcp://{ip}:{port}\";" + 19 | " return argv;" + 20 | "}"; 21 | 22 | // update with the target ip:port 23 | source = source.replace("{ip}", ip); 24 | source = source.replace("{port}", port); 25 | 26 | const cm = new CModule(source); 27 | const argv = new NativeFunction(cm.getargs, 'pointer', []); 28 | 29 | const mettle = Process.getModuleByName(dlib); 30 | const mettleMainPtr = mettle.findExportByName('main'); 31 | console.log('Found mettle::main @ ' + mettleMainPtr); 32 | const mettleMain = new NativeFunction(mettleMainPtr, 'void', ['int', 'pointer']); 33 | 34 | // don't block the ui 35 | ObjC.schedule(ObjC.mainQueue, function () { 36 | console.log('Calling mettleMain()'); 37 | mettleMain(3, argv()); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /objection/commands/device.py: -------------------------------------------------------------------------------- 1 | import click 2 | from tabulate import tabulate 3 | 4 | from ..state.connection import state_connection 5 | from ..state.device import device_state, Android, Ios 6 | 7 | 8 | def get_environment(args: list = None) -> None: 9 | """ 10 | Get information about the current environment. 11 | 12 | This method will call the correct runtime specific 13 | method to get the information that it can. 14 | 15 | :param args: 16 | :return: 17 | """ 18 | 19 | if device_state.platform == Ios: 20 | _get_ios_environment() 21 | 22 | if device_state.platform == Android: 23 | _get_android_environment() 24 | 25 | 26 | def _get_ios_environment() -> None: 27 | """ 28 | Prints information about the iOS environment. 29 | 30 | This includes the current OS version as well as directories 31 | of interest for the current applications Documents, Library and 32 | main application bundle. 33 | 34 | :return: 35 | """ 36 | 37 | paths = state_connection.get_api().env_ios_paths() 38 | 39 | click.secho('') 40 | click.secho(tabulate(paths.items(), headers=['Name', 'Path'])) 41 | 42 | 43 | def _get_android_environment() -> None: 44 | """ 45 | Prints information about the Android environment. 46 | 47 | :return: 48 | """ 49 | 50 | paths = state_connection.get_api().env_android_paths() 51 | 52 | click.secho('') 53 | click.secho(tabulate(paths.items(), headers=['Name', 'Path'])) 54 | -------------------------------------------------------------------------------- /tests/commands/test_command_history.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.command_history import history, save, clear 5 | from objection.state.app import app_state 6 | from ..helpers import capture 7 | 8 | 9 | class TestCommandHistory(unittest.TestCase): 10 | def setUp(self): 11 | app_state.successful_commands = ['foo', 'bar'] 12 | 13 | def tearDown(self): 14 | app_state.successful_commands = [] 15 | 16 | def test_prints_command_history(self): 17 | with capture(history, []) as o: 18 | output = o 19 | 20 | expected_output = """Unique commands run in current session: 21 | foo 22 | bar 23 | """ 24 | 25 | self.assertEqual(output, expected_output) 26 | 27 | def test_save_validates_arguments(self): 28 | with capture(save, []) as o: 29 | output = o 30 | 31 | self.assertEqual(output, 'Usage: commands save \n') 32 | 33 | @mock.patch('objection.commands.command_history.open', create=True) 34 | def test_save_saves_to_file(self, mock_open): 35 | with capture(save, ['foo.rc']) as o: 36 | output = o 37 | 38 | self.assertEqual(output, 'Saved commands to: foo.rc\n') 39 | self.assertTrue(mock_open.called) 40 | 41 | def test_clear_clears_command_history(self): 42 | with capture(clear, []) as o: 43 | output = o 44 | 45 | self.assertEqual(output, 'Command history cleared.\n') 46 | self.assertEqual(len(app_state.successful_commands), 0) 47 | -------------------------------------------------------------------------------- /tests/commands/android/test_intents.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.android.intents import launch_activity, launch_service, analyze_implicit_intents 5 | from ...helpers import capture 6 | 7 | 8 | class TestIntents(unittest.TestCase): 9 | def test_launch_activity_validates_arguments(self): 10 | with capture(launch_activity, []) as o: 11 | output = o 12 | 13 | self.assertEqual(output, 'Usage: android intent launch_activity \n') 14 | 15 | @mock.patch('objection.state.connection.state_connection.get_api') 16 | def test_launch_activity(self, mock_api): 17 | launch_activity(['com.foo.bar']) 18 | 19 | self.assertTrue(mock_api.return_value.android_intent_start_activity.called) 20 | 21 | def test_launch_service_validates_arguments(self): 22 | with capture(launch_service, []) as o: 23 | output = o 24 | 25 | self.assertEqual(output, 'Usage: android intent launch_service \n') 26 | 27 | @mock.patch('objection.state.connection.state_connection.get_api') 28 | def test_launch_service(self, mock_api): 29 | launch_service(['com.foo.bar']) 30 | 31 | self.assertTrue(mock_api.return_value.android_intent_start_service.called) 32 | 33 | @mock.patch('objection.state.connection.state_connection.get_api') 34 | def test_analyze_implicit_intents(self, mock_api): 35 | analyze_implicit_intents([]) 36 | 37 | self.assertTrue(mock_api.return_value.android_intent_analyze.called) 38 | -------------------------------------------------------------------------------- /agent/src/android/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type JavaClass = any; 2 | 3 | export type JavaMethodsOverloadsResult = any; 4 | 5 | export type ClipboardManager = JavaClass | any; 6 | export type File = JavaClass | any; 7 | export type Throwable = JavaClass | any; 8 | export type PackageManager = JavaClass | any; 9 | export type ArrayMap = JavaClass | any; 10 | export type ActivityThread = JavaClass | any; 11 | export type Intent = JavaClass | any; 12 | export type KeyStore = JavaClass | any; 13 | export type KeyFactory = JavaClass | any; 14 | export type KeyInfo = JavaClass | any; 15 | export type SecretKeyFactory = JavaClass | any; 16 | export type X509TrustManager = JavaClass | any; 17 | export type SSLContext = JavaClass | any; 18 | export type CertificatePinner = JavaClass | any; 19 | export type PinningTrustManager = JavaClass | any; 20 | export type SSLCertificateChecker = JavaClass | any; 21 | export type TrustManagerImpl = JavaClass | any; 22 | export type ArrayList = JavaClass | any; 23 | export type JavaString = JavaClass | any; 24 | export type Runtime = JavaClass | any; 25 | export type IOException = JavaClass | any; 26 | export type InputStreamReader = JavaClass | any; 27 | export type BufferedReader = JavaClass | any; 28 | export type StringBuilder = JavaClass | any; 29 | export type Activity = JavaClass | any; 30 | export type ActivityClientRecord = JavaClass | any; 31 | export type Bitmap = JavaClass | any; 32 | export type ByteArrayOutputStream = JavaClass | any; 33 | export type CompressFormat = JavaClass | any; 34 | export type FridaOverload = { 35 | implementation: (...args: any[]) => any; 36 | apply: (thisArg: any, args: any[]) => any; 37 | }; -------------------------------------------------------------------------------- /tests/commands/android/test_keystore.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.android.keystore import entries, clear 5 | from ...helpers import capture 6 | 7 | 8 | class TestKeystore(unittest.TestCase): 9 | @mock.patch('objection.state.connection.state_connection.get_api') 10 | def test_entries_handles_empty_data(self, mock_api): 11 | mock_api.return_value.android_keystore_list.return_value = [] 12 | 13 | with capture(entries, []) as o: 14 | output = o 15 | 16 | expected_output = """Alias Key Certificate 17 | ------- ----- ------------- 18 | """ 19 | 20 | self.assertEqual(output, expected_output) 21 | 22 | @mock.patch('objection.state.connection.state_connection.get_api') 23 | def test_entries_handles(self, mock_api): 24 | mock_api.return_value.android_keystore_list.return_value = [{ 25 | 'alias': 'test', 26 | 'is_key': True, 27 | 'is_certificate': True 28 | }] 29 | 30 | with capture(entries, []) as o: 31 | output = o 32 | 33 | expected_output = """Alias Key Certificate 34 | ------- ----- ------------- 35 | test True True 36 | """ 37 | 38 | self.assertEqual(output, expected_output) 39 | 40 | @mock.patch('objection.state.connection.state_connection.get_api') 41 | @mock.patch('objection.commands.android.keystore.click.confirm') 42 | def test_clear(self, mock_confirm, mock_api): 43 | mock_confirm.return_value = True 44 | 45 | clear() 46 | 47 | self.assertTrue(mock_api.return_value.android_keystore_clear.called) 48 | -------------------------------------------------------------------------------- /tests/commands/test_plugin_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from unittest import mock 4 | 5 | from objection.commands.plugin_manager import load_plugin 6 | from ..helpers import capture 7 | 8 | 9 | class TestPluginManager(unittest.TestCase): 10 | def setUp(self): 11 | self.plugin_path = os.path.abspath(os.path.dirname(__file__) + '/../data/plugin') 12 | 13 | def test_load_plugin_validates_arguments(self): 14 | with capture(load_plugin, []) as o: 15 | output = o 16 | 17 | expected_output = 'Usage: plugin load ()\n' 18 | self.assertEqual(output, expected_output) 19 | 20 | @mock.patch('objection.commands.plugin_manager.os.path.exists') 21 | def test_load_plugin_validates_plugin_init_exists(self, mock_exists): 22 | mock_exists.return_value = False 23 | with capture(load_plugin, [self.plugin_path]) as o: 24 | output = o 25 | 26 | self.assertTrue('tests/data/plugin does not appear to be a valid plugin. Missing __init__.py' in output) 27 | 28 | @mock.patch('objection.utils.plugin.state_connection') 29 | def test_load_plugin_loads_plugin(self, mock_state_connection): 30 | with capture(load_plugin, [self.plugin_path]) as o: 31 | output = o 32 | 33 | from objection.console import commands 34 | self.assertTrue(commands.COMMANDS['plugin']['commands']['version']['commands']['info'] 35 | ['meta'] == 'Get the current Frida version') 36 | self.assertEqual('Loaded plugin: version\n', output) 37 | self.assertTrue(mock_state_connection.get_agent.called) 38 | -------------------------------------------------------------------------------- /objection/commands/ios/generate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from objection.state.connection import state_connection 6 | 7 | 8 | def clazz(args: list) -> None: 9 | """ 10 | Simply echoes the source for a generic Hook Manager 11 | sample for Objective-C hooks with Frida. 12 | 13 | :param args: 14 | :return: 15 | """ 16 | 17 | js_path = os.path.join( 18 | os.path.abspath(os.path.dirname(__file__)), 19 | '../../utils/assets', 'objchookmanager.js' 20 | ) 21 | 22 | with open(js_path, 'r') as f: 23 | click.secho(f.read(), dim=True) 24 | 25 | 26 | def simple(args: list) -> None: 27 | """ 28 | Generate simple hooks for all methods in a class. 29 | 30 | :param args: 31 | :return: 32 | """ 33 | 34 | if len(args) <= 0: 35 | click.secho('Usage: ios hooking generate simple ', bold=True) 36 | return 37 | 38 | classname = args[0] 39 | 40 | api = state_connection.get_api() 41 | methods = api.ios_hooking_get_class_methods(classname, False) 42 | 43 | if len(methods) <= 0: 44 | click.secho('No class / methods found') 45 | return 46 | 47 | click.secho("var target = ObjC.classes.{};".format(classname), dim=True) 48 | 49 | for method in methods: 50 | hook = """ 51 | Interceptor.attach(target['{method}'].implementation, { 52 | onEnter: function (args) { 53 | console.log('Entering {method}!'); 54 | }, 55 | onLeave: function (retval) { 56 | console.log('Leaving {method}'); 57 | }, 58 | }); 59 | """.replace('{method}', method) 60 | 61 | click.secho(hook, dim=True) 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[bug] Bug Description Here" 5 | labels: freshissue 6 | assignees: '' 7 | 8 | --- 9 | 10 | - Please, fill in all of the sections in this template. 11 | - Please read each section carefully. Each has a description of what information will help. 12 | - Windows support is limited. There is a good chance that a pull request would help! 13 | - The more information you give, the better. 14 | - Remove this section when you are done. 15 | 16 | **Describe the bug** 17 | A clear and concise description of what the bug is. 18 | 19 | **To Reproduce** 20 | Steps to reproduce the behavior: 21 | 1. Run command '...' 22 | 2. Run command '...' 23 | 24 | **Similar issues** 25 | Please link the issues in this repository that is similar to yours. 26 | 27 | For example: #358, #229 etc. 28 | 29 | **Expected behavior** 30 | A clear and concise description of what you expected to happen. 31 | 32 | **Evidence / Logs / Screenshots** 33 | 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: 34 | 35 | ```text 36 | objection --debug explore 37 | ``` 38 | 39 | **Environment (please complete the following information):** 40 | - Device: [e.g. iPhone6] 41 | - OS: [e.g. iOS8.1] 42 | - Frida Version [e.g. 22] 43 | - Objection Version [e.g. 1.6.2] 44 | 45 | **Application** 46 | If possible, please attach the target application where you can reproduce this bug to the issue. 47 | 48 | **Additional context** 49 | Add any other context about the problem here. 50 | -------------------------------------------------------------------------------- /objection/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from importlib import metadata 3 | from pathlib import Path 4 | 5 | import tomllib 6 | 7 | 8 | def _load_version() -> str: 9 | """ 10 | Prefer the installed package metadata and fall back to pyproject.toml 11 | when running from a checkout. 12 | """ 13 | 14 | try: 15 | return metadata.version("objection") 16 | except metadata.PackageNotFoundError: 17 | pyproject_path = Path(__file__).resolve().parent.parent / "pyproject.toml" 18 | try: 19 | with pyproject_path.open("rb") as f: 20 | return tomllib.load(f)["project"]["version"] 21 | except Exception: 22 | return "0.0.0" 23 | 24 | 25 | __version__ = _load_version() 26 | 27 | # helper containing a python 3 related warning 28 | # if this is run with python 2 29 | if sys.version_info < (3,): 30 | raise ImportError( 31 | ''' 32 | You are running objection {0} on Python 2 33 | 34 | Unfortunately objection {0} and above are not compatible with Python 2. 35 | That's a bummer; sorry about that. Make sure you have Python 3, pip >= and 36 | setuptools >= 24.2 to avoid these kinds of issues in the future: 37 | 38 | $ pip install pip setuptools --upgrade 39 | 40 | You could also setup a virtual Python 3 environment. 41 | 42 | $ pip install pip setuptools --upgrade 43 | $ pip install virtualenv 44 | $ virtualenv --python=python3 ~/virt-python3 45 | $ source ~/virt-python3/bin/activate 46 | 47 | This will make an isolated Python 3 installation available and active, ready 48 | to install and use objection. 49 | '''.format(__version__)) 50 | -------------------------------------------------------------------------------- /objection/commands/custom.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | import frida 5 | from prompt_toolkit import prompt 6 | from prompt_toolkit.lexers import PygmentsLexer 7 | from pygments.lexers.javascript import JavascriptLexer 8 | 9 | from ..state.connection import state_connection 10 | 11 | 12 | def evaluate(args: list) -> None: 13 | """ 14 | Evaluate JavaScript within the agent's context. 15 | 16 | :param args: 17 | :return: 18 | """ 19 | 20 | target_file = None 21 | 22 | # if we have an argument, let's assume it is a file path 23 | if len(args) > 0: 24 | 25 | target_file = args[0] 26 | p = os.path.expanduser(target_file) 27 | if os.path.exists(p): 28 | target_file = p 29 | else: 30 | click.secho('Could not find file {p}.'.format(p=target_file), fg='red') 31 | return 32 | 33 | if target_file: 34 | with open(target_file, 'r', encoding='utf-8') as f: 35 | javascript = ''.join(f.readlines()) 36 | else: 37 | javascript = prompt( 38 | multiline=True, lexer=PygmentsLexer(JavascriptLexer), 39 | bottom_toolbar='JavaScript edit mode. [ESC] and then [ENTER] to accept. [CTRL] + C to cancel.').strip() 40 | 41 | if len(javascript) <= 0: 42 | click.secho('JavaScript to evaluate appears empty. Skipping.', fg='yellow') 43 | return 44 | 45 | click.secho('JavaScript capture complete. Evaluating...', dim=True) 46 | try: 47 | state_connection.get_api().evaluate(javascript) 48 | except frida.core.RPCException as e: 49 | click.secho('Failed to load script: {}'.format(e), fg='red', bold=True) 50 | -------------------------------------------------------------------------------- /tests/data/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | __description__ = "An example plugin, also used in a UnitTest" 2 | 3 | from objection.utils.plugin import Plugin 4 | 5 | s = """ 6 | rpc.exports = { 7 | getInformation: function() { 8 | console.log('hello from Frida'); // direct output 9 | send('Incoming message'); // output via send for 'message' signal 10 | return Frida.version; // return type 11 | } 12 | } 13 | """ 14 | 15 | 16 | class VersionInfo(Plugin): 17 | """ VersionInfo is a sample plugin to get Frida version information """ 18 | 19 | def __init__(self, ns): 20 | """ 21 | Creates a new instance of the plugin 22 | 23 | :param ns: 24 | """ 25 | 26 | self.script_src = s 27 | # self.script_path = os.path.join(os.path.dirname(__file__), "script.js") 28 | 29 | implementation = { 30 | 'meta': 'Work with Frida version information', 31 | 'commands': { 32 | 'info': { 33 | 'meta': 'Get the current Frida version', 34 | 'exec': self.version 35 | } 36 | } 37 | } 38 | 39 | super().__init__(__file__, ns, implementation) 40 | 41 | self.inject() 42 | 43 | def version(self, args: list): 44 | """ 45 | Tests a plugin by calling an RPC export method 46 | called getInformation, and printing the result. 47 | 48 | :param args: 49 | :return: 50 | """ 51 | 52 | v = self.api.get_information() 53 | print('Frida version: {0}'.format(v)) 54 | 55 | 56 | namespace = 'version' 57 | plugin = VersionInfo 58 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=70.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "objection" 7 | version = "1.12.2" 8 | description = "Instrumented Mobile Pentest Framework" 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | license = "GPL-3.0-or-later" 12 | authors = [{name = "Leon Jacobs", email = "leon@sensepost.com"}] 13 | keywords = ["mobile", "instrumentation", "pentest", "frida", "hook"] 14 | classifiers = [ 15 | "Natural Language :: English", 16 | "Operating System :: OS Independent", 17 | "Programming Language :: Python :: 3 :: Only", 18 | "Programming Language :: JavaScript", 19 | ] 20 | dependencies = [ 21 | "click>=8.2.0", 22 | "delegator-py>=0.1.1", 23 | "flask>=3.0.0", 24 | "frida>=16.0.0", 25 | "frida-tools>=10.0.0", 26 | "litecli>=1.3.0", 27 | "packaging>=23.0", 28 | "prompt-toolkit>=3.0.30,<4.0.0", 29 | "pygments>=2.0.0", 30 | "requests>=2.32.0", 31 | "semver>=2", 32 | "setuptools>=70.0.0", 33 | "tabulate>=0.9.0", 34 | ] 35 | 36 | [project.urls] 37 | Homepage = "https://github.com/sensepost/objection" 38 | Repository = "https://github.com/sensepost/objection" 39 | "Bug Tracker" = "https://github.com/sensepost/objection/issues" 40 | 41 | [project.scripts] 42 | objection = "objection.console.cli:cli" 43 | 44 | [tool.setuptools] 45 | package-dir = {"" = "."} 46 | 47 | [tool.setuptools.packages.find] 48 | include = ["objection", "objection.*"] 49 | 50 | [tool.setuptools.package-data] 51 | objection = [ 52 | "console/helpfiles/*.txt", 53 | "utils/assets/*.jks", 54 | "utils/assets/*.js", 55 | "utils/assets/*.xml", 56 | "agent.js", 57 | ] 58 | -------------------------------------------------------------------------------- /agent/src/android/lib/libjava.ts: -------------------------------------------------------------------------------- 1 | import Java_bridge from "frida-java-bridge"; 2 | import { colors as c } from "../../lib/color.js"; 3 | 4 | let Java: typeof Java_bridge; 5 | // Compatibility with frida < 17 6 | if (globalThis.Java) { 7 | send(c.blackBright("Pre-v17 version of Frida detected. Attempting to use old bridge interface.")) 8 | Java = globalThis.Java 9 | } else { 10 | Java = Java_bridge 11 | } 12 | 13 | export { Java } 14 | 15 | // all Java calls need to be wrapped in a Java.perform(). 16 | // this helper just wraps that into a Promise that the 17 | // rpc export will sniff and resolve before returning 18 | // the result when its ready. 19 | export const wrapJavaPerform = (fn: any): Promise => { 20 | return new Promise((resolve, reject) => { 21 | Java.perform(() => { 22 | try { 23 | resolve(fn()); 24 | } catch (e) { 25 | reject(e); 26 | } 27 | }); 28 | }); 29 | }; 30 | 31 | export const getApplicationContext = (): any => { 32 | const ActivityThread = Java.use("android.app.ActivityThread"); 33 | const currentApplication = ActivityThread.currentApplication(); 34 | 35 | return currentApplication.getApplicationContext(); 36 | }; 37 | 38 | // A helper method to access the R class for the app. 39 | // Typical usage within an app would be something like: 40 | // R.id.content_frame. 41 | // 42 | // Using this method, the above example would be: 43 | // R("content_frame", "id") 44 | export const R = (name: string, type: string): any => { 45 | const context = getApplicationContext(); 46 | // https://github.com/bitpay/android-sdk/issues/14#issue-202495610 47 | return context.getResources().getIdentifier(name, type, context.getPackageName()); 48 | }; 49 | -------------------------------------------------------------------------------- /objection/commands/android/generate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from objection.state.connection import state_connection 6 | 7 | 8 | def clazz(args: list) -> None: 9 | """ 10 | Simply echoes the source for a generic Hook Manager 11 | sample for Objective-C hooks with Frida. 12 | 13 | :param args: 14 | :return: 15 | """ 16 | 17 | js_path = os.path.join( 18 | os.path.abspath(os.path.dirname(__file__)), 19 | '../../utils/assets', 'javahookmanager.js' 20 | ) 21 | 22 | with open(js_path, 'r') as f: 23 | click.secho(f.read(), dim=True) 24 | 25 | 26 | def simple(args: list) -> None: 27 | """ 28 | Generate simple hooks for all methods in a Java class. 29 | 30 | :param args: 31 | :return: 32 | """ 33 | 34 | if len(args) <= 0: 35 | click.secho('Usage: android hooking generate simple ', bold=True) 36 | return 37 | 38 | classname = args[0] 39 | 40 | api = state_connection.get_api() 41 | methods = api.android_hooking_get_class_methods(classname, False) 42 | 43 | if len(methods) <= 0: 44 | click.secho('No class / methods found') 45 | return 46 | 47 | # nasty! :D 48 | unique_methods = set([x.split('(')[0].split('.')[-1] for x in methods]) 49 | 50 | for method in unique_methods: 51 | hook = """ 52 | Java.perform(function() { 53 | var clazz = Java.use('{clazz}'); 54 | clazz.{method}.implementation = function() { 55 | 56 | // 57 | 58 | return clazz.{method}.apply(this, arguments); 59 | } 60 | }); 61 | """.replace('{clazz}', classname).replace('{method}', method) 62 | 63 | click.secho(hook, dim=True) 64 | -------------------------------------------------------------------------------- /agent/src/ios/binarycookies.ts: -------------------------------------------------------------------------------- 1 | import { ObjC } from "../ios/lib/libobjc.js"; 2 | import { IIosCookie } from "./lib/interfaces.js"; 3 | import { 4 | NSArray, 5 | NSData, 6 | NSHTTPCookieStorage 7 | } from "./lib/types.js"; 8 | 9 | 10 | export const get = (): IIosCookie[] => { 11 | 12 | // -- Sample Objective-C 13 | // 14 | // NSHTTPCookieStorage *cs = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 15 | // NSArray *cookies = [cs cookies]; 16 | const cookies: IIosCookie[] = []; 17 | 18 | const HTTPCookieStorage = ObjC.classes.NSHTTPCookieStorage; 19 | const cookieStore: NSHTTPCookieStorage = HTTPCookieStorage.sharedHTTPCookieStorage(); 20 | const cookieJar: NSArray = cookieStore.cookies(); 21 | 22 | if (cookieJar.count() <= 0) { 23 | return cookies; 24 | } 25 | 26 | for (let i = 0; i < cookieJar.count(); i++) { 27 | 28 | // get the actual cookie from the jar 29 | const cookie: NSData = cookieJar.objectAtIndex_(i); 30 | 31 | // 34 | const cookieData: IIosCookie = { 35 | domain: cookie.domain().toString(), 36 | expiresDate: cookie.expiresDate() ? cookie.expiresDate().toString() : "null", 37 | isHTTPOnly: cookie.isHTTPOnly().toString(), 38 | isSecure: cookie.isSecure().toString(), 39 | name: cookie.name().toString(), 40 | path: cookie.path().toString(), 41 | value: cookie.value().toString(), 42 | version: cookie.version().toString(), 43 | }; 44 | 45 | cookies.push(cookieData); 46 | } 47 | 48 | return cookies; 49 | }; 50 | -------------------------------------------------------------------------------- /objection/api/rpc.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request, abort 2 | 3 | from objection.state.connection import state_connection 4 | from ..utils.helpers import to_snake_case 5 | 6 | bp = Blueprint('rpc', __name__, url_prefix='/rpc') 7 | 8 | 9 | @bp.route('/invoke/', methods=('GET', 'POST')) 10 | def invoke(method): 11 | """ 12 | Bridge a call to the Frida RPC. Endpoints may be sourced from 13 | the agent's RPC exports. 14 | 15 | Responses are JSON encoded by default, but can be raw by adding 16 | ?json=false as a query string parameter. 17 | 18 | :param method: 19 | :return: 20 | """ 21 | 22 | method = to_snake_case(method) 23 | 24 | # post requests require a little more validation, so do that 25 | if request.method == 'POST': 26 | 27 | # ensure we have some JSON formatted post data 28 | post_data = request.get_json(force=True, silent=True) 29 | if not post_data: 30 | return abort(jsonify(message='POST request without a valid body received')) 31 | 32 | try: 33 | 34 | rpc = state_connection.get_api() 35 | 36 | except Exception as e: 37 | return abort(jsonify(message='Failed to talk to the Frida RPC: {e}'.format(e=str(e)))) 38 | 39 | try: 40 | 41 | # invoke the method based on the http request type 42 | if request.method == 'POST': 43 | response = getattr(rpc, method)(*post_data.values()) 44 | 45 | if request.method == 'GET': 46 | response = getattr(rpc, method)() 47 | 48 | if 'json' in request.args and request.args.get('json').lower() == 'false': 49 | return response 50 | 51 | except Exception as e: 52 | return abort(jsonify(message='Failed to call method: {e}'.format(e=str(e)))) 53 | 54 | return jsonify(response) 55 | -------------------------------------------------------------------------------- /agent/src/lib/color.ts: -------------------------------------------------------------------------------- 1 | export namespace colors { 2 | 3 | const base: string = `\x1B[%dm`; 4 | const reset: string = `\x1b[39m`; 5 | 6 | // return an ansified string 7 | export const ansify = (color: number, ...msg: string[]): string => 8 | base.replace(`%d`, color.toString()) + msg.join(``) + reset; 9 | 10 | // tslint:disable-next-line:no-eval 11 | export const clog = (color: number, ...msg: string[]): void => eval("console").log(ansify(color, ...msg)); 12 | // tslint:disable-next-line:no-eval 13 | export const log = (...msg: string[]): void => eval("console").log(msg.join(``)); 14 | 15 | // log based on a quiet flag 16 | export const qlog = (quiet: boolean, ...msg: string[]): void => { 17 | if (quiet === false) { 18 | log(...msg); 19 | } 20 | }; 21 | 22 | export const black = (message: string) => ansify(30, message); 23 | export const blue = (message: string) => ansify(34, message); 24 | export const cyan = (message: string) => ansify(36, message); 25 | export const green = (message: string) => ansify(32, message); 26 | export const magenta = (message: string) => ansify(35, message); 27 | export const red = (message: string) => ansify(31, message); 28 | export const white = (message: string) => ansify(37, message); 29 | export const yellow = (message: string) => ansify(33, message); 30 | export const blackBright = (message: string) => ansify(90, message); 31 | export const redBright = (message: string) => ansify(91, message); 32 | export const greenBright = (message: string) => ansify(92, message); 33 | export const yellowBright = (message: string) => ansify(93, message); 34 | export const blueBright = (message: string) => ansify(94, message); 35 | export const cyanBright = (message: string) => ansify(96, message); 36 | export const whiteBright = (message: string) => ansify(97, message); 37 | } 38 | -------------------------------------------------------------------------------- /objection/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import threading 4 | 5 | import click 6 | 7 | from .update_checker import check_version 8 | 9 | 10 | class MakeFileHandler(logging.FileHandler): 11 | """ 12 | Wrapper Class around the builtin Filehandler. 13 | """ 14 | 15 | def __init__(self, filename: str, mode: str = 'a', encoding: str = None, delay: bool = False) -> None: 16 | """ 17 | The original FileHandler's init is called, right after the 18 | directory used to store the objection logfile is created. 19 | 20 | :param filename: 21 | :param mode: 22 | :param encoding: 23 | :param delay: 24 | """ 25 | 26 | os.makedirs(os.path.dirname(filename), exist_ok=True) 27 | logging.FileHandler.__init__(self, filename, mode, encoding, delay) 28 | 29 | 30 | def new_secho(text: str, **kwargs) -> None: 31 | """ 32 | Patch the secho method from the click package so that 33 | the text that should be echoed is logged first. 34 | 35 | :param text: 36 | :param kwargs: 37 | :return: 38 | """ 39 | 40 | logging.info(text) 41 | real_secho(text, **kwargs) 42 | 43 | 44 | # Configure the logging used in objection 45 | logger = logging.getLogger() 46 | handler = MakeFileHandler(os.path.expanduser('~/.objection/objection.log')) 47 | formatter = logging.Formatter('%(asctime)s %(levelname)-8s\n%(message)s\n') 48 | handler.setFormatter(formatter) 49 | logger.addHandler(handler) 50 | logger.setLevel(logging.DEBUG) 51 | 52 | # monkey patch secho to log to file 53 | real_secho = click.secho 54 | click.secho = new_secho 55 | 56 | try: 57 | # kick off a background thread to check the version of objection 58 | threading.Thread(target=check_version).start() 59 | except Exception: 60 | pass 61 | -------------------------------------------------------------------------------- /agent/src/ios/binary.ts: -------------------------------------------------------------------------------- 1 | import macho from "macho-ts"; 2 | 3 | import * as iosfilesystem from "./filesystem.js"; 4 | import { IBinaryModuleDictionary } from "./lib/interfaces.js"; 5 | 6 | 7 | const isEncrypted = (cmds: any[]): boolean => { 8 | for (const cmd of cmds) { 9 | // https://opensource.apple.com/source/cctools/cctools-921/include/mach-o/loader.h.auto.html 10 | // struct encryption_info_command { 11 | // [ ... ] 12 | // uint32_t cryptid; /* which enryption system, 0 means not-encrypted yet */ 13 | // }; 14 | if (cmd.type === "encryption_info" || cmd.type === "encryption_info_64") { 15 | if (cmd.id !== 0) { 16 | return true; 17 | } 18 | } 19 | } 20 | return false; 21 | }; 22 | 23 | export const info = (): IBinaryModuleDictionary => { 24 | const modules = Process.enumerateModules(); 25 | const parsedModules: IBinaryModuleDictionary = {}; 26 | 27 | modules.forEach((a) => { 28 | if (!a.path.includes(".app")) { 29 | return; 30 | } 31 | 32 | const imports: Set = new Set(a.enumerateImports().map((i) => i.name)); 33 | const fb = iosfilesystem.readFile(a.path); 34 | if (typeof(fb) == 'string') { 35 | return; 36 | } 37 | 38 | try { 39 | const exe = macho.parse(fb); 40 | 41 | parsedModules[a.name] = { 42 | arc: imports.has("objc_release"), 43 | canary: imports.has("__stack_chk_fail"), 44 | encrypted: isEncrypted(exe.cmds), 45 | pie: exe.flags.pie ? true : false, 46 | rootSafe: exe.flags.root_safe ? true : false, 47 | stackExec: exe.flags.allow_stack_execution ? true : false, 48 | type: exe.filetype, 49 | }; 50 | 51 | } catch (e) { 52 | // ignore any errors. especially ones where 53 | // the target path is not a mach-o 54 | } 55 | }); 56 | 57 | return parsedModules; 58 | }; 59 | -------------------------------------------------------------------------------- /agent/src/android/lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { default as JavaTypes } from "frida-java-bridge"; 2 | 3 | export interface IAndroidFilesystem { 4 | files: any; 5 | path: string; 6 | readable: boolean; 7 | writable: boolean; 8 | } 9 | 10 | export interface IExecutedCommand { 11 | command: string; 12 | stdOut: string; 13 | stdErr: string; 14 | } 15 | 16 | export interface IKeyStoreEntry { 17 | alias: string; 18 | is_certificate: boolean; 19 | is_key: boolean; 20 | } 21 | 22 | export interface ICurrentActivityFragment { 23 | activivity: string | null; 24 | fragment: string | null; 25 | } 26 | 27 | export interface IHeapClassDictionary { 28 | [index: string]: IHeapObject[]; 29 | } 30 | 31 | export interface IHeapObject { 32 | hashcode: number; 33 | instance: JavaTypes.Wrapper; 34 | } 35 | 36 | export interface IHeapNormalised { 37 | hashcode: number; 38 | classname: string; 39 | tostring: string; 40 | } 41 | 42 | export interface IJavaField { 43 | name: string; 44 | value: string; 45 | } 46 | 47 | export interface IKeyStoreDetail { 48 | keyAlgorithm?: string; 49 | keySize?: string; 50 | blockModes?: string; 51 | digests?: string; 52 | encryptionPaddings?: string; 53 | keyValidityForConsumptionEnd?: string; 54 | keyValidityForOriginationEnd?: string; 55 | keyValidityStart?: string; 56 | keystoreAlias?: string; 57 | origin?: string; 58 | purposes?: string; 59 | signaturePaddings?: string; 60 | userAuthenticationValidityDurationSeconds?: string; 61 | isInsideSecureHardware?: string; 62 | isInvalidatedByBiometricEnrollment?: string; 63 | isUserAuthenticationRequired?: string; 64 | isUserAuthenticationRequirementEnforcedBySecureHardware?: string; 65 | isUserAuthenticationValidWhileOnBody?: string; 66 | // "crashy" fields 67 | isTrustedUserPresenceRequired?: string; 68 | isUserConfirmationRequired?: string; 69 | } -------------------------------------------------------------------------------- /agent/src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import util from "util"; 2 | import { colors as c } from "./color.js"; 3 | 4 | // sure, TS does not support this, but meh. 5 | // https://www.reddit.com/r/typescript/comments/87i59e/beginner_advice_strongly_typed_function_for/ 6 | export function reverseEnumLookup(enumType: T, value: string): string { 7 | for (const key in enumType) { 8 | 9 | if (Object.hasOwnProperty.call(enumType, key) && enumType[key] as any === value) { 10 | return key; 11 | } 12 | } 13 | 14 | return ""; 15 | } 16 | 17 | // converts a hexstring to a bytearray 18 | export const hexStringToBytes = (str: string): Uint8Array => { 19 | var a: number[] = []; 20 | for (let i = 0, len = str.length; i < len; i += 2) { 21 | a.push(parseInt(str.substring(i, i+2), 16)); 22 | } 23 | 24 | return new Uint8Array(a); 25 | }; 26 | 27 | // only send if quiet is not true 28 | export const qsend = (quiet: boolean, message: any): void => { 29 | if (quiet === false) { 30 | send(message); 31 | } 32 | }; 33 | 34 | // send a preformated dict 35 | export const fsend = (ident: number, hook: string, message: any): void => { 36 | send( 37 | c.blackBright(`[${ident}] `) + 38 | c.magenta(`[${hook}]`) + 39 | printArgs(message) 40 | ); 41 | }; 42 | 43 | // a small helper method to use util to dump 44 | export const debugDump = (o: any, depth: number = 2): void => { 45 | c.log(c.blackBright("\n[start debugDump]")); 46 | c.log(util.inspect(o, true, depth, true)); 47 | c.log(c.blackBright("[end debugDump]\n")); 48 | }; 49 | 50 | // a small helper method to format JSON nicely before printing 51 | function printArgs(args: {[key: string]:object}): string { 52 | let printableString: string = " (\n"; 53 | for (const arg in args) { 54 | printableString += ` ${c.blue(arg)} : ${args[arg]}\n`; 55 | } 56 | printableString += ")"; 57 | return printableString; 58 | } -------------------------------------------------------------------------------- /objection/commands/android/intents.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from objection.state.connection import state_connection 4 | from objection.utils.helpers import clean_argument_flags 5 | 6 | def _should_dump_backtrace(args: list = None) -> bool: 7 | """ 8 | Check if --dump-backtrace is part of the arguments. 9 | 10 | :param args: 11 | :return: 12 | """ 13 | 14 | return '--dump-backtrace' in args 15 | 16 | def analyze_implicit_intents(args: list) -> None: 17 | """ 18 | Analyzes implicit intents in hooked methods. 19 | """ 20 | api = state_connection.get_api() 21 | should_backtrace = _should_dump_backtrace(args) 22 | 23 | api.android_intent_analyze(should_backtrace) 24 | if not should_backtrace: 25 | click.secho('Started implicit intent analysis', bold=True) 26 | else: 27 | click.secho('Started implicit intent analysis with backtrace', bold=True) 28 | 29 | 30 | def launch_activity(args: list) -> None: 31 | """ 32 | Launches an activity class using an Android Intent 33 | 34 | :param args: 35 | :return: 36 | """ 37 | 38 | if len(clean_argument_flags(args)) < 1: 39 | click.secho('Usage: android intent launch_activity ', bold=True) 40 | return 41 | 42 | intent_class = args[0] 43 | 44 | api = state_connection.get_api() 45 | api.android_intent_start_activity(intent_class) 46 | 47 | 48 | def launch_service(args: list) -> None: 49 | """ 50 | Launches an exported service using an Android Intent 51 | 52 | :param args: 53 | :return: 54 | """ 55 | 56 | if len(clean_argument_flags(args)) < 1: 57 | click.secho('Usage: android intent launch_service ', bold=True) 58 | return 59 | 60 | intent_class = args[0] 61 | 62 | api = state_connection.get_api() 63 | api.android_intent_start_service(intent_class) 64 | -------------------------------------------------------------------------------- /plugins/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | from objection.utils.plugin import Plugin 4 | 5 | 6 | class ApiLoader(Plugin): 7 | """ 8 | ApiLoader is a plugin that includes an API. 9 | 10 | This is just an example plugin to demonstrate how you 11 | could extend the objection API add your own endpoints. 12 | 13 | Since this plugins namespace is called api, the urls in 14 | our http_api method will therefore be: 15 | http://localhost/api/ping 16 | http://localhost/api/pong 17 | 18 | For more information on Flask blueprints, check out the 19 | documentation here: 20 | https://flask.palletsprojects.com/en/1.1.x/blueprints/ 21 | """ 22 | 23 | def __init__(self, ns): 24 | """ 25 | Creates a new instance of the plugin 26 | 27 | :param ns: 28 | """ 29 | 30 | implementation = {} 31 | 32 | super().__init__(__file__, ns, implementation) 33 | 34 | self.inject() 35 | 36 | def http_api(self) -> Blueprint: 37 | """ 38 | The API endpoints for this plugin. 39 | 40 | :return: 41 | """ 42 | 43 | # sets the uri path to /api in this case 44 | bp = Blueprint(self.namespace, __name__, url_prefix='/' + self.namespace) 45 | 46 | # the endpoint with this function as the logic will be 47 | # /api/ping. 48 | # that's because the url_prefix is the namespace name, 49 | # and the endpoint is /ping 50 | @bp.route('/ping', methods=('GET', 'POST')) 51 | def ping(): 52 | return 'pong' 53 | 54 | @bp.route('/version', methods=('GET', 'POST')) 55 | def version(): 56 | # call getVersion via the Frida RPC for this plugins 57 | # agent, defined in index.js 58 | return self.api.get_version() 59 | 60 | return bp 61 | 62 | 63 | namespace = 'api' 64 | plugin = ApiLoader 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📱objection - Runtime Mobile Exploration 2 | 3 | `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. 4 | 5 | [![Twitter](https://img.shields.io/badge/twitter-%40leonjza-blue.svg)](https://twitter.com/leonjza) 6 | [![PyPi](https://badge.fury.io/py/objection.svg)](https://pypi.python.org/pypi/objection) 7 | [![Black Hat Arsenal](https://raw.githubusercontent.com/toolswatch/badges/master/arsenal/europe/2017.svg?sanitize=true)](https://www.blackhat.com/eu-17/arsenal-overview.html) 8 | [![Black Hat Arsenal](https://raw.githubusercontent.com/toolswatch/badges/master/arsenal/usa/2019.svg?sanitize=true)](https://www.blackhat.com/us-19/arsenal-overview.html) 9 | 10 | objection 11 | 12 | - Supports both iOS and Android. 13 | - Inspect and interact with container file systems. 14 | - Bypass SSL pinning. 15 | - Dump keychains. 16 | - Perform memory related tasks, such as dumping & patching. 17 | - Explore and manipulate objects on the heap. 18 | - And much, much [more](https://github.com/sensepost/objection/wiki/Features)... 19 | 20 | Screenshots are available in the [wiki](https://github.com/sensepost/objection/wiki/Screenshots). 21 | 22 | ## installation 23 | 24 | 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`. 25 | 26 | For more detailed update and installation instructions, please refer to the wiki page [here](https://github.com/sensepost/objection/wiki/Installation). 27 | 28 | ## license 29 | 30 | `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/). 31 | -------------------------------------------------------------------------------- /objection/commands/plugin_manager.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import os 3 | import traceback 4 | import uuid 5 | 6 | import click 7 | 8 | from ..utils.plugin import Plugin as PluginType 9 | 10 | 11 | def load_plugin(args: list = None) -> None: 12 | """ 13 | Loads an objection plugin. 14 | 15 | :param args: 16 | :return: 17 | """ 18 | 19 | if len(args) <= 0: 20 | click.secho('Usage: plugin load ()', bold=True) 21 | return 22 | 23 | path = os.path.abspath(args[0]) 24 | if os.path.isdir(path): 25 | path = os.path.join(path, '__init__.py') 26 | 27 | if not os.path.exists(path): 28 | click.secho('[plugin] {0} does not appear to be a valid plugin. Missing __init__.py'.format( 29 | os.path.dirname(path)), fg='red', dim=True) 30 | return 31 | 32 | spec = importlib.util.spec_from_file_location(str(uuid.uuid4())[:8], path) 33 | plugin = importlib.util.module_from_spec(spec) 34 | spec.loader.exec_module(plugin) 35 | 36 | namespace = plugin.namespace 37 | if len(args) >= 2: 38 | namespace = args[1] 39 | 40 | plugin.__name__ = namespace 41 | 42 | # try and load the plugin (aka: run its __init__) 43 | try: 44 | 45 | instance = plugin.plugin(namespace) 46 | assert isinstance(instance, PluginType) 47 | 48 | except AssertionError: 49 | click.secho('Failed to load plugin \'{0}\'. Invalid plugin type.'.format(namespace), fg='red', bold=True) 50 | return 51 | 52 | except Exception as e: 53 | click.secho('Failed to load plugin \'{0}\' with error: {1}'.format(namespace, str(e)), fg='red', bold=True) 54 | click.secho('{0}'.format(traceback.format_exc()), dim=True) 55 | return 56 | 57 | from ..console import commands 58 | commands.COMMANDS['plugin']['commands'][instance.namespace] = instance.implementation 59 | click.secho('Loaded plugin: {0}'.format(plugin.__name__), bold=True) 60 | -------------------------------------------------------------------------------- /agent/src/generic/memory.ts: -------------------------------------------------------------------------------- 1 | import { colors } from "../lib/color.js" 2 | 3 | export const listModules = (): Module[] => { 4 | return Process.enumerateModules(); 5 | }; 6 | 7 | export const listExports = (name: string): ModuleExportDetails[] => { 8 | const mod: Module[] = Process.enumerateModules().filter((m) => m.name === name); 9 | if (mod.length <= 0) { 10 | return []; 11 | } 12 | return mod[0].enumerateExports(); 13 | }; 14 | 15 | export const listRanges = (protection: string = "rw-"): RangeDetails[] => { 16 | return Process.enumerateRanges(protection); 17 | }; 18 | 19 | export const dump = (address: string, size: number): ArrayBuffer => { 20 | // Originally part of Frida <=11 but got removed in 12. 21 | // https://github.com/frida/frida-python/commit/72899a4315998289fb171149d62477ba7d1fcb91 22 | const data = new NativePointer(address).readByteArray(size); 23 | if (data) { 24 | return data; 25 | } 26 | else { 27 | return new ArrayBuffer(0); 28 | } 29 | }; 30 | 31 | export const search = (pattern: string, onlyOffsets: boolean = false): string[] => { 32 | const addresses = listRanges("rw-") 33 | .map((range) => { 34 | return Memory.scanSync(range.base, range.size, pattern) 35 | .map((match) => { 36 | if (!onlyOffsets) { 37 | colors.log(hexdump(match.address, { 38 | ansi: true, 39 | header: false, 40 | length: 48, 41 | })); 42 | } 43 | return match.address.toString(); 44 | }); 45 | }).filter((m) => m.length !== 0); 46 | 47 | if (addresses.length <= 0) { 48 | return []; 49 | } 50 | 51 | return addresses.reduce((a, b) => a.concat(b)); 52 | }; 53 | 54 | export const replace = (pattern: string, replace: number[]): string[] => { 55 | return search(pattern, true).map((match) => { 56 | write(match, replace); 57 | return match; 58 | }) 59 | }; 60 | 61 | export const write = (address: string, value: number[]): void => { 62 | new NativePointer(address).writeByteArray(value); 63 | }; 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Objection 2 | 3 | First off, thanks for taking the time to contribute! 🎉💥 4 | 5 | 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. 6 | 7 | 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. 8 | 9 | Thanks! 🤘 10 | 11 | ## Code Structure 12 | 13 | Objection consists of two major parts. The Python command line environment and the TypeScript agent. Both of these parts live in this single, monorepo. 14 | 15 | - The Python command line lives [here](https://github.com/sensepost/objection/tree/master/objection). 16 | - The TypeScript agent lives [here](https://github.com/sensepost/objection/tree/master/agent). 17 | 18 | ## Environment Setup 19 | 20 | Whether you want to contribute to the TypeScript agent or the Python command line, both components would require some setup. 21 | 22 | ### Python Command Line 23 | 24 | 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. 25 | 26 | ### TypeScript Agent 27 | 28 | 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. 29 | 30 | For more information on developing for the agent, please see the Wiki article [here](https://github.com/sensepost/objection/wiki/Agent-Development-Environment). 31 | -------------------------------------------------------------------------------- /agent/src/android/clipboard.ts: -------------------------------------------------------------------------------- 1 | import { colors as c } from "../lib/color.js"; 2 | import { 3 | getApplicationContext, 4 | wrapJavaPerform, 5 | Java 6 | } from "./lib/libjava.js"; 7 | import { ClipboardManager } from "./lib/types.js"; 8 | 9 | export const monitor = (): Promise => { 10 | // -- Sample Java 11 | // 12 | // ClipboardManager f = (ClipboardManager)getApplicationContext().getSystemService(CLIPBOARD_SERVICE); 13 | // ClipData.Item i = f.getPrimaryClip().getItemAt(0); 14 | // Log.e("t", "?:" + i.getText()); 15 | 16 | send(`${c.yellowBright("Warning!")} This module is still broken. A pull request fixing it would be awesome!`); 17 | 18 | // https://developer.android.com/reference/android/content/Context.html#CLIPBOARD_SERVICE 19 | const CLIPBOARD_SERVICE: string = "clipboard"; 20 | 21 | // a variable for clipboard text 22 | let data: string; 23 | 24 | return wrapJavaPerform(() => { 25 | 26 | const clipboardManager: ClipboardManager = Java.use("android.content.ClipboardManager"); 27 | const context = getApplicationContext(); 28 | const clipboardHandle = context.getApplicationContext().getSystemService(CLIPBOARD_SERVICE); 29 | const cp = Java.cast(clipboardHandle, clipboardManager); 30 | 31 | setInterval(() => { 32 | 33 | const primaryClip = cp.getPrimaryClip(); 34 | 35 | // Check if there is at least some data 36 | if (primaryClip == null || primaryClip.getItemCount() <= 0) { 37 | return; 38 | } 39 | 40 | // If we have managed to get the primary clipboard and there are 41 | // items stored in it, process an update. 42 | const currentString = primaryClip.getItemAt(0).coerceToText(context).toString(); 43 | 44 | // If the data is the same, just stop. 45 | if (data === currentString) { 46 | return; 47 | } 48 | 49 | // Update the data with the new string and report back. 50 | data = currentString; 51 | 52 | send(`${c.blackBright(`[pasteboard-monitor]`)} Data: ${c.greenBright(data.toString())}`); 53 | 54 | }, 1000 * 5); 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /objection/utils/patchers/github.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class Github(object): 5 | """ Interact with Github """ 6 | 7 | GITHUB_LATEST_RELEASE = 'https://api.github.com/repos/frida/frida/releases/latest' 8 | GITHUB_TAGGED_RELEASE = 'https://api.github.com/repos/frida/frida/releases/tags/{tag}' 9 | 10 | # the 'context' of this Github instance 11 | gadget_version = None 12 | 13 | def __init__(self, gadget_version: str = None): 14 | """ 15 | Init a new instance of Github 16 | """ 17 | 18 | if gadget_version: 19 | self.gadget_version = gadget_version 20 | 21 | self.request_cache = {} 22 | 23 | def _call(self, endpoint: str) -> dict: 24 | """ 25 | Make a call to Github and cache the response. 26 | 27 | :param endpoint: 28 | :return: 29 | """ 30 | 31 | # return a cached response if possible 32 | if endpoint in self.request_cache: 33 | return self.request_cache[endpoint] 34 | 35 | # get a new response 36 | results = requests.get(endpoint).json() 37 | 38 | # cache it 39 | self.request_cache[endpoint] = results 40 | 41 | # and return it 42 | return results 43 | 44 | def get_latest_version(self) -> str: 45 | """ 46 | Call Github and get the tag_name of the latest 47 | release. 48 | 49 | :return: 50 | """ 51 | 52 | self.gadget_version = self._call(self.GITHUB_LATEST_RELEASE)['tag_name'] 53 | 54 | return self.gadget_version 55 | 56 | def get_assets(self) -> dict: 57 | """ 58 | Gets the assets for the currently selected gadget_version. 59 | 60 | :return: 61 | """ 62 | 63 | assets = self._call(self.GITHUB_TAGGED_RELEASE.format(tag=self.gadget_version)) 64 | 65 | if 'assets' not in assets: 66 | raise Exception(('Unable to determine assets for gadget version \'{0}\'. ' 67 | 'Are you sure this version is available on Github?').format(self.gadget_version)) 68 | 69 | return assets['assets'] 70 | -------------------------------------------------------------------------------- /agent/src/ios/lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { NSDictionary } from "./types.js"; 2 | 3 | export interface IKeychainData { 4 | clazz: string; 5 | data: NSDictionary; 6 | } 7 | 8 | export interface IKeychainItem { 9 | item_class: string; 10 | create_date: string; 11 | modification_date: string; 12 | description: string; 13 | comment: string; 14 | creator: string; 15 | type: string; 16 | script_code: string; 17 | alias: string; 18 | invisible: string; 19 | negative: string; 20 | custom_icon: string; 21 | protected: string; 22 | access_control: string; 23 | accessible_attribute: string; 24 | entitlement_group: string; 25 | generic: string; 26 | service: string; 27 | account: string; 28 | label: string; 29 | data: string; 30 | dataHex: string; 31 | } 32 | 33 | export interface IIosFileSystem { 34 | files: any; 35 | path: string; 36 | readable: boolean; 37 | writable: boolean; 38 | } 39 | 40 | export interface IIosFilePath { 41 | attributes: any; 42 | fileName: string; 43 | readable: boolean | undefined; 44 | writable: boolean | undefined; 45 | } 46 | 47 | export interface IIosCookie { 48 | name: string; 49 | version: string; 50 | value: string; 51 | expiresDate: string | undefined; 52 | domain: string; 53 | path: string; 54 | isSecure: boolean; 55 | isHTTPOnly: boolean; 56 | } 57 | 58 | export interface ICredential { 59 | authMethod: string; 60 | host: string; 61 | password: string; 62 | port: string; 63 | protocol: string; 64 | user: string; 65 | } 66 | 67 | export interface IFramework { 68 | version: string | null; 69 | executable: string | null; 70 | bundle: string | null; 71 | path: string | null; 72 | } 73 | 74 | export interface IHeapObject { 75 | className: string; 76 | handle: string; 77 | ivars: any[string]; 78 | kind: string; 79 | methods: string[]; 80 | superClass: string; 81 | } 82 | 83 | export interface IBinaryModuleDictionary { 84 | [index: string]: IBinaryInfo; 85 | } 86 | 87 | export interface IBinaryInfo { 88 | arc: boolean; 89 | canary: boolean; 90 | encrypted: boolean; 91 | pie: boolean; 92 | rootSafe: boolean; 93 | stackExec: boolean; 94 | type: string; 95 | } 96 | -------------------------------------------------------------------------------- /plugins/flex/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | from objection.utils.plugin import Plugin 5 | from objection.commands.filemanager import _path_exists_ios, _upload_ios 6 | from objection.commands.device import _get_ios_environment 7 | from objection.state.connection import state_connection 8 | 9 | 10 | class FlexLoader(Plugin): 11 | """ FlexLoader loads Flex """ 12 | 13 | def __init__(self, ns): 14 | """ 15 | Creates a new instance of the plugin 16 | 17 | :param ns: 18 | """ 19 | 20 | implementation = { 21 | 'meta': 'Work with Flex', 22 | 'commands': { 23 | 'load': { 24 | 'meta': 'Load flex', 25 | 'exec': self.load_flex 26 | } 27 | } 28 | } 29 | 30 | super().__init__(__file__, ns, implementation) 31 | 32 | self.inject() 33 | 34 | self.flex_dylib = 'libFlex.arm64.dylib' 35 | 36 | def load_flex(self, args: list): 37 | """ 38 | Loads flex. 39 | 40 | :param args: 41 | :return: 42 | """ 43 | 44 | agent = state_connection.get_api() 45 | device_dylib_path = os.path.join(agent.env_ios_paths()['DocumentDirectory'], self.flex_dylib) 46 | 47 | if not _path_exists_ios(device_dylib_path): 48 | print('Flex not uploaded, uploading...') 49 | if not self._upload_flex(device_dylib_path): 50 | return 51 | 52 | click.secho('Asking flex to load...', dim=True) 53 | self.api.init_flex(self.flex_dylib) 54 | click.secho('Flex should be up!', fg='green') 55 | 56 | def _upload_flex(self, location: str) -> bool: 57 | """ 58 | Uploads Flex to the remote filesystem. 59 | 60 | :return: 61 | """ 62 | 63 | local_flex = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.flex_dylib) 64 | 65 | if not os.path.exists(local_flex): 66 | click.secho('{0} not available next to plugin file. Please build it!'.format(self.flex_dylib), fg='red') 67 | return False 68 | 69 | _upload_ios(local_flex, location) 70 | 71 | return True 72 | 73 | namespace = 'flex' 74 | plugin = FlexLoader -------------------------------------------------------------------------------- /objection/state/connection.py: -------------------------------------------------------------------------------- 1 | class StateConnection(object): 2 | """ A class controlling the connection state of a device. """ 3 | 4 | def __init__(self) -> None: 5 | """ 6 | Init a new connection state, defaulting to a USB 7 | connection. 8 | """ 9 | 10 | self.network = False 11 | self.host = None 12 | self.port = None 13 | self.device_type = 'usb' 14 | self.device_id = None 15 | 16 | self.spawn = False 17 | self.no_pause = False 18 | self.foremost = False 19 | self.debugger = False 20 | 21 | self.name = None 22 | self.agent = None 23 | self.api = None 24 | self.uid = None 25 | 26 | def use_usb(self) -> None: 27 | """ 28 | Sets the values required to have a USB connection. 29 | 30 | :return: 31 | """ 32 | 33 | self.network = False 34 | self.device_type = 'usb' 35 | 36 | def use_network(self) -> None: 37 | """ 38 | Sets the values required to have a Network connection. 39 | 40 | :return: 41 | """ 42 | 43 | self.network = True 44 | self.device_type = 'remote' 45 | 46 | def get_comms_type(self) -> int: 47 | """ 48 | Returns the currently configured connection type. 49 | 50 | :return: 51 | """ 52 | 53 | def get_api(self): 54 | """ 55 | Return a Frida RPC API session 56 | 57 | :return: 58 | """ 59 | 60 | if not self.agent: 61 | raise Exception('No session available to get API') 62 | 63 | return self.agent.exports() 64 | 65 | def set_agent(self, agent): 66 | """ 67 | Sets the active agent to use for communications. 68 | 69 | :param agent: 70 | :return: 71 | """ 72 | 73 | self.agent = agent 74 | 75 | def get_agent(self): 76 | 77 | if not self.agent: 78 | raise Exception('No Agent available') 79 | 80 | return self.agent 81 | 82 | def __repr__(self) -> str: 83 | return f' None: 9 | """ 10 | Show all of the jobs that are currently running 11 | 12 | :return: 13 | """ 14 | 15 | sync_job_manager() 16 | jobs = job_manager_state.jobs 17 | 18 | # click.secho(tabulate( 19 | # [[ 20 | # entry['uuid'], 21 | # sum([ 22 | # len(entry[x]) for x in [ 23 | # 'invocations', 'replacements', 'implementations' 24 | # ] if x in entry 25 | # ]), 26 | # entry['type'], 27 | # ] for entry in jobs], headers=['Job ID', 'Hooks', 'Name'], 28 | # )) 29 | click.secho(tabulate( 30 | [[ 31 | uuid, 32 | job.job_type, 33 | job.name, 34 | ] for uuid, job in jobs.items()], headers=['Job ID', 'Type', 'Name'], 35 | )) 36 | 37 | 38 | def kill(args: list) -> None: 39 | """ 40 | Kills a specific objection job. 41 | 42 | :param args: 43 | :return: 44 | """ 45 | 46 | if len(args) <= 0: 47 | click.secho('Usage: jobs kill ', bold=True) 48 | return 49 | 50 | job_uuid = int(args[0]) 51 | 52 | job_manager_state.remove_job(job_uuid) 53 | 54 | 55 | def list_current_jobs() -> dict: 56 | """ 57 | Return a list of the currently listed objection jobs. 58 | Used for tab completion in the repl. 59 | """ 60 | 61 | sync_job_manager() 62 | resp = {} 63 | 64 | for uuid, job in job_manager_state.jobs.items(): 65 | resp[str(uuid)] = str(uuid) 66 | 67 | return resp 68 | 69 | 70 | def sync_job_manager() -> dict[int, Job]: 71 | try: 72 | api = state_connection.get_api() 73 | jobs = api.jobs_get() 74 | 75 | for job in jobs: 76 | job_uuid = int(job['identifier']) 77 | job_name = job['type'] 78 | if job_uuid not in job_manager_state.jobs: 79 | job_manager_state.jobs[job_uuid] = Job(job_name, 'hook', None, job_uuid) 80 | 81 | return job_manager_state.jobs 82 | except: 83 | print("REPL not ready") 84 | 85 | -------------------------------------------------------------------------------- /agent/src/ios/bundles.ts: -------------------------------------------------------------------------------- 1 | import { ObjC } from "../ios/lib/libobjc.js"; 2 | import { BundleType } from "./lib/constants.js"; 3 | import { IFramework } from "./lib/interfaces.js"; 4 | import { 5 | NSArray, 6 | NSBundle, 7 | NSDictionary 8 | } from "./lib/types.js"; 9 | 10 | 11 | // https://developer.apple.com/documentation/foundation/nsbundle/1408056-allframeworks?language=objc 12 | // https://developer.apple.com/documentation/foundation/nsbundle/1413705-allbundles?language=objc 13 | export const getBundles = (type: BundleType): IFramework[] => { 14 | 15 | // -- Sample ObjC 16 | // 17 | // for (id ob in [NSBundle allBundles]) { 18 | // NSDictionary *i = [ob infoDictionary]; 19 | // NSString *p = [ob bundlePath]; 20 | // NSLog(@"%@:%@ @ %@", [i objectForKey:@"CFBundleIdentifier"], 21 | // [i objectForKey:@"CFBundleShortVersionString"], p); 22 | // } 23 | 24 | // Figure out which bundle type to enumerate 25 | let frameworks: NSArray; 26 | if (type === BundleType.NSBundleFramework) { 27 | frameworks = ObjC.classes.NSBundle.allFrameworks(); 28 | } else if (type === BundleType.NSBundleAllBundles) { 29 | frameworks = ObjC.classes.NSBundle.allBundles(); 30 | } 31 | 32 | const appBundles: IFramework[] = []; 33 | const frameworksLength: number = frameworks.count().valueOf(); 34 | 35 | for (let i = 0; i !== frameworksLength; i++) { 36 | 37 | // get information about the bundle itself 38 | const bundle: NSBundle = frameworks.objectAtIndex_(i); 39 | const bundleInfo: NSDictionary = bundle.infoDictionary(); 40 | 41 | // get values for the keys we are interested in 42 | const bundlePath: string = bundle.bundlePath(); 43 | const CFBundleIdentifier: string = bundleInfo.objectForKey_("CFBundleIdentifier"); 44 | const CFBundleShortVersionString: string = bundleInfo.objectForKey_("CFBundleShortVersionString"); 45 | const CFBundleExecutable: string = bundleInfo.objectForKey_("CFBundleExecutable"); 46 | 47 | appBundles.push({ 48 | bundle: CFBundleIdentifier ? CFBundleIdentifier.toString() : null, 49 | executable: CFBundleExecutable ? CFBundleExecutable.toString() : null, 50 | path: bundlePath.toString(), 51 | version: CFBundleShortVersionString ? CFBundleShortVersionString.toString() : null, 52 | }); 53 | } 54 | 55 | return appBundles; 56 | }; 57 | -------------------------------------------------------------------------------- /tests/commands/test_jobs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.jobs import show, kill 5 | from objection.state.jobs import job_manager_state 6 | from ..helpers import capture 7 | 8 | 9 | class MockJob: 10 | """ 11 | A mock job for testing purposes 12 | """ 13 | 14 | def __init__(self): 15 | self.id = '3c3c65c7-67d2-4617-8fba-b96b6d2130d7' 16 | self.started = '2017-10-14 09:21:01' 17 | self.name = 'test' 18 | self.args = ['--foo', 'bar'] 19 | 20 | def end(self): 21 | pass 22 | 23 | 24 | class TestJobs(unittest.TestCase): 25 | def setUp(self): 26 | self.mock_job = MockJob() 27 | 28 | def tearDown(self): 29 | job_manager_state.jobs = [] 30 | 31 | @mock.patch('objection.state.connection.state_connection.get_api') 32 | def test_displays_empty_jobs_message(self, mock_api): 33 | mock_api.return_value.jobs_get.return_value = [] 34 | with capture(show) as o: 35 | output = o 36 | 37 | expected_output = """Job ID Hooks Type 38 | -------- ------- ------ 39 | """ 40 | 41 | self.assertEqual(output, expected_output) 42 | 43 | @mock.patch('objection.state.connection.state_connection.get_api') 44 | def test_displays_list_of_jobs(self, mock_api): 45 | mock_api.return_value.jobs_get.return_value = [ 46 | {'identifier': 'rdcjq16g8xi', 'invocations': [{}], 'type': 'ios-jailbreak-disable'}] 47 | 48 | with capture(show, []) as o: 49 | output = o 50 | 51 | expected_outut = """Job ID Hooks Type 52 | ----------- ------- --------------------- 53 | rdcjq16g8xi 1 ios-jailbreak-disable 54 | """ 55 | 56 | self.assertEqual(output, expected_outut) 57 | 58 | def test_kill_validates_arguments(self): 59 | with capture(kill, []) as o: 60 | output = o 61 | 62 | self.assertEqual(output, 'Usage: jobs kill \n') 63 | 64 | @mock.patch('objection.state.connection.state_connection.get_api') 65 | def test_cant_find_job_by_uuid(self, mock_api): 66 | kill('foo') 67 | 68 | self.assertTrue(mock_api.return_value.jobs_kill.called) 69 | 70 | @mock.patch('objection.state.connection.state_connection.get_api') 71 | def test_kills_job_by_uuid(self, mock_api): 72 | kill('foo') 73 | 74 | self.assertTrue(mock_api.return_value.jobs_kill.called) 75 | -------------------------------------------------------------------------------- /agent/src/ios/credentialstorage.ts: -------------------------------------------------------------------------------- 1 | import { ObjC } from "../ios/lib/libobjc.js"; 2 | import { ICredential } from "./lib/interfaces.js"; 3 | import { 4 | NSArray, 5 | NSData, 6 | NSURLCredentialStorage 7 | } from "./lib/types.js"; 8 | 9 | 10 | export const dump = (): ICredential[] => { 11 | 12 | // -- Sample ObjC to create and dump a credential 13 | // NSURLProtectionSpace *ps = [[NSURLProtectionSpace alloc] 14 | // initWithHost:@"foo.com" port:80 protocol:@"https" realm:NULL 15 | // authenticationMethod:NSURLAuthenticationMethodHTTPBasic]; 16 | // NSURLCredential *creds = [[NSURLCredential alloc] 17 | // initWithUser:@"user" password:@"password" persistence:NSURLCredentialPersistencePermanent]; 18 | // NSURLCredentialStorage *cs = [NSURLCredentialStorage sharedCredentialStorage]; 19 | 20 | // [cs setCredential:creds forProtectionSpace:ps]; 21 | 22 | // NSDictionary *allcreds = [cs allCredentials]; 23 | // NSLog(@"%@", allcreds); 24 | 25 | const credentialStorage: NSURLCredentialStorage = ObjC.classes.NSURLCredentialStorage; 26 | const data: ICredential[] = []; 27 | const credentialsDict: NSArray = credentialStorage.sharedCredentialStorage().allCredentials(); 28 | 29 | if (credentialsDict.count() <= 0) { 30 | return data; 31 | } 32 | 33 | const protectionSpaceEnumerator = credentialsDict.keyEnumerator(); 34 | let urlProtectionSpace; 35 | 36 | // tslint:disable-next-line:no-conditional-assignment 37 | while ((urlProtectionSpace = protectionSpaceEnumerator.nextObject()) !== null) { 38 | 39 | const userNameEnumerator = credentialsDict.objectForKey_(urlProtectionSpace).keyEnumerator(); 40 | let userName; 41 | 42 | // tslint:disable-next-line:no-conditional-assignment 43 | while ((userName = userNameEnumerator.nextObject()) !== null) { 44 | 45 | const creds: NSData = credentialsDict.objectForKey_(urlProtectionSpace).objectForKey_(userName); 46 | 47 | // Add the creds for this protection space. 48 | const credentialData: ICredential = { 49 | authMethod: urlProtectionSpace.authenticationMethod().toString(), 50 | host: urlProtectionSpace.host().toString(), 51 | password: creds.password().toString(), 52 | port: urlProtectionSpace.port(), 53 | protocol: urlProtectionSpace.protocol().toString(), 54 | user: creds.user().toString(), 55 | }; 56 | 57 | data.push(credentialData); 58 | } 59 | } 60 | 61 | return data; 62 | }; 63 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 7 * * 6' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['python', 'javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v3 38 | with: 39 | languages: ${{ matrix.language }} 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v3 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v3 63 | -------------------------------------------------------------------------------- /objection/commands/frida_commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | from tabulate import tabulate 5 | 6 | from objection.state.connection import state_connection 7 | from ..utils.helpers import sizeof_fmt, clean_argument_flags 8 | 9 | 10 | def _should_disable_exception_handler(args: list = None) -> bool: 11 | """ 12 | Checks the arguments if '--no-exception-handler' 13 | is part of it. 14 | 15 | :param args: 16 | :return: 17 | """ 18 | 19 | return len(args) > 0 and '--no-exception-handler' in args 20 | 21 | 22 | def frida_environment(args: list = None) -> None: 23 | """ 24 | Prints information about the current Frida environment. 25 | 26 | :param args: 27 | :return: 28 | """ 29 | 30 | frida_env = state_connection.get_api().env_frida() 31 | 32 | click.secho(tabulate([ 33 | ('Frida Version', frida_env['version']), 34 | ('Process Architecture', frida_env['arch']), 35 | ('Process Platform', frida_env['platform']), 36 | ('Debugger Attached', frida_env['debugger']), 37 | ('Script Runtime', frida_env['runtime']), 38 | ('Frida Heap Size', sizeof_fmt(frida_env['heap'])) 39 | ])) 40 | 41 | 42 | def ping(args: list = None) -> None: 43 | """ 44 | Pings the agent. 45 | 46 | :param args: 47 | :return: 48 | """ 49 | 50 | agent = state_connection.get_api() 51 | if agent.ping(): 52 | click.secho('The agent responds ok!', fg='green') 53 | else: 54 | click.secho('The agent did not respond ok!', fg='red') 55 | 56 | 57 | def load_background(args: list = None) -> None: 58 | """ 59 | Loads a Frida script and runs it in the background. 60 | 61 | :param args: 62 | :return: 63 | """ 64 | 65 | if len(clean_argument_flags(args)) <= 0: 66 | click.secho('Usage: import (optional name)', 67 | bold=True) 68 | return 69 | 70 | source = args[0] 71 | 72 | # support ~ syntax 73 | if source.startswith('~'): 74 | source = os.path.expanduser(source) 75 | 76 | if not os.path.isfile(source): 77 | click.secho('Unable to import file {0}'.format(source), fg='red') 78 | return 79 | 80 | # read the hook sources 81 | with open(source, 'r') as f: 82 | hook = ''.join(f.read()) 83 | 84 | agent = state_connection.get_agent() 85 | agent.attach_script(source, hook) 86 | 87 | -------------------------------------------------------------------------------- /plugins/stetho/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | from objection.utils.plugin import Plugin 5 | from objection.commands.filemanager import _path_exists_android, _upload_android 6 | from objection.commands.device import _get_android_environment 7 | from objection.state.connection import state_connection 8 | 9 | 10 | class StethoLoader(Plugin): 11 | """ StethoLoader loads Facebook's stetho """ 12 | 13 | def __init__(self, ns): 14 | """ 15 | Creates a new instance of the plugin 16 | 17 | :param ns: 18 | """ 19 | 20 | implementation = { 21 | 'meta': 'Work with Facebook\'s stetho', 22 | 'commands': { 23 | 'load': { 24 | 'meta': 'Load stetho', 25 | 'exec': self.load_stetho 26 | } 27 | } 28 | } 29 | 30 | super().__init__(__file__, ns, implementation) 31 | 32 | self.inject() 33 | 34 | self.stetho_jar = 'stetho.apk' 35 | 36 | def load_stetho(self, args: list): 37 | """ 38 | Loads stetho. 39 | 40 | :param args: 41 | :return: 42 | """ 43 | 44 | agent = state_connection.get_api() 45 | device_jar_path = os.path.join(agent.env_android_paths()['cacheDirectory'], self.stetho_jar) 46 | 47 | if not _path_exists_android(device_jar_path): 48 | print('Stetho not uploaded, uploading...') 49 | if not self._upload_stetho(device_jar_path): 50 | return 51 | 52 | click.secho('Asking stetho to load...', dim=True) 53 | self.api.init_stetho() 54 | 55 | def _upload_stetho(self, location: str) -> bool: 56 | """ 57 | Uploads Stetho to the remote filesystem. 58 | 59 | :return: 60 | """ 61 | 62 | local_stetho = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.stetho_jar) 63 | 64 | if not os.path.exists(local_stetho): 65 | click.secho('{0} not available next to plugin file. Please download Stetho and convert first!'.format(self.stetho_jar), fg='red') 66 | click.secho(' curl -sL https://github.com/facebook/stetho/releases/download/v1.5.1/stetho-1.5.1-fatjar.jar -O', dim=True) 67 | click.secho(' dx --dex --output="stetho.apk" stetho-1.5.1.jar', dim=True) 68 | return False 69 | 70 | _upload_android(local_stetho, location) 71 | 72 | return True 73 | 74 | namespace = 'stetho' 75 | plugin = StethoLoader 76 | -------------------------------------------------------------------------------- /agent/src/android/shell.ts: -------------------------------------------------------------------------------- 1 | import { IExecutedCommand } from "./lib/interfaces.js"; 2 | import { 3 | wrapJavaPerform, 4 | Java 5 | } from "./lib/libjava.js"; 6 | import { 7 | BufferedReader, 8 | InputStreamReader, 9 | Runtime, 10 | StringBuilder 11 | } from "./lib/types.js"; 12 | 13 | 14 | // Executes shell commands on an Android device using Runtime.getRuntime().exec() 15 | export const execute = (cmd: string): Promise => { 16 | // -- Sample Java 17 | // 18 | // Process command = Runtime.getRuntime().exec("ls -l /"); 19 | // InputStreamReader isr = new InputStreamReader(command.getInputStream()); 20 | // BufferedReader br = new BufferedReader(isr); 21 | // 22 | // StringBuilder sb = new StringBuilder(); 23 | // String line = ""; 24 | // 25 | // while ((line = br.readLine()) != null) { 26 | // sb.append(line + "\n"); 27 | // } 28 | // 29 | // String output = sb.toString(); 30 | return wrapJavaPerform(() => { 31 | 32 | const runtime: Runtime = Java.use("java.lang.Runtime"); 33 | const inputStreamReader: InputStreamReader = Java.use("java.io.InputStreamReader"); 34 | const bufferedReader: BufferedReader = Java.use("java.io.BufferedReader"); 35 | const stringBuilder: StringBuilder = Java.use("java.lang.StringBuilder"); 36 | 37 | // Run the command 38 | const command = runtime.getRuntime().exec(cmd); 39 | 40 | // Read 'stderr' 41 | const stdErrInputStreamReader: InputStreamReader = inputStreamReader.$new(command.getErrorStream()); 42 | let bufferedReaderInstance: BufferedReader = bufferedReader.$new(stdErrInputStreamReader); 43 | 44 | const stdErrStringBuilder: StringBuilder = stringBuilder.$new(); 45 | let lineBuffer: string; 46 | 47 | // tslint:disable-next-line:no-conditional-assignment 48 | while ((lineBuffer = bufferedReaderInstance.readLine()) != null) { 49 | stdErrStringBuilder.append(lineBuffer + "\n"); 50 | } 51 | 52 | // Read 'stdout' 53 | const stdOutInputStreamReader: InputStreamReader = inputStreamReader.$new(command.getInputStream()); 54 | bufferedReaderInstance = bufferedReader.$new(stdOutInputStreamReader); 55 | 56 | const stdOutStringBuilder = stringBuilder.$new(); 57 | lineBuffer = ""; 58 | 59 | // tslint:disable-next-line:no-conditional-assignment 60 | while ((lineBuffer = bufferedReaderInstance.readLine()) != null) { 61 | stdOutStringBuilder.append(lineBuffer + "\n"); 62 | } 63 | 64 | return { 65 | command: cmd, 66 | stdErr: stdErrStringBuilder.toString(), 67 | stdOut: stdOutStringBuilder.toString(), 68 | }; 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /tests/commands/test_device.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.commands.device import get_environment, _get_ios_environment, _get_android_environment 5 | from objection.state.device import Android, Ios 6 | from ..helpers import capture 7 | 8 | 9 | class TestDevice(unittest.TestCase): 10 | 11 | @mock.patch('objection.commands.device._get_ios_environment') 12 | @mock.patch('objection.commands.device.device_state') 13 | def test_gets_environment_and_calls_ios_platform_specific_method(self, mock_device_state, mock_ios_environment): 14 | type(mock_device_state).platform = mock.PropertyMock(return_value=Ios) 15 | 16 | get_environment() 17 | 18 | self.assertTrue(mock_ios_environment.called) 19 | 20 | @mock.patch('objection.commands.device._get_android_environment') 21 | @mock.patch('objection.commands.device.device_state') 22 | def test_gets_environment_and_calls_android_platform_specific_method(self, mock_device_state, 23 | mock_android_environment): 24 | type(mock_device_state).platform = mock.PropertyMock(return_value=Android) 25 | 26 | get_environment() 27 | 28 | self.assertTrue(mock_android_environment.called) 29 | 30 | @mock.patch('objection.state.connection.state_connection.get_api') 31 | def test_prints_ios_environment_via_platform_helpers(self, mock_api): 32 | mock_api.return_value.env_ios_paths.return_value = { 33 | 'LibraryDirectory': '/var/mobile/Containers/Data/Application/C1D04553/Library'} 34 | 35 | with capture(_get_ios_environment) as o: 36 | output = o 37 | 38 | expected_output = """ 39 | Name Path 40 | ---------------- -------------------------------------------------------- 41 | LibraryDirectory /var/mobile/Containers/Data/Application/C1D04553/Library 42 | """ 43 | 44 | self.assertEqual(output, expected_output) 45 | 46 | @mock.patch('objection.state.connection.state_connection.get_api') 47 | def test_prints_android_environment_via_platform_helpers(self, mock_api): 48 | mock_api.return_value.env_android_paths.return_value = { 49 | 'packageCodePath': '/data/app/com.sensepost.apewpew-1/base.apk'} 50 | 51 | with capture(_get_android_environment) as o: 52 | output = o 53 | 54 | expected_output = """ 55 | Name Path 56 | --------------- ------------------------------------------ 57 | packageCodePath /data/app/com.sensepost.apewpew-1/base.apk 58 | """ 59 | 60 | self.assertEqual(output, expected_output) 61 | -------------------------------------------------------------------------------- /objection/commands/android/keystore.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import click 4 | from tabulate import tabulate 5 | 6 | from objection.state.connection import state_connection 7 | 8 | 9 | def _should_output_json(args: list) -> bool: 10 | """ 11 | Checks if --json is in the list of tokens received from the 12 | command line. 13 | 14 | :param args: 15 | :return: 16 | """ 17 | 18 | return len(args) > 0 and '--json' in args 19 | 20 | 21 | def entries(args: list = None) -> None: 22 | """ 23 | Lists entries in the Android KeyStore 24 | 25 | :param args: 26 | :return: 27 | """ 28 | 29 | api = state_connection.get_api() 30 | ks = api.android_keystore_list() 31 | 32 | output = [[x['alias'], x['is_key'], x['is_certificate']] for x in ks] 33 | click.secho(tabulate(output, headers=['Alias', 'Key', 'Certificate'])) 34 | 35 | 36 | def detail(args: list = None) -> None: 37 | """ 38 | Lists details of all items in the Android KeyStore 39 | 40 | :param args: 41 | :return: 42 | """ 43 | 44 | click.secho('Listing details for all items in the Android KeyStore...', dim=True) 45 | api = state_connection.get_api() 46 | ks = api.android_keystore_detail() 47 | 48 | if _should_output_json(args): 49 | click.secho(json.dumps(ks, indent=2, sort_keys=True)) 50 | return 51 | 52 | output = [[ 53 | x['keystoreAlias'], 54 | x['keyAlgorithm'], 55 | x['keySize'], 56 | ','.join(x['blockModes']), 57 | ','.join(x['encryptionPaddings']), 58 | ','.join(x['digests']), 59 | x['keyValidityStart'], 60 | x['origin'], 61 | x['purposes'], 62 | ','.join(x['signaturePaddings']), 63 | x['isInsideSecureHardware'], 64 | ] for x in ks] 65 | 66 | click.secho(tabulate(output, headers=[ 67 | 'Alias', 'Alg', 'Size', 'Modes', 'Paddings', 'Digests', 68 | 'Validity Start', 'Origin', 'Purposes', 'Sig Paddings', 'Sec Hardware' 69 | ])) 70 | 71 | 72 | def clear(args: list = None) -> None: 73 | """ 74 | Clears out an Android KeyStore 75 | 76 | :param args: 77 | :return: 78 | """ 79 | 80 | if not click.confirm('Are you sure you want to clear the Android keystore?'): 81 | return 82 | 83 | api = state_connection.get_api() 84 | api.android_keystore_clear() 85 | 86 | 87 | def watch(args: list = None) -> None: 88 | """ 89 | Watches usage of the Android KeyStore 90 | 91 | :param args: 92 | :return: 93 | """ 94 | 95 | api = state_connection.get_api() 96 | api.android_keystore_watch() 97 | -------------------------------------------------------------------------------- /tests/utils/test_helpers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from objection.state.device import device_state, Ios 4 | from objection.utils.helpers import clean_argument_flags 5 | from objection.utils.helpers import get_tokens 6 | from objection.utils.helpers import pretty_concat 7 | from objection.utils.helpers import print_frida_connection_help 8 | from objection.utils.helpers import sizeof_fmt 9 | from objection.utils.helpers import warn_about_older_operating_systems 10 | from ..helpers import capture 11 | 12 | 13 | class TestHelpers(unittest.TestCase): 14 | def test_pretty_concat_with_less_than_seventy_five_chars(self): 15 | result = pretty_concat('test') 16 | 17 | self.assertEqual(result, 'test') 18 | 19 | def test_pretty_concat_with_more_than_max_chars(self): 20 | result = pretty_concat('testing', 5) 21 | 22 | self.assertEqual(result, 'testi...') 23 | 24 | def test_pretty_concat_with_more_than_max_chars_to_the_left(self): 25 | result = pretty_concat('testing', 5, left=True) 26 | 27 | self.assertEqual(result, '...sting') 28 | 29 | def test_sizeof_formats_values(self): 30 | result = sizeof_fmt(3000) 31 | 32 | self.assertEqual(result, '2.9 KiB') 33 | 34 | def test_gets_tokens_without_quotes(self): 35 | result = get_tokens('this is a test') 36 | 37 | self.assertEqual(result, ['this', 'is', 'a', 'test']) 38 | 39 | def test_gets_tokens_with_quotes(self): 40 | result = get_tokens('this is "a test"') 41 | 42 | self.assertEqual(result, ['this', 'is', 'a test']) 43 | 44 | def test_gets_tokens_and_handles_missing_quotes(self): 45 | result = get_tokens('this is "a test') 46 | 47 | self.assertEqual(result, ['lajfhlaksjdfhlaskjfhafsdlkjh']) 48 | 49 | def test_cleans_argument_lists_with_flags(self): 50 | result = clean_argument_flags(['foo', '--bar']) 51 | self.assertEqual(result, ['foo']) 52 | 53 | def test_prints_frida_connection_help(self): 54 | with capture(print_frida_connection_help) as o: 55 | output = o 56 | 57 | expected_output = """If you are using a rooted/jailbroken device, specify a process with the --gadget flag. Eg: objection --gadget "Calendar" explore 58 | If you are using a non rooted/jailbroken device, ensure that your patched application is running and in the foreground. 59 | 60 | 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. 61 | 62 | For more information, please refer to the objection wiki at: https://github.com/sensepost/objection/wiki 63 | """ 64 | 65 | self.assertEqual(output, expected_output) 66 | -------------------------------------------------------------------------------- /agent/src/android/lib/intentUtils.ts: -------------------------------------------------------------------------------- 1 | import { Java } from "./libjava.js"; 2 | import { colors as c } from "../../lib/color.js"; 3 | 4 | export const analyseIntent = (methodName: string, intent: any, backtrace: boolean = false): void => { 5 | try { 6 | send(`\nAnalyzing Intent from: ${c.green(`${methodName}`)}`); 7 | 8 | // Get Component 9 | const component = intent.getComponent(); 10 | if (component) { 11 | send(`[-] ${c.green('Intent Type: Explicit Intent')}`); 12 | } else { 13 | send(`[+] ${c.redBright('Intent Type: Implicit Intent Detected!')}`); 14 | if (backtrace) { 15 | send( 16 | Java.use('android.util.Log') 17 | .getStackTraceString(Java.use('java.lang.Exception').$new()) 18 | ) 19 | } 20 | 21 | // Log intent details 22 | send(`[+] Action: ${`${c.green(`${intent.getAction()}`)}` || `${c.redBright(`[None]`)}`}`); 23 | send(`[+] Data URI: ${`${c.green(`${intent.getDataString()}`)}` || `${c.redBright(`[None]`)}`}`); 24 | send(`[+] Type: ${`${c.green(`${intent.getType()}`)}` || `${c.redBright(`[None]`)}`}`); 25 | send(`[+] Flags: ${c.green(`0x${intent.getFlags().toString(16)}`)}`); 26 | 27 | // Categories 28 | const categories = intent.getCategories(); 29 | if (categories) { 30 | send("\n[+] Categories:"); 31 | const iterator = categories.iterator(); 32 | while (iterator.hasNext()) { 33 | send(`[+] Category: ${c.green(`${iterator.next()}`)} `); 34 | } 35 | } else { 36 | send(`[-] Category: ${`${c.redBright(`[None]`)}`}`); 37 | } 38 | 39 | // Extras 40 | const extras = intent.getExtras(); 41 | if (extras) { 42 | send(`[+] Extras: ${c.green(`${extras}`)}`); 43 | } else { 44 | send(`[-] Extras: ${`${c.redBright(`[None]`)}`}`); 45 | } 46 | 47 | // Resolving implicit intents 48 | const activityContext = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext(); 49 | if (activityContext) { 50 | const packageManager = activityContext.getPackageManager(); 51 | const resolveInfoList = packageManager.queryIntentActivities(intent, Java.use("android.content.pm.PackageManager").MATCH_ALL.value); 52 | 53 | send("[+] Responding apps:"); 54 | for (let i = 0; i < resolveInfoList.size(); i++) { 55 | const resolveInfo = resolveInfoList.get(i); 56 | send(`[*] Resolve Info List at position ${i}: ${c.green(`${resolveInfo.toString()}`)}`); 57 | } 58 | } else { 59 | send("[-] No activity context available"); 60 | } 61 | 62 | } 63 | } catch (e) { 64 | send(`[!] Error analyzing intent: ${e}`); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /plugins/mettle/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from objection.commands.filemanager import _path_exists_ios, _upload_ios 6 | from objection.state.connection import state_connection 7 | from objection.utils.plugin import Plugin 8 | 9 | 10 | class MettleLoader(Plugin): 11 | """ MettleLoader loads Mettle """ 12 | 13 | def __init__(self, ns): 14 | """ 15 | Creates a new instance of the plugin 16 | 17 | :param ns: 18 | """ 19 | 20 | implementation = { 21 | 'meta': 'Work with Mettle', 22 | 'commands': { 23 | 'load': { 24 | 'meta': 'Load mettle', 25 | 'exec': self.load_mettle 26 | }, 27 | 'connect': { 28 | 'meta': 'Connect mettle', 29 | 'exec': self.connect_mettle 30 | } 31 | } 32 | } 33 | 34 | super().__init__(__file__, ns, implementation) 35 | 36 | self.inject() 37 | 38 | self.mettle_dylib = 'mettle.dylib' 39 | 40 | def load_mettle(self, args: list): 41 | """ 42 | Loads mettle. 43 | 44 | :param args: 45 | :return: 46 | """ 47 | 48 | agent = state_connection.get_api() 49 | device_dylib_path = os.path.join(agent.env_ios_paths()['DocumentDirectory'], self.mettle_dylib) 50 | 51 | if not _path_exists_ios(device_dylib_path): 52 | print('Mettle not uploaded, uploading...') 53 | if not self._upload_mettle(device_dylib_path): 54 | return 55 | 56 | click.secho('Loading dylib...', dim=True) 57 | self.api.init_mettle(self.mettle_dylib) 58 | 59 | click.secho('Mettle should be loaded! You can now issue the connect command.', fg='green') 60 | 61 | def _upload_mettle(self, location: str) -> bool: 62 | """ 63 | Uploads Mettle to the remote filesystem. 64 | 65 | :return: 66 | """ 67 | 68 | local_mettle = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.mettle_dylib) 69 | if not os.path.exists(local_mettle): 70 | click.secho('{0} not available next to plugin file. Please build it and copy it there!'.format( 71 | self.mettle_dylib), fg='red') 72 | return False 73 | 74 | _upload_ios(local_mettle, location) 75 | 76 | return True 77 | 78 | def connect_mettle(self, args: list): 79 | if len(args) < 2: 80 | click.secho("Usage: plugin mettle connect ") 81 | return 82 | 83 | ip = args[0] 84 | port = args[1] 85 | 86 | click.secho("Connecting to {}:{}".format(ip, port), dim=True) 87 | self.api.connect_mettle(self.mettle_dylib, ip, port) 88 | 89 | 90 | namespace = 'mettle' 91 | plugin = MettleLoader 92 | -------------------------------------------------------------------------------- /tests/utils/patchers/test_ios.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from objection.utils.patchers.ios import IosGadget, IosPatcher 5 | 6 | 7 | class TestIosGadget(unittest.TestCase): 8 | @mock.patch('objection.utils.patchers.ios.Github') 9 | @mock.patch('objection.utils.patchers.android.os') 10 | def setUp(self, mock_github, mock_os): 11 | mock_os.path.exists.return_value = True 12 | 13 | self.ios_gadget = IosGadget(github=mock_github) 14 | 15 | self.github_get_assets_sample = [ 16 | { 17 | "url": "https://api.github.com/repos/frida/frida/releases/assets/5005221", 18 | "id": 5005221, 19 | "name": "frida-gadget-10.6.8-ios-universal.dylib.xz", 20 | "label": "", 21 | "uploader": { 22 | "id": 735197, 23 | }, 24 | "state": "uploaded", 25 | "size": 12912624, 26 | "download_count": 1, 27 | "created_at": "2017-10-07T00:01:10Z", 28 | "updated_at": "2017-10-07T00:01:17Z", 29 | "browser_download_url": "https://github.com/frida/frida/releases/download/" 30 | "frida-gadget-10.6.8-ios-universal.dylib.xz" 31 | } 32 | ] 33 | 34 | def test_gets_gadget_path(self): 35 | self.ios_gadget.ios_dylib_gadget_path = '/tmp/foo' 36 | 37 | result = self.ios_gadget.get_gadget_path() 38 | 39 | self.assertEqual(result, '/tmp/foo') 40 | 41 | @mock.patch('objection.utils.patchers.ios.os') 42 | def test_checks_if_gadget_exists(self, mock_os): 43 | mock_os.path.exists.return_value = True 44 | 45 | result = self.ios_gadget.gadget_exists() 46 | 47 | self.assertTrue(result) 48 | 49 | def test_can_find_asset_download_url(self): 50 | mock_github = mock.MagicMock() 51 | mock_github.get_assets.return_value = self.github_get_assets_sample 52 | 53 | self.ios_gadget.github = mock_github 54 | 55 | result = self.ios_gadget._get_download_url() 56 | 57 | self.assertEqual(result, 'https://github.com/frida/frida/releases/download/' 58 | 'frida-gadget-10.6.8-ios-universal.dylib.xz') 59 | 60 | 61 | class TestIosPatcher(unittest.TestCase): 62 | @mock.patch('objection.utils.patchers.ios.IosPatcher.__init__', mock.Mock(return_value=None)) 63 | @mock.patch('objection.utils.patchers.ios.IosPatcher.__del__', mock.Mock(return_value=None)) 64 | @mock.patch('objection.utils.patchers.ios.click.secho', mock.Mock(return_value=None)) 65 | def test_sets_provisioning_profile(self): 66 | patcher = IosPatcher() 67 | patcher.set_provsioning_profile('profile.mobileprovision', 'com.foo.bar') 68 | 69 | self.assertEqual(patcher.provision_file, 'profile.mobileprovision') 70 | -------------------------------------------------------------------------------- /objection/commands/ios/bundles.py: -------------------------------------------------------------------------------- 1 | import click 2 | from tabulate import tabulate 3 | 4 | from objection.state.connection import state_connection 5 | from objection.utils.helpers import pretty_concat 6 | 7 | 8 | def _should_include_apple_bundles(args: list) -> bool: 9 | """ 10 | Checks if arguments have the --include-apple-frameworks flag 11 | 12 | :param args: 13 | :return: 14 | """ 15 | 16 | return len(args) > 0 and '--include-apple-frameworks' in args 17 | 18 | 19 | def _should_print_full_path(args: list) -> bool: 20 | """ 21 | Checks if arguments have the --full-path flag 22 | 23 | :param args: 24 | :return: 25 | """ 26 | 27 | return len(args) > 0 and '--full-path' in args 28 | 29 | 30 | def _is_apple_bundle(bundle: str) -> bool: 31 | """ 32 | Check if a string bundle identifier is considered an Apple 33 | bundle based on the fact that the bundle name starts with 34 | the string com.apple 35 | 36 | :param bundle: 37 | :return: 38 | """ 39 | 40 | # This is a bit of an assumption, but ok. 41 | if bundle is None: 42 | return False 43 | 44 | if bundle.startswith('com.apple'): 45 | return True 46 | 47 | return False 48 | 49 | 50 | def show_frameworks(args: list = None) -> None: 51 | """ 52 | Prints information about bundles that represent frameworks. 53 | 54 | https://developer.apple.com/documentation/foundation/nsbundle/1408056-allframeworks?language=objc 55 | 56 | :param args: 57 | :return: 58 | """ 59 | 60 | api = state_connection.get_api() 61 | frameworks = api.ios_bundles_get_frameworks() 62 | 63 | # apply filters 64 | if not _should_include_apple_bundles(args): 65 | frameworks = [f for f in frameworks if not _is_apple_bundle(f['bundle'])] 66 | 67 | # Just dump it to the screen 68 | click.secho(tabulate( 69 | [[ 70 | entry['executable'], 71 | entry['bundle'], 72 | entry['version'], 73 | entry['path'] if _should_print_full_path(args) else pretty_concat(entry['path'], 40, True), 74 | ] for entry in frameworks 75 | ], headers=['Executable', 'Bundle', 'Version', 'Path'], 76 | )) 77 | 78 | 79 | def show_bundles(args: list = None) -> None: 80 | """ 81 | Prints information about bundles that are not necessarily frameworks 82 | 83 | https://developer.apple.com/documentation/foundation/nsbundle/1413705-allbundles?language=objc 84 | 85 | :param args: 86 | :return: 87 | """ 88 | 89 | api = state_connection.get_api() 90 | bundles = api.ios_bundles_get_bundles() 91 | 92 | # Just dump it to the screen 93 | click.secho(tabulate( 94 | [[ 95 | entry['executable'], 96 | entry['bundle'], 97 | entry['version'], 98 | entry['path'] if _should_print_full_path(args) else pretty_concat(entry['path'], 40, True), 99 | ] for entry in bundles 100 | ], headers=['Executable', 'Bundle', 'Version', 'Path'], 101 | )) 102 | --------------------------------------------------------------------------------