From 3ebf3868a652027b520cc64208f2ef53a5778199 Mon Sep 17 00:00:00 2001 From: Isak Van Der Walt Date: Sun, 13 Oct 2024 13:58:29 +0200 Subject: [PATCH] (fix/feat) Job handler update. --- agent/src/android/hooking.ts | 41 ++-------- agent/src/android/keystore.ts | 21 +++-- agent/src/android/pinning.ts | 68 ++++++++--------- agent/src/android/root.ts | 89 ++++++++++------------ agent/src/ios/crypto.ts | 35 ++++----- agent/src/ios/hooking.ts | 29 ++----- agent/src/ios/jailbreak.ts | 47 +++++------- agent/src/ios/pinning.ts | 67 ++++++++-------- agent/src/ios/userinterface.ts | 25 +----- agent/src/lib/helpers.ts | 2 +- agent/src/lib/interfaces.ts | 7 -- agent/src/lib/jobs.ts | 110 +++++++++++++++++++-------- agent/src/rpc/android.ts | 2 +- agent/src/rpc/jobs.ts | 2 +- objection/commands/frida_commands.py | 3 +- objection/commands/jobs.py | 65 ++++++++++++---- objection/console/cli.py | 3 +- objection/console/commands.py | 3 +- objection/console/repl.py | 2 +- objection/state/jobs.py | 74 ++++++++++++++---- objection/utils/agent.py | 14 ++-- objection/utils/helpers.py | 14 ---- 22 files changed, 371 insertions(+), 352 deletions(-) diff --git a/agent/src/android/hooking.ts b/agent/src/android/hooking.ts index fc9fac45..2810f5d0 100644 --- a/agent/src/android/hooking.ts +++ b/agent/src/android/hooking.ts @@ -1,5 +1,4 @@ import { colors as c } from "../lib/color.js"; -import { IJob } from "../lib/interfaces.js"; import * as jobs from "../lib/jobs.js"; import { ICurrentActivityFragment } from "./lib/interfaces.js"; import { @@ -67,11 +66,7 @@ const getPatternType = (pattern: string): PatternType => { export const lazyWatchForPattern = (query: string, watch: boolean, dargs: boolean, dret: boolean, dbt: boolean): void => { // TODO: Use param to control interval let found = false; - const job: IJob = { - identifier: jobs.identifier(), - implementations: [], - type: `notify-class for: ${query}`, - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(),`notify-class for: ${query}`); // This method loops over all enumerate matches and then calls watch // with the arguments specified in the parent function @@ -262,11 +257,7 @@ export const watch = (pattern: string, dargs: boolean, dbt: boolean, dret: boole if (patternType === PatternType.Klass) { // start a new job container - const job: IJob = { - identifier: jobs.identifier(), - implementations: [], - type: `watch-class for: ${pattern}`, - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(),`watch-class for: ${pattern}`); const w = watchClass(pattern, job, dargs, dbt, dret); jobs.add(job); @@ -275,11 +266,7 @@ export const watch = (pattern: string, dargs: boolean, dbt: boolean, dret: boole } // assume we have PatternType.Regex - const job: IJob = { - identifier: jobs.identifier(), - implementations: [], - type: `watch-pattern for: ${pattern}`, - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(),`watch-pattern for: ${pattern}`); jobs.add(job); return new Promise((resolve, reject) => { @@ -299,7 +286,7 @@ export const watch = (pattern: string, dargs: boolean, dbt: boolean, dret: boole }); }; -const watchClass = (clazz: string, job: IJob, dargs: boolean = false, dbt: boolean = false, dret: boolean = false): Promise => { +const watchClass = (clazz: string, job: jobs.Job, dargs: boolean = false, dbt: boolean = false, dret: boolean = false): Promise => { return wrapJavaPerform(() => { const clazzInstance: JavaClass = Java.use(clazz); @@ -337,7 +324,7 @@ const watchClass = (clazz: string, job: IJob, dargs: boolean = false, dbt: boole }; const watchMethod = ( - fqClazz: string, job: IJob, dargs: boolean, dbt: boolean, dret: boolean, + fqClazz: string, job: jobs.Job, dargs: boolean, dbt: boolean, dret: boolean, ): Promise => { const [clazz, method] = splitClassMethod(fqClazz); // send(`Attempting to watch class ${c.green(clazz)} and method ${c.green(method)}.`); @@ -400,11 +387,7 @@ const watchMethod = ( }; // Push the implementation so that it can be nulled later - if (job.implementations) { - job.implementations.push(m); - } else { - job.implementations = [ m ]; - } + job.addImplementation(m); }); }); @@ -534,11 +517,7 @@ export const setReturnValue = (fqClazz: string, filterOverload: string | null, n } return wrapJavaPerform(() => { - const job: IJob = { - identifier: jobs.identifier(), - implementations: [], - type: `set-return for: ${fqClazz}`, - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(), `set-return for: ${fqClazz}`); const targetClazz: JavaClass = Java.use(clazz); @@ -570,11 +549,7 @@ export const setReturnValue = (fqClazz: string, filterOverload: string | null, n }; // record override - if (job.implementations) { - job.implementations.push(m); - } else { - job.implementations = [ m ]; - } + job.addImplementation(m); }); diff --git a/agent/src/android/keystore.ts b/agent/src/android/keystore.ts index c216b805..953befcc 100644 --- a/agent/src/android/keystore.ts +++ b/agent/src/android/keystore.ts @@ -10,7 +10,6 @@ import { KeyStore, SecretKeyFactory } from "./lib/types.js"; -import { IJob } from "../lib/interfaces.js"; import * as jobs from "../lib/jobs.js"; // Dump entries in the Android Keystore, together with a flag @@ -181,7 +180,7 @@ export const clear = () => { // Watch for KeyStore.load(); // TODO: Store the keystores themselves maybe? -const keystoreLoad = (ident: string): any | undefined => { +const keystoreLoad = (ident: number): Promise => { return wrapJavaPerform(() => { const ks: KeyStore = Java.use("java.security.KeyStore"); const ksLoad = ks.load.overload("java.io.InputStream", "[C"); @@ -193,12 +192,14 @@ const keystoreLoad = (ident: string): any | undefined => { `called, loading a ${c.cyanBright(this.getType())} keystore.`); return this.load(stream, password); }; + + return ksLoad }); }; // Watch for Keystore.getKey(). // TODO: Extract more information, like the key itself maybe? -const keystoreGetKey = (ident: string): any | undefined => { +const keystoreGetKey = (ident: number): Promise => { return wrapJavaPerform(() => { const ks: KeyStore = Java.use("java.security.KeyStore"); const ksGetKey = ks.getKey.overload("java.lang.String", "[C"); @@ -211,20 +212,18 @@ const keystoreGetKey = (ident: string): any | undefined => { `called, returning a ${c.greenBright(key.$className)} instance.`); return key; }; + return ksGetKey; }); }; // Android KeyStore watcher. // Many, many more methods can be added here.. -export const watchKeystore = (): void => { - const job: IJob = { - identifier: jobs.identifier(), - type: "android-keystore-watch", - }; - job.implementations = []; +export const watchKeystore = async (): Promise => { + const job: jobs.Job = new jobs.Job(jobs.identifier(), "android-keystore-watch"); - job.implementations.push(keystoreLoad(job.identifier)); - job.implementations.push(keystoreGetKey(job.identifier)); + job.addImplementation(await keystoreLoad(job.identifier)); + job.addImplementation(await keystoreGetKey(job.identifier)); + jobs.add(job); }; diff --git a/agent/src/android/pinning.ts b/agent/src/android/pinning.ts index 0bfa9d25..73625e0e 100644 --- a/agent/src/android/pinning.ts +++ b/agent/src/android/pinning.ts @@ -1,6 +1,5 @@ import { colors as c } from "../lib/color.js"; import { qsend } from "../lib/helpers.js"; -import { IJob } from "../lib/interfaces.js"; import * as jobs from "../lib/jobs.js"; import { wrapJavaPerform } from "./lib/libjava.js"; import { @@ -17,7 +16,7 @@ import { // a simple flag to control if we should be quiet or not let quiet: boolean = false; -const sslContextEmptyTrustManager = (ident: string): any => { +const sslContextEmptyTrustManager = (ident: number): Promise => { // -- Sample Java // // "Generic" TrustManager Example @@ -83,7 +82,7 @@ const sslContextEmptyTrustManager = (ident: string): any => { }); }; -const okHttp3CertificatePinnerCheck = (ident: string): any | undefined => { +const okHttp3CertificatePinnerCheck = (ident: number): Promise => { // -- Sample Java // // Example used to test this bypass. @@ -125,7 +124,7 @@ const okHttp3CertificatePinnerCheck = (ident: string): any | undefined => { }); }; -const okHttp3CertificatePinnerCheckOkHttp = (ident: string): any | undefined => { +const okHttp3CertificatePinnerCheckOkHttp = (ident: number): Promise => { // -- Sample Java // // Example used to test this bypass. @@ -167,7 +166,7 @@ const okHttp3CertificatePinnerCheckOkHttp = (ident: string): any | undefined => }); }; -const appceleratorTitaniumPinningTrustManager = (ident: string): any | undefined => { +const appceleratorTitaniumPinningTrustManager = (ident: number): Promise => { return wrapJavaPerform(() => { try { const pinningTrustManager: PinningTrustManager = Java.use("appcelerator.https.PinningTrustManager"); @@ -204,7 +203,7 @@ const appceleratorTitaniumPinningTrustManager = (ident: string): any | undefined // blogs/2017/november/bypassing-androids-network-security-configuration/ // // More information: https://sensepost.com/blog/2018/tip-toeing-past-android-7s-network-security-configuration/ -const trustManagerImplVerifyChainCheck = (ident: string): any | undefined => { +const trustManagerImplVerifyChainCheck = (ident: number): Promise => { return wrapJavaPerform(() => { try { const trustManagerImpl: TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl"); @@ -241,7 +240,7 @@ const trustManagerImplVerifyChainCheck = (ident: string): any | undefined => { // Android 7+ TrustManagerImpl.checkTrustedRecursive() // The work in the following method is based on: // https://techblog.mediaservice.net/2018/11/universal-android-ssl-pinning-bypass-2/ -const trustManagerImplCheckTrustedRecursiveCheck = (ident: string): any | undefined => { +const trustManagerImplCheckTrustedRecursiveCheck = (ident: number): Promise => { return wrapJavaPerform(() => { try { const arrayList: ArrayList = Java.use("java.util.ArrayList"); @@ -276,7 +275,7 @@ const trustManagerImplCheckTrustedRecursiveCheck = (ident: string): any | undefi }); }; -const phoneGapSSLCertificateChecker = (ident: string): any | undefined => { +const phoneGapSSLCertificateChecker = (ident: number): Promise => { return wrapJavaPerform(() => { try { const sslCertificateChecker: SSLCertificateChecker = Java.use("nl.xservices.plugins.SSLCertificateChecker"); @@ -285,20 +284,20 @@ const phoneGapSSLCertificateChecker = (ident: string): any | undefined => { `overriding SSLCertificateChecker.execute()`), ); - const SSLCertificateCheckerExecute = sslCertificateChecker.execute; - - SSLCertificateCheckerExecute.overload( - "java.lang.String", "org.json.JSONArray", "org.apache.cordova.CallbackContext").implementation = - // tslint:disable-next-line:only-arrow-functions - function (str, jsonArray, callBackContext) { - qsend(quiet, - c.blackBright(`[${ident}] `) + `Called ` + - c.green(`SSLCertificateChecker.execute()`) + - `, not throwing an exception.`, - ); - callBackContext.success("CONNECTION_SECURE"); - return true; - }; + const SSLCertificateCheckerExecute = sslCertificateChecker.execute.overload("java.lang.String", + "org.json.JSONArray", "org.apache.cordova.CallbackContext"); + + SSLCertificateCheckerExecute.implementation = function (str, jsonArray, callBackContext) { + qsend(quiet, + c.blackBright(`[${ident}] `) + `Called ` + + c.green(`SSLCertificateChecker.execute()`) + + `, not throwing an exception.`, + ); + callBackContext.success("CONNECTION_SECURE"); + return true; + }; + + return SSLCertificateCheckerExecute; } catch (err) { if ((err as Error).message.indexOf("ClassNotFoundException") === 0) { @@ -309,25 +308,22 @@ const phoneGapSSLCertificateChecker = (ident: string): any | undefined => { }; // the main exported function to run all of the pinning bypass methods known -export const disable = (q: boolean): void => { +export const disable = async (q: boolean): Promise => { if (q) { send(c.yellow(`Quiet mode enabled. Not reporting invocations.`)); quiet = true; } - const job: IJob = { - identifier: jobs.identifier(), - type: "android-sslpinning-disable", - }; - - job.implementations = []; + const job: jobs.Job = new jobs.Job(jobs.identifier(), "android-sslpinning-disable"); + + job.addImplementation(await sslContextEmptyTrustManager(job.identifier)); + // Exceptions can cause undefined values if classes are not found. Thus addImplementation only adds if function was hooked + job.addImplementation(await okHttp3CertificatePinnerCheck(job.identifier)); + job.addImplementation(await okHttp3CertificatePinnerCheckOkHttp(job.identifier)); + job.addImplementation(await appceleratorTitaniumPinningTrustManager(job.identifier)); + job.addImplementation(await trustManagerImplVerifyChainCheck(job.identifier)); + job.addImplementation(await trustManagerImplCheckTrustedRecursiveCheck(job.identifier)); + job.addImplementation(await phoneGapSSLCertificateChecker(job.identifier)); - job.implementations.push(sslContextEmptyTrustManager(job.identifier)); - job.implementations.push(okHttp3CertificatePinnerCheck(job.identifier)); - job.implementations.push(okHttp3CertificatePinnerCheckOkHttp(job.identifier)); - job.implementations.push(appceleratorTitaniumPinningTrustManager(job.identifier)); - job.implementations.push(trustManagerImplVerifyChainCheck(job.identifier)); - job.implementations.push(trustManagerImplCheckTrustedRecursiveCheck(job.identifier)); - job.implementations.push(phoneGapSSLCertificateChecker(job.identifier)); jobs.add(job); }; diff --git a/agent/src/android/root.ts b/agent/src/android/root.ts index 4b8aadab..34701129 100644 --- a/agent/src/android/root.ts +++ b/agent/src/android/root.ts @@ -1,5 +1,4 @@ import { colors as c } from "../lib/color.js"; -import { IJob } from "../lib/interfaces.js"; import * as jobs from "../lib/jobs.js"; import { wrapJavaPerform } from "./lib/libjava.js"; import { @@ -25,7 +24,7 @@ const commonPaths = [ "/system/xbin/su", ]; -const testKeysCheck = (success: boolean, ident: string): any => { +const testKeysCheck = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const JavaString: JavaString = Java.use("java.lang.String"); @@ -45,7 +44,7 @@ const testKeysCheck = (success: boolean, ident: string): any => { }); }; -const execSuCheck = (success: boolean, ident: string): any => { +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"); @@ -67,7 +66,7 @@ const execSuCheck = (success: boolean, ident: string): any => { }); }; -const fileExistsCheck = (success: boolean, ident: string): any => { +const fileExistsCheck = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const JavaFile: File = Java.use("java.io.File"); JavaFile.exists.implementation = function () { @@ -96,7 +95,7 @@ const fileExistsCheck = (success: boolean, ident: string): any => { // RootBeer: https://github.com/scottyab/rootbeer -const rootBeerIsRooted = (success: boolean, ident: string): any => { +const rootBeerIsRooted = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.isRooted.overload().implementation = function () { @@ -117,7 +116,7 @@ const rootBeerIsRooted = (success: boolean, ident: string): any => { }); }; -const rootBeerCheckForBinary = (success: boolean, ident: string): any => { +const rootBeerCheckForBinary = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.checkForBinary.overload('java.lang.String').implementation = function () { @@ -138,7 +137,7 @@ const rootBeerCheckForBinary = (success: boolean, ident: string): any => { }); }; -const rootBeerCheckForDangerousProps = (success: boolean, ident: string): any => { +const rootBeerCheckForDangerousProps = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.checkForDangerousProps.overload().implementation = function () { @@ -159,7 +158,7 @@ const rootBeerCheckForDangerousProps = (success: boolean, ident: string): any => }); }; -const rootBeerDetectRootCloakingApps = (success: boolean, ident: string): any => { +const rootBeerDetectRootCloakingApps = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.detectRootCloakingApps.overload().implementation = function () { @@ -180,7 +179,7 @@ const rootBeerDetectRootCloakingApps = (success: boolean, ident: string): any => }); }; -const rootBeerCheckSuExists = (success: boolean, ident: string): any => { +const rootBeerCheckSuExists = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.checkSuExists.overload().implementation = function () { @@ -201,7 +200,7 @@ const rootBeerCheckSuExists = (success: boolean, ident: string): any => { }); }; -const rootBeerDetectTestKeys = (success: boolean, ident: string): any => { +const rootBeerDetectTestKeys = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer"); RootBeer.detectTestKeys.overload().implementation = function () { @@ -222,7 +221,7 @@ const rootBeerDetectTestKeys = (success: boolean, ident: string): any => { }); }; -const rootBeerCheckSeLinux = (success: boolean, ident: string): any => { +const rootBeerCheckSeLinux = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const Util = Java.use("com.scottyab.rootbeer.util"); Util.isSelinuxFlagInEnabled.overload().implementation = function () { @@ -243,7 +242,7 @@ const rootBeerCheckSeLinux = (success: boolean, ident: string): any => { }); }; -const rootBeerNative = (success: boolean, ident: string): any => { +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 () { @@ -265,7 +264,7 @@ const rootBeerNative = (success: boolean, ident: string): any => { }; // ref: https://www.ayrx.me/gantix-jailmonkey-root-detection-bypass/ -const jailMonkeyBypass = (success: boolean, ident: string): any => { +const jailMonkeyBypass = (success: boolean, ident: number): any => { return wrapJavaPerform(() => { const JavaJailMonkeyModule = Java.use("com.gantix.JailMonkey.JailMonkeyModule"); const JavaHashMap = Java.use("java.util.HashMap"); @@ -292,53 +291,43 @@ const jailMonkeyBypass = (success: boolean, ident: string): any => { }; export const disable = (): void => { - const job: IJob = { - identifier: jobs.identifier(), - type: "root-detection-disable", - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(), 'root-detection-disable'); - job.implementations = []; - - job.implementations.push(testKeysCheck(false, job.identifier)); - job.implementations.push(execSuCheck(false, job.identifier)); - job.implementations.push(fileExistsCheck(false, job.identifier)); - job.implementations.push(jailMonkeyBypass(false, job.identifier)); + job.addImplementation(testKeysCheck(false, job.identifier)); + job.addImplementation(execSuCheck(false, job.identifier)); + job.addImplementation(fileExistsCheck(false, job.identifier)); + job.addImplementation(jailMonkeyBypass(false, job.identifier)); // RootBeer functions - job.implementations.push(rootBeerIsRooted(false, job.identifier)); - job.implementations.push(rootBeerCheckForBinary(false, job.identifier)); - job.implementations.push(rootBeerCheckForDangerousProps(false, job.identifier)); - job.implementations.push(rootBeerDetectRootCloakingApps(false, job.identifier)); - job.implementations.push(rootBeerCheckSuExists(false, job.identifier)); - job.implementations.push(rootBeerDetectTestKeys(false, job.identifier)); - job.implementations.push(rootBeerNative(false, job.identifier)); - job.implementations.push(rootBeerCheckSeLinux(false, job.identifier)); + 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)); jobs.add(job); }; export const enable = (): void => { - const job: IJob = { - identifier: jobs.identifier(), - implementations: [], - type: "root-detection-enable", - }; - job.implementations = []; - - job.implementations.push(testKeysCheck(true, job.identifier)); - job.implementations.push(execSuCheck(true, job.identifier)); - job.implementations.push(fileExistsCheck(true, job.identifier)); - job.implementations.push(jailMonkeyBypass(true, job.identifier)); + 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)); // RootBeer functions - job.implementations.push(rootBeerIsRooted(true, job.identifier)); - job.implementations.push(rootBeerCheckForBinary(true, job.identifier)); - job.implementations.push(rootBeerCheckForDangerousProps(true, job.identifier)); - job.implementations.push(rootBeerDetectRootCloakingApps(true, job.identifier)); - job.implementations.push(rootBeerCheckSuExists(true, job.identifier)); - job.implementations.push(rootBeerDetectTestKeys(true, job.identifier)); - job.implementations.push(rootBeerNative(true, job.identifier)); - job.implementations.push(rootBeerCheckSeLinux(false, job.identifier)); + 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)); jobs.add(job); }; diff --git a/agent/src/ios/crypto.ts b/agent/src/ios/crypto.ts index 7e3e93f3..c2d5975c 100644 --- a/agent/src/ios/crypto.ts +++ b/agent/src/ios/crypto.ts @@ -1,6 +1,5 @@ import { colors as c } from "../lib/color.js"; import { fsend } from "../lib/helpers.js"; -import { IJob } from "../lib/interfaces.js"; import * as jobs from "../lib/jobs.js"; import { arrayBufferToHex, @@ -53,7 +52,7 @@ const CCPseudoRandomAlgorithm: AlgorithmType = { // ident for crypto hooks job -let cryptoidentifier: string = ""; +let cryptoidentifier: number = 0; // operation being performed 0=encrypt 1=decrypt let op = 0; @@ -67,7 +66,7 @@ let alg = 0; // append the final block from CCCryptorFinal let dataOutBytes: string = ""; -const secrandomcopybytes = (ident: string): InvocationListener => { +const secrandomcopybytes = (ident: number): InvocationListener => { const hook = "SecRandomCopyBytes"; return Interceptor.attach( Module.getExportByName(null, hook), { @@ -87,7 +86,7 @@ const secrandomcopybytes = (ident: string): InvocationListener => { }); }; -const cckeyderivationpbkdf = (ident: string): InvocationListener => { +const cckeyderivationpbkdf = (ident: number): InvocationListener => { const hook = "CCKeyDerivationPBKDF"; return Interceptor.attach( Module.getExportByName(null, hook), { @@ -141,7 +140,7 @@ const cckeyderivationpbkdf = (ident: string): InvocationListener => { }); }; -const cccrypt = (ident: string): InvocationListener => { +const cccrypt = (ident: number): InvocationListener => { const hook = "CCCrypt"; return Interceptor.attach( Module.getExportByName(null, hook), { @@ -212,7 +211,7 @@ const cccrypt = (ident: string): InvocationListener => { }); }; -const cccryptorcreate = (ident: string): InvocationListener => { +const cccryptorcreate = (ident: number): InvocationListener => { const hook = "CCCryptorCreate"; return Interceptor.attach( Module.getExportByName(null, hook), { @@ -258,7 +257,7 @@ const cccryptorcreate = (ident: string): InvocationListener => { }); }; -const cccryptorupdate = (ident: string): InvocationListener => { +const cccryptorupdate = (ident: number): InvocationListener => { const hook = "CCCryptorUpdate"; return Interceptor.attach( Module.getExportByName(null, hook), { @@ -301,7 +300,7 @@ const cccryptorupdate = (ident: string): InvocationListener => { }); }; -const cccryptorfinal = (ident: string): InvocationListener => { +const cccryptorfinal = (ident: number): InvocationListener => { const hook = "CCCryptorFinal"; return Interceptor.attach( Module.getExportByName(null, hook), { @@ -336,24 +335,20 @@ const cccryptorfinal = (ident: string): InvocationListener => { export const monitor = (): void => { // if we already have a job registered then return if (jobs.hasIdent(cryptoidentifier)) { - send(`${c.greenBright("Job already registered")}: ${c.blueBright(cryptoidentifier)}`); + send(`${c.greenBright("Job already registered")}: ${c.blueBright(cryptoidentifier.toString())}`); return; } - const job: IJob = { - identifier: jobs.identifier(), - type: "ios-crypto-monitor", - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(), "ios-crypto-monitor"); - job.invocations = []; cryptoidentifier = job.identifier; - job.invocations.push(secrandomcopybytes(job.identifier)); - job.invocations.push(cckeyderivationpbkdf(job.identifier)); - job.invocations.push(cccrypt(job.identifier)); - job.invocations.push(cccryptorcreate(job.identifier)); - job.invocations.push(cccryptorupdate(job.identifier)); - job.invocations.push(cccryptorfinal(job.identifier)); + job.addInvocation(secrandomcopybytes(job.identifier)); + job.addInvocation(cckeyderivationpbkdf(job.identifier)); + job.addInvocation(cccrypt(job.identifier)); + job.addInvocation(cccryptorcreate(job.identifier)); + job.addInvocation(cccryptorupdate(job.identifier)); + job.addInvocation(cccryptorfinal(job.identifier)); jobs.add(job); }; diff --git a/agent/src/ios/hooking.ts b/agent/src/ios/hooking.ts index 64e61cfa..5d4271ee 100644 --- a/agent/src/ios/hooking.ts +++ b/agent/src/ios/hooking.ts @@ -1,5 +1,4 @@ import { colors as c } from "../lib/color.js"; -import { IJob } from "../lib/interfaces.js"; import * as jobs from "../lib/jobs.js"; @@ -38,11 +37,7 @@ export const watch = (patternOrClass: string, dargs: boolean = false, dbt: boole // Add the job // We init a new job here as the child watch* calls will be grouped in a single job. // mostly commandline fluff - const job: IJob = { - identifier: jobs.identifier(), - invocations: [], - type: `ios-watch for: ${patternOrClass}`, - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(), `ios-watch for: ${patternOrClass}`); jobs.add(job); const isPattern = patternOrClass.includes('['); @@ -60,7 +55,7 @@ export const watch = (patternOrClass: string, dargs: boolean = false, dbt: boole watchClass(patternOrClass, job, dargs, dbt, dret, watchParents); }; -const watchClass = (clazz: string, job: IJob, dargs: boolean = false, dbt: boolean = false, +const watchClass = (clazz: string, job: jobs.Job, dargs: boolean = false, dbt: boolean = false, dret: boolean = false, parents: boolean = false): void => { const target = ObjC.classes[clazz]; @@ -81,7 +76,7 @@ const watchClass = (clazz: string, job: IJob, dargs: boolean = false, dbt: boole }; -const watchMethod = (selector: string, job: IJob, dargs: boolean, dbt: boolean, +const watchMethod = (selector: string, job: jobs.Job, dargs: boolean, dbt: boolean, dret: boolean): void => { const resolver = new ApiResolver("objc"); @@ -164,12 +159,8 @@ const watchMethod = (selector: string, job: IJob, dargs: boolean, dbt: boolean, send(c.blackBright(`[${job.identifier}] `) + `Return Value: ${c.red(retval.toString())}`); }, }); - if (job.invocations) { - job.invocations.push(watchInvocation); - } else { - job.invocations = [ watchInvocation ]; - } + job.addInvocation(watchInvocation); }; export const setMethodReturn = (selector: string, returnValue: boolean): void => { @@ -202,11 +193,7 @@ export const setMethodReturn = (selector: string, returnValue: boolean): void => } // Start a new Job - const job: IJob = { - identifier: jobs.identifier(), - invocations: [], - type: `set-method-return for: ${selector}`, - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(), `set-method-return for: ${selector}`); // Attach to the discovered match // TODO: loop correctly when globbing @@ -243,10 +230,6 @@ export const setMethodReturn = (selector: string, returnValue: boolean): void => }); // register the job - if (job.invocations) { - job.invocations.push(watchInvocation); - } else { - job.invocations = [ watchInvocation ]; - }; + job.addInvocation(watchInvocation); jobs.add(job); }; diff --git a/agent/src/ios/jailbreak.ts b/agent/src/ios/jailbreak.ts index c08907e2..cd2e00ad 100644 --- a/agent/src/ios/jailbreak.ts +++ b/agent/src/ios/jailbreak.ts @@ -1,5 +1,4 @@ import { colors as c } from "../lib/color.js"; -import { IJob } from "../lib/interfaces.js"; import * as jobs from "../lib/jobs.js"; // Attempts to disable Jailbreak detection. @@ -49,7 +48,7 @@ const jailbreakPaths = [ // toggles replies to fileExistsAtPath: for the paths in jailbreakPaths -const fileExistsAtPath = (success: boolean, ident: string): InvocationListener => { +const fileExistsAtPath = (success: boolean, ident: number): InvocationListener => { return Interceptor.attach( ObjC.classes.NSFileManager["- fileExistsAtPath:"].implementation, { @@ -114,7 +113,7 @@ const fileExistsAtPath = (success: boolean, ident: string): InvocationListener = // toggles replies to fopen: for the paths in jailbreakPaths -const fopen = (success: boolean, ident: string): InvocationListener => { +const fopen = (success: boolean, ident: number): InvocationListener => { const fopen_addr = Module.findExportByName(null, "fopen"); if (!fopen_addr) { send(c.red(`fopen function not found!`)); @@ -180,7 +179,7 @@ const fopen = (success: boolean, ident: string): InvocationListener => { }; // toggles replies to canOpenURL for Cydia -const canOpenURL = (success: boolean, ident: string): InvocationListener => { +const canOpenURL = (success: boolean, ident: number): InvocationListener => { return Interceptor.attach( ObjC.classes.UIApplication["- canOpenURL:"].implementation, { @@ -237,7 +236,7 @@ const canOpenURL = (success: boolean, ident: string): InvocationListener => { }; -const libSystemBFork = (success: boolean, ident: string): InvocationListener => { +const libSystemBFork = (success: boolean, ident: number): InvocationListener => { // Hook fork() in libSystem.B.dylib and return 0 // TODO: Hook vfork const libSystemBdylibFork = Module.findExportByName("libSystem.B.dylib", "fork"); @@ -285,7 +284,7 @@ const libSystemBFork = (success: boolean, ident: string): InvocationListener => }; // ref: https://www.ayrx.me/gantix-jailmonkey-root-detection-bypass/ -const jailMonkeyBypass = (success: boolean, ident: string): InvocationListener => { +const jailMonkeyBypass = (success: boolean, ident: number): InvocationListener => { const JailMonkeyClass = ObjC.classes.JailMonkey; if (JailMonkeyClass === undefined) return new InvocationListener(); @@ -300,35 +299,25 @@ const jailMonkeyBypass = (success: boolean, ident: string): InvocationListener = }; export const disable = (): void => { - const job: IJob = { - identifier: jobs.identifier(), - type: "ios-jailbreak-disable", - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(), "ios-jailbreak-disable"); - job.invocations = []; - - job.invocations.push(fileExistsAtPath(false, job.identifier)); - job.invocations.push(libSystemBFork(false, job.identifier)); - job.invocations.push(fopen(false, job.identifier)); - job.invocations.push(canOpenURL(false, job.identifier)); - job.invocations.push(jailMonkeyBypass(false, job.identifier)); + job.addInvocation(fileExistsAtPath(false, job.identifier)); + job.addInvocation(libSystemBFork(false, job.identifier)); + job.addInvocation(fopen(false, job.identifier)); + job.addInvocation(canOpenURL(false, job.identifier)); + job.addInvocation(jailMonkeyBypass(false, job.identifier)); jobs.add(job); }; export const enable = (): void => { - const job: IJob = { - identifier: jobs.identifier(), - type: "ios-jailbreak-enable", - }; - - job.invocations = []; - - job.invocations.push(fileExistsAtPath(true, job.identifier)); - job.invocations.push(libSystemBFork(true, job.identifier)); - job.invocations.push(fopen(true, job.identifier)); - job.invocations.push(canOpenURL(true, job.identifier)); - job.invocations.push(jailMonkeyBypass(true, job.identifier)); + const job: jobs.Job = new jobs.Job(jobs.identifier(), "ios-jailbreak-enable"); + + job.addInvocation(fileExistsAtPath(true, job.identifier)); + job.addInvocation(libSystemBFork(true, job.identifier)); + job.addInvocation(fopen(true, job.identifier)); + job.addInvocation(canOpenURL(true, job.identifier)); + job.addInvocation(jailMonkeyBypass(true, job.identifier)); jobs.add(job); }; diff --git a/agent/src/ios/pinning.ts b/agent/src/ios/pinning.ts index 41f815f1..11894b39 100644 --- a/agent/src/ios/pinning.ts +++ b/agent/src/ios/pinning.ts @@ -1,6 +1,5 @@ import { colors as c } from "../lib/color.js"; import { qsend } from "../lib/helpers.js"; -import { IJob } from "../lib/interfaces.js"; import * as jobs from "../lib/jobs.js"; import { libObjc } from "./lib/libobjc.js"; @@ -45,7 +44,7 @@ import { libObjc } from "./lib/libobjc.js"; // a simple flag to control if we should be quiet or not let quiet: boolean = false; -const afNetworking = (ident: string): InvocationListener[] => { +const afNetworking = (ident: number): InvocationListener[] => { const { AFHTTPSessionManager, AFSecurityPolicy } = ObjC.classes; // If AFNetworking is not a thing, just move on. @@ -165,7 +164,7 @@ const afNetworking = (ident: string): InvocationListener[] => { args[2] = new NativePointer(0x0); } }, - }) : new InvocationListener(); + }) : null; return [ setSSLPinningmode, @@ -175,7 +174,7 @@ const afNetworking = (ident: string): InvocationListener[] => { ]; }; -const nsUrlSession = (ident: string): InvocationListener[] => { +const nsUrlSession = (ident: number): InvocationListener[] => { const NSURLCredential: ObjC.Object = ObjC.classes.NSURLCredential; const resolver = new ApiResolver("objc"); // - [NSURLSession URLSession:didReceiveChallenge:completionHandler:] @@ -251,11 +250,11 @@ const nsUrlSession = (ident: string): InvocationListener[] => { }; // TrustKit -const trustKit = (ident: string): InvocationListener => { +const trustKit = (ident: number): InvocationListener => { // https://github.com/datatheorem/TrustKit/blob/ // 71878dce8c761fc226fecc5dbb6e86fbedaee05e/TrustKit/TSKPinningValidator.m#L84 if (!ObjC.classes.TSKPinningValidator) { - return new InvocationListener(); + return null; } send(c.blackBright(`[${ident}] `) + `Found TrustKit. Hooking known pinning methods.`); @@ -282,11 +281,11 @@ const trustKit = (ident: string): InvocationListener => { }); }; -const cordovaCustomURLConnectionDelegate = (ident: string): InvocationListener => { +const cordovaCustomURLConnectionDelegate = (ident: number): InvocationListener => { // https://github.com/EddyVerbruggen/SSLCertificateChecker-PhoneGap-Plugin/blob/ // 67634bfdf4a31bb09b301db40f8f27fbd8818f61/src/ios/SSLCertificateChecker.m#L109-L116 if (!ObjC.classes.CustomURLConnectionDelegate) { - return new InvocationListener(); + return null; } send(c.blackBright(`[${ident}] `) + `Found SSLCertificateChecker-PhoneGap-Plugin.` + @@ -314,7 +313,7 @@ const cordovaCustomURLConnectionDelegate = (ident: string): InvocationListener = }); }; -const sSLSetSessionOption = (ident: string): NativePointerValue => { +const sSLSetSessionOption = (ident: number): NativePointerValue => { const kSSLSessionOptionBreakOnServerAuth = 0; const noErr = 0; const SSLSetSessionOption = libObjc.SSLSetSessionOption; @@ -339,7 +338,7 @@ const sSLSetSessionOption = (ident: string): NativePointerValue => { return SSLSetSessionOption; }; -const sSLCreateContext = (ident: string): NativePointerValue => { +const sSLCreateContext = (ident: number): NativePointerValue => { const kSSLSessionOptionBreakOnServerAuth = 0; const SSLSetSessionOption = libObjc.SSLSetSessionOption; const SSLCreateContext = libObjc.SSLCreateContext; @@ -364,7 +363,7 @@ const sSLCreateContext = (ident: string): NativePointerValue => { return SSLCreateContext; }; -const sSLHandshake = (ident: string): NativePointerValue => { +const sSLHandshake = (ident: number): NativePointerValue => { const errSSLServerAuthCompared = -9481; const SSLHandshake = libObjc.SSLHandshake; @@ -387,7 +386,7 @@ const sSLHandshake = (ident: string): NativePointerValue => { }; // tls_helper_create_peer_trust -const tlsHelperCreatePeerTrust = (ident: string): NativePointerValue => { +const tlsHelperCreatePeerTrust = (ident: number): NativePointerValue => { const noErr = 0; const tlsHelper = libObjc.tls_helper_create_peer_trust; @@ -409,11 +408,11 @@ const tlsHelperCreatePeerTrust = (ident: string): NativePointerValue => { }; // nw_tls_create_peer_trust -const nwTlsCreatePeerTrust = (ident: string): InvocationListener => { +const nwTlsCreatePeerTrust = (ident: number): InvocationListener => { const peerTrust = libObjc.nw_tls_create_peer_trust; if (peerTrust.isNull()) { - return new InvocationListener(); + return null; } return Interceptor.attach(peerTrust, { @@ -445,7 +444,7 @@ const nwTlsCreatePeerTrust = (ident: string): InvocationListener => { }; // SSL_CTX_set_custom_verify -const sSLCtxSetCustomVerify = (ident: string) => { +const sSLCtxSetCustomVerify = (ident: number): NativePointerValue[] => { const getPskIdentity = libObjc.SSL_get_psk_identity; let setCustomVerify = libObjc.SSL_set_custom_verify; if (setCustomVerify.isNull()) { @@ -454,7 +453,7 @@ const sSLCtxSetCustomVerify = (ident: string) => { } if (setCustomVerify.isNull() || getPskIdentity.isNull()) { - return new InvocationListener(); + return []; } // tslint:disable-next-line:only-arrow-functions variable-name @@ -486,6 +485,11 @@ const sSLCtxSetCustomVerify = (ident: string) => { ); return Memory.allocUtf8String("fakePSKidentity"); }, "pointer", ["pointer"])); + + return [ + setCustomVerify, + getPskIdentity, + ]; }; // exposed method to setup all of the interceptor invocations and replacements @@ -496,42 +500,39 @@ export const disable = (q: boolean): void => { quiet = true; } - const job: IJob = { - identifier: jobs.identifier(), - type: "ios-sslpinning-disable", - }; + const job: jobs.Job = new jobs.Job(jobs.identifier(), "ios-sslpinning-disable"); - job.invocations = []; - job.replacements = []; // Framework hooks. send(c.blackBright(`Hooking common framework methods`)); afNetworking(job.identifier).forEach((i) => { - job.invocations!.push(i); + job.addInvocation(i); }); nsUrlSession(job.identifier).forEach((i) => { - job.invocations!.push(i); + job.addInvocation(i); }); - job.invocations.push(trustKit(job.identifier)); - job.invocations.push(cordovaCustomURLConnectionDelegate(job.identifier)); + job.addInvocation(trustKit(job.identifier)); + job.addInvocation(cordovaCustomURLConnectionDelegate(job.identifier)); // Low level hooks. // iOS 9< send(c.blackBright(`Hooking lower level SSL methods`)); - job.replacements.push(sSLSetSessionOption(job.identifier)); - job.replacements.push(sSLCreateContext(job.identifier)); - job.replacements.push(sSLHandshake(job.identifier)); + job.addReplacement(sSLSetSessionOption(job.identifier)); + job.addReplacement(sSLCreateContext(job.identifier)); + job.addReplacement(sSLHandshake(job.identifier)); // iOS 10> send(c.blackBright(`Hooking lower level TLS methods`)); - job.replacements.push(tlsHelperCreatePeerTrust(job.identifier)); - job.invocations.push(nwTlsCreatePeerTrust(job.identifier)); + job.addReplacement(tlsHelperCreatePeerTrust(job.identifier)); + job.addInvocation(nwTlsCreatePeerTrust(job.identifier)); // iOS 11> send(c.blackBright(`Hooking BoringSSL methods`)); - sSLCtxSetCustomVerify(job.identifier) - // job.invocations.push(sSLCtxSetCustomVerify(job.identifier)); + // sSLCtxSetCustomVerify(job.identifier) + sSLCtxSetCustomVerify(job.identifier).forEach((i) => { + job.addReplacement(i); + }); jobs.add(job); }; diff --git a/agent/src/ios/userinterface.ts b/agent/src/ios/userinterface.ts index a0929fc9..c6c1e63d 100644 --- a/agent/src/ios/userinterface.ts +++ b/agent/src/ios/userinterface.ts @@ -1,7 +1,6 @@ // tslint:disable-next-line:no-var-requires import screenshot from "frida-screenshot"; import { colors as c } from "../lib/color.js"; -import { IJob } from "../lib/interfaces.js"; import * as jobs from "../lib/jobs.js"; @@ -76,11 +75,7 @@ export const biometricsBypass = (): void => { // } // }]; - const policyJob: IJob = { - identifier: jobs.identifier(), - invocations: [], - type: "ios-biometrics-disable-evaluatePolicy", - }; + const policyJob: jobs.Job = new jobs.Job(jobs.identifier(), "ios-biometrics-disable-evaluatePolicy"); const lacontext1: InvocationListener = Interceptor.attach( ObjC.classes.LAContext["- evaluatePolicy:localizedReason:reply:"].implementation, { @@ -128,21 +123,13 @@ export const biometricsBypass = (): void => { }); // register the job - if (policyJob.invocations) { - policyJob.invocations.push(lacontext1); - } else { - policyJob.invocations = [lacontext1]; - } + policyJob.addInvocation(lacontext1); jobs.add(policyJob); // -- Sample Swift // https://gist.github.com/algrid/f3f03915f264f243b9d06e875ad198c8/raw/03998319903ad9d939f85bbcc94ce9c23042b82b/KeychainBio.swift - const accessControlJob: IJob = { - identifier: jobs.identifier(), - invocations: [], - type: "ios-biometrics-disable-evaluateAccessControl", - }; + const accessControlJob: jobs.Job = new jobs.Job(jobs.identifier(), "ios-biometrics-disable-evaluateAccessControl"); const lacontext2: InvocationListener = Interceptor.attach( ObjC.classes.LAContext["- evaluateAccessControl:operation:localizedReason:reply:"].implementation, { @@ -190,10 +177,6 @@ export const biometricsBypass = (): void => { }); // register the job - if (accessControlJob.invocations) { - accessControlJob.invocations.push(lacontext2); - } else { - accessControlJob.invocations = [lacontext2]; - } + accessControlJob.addInvocation(lacontext2); jobs.add(accessControlJob); }; diff --git a/agent/src/lib/helpers.ts b/agent/src/lib/helpers.ts index 24f1abb6..77d7dc54 100644 --- a/agent/src/lib/helpers.ts +++ b/agent/src/lib/helpers.ts @@ -32,7 +32,7 @@ export const qsend = (quiet: boolean, message: any): void => { }; // send a preformated dict -export const fsend = (ident: string, hook: string, message: any): void => { +export const fsend = (ident: number, hook: string, message: any): void => { send( c.blackBright(`[${ident}] `) + c.magenta(`[${hook}]`) + diff --git a/agent/src/lib/interfaces.ts b/agent/src/lib/interfaces.ts index 17e10403..389a819b 100644 --- a/agent/src/lib/interfaces.ts +++ b/agent/src/lib/interfaces.ts @@ -36,10 +36,3 @@ export interface IIosBundlePaths { LibraryDirectory: string; } -export interface IJob { - identifier: string; - invocations?: InvocationListener[]; - replacements?: any[]; - implementations?: any[]; - type: string; -} diff --git a/agent/src/lib/jobs.ts b/agent/src/lib/jobs.ts index 9db1885f..f20fb94f 100644 --- a/agent/src/lib/jobs.ts +++ b/agent/src/lib/jobs.ts @@ -1,23 +1,86 @@ import { colors as c } from "./color.js"; -import { IJob } from "./interfaces.js"; + +export class Job { + identifier: number; + private invocations?: InvocationListener[] = []; + private replacements?: any[] = []; + private implementations?: any[] = []; + type: string; + + constructor(identifier: number, type: string) { + this.identifier = identifier; + this.type = type; + } + + addInvocation(invocation: any): void { + if (invocation === undefined) { + // c.log(c.redBright(`[warn] Undefined Invocation!`)); + c.log(c.redBright(`[warn] Undefined invocation`)); + } + if (invocation !== null) + this.invocations.push(invocation); + + }; + + addImplementation(implementation: any): void { + if (implementation !== undefined) + this.implementations.push(implementation); + }; + + addReplacement(replacement: any): void { + if (replacement !== undefined) + this.replacements.push(replacement); + }; + + killAll(): void { + // remove all invocations + if (this.invocations && this.invocations.length > 0) { + this.invocations.forEach((invocation) => { + (invocation) ? invocation.detach() : + c.log(c.blackBright(`[warn] Skipping detach on null`)); + }); + } + + // revert any replacements + if (this.replacements && this.replacements.length > 0) { + this.replacements.forEach((replacement) => { + Interceptor.revert(replacement); + }); + } + + // remove implementation replacements + if (this.implementations && this.implementations.length > 0) { + this.implementations.forEach((method) => { + if (method.implementation == undefined) { + c.log(c.red(`[warn] ${this.type} job missing implementation value`)); + } + + send(c.blackBright(`(`)+ c.blueBright(this.identifier.toString())+ c.blackBright(`) Removing ${method.holder} `)) + + // TODO: May be racy if the method is currently used. + method.implementation = null; + }); + } + } +} // a record of all of the jobs in the current process -let currentJobs: IJob[] = []; +let currentJobs: Job[] = []; -export const identifier = (): string => Math.random().toString(36).substring(2, 8); -export const all = (): IJob[] => currentJobs; +export const identifier = (): number => Number(Math.random().toString(36).substring(2, 8)); +export const all = (): Job[] => currentJobs; -export const add = (jobData: IJob): void => { +export const add = (jobData: Job): void => { send(`Registering job ` + c.blueBright(`${jobData.identifier}`) + - `. Type: ` + c.greenBright(`${jobData.type}`)); + `. Name: ` + c.greenBright(`${jobData.type}`)); currentJobs.push(jobData); }; // determine of a job already exists based on an identifier -export const hasIdent = (ident: string): boolean => { +export const hasIdent = (ident: number): boolean => { - const m: IJob[] = currentJobs.filter((job) => { + const m: Job[] = currentJobs.filter((job) => { if (job.identifier === ident) { return true; } @@ -29,7 +92,7 @@ export const hasIdent = (ident: string): boolean => { // determine if a job already exists based on a type export const hasType = (type: string): boolean => { - const m: IJob[] = currentJobs.filter((job) => { + const m: Job[] = currentJobs.filter((job) => { if (job.type === type) { return true; } @@ -40,34 +103,17 @@ export const hasType = (type: string): boolean => { // kills a job by detaching any invocations and removing // the job by identifier -export const kill = (ident: string): boolean => { +export const kill = (ident: number): boolean => { currentJobs.forEach((job) => { if (job.identifier !== ident) return; - // detach any invocations - if (job.invocations && job.invocations.length > 0) { - job.invocations.forEach((invocation) => { - (invocation) ? invocation.detach() : - c.log(c.blackBright(`[warn] Skipping detach on null`)); - }); - } - - // revert any replacements - if (job.replacements && job.replacements.length > 0) { - job.replacements.forEach((replacement) => { - Interceptor.revert(replacement); - }); - } - - // remove implementation replacements - if (job.implementations && job.implementations.length > 0) { - job.implementations.forEach((method) => { - // TODO: May be racy if the method is currently used. - method.implementation = null; - }); - } + send(`Killing job ` + c.blueBright(`${job.identifier}`) + + `. Name: ` + c.greenBright(`${job.type}`)); + // remove any hooks + job.killAll(); + // remove the job from the current jobs currentJobs = currentJobs.filter((j) => { return j.identifier !== job.identifier; diff --git a/agent/src/rpc/android.ts b/agent/src/rpc/android.ts index 82d6797c..2ebfd4e5 100644 --- a/agent/src/rpc/android.ts +++ b/agent/src/rpc/android.ts @@ -76,7 +76,7 @@ export const android = { androidKeystoreClear: () => keystore.clear(), androidKeystoreList: (): Promise => keystore.list(), androidKeystoreDetail: (): Promise => keystore.detail(), - androidKeystoreWatch: (): void => keystore.watchKeystore(), + androidKeystoreWatch: (): Promise => keystore.watchKeystore(), // android ssl pinning androidSslPinningDisable: (quiet: boolean) => sslpinning.disable(quiet), diff --git a/agent/src/rpc/jobs.ts b/agent/src/rpc/jobs.ts index 8ba8aa7f..f3c8d703 100644 --- a/agent/src/rpc/jobs.ts +++ b/agent/src/rpc/jobs.ts @@ -3,5 +3,5 @@ import * as j from "../lib/jobs.js"; export const jobs = { // jobs jobsGet: () => j.all(), - jobsKill: (ident: string) => j.kill(ident), + jobsKill: (ident: number) => j.kill(ident), }; diff --git a/objection/commands/frida_commands.py b/objection/commands/frida_commands.py index bdcfaffd..a607313a 100644 --- a/objection/commands/frida_commands.py +++ b/objection/commands/frida_commands.py @@ -82,4 +82,5 @@ def load_background(args: list = None) -> None: hook = ''.join(f.read()) agent = state_connection.get_agent() - agent.background(hook) + agent.attach_script(source, hook) + diff --git a/objection/commands/jobs.py b/objection/commands/jobs.py index d1615f1c..ce653d9f 100644 --- a/objection/commands/jobs.py +++ b/objection/commands/jobs.py @@ -2,6 +2,7 @@ from tabulate import tabulate from objection.state.connection import state_connection +from ..state.jobs import job_manager_state, Job def show(args: list = None) -> None: @@ -11,19 +12,26 @@ def show(args: list = None) -> None: :return: """ - api = state_connection.get_api() - jobs = api.jobs_get() + sync_job_manager() + jobs = job_manager_state.jobs + # click.secho(tabulate( + # [[ + # entry['uuid'], + # sum([ + # len(entry[x]) for x in [ + # 'invocations', 'replacements', 'implementations' + # ] if x in entry + # ]), + # entry['type'], + # ] for entry in jobs], headers=['Job ID', 'Hooks', 'Name'], + # )) click.secho(tabulate( [[ - entry['identifier'], - sum([ - len(entry[x]) for x in [ - 'invocations', 'replacements', 'implementations' - ] if x in entry - ]), - entry['type'], - ] for entry in jobs], headers=['Job ID', 'Hooks', 'Type'], + uuid, + job.job_type, + job.name, + ] for uuid, job in jobs.items()], headers=['Job ID', 'Type', 'Name'], )) @@ -39,7 +47,38 @@ def kill(args: list) -> None: click.secho('Usage: jobs kill ', bold=True) return - job_uuid = args[0] + job_uuid = int(args[0]) + + job_manager_state.remove_job(job_uuid) + + +def list_current_jobs() -> dict: + """ + Return a list of the currently listed objection jobs. + Used for tab completion in the repl. + """ + + sync_job_manager() + resp = {} + + for uuid, job in job_manager_state.jobs.items(): + resp[str(uuid)] = str(uuid) + + return resp + + +def sync_job_manager() -> dict[int, Job]: + try: + api = state_connection.get_api() + jobs = api.jobs_get() + + for job in jobs: + job_uuid = int(job['identifier']) + job_name = job['type'] + if job_uuid not in job_manager_state.jobs: + job_manager_state.jobs[job_uuid] = Job(job_name, 'hook', None, job_uuid) + + return job_manager_state.jobs + except: + print("REPL not ready") - api = state_connection.get_api() - api.jobs_kill(job_uuid) diff --git a/objection/console/cli.py b/objection/console/cli.py index 14e6c701..c2944b0e 100644 --- a/objection/console/cli.py +++ b/objection/console/cli.py @@ -141,7 +141,8 @@ def start(plugin_folder: str, quiet: bool, startup_command: str, file_commands, if startup_script: click.secho(f'Importing and running startup script at: {startup_script}', dim=True) - agent.attach_script(startup_script.read()) + script_name = f'startup_script<{startup_script.name}>' + agent.attach_script(script_name, startup_script.read()) if startup_command: for command in startup_command: diff --git a/objection/console/commands.py b/objection/console/commands.py index 14ee58f7..0ad092ba 100644 --- a/objection/console/commands.py +++ b/objection/console/commands.py @@ -34,7 +34,6 @@ from ..commands.ios import pasteboard from ..commands.ios import pinning as ios_pinning from ..commands.ios import plist -from ..utils.helpers import list_current_jobs # commands are defined with their name being the key, then optionally # have a meta, dynamic and commands key. @@ -265,7 +264,7 @@ }, 'kill': { 'meta': 'Kill a job. This unloads the script', - 'dynamic': list_current_jobs, + 'dynamic': jobs.list_current_jobs, 'exec': jobs.kill } } diff --git a/objection/console/repl.py b/objection/console/repl.py index ec77370c..1eafc20b 100644 --- a/objection/console/repl.py +++ b/objection/console/repl.py @@ -302,7 +302,7 @@ def handle_reconnect(document: str) -> bool: # agent.inject() # state_connection.a = agent - click.secho('Reconnection successful!', fg='green') + click.secho('Not yet implemented!', fg='yellow') except (frida.ServerNotRunningError, frida.TimedOutError) as e: click.secho('Failed to reconnect with error: {0}'.format(e), fg='red') diff --git a/objection/state/jobs.py b/objection/state/jobs.py index 3f509c31..409c761d 100644 --- a/objection/state/jobs.py +++ b/objection/state/jobs.py @@ -1,8 +1,50 @@ import atexit +from random import randint import click import frida +from objection.state.connection import state_connection + + +class Job(object): + """ A class representing a REPL Job or agent Job with one or more hooks. """ + + def __init__(self, name, job_type, handle, uuid: int = None) -> None: + """ + Init a new job. This requires the job_type to know how to manage the job as well as a handle + to manage and kill the job. + + :param name: + :param job_type: + :param handle: + :return: + """ + if uuid is not None: + self.uuid = int(uuid) + else: + self.uuid = randint(100000, 999999) + self.name = name + self.job_type = job_type + self.handle = handle + + def end(self): + """ + Revert hooks that the job created. + + :return: + """ + if self.job_type == "script": + + click.secho("[job manager] Killing job {0}. Name: {1}. Type: {2}" + .format(self.uuid, self.name, self.job_type), dim=True) + self.handle.unload() + elif self.job_type == "hook": + api = state_connection.get_api() + api.jobs_kill(self.uuid) + else: + click.secho(('[job {0}] - Unknown job type {1}'.format(self.uuid, self.job_type)), fg='red', dim=True) + class JobManagerState(object): """ A class representing the current Job manager. """ @@ -14,33 +56,36 @@ def __init__(self) -> None: are performed on jobs when this class is GC'd. """ - self.jobs = [] + self.jobs: dict[int, Job] = {} atexit.register(self.cleanup) - def add_job(self, job) -> None: + def add_job(self, new_job: Job) -> None: """ Adds a job to the job state manager. - :param job: + :param new_job: :return: """ - self.jobs.append(job) + # avoid duplicate jobs. + if new_job.uuid not in self.jobs: + self.jobs[new_job.uuid] = new_job - def remove_job(self, job) -> None: + def remove_job(self, job_uuid: int) -> Job: """ Removes a job from the job state manager. - :param job: - :return: + :param job_uuid: + :return Job: """ - - self.jobs.remove(job) + job_to_remove = self.jobs.pop(job_uuid) + job_to_remove.end() + return job_to_remove def cleanup(self) -> None: """ - Clean up all of the job in the job manager. + Clean up all the jobs in the job manager. This method is typical called when at the end of an objection session. @@ -48,17 +93,14 @@ def cleanup(self) -> None: :return: """ - for job in self.jobs: - + for uuid in list(self.jobs.keys()): try: - - click.secho('[job manager] Job: {0} - Stopping'.format(job.id), dim=True) + job = self.jobs.pop(uuid) job.end() except frida.InvalidOperationError: - click.secho(('[job manager] Job: {0} - An error occurred stopping job. Device may ' - 'no longer be available.'.format(job.id)), fg='red', dim=True) + 'no longer be available.'.format(uuid)), fg='red', dim=True) job_manager_state = JobManagerState() diff --git a/objection/utils/agent.py b/objection/utils/agent.py index cf2a4edd..060fcabd 100644 --- a/objection/utils/agent.py +++ b/objection/utils/agent.py @@ -12,7 +12,7 @@ from objection.state.app import app_state from objection.state.connection import state_connection from objection.state.device import device_state, Ios, Android -from objection.state.jobs import job_manager_state +from objection.state.jobs import job_manager_state, Job from objection.utils.helpers import debug_print @@ -261,21 +261,23 @@ def attach(self): self.script.on('message', self.handlers.script_on_message) self.script.load() - def attach_script(self, source): + def attach_script(self, job_name, source): """ Attaches an arbitrary script session. - # TODO: Implement some script management so we could unload these later. - + :param job_name: :param source: :return: """ - session = self.device.attach(self.pid) - script = session.create_script(source=source) + session: frida.core.Session = self.device.attach(self.pid) + script: frida.core.Script = session.create_script(source=source) script.on('message', self.handlers.script_on_message) script.load() + script_job = Job(job_name, 'script', script) + job_manager_state.add_job(script_job) + def update_device_state(self): """ Updates the device_state. Useful in other parts where we diff --git a/objection/utils/helpers.py b/objection/utils/helpers.py index 79995e87..6bd680ae 100644 --- a/objection/utils/helpers.py +++ b/objection/utils/helpers.py @@ -22,20 +22,6 @@ def debug_print(message: str) -> None: click.secho('[debug] {message}'.format(message=message), dim=True) -def list_current_jobs() -> dict: - """ - Return a list of the currently listed objection jobs. - Used for tab completion in the repl. - """ - - resp = {} - - for job in job_manager_state.jobs: - resp[str(job.id)] = str(job.id) - - return resp - - def pretty_concat(data: str, at_most: int = 75, left: bool = False) -> str: """ Limits a string to the maximum value of 'at_most',