From ae069c6313b464c19f0253de027e9a215227f659 Mon Sep 17 00:00:00 2001 From: Isak Van Der Walt Date: Wed, 18 Jun 2025 17:24:58 +0200 Subject: [PATCH 1/4] (feat/fix) Attempt attach using identifier. --- objection/commands/filemanager.py | 6 +++--- objection/console/cli.py | 2 +- objection/utils/agent.py | 32 ++++++++++++++++++++++++++----- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/objection/commands/filemanager.py b/objection/commands/filemanager.py index e7d549c1..804bc851 100644 --- a/objection/commands/filemanager.py +++ b/objection/commands/filemanager.py @@ -407,7 +407,7 @@ def download(args: list) -> None: """ if len(args) < 1: - click.secho('Usage: file download (optional: )', bold=True) + click.secho('Usage: filesystem download (optional: )', bold=True) return # determine the source and destination file names. @@ -564,7 +564,7 @@ def upload(args: list) -> None: """ if len(args) < 1: - click.secho('Usage: file upload (optional: )', bold=True) + click.secho('Usage: filesystem upload (optional: )', bold=True) return source = args[0] @@ -733,7 +733,7 @@ def cat(args: list): """ if len(args) < 1: - click.secho('Usage: file cat ', bold=True) + click.secho('Usage: filesystem cat ', bold=True) return # determine the source and destination file names. diff --git a/objection/console/cli.py b/objection/console/cli.py index 867c1070..ff490b49 100644 --- a/objection/console/cli.py +++ b/objection/console/cli.py @@ -190,7 +190,7 @@ def api_thread(): repl.run(quiet=quiet) # Some ugly backwards compatibility -@cli.command(deprecated="Use 'objection start' instead of 'objection explore'") +@cli.command(deprecated="Use 'objection start' instead of 'objection explore'", hidden=True) @click.option('--plugin-folder', '-P', required=False, default=None, help='The folder to load plugins from.') @click.option('--quiet', '-q', required=False, default=False, is_flag=True) @click.option('--startup-command', '-s', required=False, multiple=True, diff --git a/objection/utils/agent.py b/objection/utils/agent.py index 314f53cd..431d266b 100644 --- a/objection/utils/agent.py +++ b/objection/utils/agent.py @@ -199,16 +199,19 @@ def set_target_pid(self): """ if (self.config.name is None) and (not self.config.foremost): - raise Exception('Need a target name to spawn/attach to') + click.secho('Need a target name to spawn/attach to', fg='red') + sys.exit(1) if self.config.foremost: try: app = self.device.get_frontmost_application() except Exception as e: - raise Exception(f'Could not get foremost application on {self.device.name}: {e}') + click.secho(f'Could not get foremost application on {self.device.name}: {e}', fg='red') + sys.exit(1) if app is None: - raise Exception(f'No foremost application on {self.device.name}') + click.secho(f'No foremost application on {self.device.name}', fg='red') + sys.exit(1) self.pid = app.pid # update the global state for the prompt etc. @@ -230,9 +233,28 @@ def set_target_pid(self): except ValueError: pass + # maybe we have a process name if self.pid is None: - # last resort, maybe we have a process name - self.pid = self.device.get_process(self.config.name).pid + try: + self.pid = self.device.get_process(self.config.name).pid + except frida.ProcessNotFoundError: + pass + + if self.pid is None: + # maybe we have an app identifier + app_list = self.device.enumerate_applications() + app_name_lc = self.config.name.lower() + matching_app = [app for app in app_list if app.identifier.lower() == app_name_lc] + if len(matching_app) == 1 and matching_app[0].pid is not None: + self.pid = matching_app[0].pid + elif len(matching_app) > 1: + app_list_str = ', '.join([f"{app.identifier}: {app.pid}" for app in matching_app]) + click.secho("Ambiguous identifier. Applications with the same identifier found: "+ app_list_str, fg='red') + sys.exit(1) + + if self.pid is None: + click.secho("Unable to find target application.", fg='red', bold=True) + sys.exit(1) debug_print(f'process PID determined as {self.pid}') From 0f3f01c0c9c8c56ba386a63e92353bf64f414c8d Mon Sep 17 00:00:00 2001 From: Isak Van Der Walt Date: Wed, 18 Jun 2025 17:27:10 +0200 Subject: [PATCH 2/4] (feat) Job handler fix (root.ts), additional error detection. --- agent/src/android/pinning.ts | 18 ++- agent/src/android/root.ts | 189 +++++++++++++++++++---------- agent/src/lib/jobs.ts | 8 +- objection/state/jobs.py | 7 +- tests/commands/test_filemanager.py | 4 +- 5 files changed, 149 insertions(+), 77 deletions(-) diff --git a/agent/src/android/pinning.ts b/agent/src/android/pinning.ts index 516ec86b..69dc7325 100644 --- a/agent/src/android/pinning.ts +++ b/agent/src/android/pinning.ts @@ -120,9 +120,10 @@ const okHttp3CertificatePinnerCheck = (ident: number): Promise return CertificatePinnerCheck; } catch (err) { - if ((err as Error).message.indexOf("ClassNotFoundException") === 0) { + if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } + return null; } }); }; @@ -162,9 +163,10 @@ const okHttp3CertificatePinnerCheckOkHttp = (ident: number): Promise => { return TrustManagerImplverifyChain; } catch (err) { - if ((err as Error).message.indexOf("ClassNotFoundException") === 0) { + if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } + return null; } }); }; @@ -271,9 +275,10 @@ const trustManagerImplCheckTrustedRecursiveCheck = (ident: number): Promise return TrustManagerImplcheckTrustedRecursive; } catch (err) { - if ((err as Error).message.indexOf("ClassNotFoundException") === 0) { + if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } + return null; } }); }; @@ -303,9 +308,10 @@ const phoneGapSSLCertificateChecker = (ident: number): Promise => { return SSLCertificateCheckerExecute; } catch (err) { - if ((err as Error).message.indexOf("ClassNotFoundException") === 0) { + if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") !== 0) { throw err; } + return null; } }); }; diff --git a/agent/src/android/root.ts b/agent/src/android/root.ts index b4abe719..3fede273 100644 --- a/agent/src/android/root.ts +++ b/agent/src/android/root.ts @@ -44,6 +44,8 @@ const testKeysCheck = (success: boolean, ident: number): any => { send(c.blackBright(`[${ident}] `) + `Marking "test-keys" check as ` + c.green(`failed`) + `.`); return false; }; + + return JavaString.contains; }); }; @@ -51,8 +53,9 @@ const execSuCheck = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const JavaRuntime: Runtime = Java.use("java.lang.Runtime"); const iOException: IOException = Java.use("java.io.IOException"); + const JavaRuntime_exec = JavaRuntime.exec.overload("java.lang.String"); - JavaRuntime.exec.overload("java.lang.String").implementation = function (command: string) { + JavaRuntime_exec.implementation = function (command: string) { if (command.endsWith("su")) { if (success) { send(c.blackBright(`[${ident}] `) + `Check for 'su' using command exec detected, allowing.`); @@ -66,6 +69,8 @@ const execSuCheck = (success: boolean, ident: number): any => { // call the original method return this.exec.overload("java.lang.String").call(this, command); }; + + return JavaRuntime_exec; }); }; @@ -93,6 +98,8 @@ const fileExistsCheck = (success: boolean, ident: number): any => { // call the original method return this.exists.call(this); }; + + return JavaFile.exists; }); }; @@ -101,7 +108,9 @@ const fileExistsCheck = (success: boolean, ident: number): any => { const rootBeerIsRooted = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); - RootBeer.isRooted.overload().implementation = function () { + const RootBeer_isRooted = RootBeer.isRooted.overload(); + + RootBeer_isRooted.implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + @@ -116,6 +125,8 @@ const rootBeerIsRooted = (success: boolean, ident: number): any => { ); return false; }; + + return RootBeer_isRooted; }); }; @@ -137,6 +148,8 @@ const rootBeerCheckForBinary = (success: boolean, ident: number): any => { ); return false; }; + + return RootBeer.checkForBinary; }); }; @@ -158,13 +171,17 @@ const rootBeerCheckForDangerousProps = (success: boolean, ident: number): any => ); return false; }; + + return RootBeer.checkForDangerousProps; }); }; const rootBeerDetectRootCloakingApps = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); - RootBeer.detectRootCloakingApps.overload().implementation = function () { + const RootBeer_detectRootCloakingApps = RootBeer.detectRootCloakingApps.overload(); + + RootBeer_detectRootCloakingApps.implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + @@ -179,6 +196,8 @@ const rootBeerDetectRootCloakingApps = (success: boolean, ident: number): any => ); return false; }; + + return RootBeer_detectRootCloakingApps; }); }; @@ -200,6 +219,8 @@ const rootBeerCheckSuExists = (success: boolean, ident: number): any => { ); return false; }; + + return RootBeer.checkSuExists; }); }; @@ -221,34 +242,46 @@ const rootBeerDetectTestKeys = (success: boolean, ident: number): any => { ); return false; }; + + return RootBeer.detectTestKeys; }); }; const rootBeerCheckSeLinux = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { - const Util = Java.use("com.scottyab.rootbeer.util"); - Util.isSelinuxFlagInEnabled.overload().implementation = function () { - if (success) { + try { + const Util = Java.use("com.scottyab.rootbeer.util"); + Util.isSelinuxFlagInEnabled.overload().implementation = function () { + if (success) { + send( + c.blackBright(`[${ident}]`) + + `Rootbeer.util->isSelinuxFlagInEnabled() check detected, marking as ${c.green("true")}`, + ); + return true; + } + send( - c.blackBright(`[${ident}]`) + - `Rootbeer.util->isSelinuxFlagInEnabled() check detected, marking as ${c.green("true")}`, + c.blackBright(`[${ident}] `) + + `Rootbeer.util->isSelinuxFlagInEnabled() check detected, marking as ${c.green("false")}`, ); - return true; - } - - send( - c.blackBright(`[${ident}] `) + - `Rootbeer.util->isSelinuxFlagInEnabled() check detected, marking as ${c.green("false")}`, - ); - return false; - }; + return false; + }; + + return Util.isSelinuxFlagInEnabled; + } catch (err) { + if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") === 0) { + return null; + }; + throw err; + } }); }; const rootBeerNative = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeerNative = Java.use("com.scottyab.rootbeer.RootBeerNative"); - RootBeerNative.checkForRoot.overload('[Ljava.lang.Object;').implementation = function () { + const RootBeerNative_checkForRoot = RootBeerNative.checkForRoot.overload('[Ljava.lang.Object;'); + RootBeerNative_checkForRoot.implementation = function () { if (success) { send( c.blackBright(`[${ident}] `) + @@ -263,74 +296,98 @@ const rootBeerNative = (success: boolean, ident: number): any => { ); return 0; }; + + return RootBeerNative_checkForRoot; }); }; // ref: https://www.ayrx.me/gantix-jailmonkey-root-detection-bypass/ -const jailMonkeyBypass = (success: boolean, ident: number): any => { +const jailMonkeyBypass = (success: boolean, ident: number): Promise => { return wrapJavaPerform(() => { - const JavaJailMonkeyModule = Java.use("com.gantix.JailMonkey.JailMonkeyModule"); - const JavaHashMap = Java.use("java.util.HashMap"); - const JavaFalseObject = Java.use("java.lang.Boolean").FALSE.value; - - JavaJailMonkeyModule.getConstants.implementation = function () { - send( - c.blackBright(`[${ident}] `) + - `JailMonkeyModule.getConstants() called, returning false for all keys.` - ); - - const hm = JavaHashMap.$new(); - hm.put("isJailBroken", JavaFalseObject); - hm.put("hookDetected", JavaFalseObject); - hm.put("canMockLocation", JavaFalseObject); - hm.put("isOnExternalStorage", JavaFalseObject); - hm.put("AdbEnabled", JavaFalseObject); - - return hm; - }; + try { + const JavaJailMonkeyModule = Java.use("com.gantix.JailMonkey.JailMonkeyModule"); + const JavaHashMap = Java.use("java.util.HashMap"); + const JavaBoolean = Java.use("java.lang.Boolean") + const JavaFalseObject = JavaBoolean.FALSE.value; + const JavaTrueObject = JavaBoolean.TRUE.value; + + JavaJailMonkeyModule.getConstants.implementation = function () { + if (success) { + send( + c.blackBright(`[${ident}] `) + + `RootBeer->checkForDangerousProps() check detected, marking as ${c.green("true")} for all keys.`, + ); + const hm = JavaHashMap.$new(); + hm.put("isJailBroken", JavaTrueObject); + hm.put("hookDetected", JavaTrueObject); + hm.put("canMockLocation", JavaTrueObject); + hm.put("isOnExternalStorage", JavaTrueObject); + hm.put("AdbEnabled", JavaTrueObject); + + return hm; + } + send( + c.blackBright(`[${ident}] `) + + `JailMonkeyModule.getConstants() called, returning ${c.green("false")} for all keys.` + ); - return JavaJailMonkeyModule; + const hm = JavaHashMap.$new(); + hm.put("isJailBroken", JavaFalseObject); + hm.put("hookDetected", JavaFalseObject); + hm.put("canMockLocation", JavaFalseObject); + hm.put("isOnExternalStorage", JavaFalseObject); + hm.put("AdbEnabled", JavaFalseObject); + + return hm; + }; + + return JavaJailMonkeyModule.getConstants; + } catch (err) { + if ((err as Error).message.indexOf("java.lang.ClassNotFoundException") === 0) { + return null; + }; + throw err; + } }); }; -export const disable = (): void => { +export const disable = async (): Promise => { const job: jobs.Job = new jobs.Job(jobs.identifier(), 'root-detection-disable'); - job.addImplementation(testKeysCheck(false, job.identifier)); - job.addImplementation(execSuCheck(false, job.identifier)); - job.addImplementation(fileExistsCheck(false, job.identifier)); - job.addImplementation(jailMonkeyBypass(false, job.identifier)); - + job.addImplementation(await testKeysCheck(false, job.identifier)); + job.addImplementation(await execSuCheck(false, job.identifier)); + job.addImplementation(await fileExistsCheck(false, job.identifier)); + job.addImplementation(await jailMonkeyBypass(false, job.identifier)); // RootBeer functions - job.addImplementation(rootBeerIsRooted(false, job.identifier)); - job.addImplementation(rootBeerCheckForBinary(false, job.identifier)); - job.addImplementation(rootBeerCheckForDangerousProps(false, job.identifier)); - job.addImplementation(rootBeerDetectRootCloakingApps(false, job.identifier)); - job.addImplementation(rootBeerCheckSuExists(false, job.identifier)); - job.addImplementation(rootBeerDetectTestKeys(false, job.identifier)); - job.addImplementation(rootBeerNative(false, job.identifier)); - job.addImplementation(rootBeerCheckSeLinux(false, job.identifier)); + job.addImplementation(await rootBeerIsRooted(false, job.identifier)); + job.addImplementation(await rootBeerCheckForBinary(false, job.identifier)); + job.addImplementation(await rootBeerCheckForDangerousProps(false, job.identifier)); + job.addImplementation(await rootBeerDetectRootCloakingApps(false, job.identifier)); + job.addImplementation(await rootBeerCheckSuExists(false, job.identifier)); + job.addImplementation(await rootBeerDetectTestKeys(false, job.identifier)); + job.addImplementation(await rootBeerNative(false, job.identifier)); + job.addImplementation(await rootBeerCheckSeLinux(false, job.identifier)); jobs.add(job); }; -export const enable = (): void => { +export const enable = async (): Promise => { const job: jobs.Job = new jobs.Job(jobs.identifier(), "root-detection-enable"); - job.addImplementation(testKeysCheck(true, job.identifier)); - job.addImplementation(execSuCheck(true, job.identifier)); - job.addImplementation(fileExistsCheck(true, job.identifier)); - job.addImplementation(jailMonkeyBypass(true, job.identifier)); + job.addImplementation(await testKeysCheck(true, job.identifier)); + job.addImplementation(await execSuCheck(true, job.identifier)); + job.addImplementation(await fileExistsCheck(true, job.identifier)); + job.addImplementation(await jailMonkeyBypass(true, job.identifier)); // RootBeer functions - job.addImplementation(rootBeerIsRooted(true, job.identifier)); - job.addImplementation(rootBeerCheckForBinary(true, job.identifier)); - job.addImplementation(rootBeerCheckForDangerousProps(true, job.identifier)); - job.addImplementation(rootBeerDetectRootCloakingApps(true, job.identifier)); - job.addImplementation(rootBeerCheckSuExists(true, job.identifier)); - job.addImplementation(rootBeerDetectTestKeys(true, job.identifier)); - job.addImplementation(rootBeerNative(true, job.identifier)); - job.addImplementation(rootBeerCheckSeLinux(false, job.identifier)); + job.addImplementation(await rootBeerIsRooted(true, job.identifier)); + job.addImplementation(await rootBeerCheckForBinary(true, job.identifier)); + job.addImplementation(await rootBeerCheckForDangerousProps(true, job.identifier)); + job.addImplementation(await rootBeerDetectRootCloakingApps(true, job.identifier)); + job.addImplementation(await rootBeerCheckSuExists(true, job.identifier)); + job.addImplementation(await rootBeerDetectTestKeys(true, job.identifier)); + job.addImplementation(await rootBeerNative(true, job.identifier)); + job.addImplementation(await rootBeerCheckSeLinux(false, job.identifier)); jobs.add(job); }; diff --git a/agent/src/lib/jobs.ts b/agent/src/lib/jobs.ts index 266ff987..e752aa4f 100644 --- a/agent/src/lib/jobs.ts +++ b/agent/src/lib/jobs.ts @@ -23,8 +23,14 @@ export class Job { }; addImplementation(implementation: any): void { - if (implementation !== undefined) + if (implementation !== undefined) { + // Functions not found, working as expected + if (implementation == null) return; this.implementations.push(implementation); + } else { + c.log(c.redBright(`[warn] Undefined implementation:`)); + c.log(c.blackBright(new Error().stack)); + } }; addReplacement(replacement: any): void { diff --git a/objection/state/jobs.py b/objection/state/jobs.py index 409c761d..dcbc020f 100644 --- a/objection/state/jobs.py +++ b/objection/state/jobs.py @@ -72,16 +72,19 @@ def add_job(self, new_job: Job) -> None: if new_job.uuid not in self.jobs: self.jobs[new_job.uuid] = new_job - def remove_job(self, job_uuid: int) -> Job: + def remove_job(self, job_uuid: int): """ Removes a job from the job state manager. :param job_uuid: :return Job: """ + if job_uuid not in self.jobs: + click.secho(f"Error: Job with ID {job_uuid} does not exist.", fg='red') + return + job_to_remove = self.jobs.pop(job_uuid) job_to_remove.end() - return job_to_remove def cleanup(self) -> None: """ diff --git a/tests/commands/test_filemanager.py b/tests/commands/test_filemanager.py index b186a89f..5bfa4c9f 100644 --- a/tests/commands/test_filemanager.py +++ b/tests/commands/test_filemanager.py @@ -380,7 +380,7 @@ def test_download_platform_proxy_validates_arguments(self): with capture(download, []) as o: output = o - self.assertEqual(output, 'Usage: file download (optional: )\n') + self.assertEqual(output, 'Usage: filesystem download (optional: )\n') @mock.patch('objection.commands.filemanager._download_ios') def test_download_platform_proxy_calls_ios_method(self, mock_download_ios): @@ -490,7 +490,7 @@ def test_file_upload_method_proxy_validates_arguments(self): with capture(upload, []) as o: output = o - self.assertEqual(output, 'Usage: file upload (optional: )\n') + self.assertEqual(output, 'Usage: filesystem upload (optional: )\n') @mock.patch('objection.commands.filemanager._upload_ios') def test_file_upload_method_proxy_calls_ios_helper_method(self, mock_upload_ios): From c6fcae2517a5cba42c011a69e4358cd52dad6213 Mon Sep 17 00:00:00 2001 From: Isak Van Der Walt Date: Thu, 10 Jul 2025 17:36:18 +0200 Subject: [PATCH 3/4] (fix) Compatibility with Apktool 2.12.0. --- objection/utils/patchers/android.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/objection/utils/patchers/android.py b/objection/utils/patchers/android.py index 7127bab5..ac9f1f86 100644 --- a/objection/utils/patchers/android.py +++ b/objection/utils/patchers/android.py @@ -236,6 +236,11 @@ def is_apktool_ready(self) -> bool: if len(o.split('\n')) > 1: o = o.split('\n')[0] + # Apktool v2.12.0 has changed the syntax `apktool -version, this grabs the version from the usage screen output + # instead of re-running as `apktool v`. + if len(o.split(' ')) > 1: + o = o.split(' ')[1] + if len(o) == 0: click.secho('Unable to determine apktool version. Is it installed') return False @@ -404,8 +409,10 @@ def unpack_apk(self, fix_concurrency_to = None): self.required_commands['apktool']['location'], 'decode', '-f', - '-r' if self.skip_resources else '', - '--only-main-classes' if self.only_main_classes else '', + ] + + (['-r'] if self.skip_resources else []) + + (['--only-main-classes'] if self.only_main_classes else []) + + [ '-o', self.apk_temp_directory, self.apk_source @@ -414,6 +421,8 @@ def unpack_apk(self, fix_concurrency_to = None): if len(o.err) > 0: click.secho('An error may have occurred while extracting the APK.', fg='red') click.secho(o.err, fg='red') + + click.secho(o.cmd, dim=True) def inject_internet_permission(self): """ From 08981534c75631d3763e096a96643d1f76786971 Mon Sep 17 00:00:00 2001 From: Isak Van Der Walt Date: Thu, 10 Jul 2025 17:45:31 +0200 Subject: [PATCH 4/4] (fix) Changed print to debug_print. --- objection/utils/patchers/android.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/objection/utils/patchers/android.py b/objection/utils/patchers/android.py index ac9f1f86..ea1881d4 100644 --- a/objection/utils/patchers/android.py +++ b/objection/utils/patchers/android.py @@ -13,6 +13,7 @@ from .base import BasePlatformGadget, BasePlatformPatcher, objection_path from .github import Github +from ..helpers import debug_print class AndroidGadget(BasePlatformGadget): @@ -405,6 +406,7 @@ def unpack_apk(self, fix_concurrency_to = None): click.secho('Unpacking {0}'.format(self.apk_source), dim=True) + o = delegator.run(self.list2cmdline([ self.required_commands['apktool']['location'], 'decode', @@ -418,11 +420,11 @@ def unpack_apk(self, fix_concurrency_to = None): self.apk_source ] + ([] if fix_concurrency_to is None else ['-j', fix_concurrency_to])), timeout=self.command_run_timeout) + debug_print("Command:" + o.cmd) + if len(o.err) > 0: click.secho('An error may have occurred while extracting the APK.', fg='red') click.secho(o.err, fg='red') - - click.secho(o.cmd, dim=True) def inject_internet_permission(self): """