diff --git a/agent/src/ios/crypto.ts b/agent/src/ios/crypto.ts new file mode 100644 index 00000000..9b632e6a --- /dev/null +++ b/agent/src/ios/crypto.ts @@ -0,0 +1,359 @@ +import { debug } from "console"; +import { colors as c } from "../lib/color"; +import { fsend } from "../lib/helpers"; +import { IJob } from "../lib/interfaces"; +import { jobs } from "../lib/jobs"; +import { arrayBufferToHex, hexToString } from "./lib/helpers" + +// Encryption algorithms implemented by this module. +const CCAlgorithm = { + 0: { name: "kCCAlgorithmAES128", blocksize: 16 }, + 1: { name: "kCCAlgorithmDES", blocksize: 8 }, + 2: { name: "kCCAlgorithm3DES", blocksize: 8 }, + 3: { name: "kCCAlgorithmCAST", blocksize: 8 }, + 4: { name: "kCCAlgorithmRC4", blocksize: 8 }, + 5: { name: "kCCAlgorithmRC2", blocksize: 8 } +}; + +// Encryption algorithms implemented by this module. +const CCOperation = { + 0: "kCCEncrypt", + 1: "kCCDecrypt" +}; + +// Options flags, passed to CCCryptorCreate(). +const CCOption = { + 1: "kCCOptionPKCS7Padding", + 2: "kCCOptionECBMode" +}; + +// alg for pbkdf. Right now only pbkdf2 is supported by CommonCrypto +const CCPBKDFAlgorithm = { + 2: "kCCPBKDF2" +}; + +// alg for prt for pbkdf +const CCPseudoRandomAlgorithm = { + 1: "kCCPRFHmacAlgSHA1", + 2: "kCCPRFHmacAlgSHA224", + 3: "kCCPRFHmacAlgSHA256", + 4: "kCCPRFHmacAlgSHA384", + 5: "kCCPRFHmacAlgSHA512" +}; + +export namespace ioscrypto { + + // ident for crypto hooks job + let cryptoidentifier: string = null; + + // operation being performed 0=encrypt 1=decrypt + let op = 0; + + // needed to keep track of CCAlgorithm so we can know + // blocksize from CCCryptorCreate to CCCryptorUpdate + let alg = 0; + + // keep track of all the output bytes. + // this is necessary because CCCryptorUpdate needs to be + // append the final block from CCCryptorFinal + let dataOutBytes = null; + + const secrandomcopybytes = (ident: string): InvocationListener => { + const hook = "SecRandomCopyBytes"; + return Interceptor.attach( + Module.getExportByName(null, hook), { + onEnter(args) { + + this.secrandomcopybytes = {}; + + this.secrandomcopybytes.rnd = args[0].toInt32(); + this.secrandomcopybytes.count = args[1].toInt32(); + this.bytes = args[2]; + }, + onLeave(retval) { + this.secrandomcopybytes.bytes = arrayBufferToHex(this.bytes.readByteArray(this.secrandomcopybytes.count)); + + fsend(ident, hook, this.secrandomcopybytes); + } + }); + }; + + const cckeyderivationpbkdf = (ident: string): InvocationListener => { + const hook = "CCKeyDerivationPBKDF"; + return Interceptor.attach( + Module.getExportByName(null, hook), { + onEnter(args) { + + this.cckeyderivationpbkdf = {}; + + // args[0] "kCCPBKDF2" is the only alg supported by CommonCrypto + this.cckeyderivationpbkdf.algorithm = CCPBKDFAlgorithm[args[0].toInt32()]; + + // args[1] The text password used as input to the derivation + // function. The actual octets present in this string + // will be used with no additional processing. It's + // extremely important that the same encoding and + // normalization be used each time this routine is + // called if the same key is expected to be derived. + // args[2] The length of the text password in bytes. + const passwordPtr = args[1]; + const passwordLen = args[2].toInt32(); + const passwordBytes = arrayBufferToHex(passwordPtr.readByteArray(passwordLen)); + try { + this.cckeyderivationpbkdf.password = hexToString(passwordBytes); + } catch { + this.cckeyderivationpbkdf.password = passwordBytes; + } + + // args[3] The salt byte values used as input to the derivation function. + // args[4] The length of the salt in bytes. + const saltPtr = args[3]; + const saltLen = args[4].toInt32(); + this.cckeyderivationpbkdf.saltBytes = arrayBufferToHex(saltPtr.readByteArray(saltLen)); + + // args[5] The Pseudo Random Algorithm to use for the derivation iterations. + this.cckeyderivationpbkdf.prf = CCPseudoRandomAlgorithm[args[5].toInt32()]; + + // args[6] The number of rounds of the Pseudo Random Algorithm to use. + this.cckeyderivationpbkdf.rounds = args[6].toInt32(); + + // args[7] The resulting derived key produced by the function. + // The space for this must be provided by the caller. + this.derivedKeyPtr = args[7]; + + // args[8] The expected length of the derived key in bytes. + this.derivedKeyLen = args[8].toInt32(); + }, + onLeave(retval) { + this.cckeyderivationpbkdf.derivedKey = arrayBufferToHex(this.derivedKeyPtr.readByteArray(this.derivedKeyLen)); + + fsend(ident, hook, this.cckeyderivationpbkdf); + } + }); + }; + + const cccrypt = (ident: string): InvocationListener => { + const hook = "CCCrypt"; + return Interceptor.attach( + Module.getExportByName(null, hook), { + onEnter(args) { + + this.cccrpyt = {}; + + // args[0] Defines the basic operation: kCCEncrypt or kCCDecrypt. + this.op = args[0].toInt32(); + this.cccrpyt.op = CCOperation[this.op]; + + // args[1] Defines the encryption algorithm. + this.alg = args[1].toInt32(); + this.cccrpyt.alg = CCAlgorithm[alg].name; + + // args[2] A word of flags defining options. See discussion for the CCOptions type. + this.cccrpyt.options = CCOption[args[2].toInt32()]; + + // args[3] Raw key material, length keyLength bytes. + // args[4] Length of key material. Must be appropriate + // for the select algorithm. Some algorithms may + // provide for varying key lengths. + const key = args[3]; + this.cccrpyt.keyLength = args[4].toInt32(); + this.cccrpyt.key = arrayBufferToHex(key.readByteArray(this.cccrpyt.keyLength)); + + // args[5] Initialization vector, optional. Used for + // Cipher Block Chaining (CBC) mode. If present, + // must be the same length as the selected + // algorithm's block size. If CBC mode is + // selected (by the absence of any mode bits in + // the options flags) and no IV is present, a + // NULL (all zeroes) IV will be used. This is + // ignored if ECB mode is used or if a stream + // cipher algorithm is selected. + const iv = args[5]; + this.cccrpyt.iv = arrayBufferToHex(iv.readByteArray(CCAlgorithm[alg].blocksize)); + + // args[6] Data to encrypt or decrypt, length dataInLength bytes. + // args[7] Length of data to encrypt or decrypt. + const dataInPtr = args[6]; + const dataInLength = args[7].toInt32(); + const dataInHex = arrayBufferToHex(dataInPtr.readByteArray(dataInLength)); + this.cccrpyt.dataIn = this.op ? dataInHex : hexToString(dataInHex); + + // args[8] Result is written here. Allocated by caller. + // Encryption and decryption can be performed + // "in-place", with the same buffer used for + // input and output. + this.dataOut = args[8]; + + // args[9] The size of the dataOut buffer in bytes. + this.dataOutAvailable = args[9].toInt32(); + + // args[10] On successful return, the number of bytes written + // to dataOut. If kCCBufferTooSmall is returned as + // a result of insufficient buffer space being + // provided, the required buffer space is returned + // here. + this.dataOutMoved = args[10]; + }, + onLeave(retval) { + const dataOutHex = arrayBufferToHex(this.dataOut.readByteArray(this.dataOutAvailable)); + this.cccrpyt.dataOut = this.op ? hexToString(dataOutHex) : dataOutHex; + + fsend(ident, hook, this.cccrpyt); + } + }); + }; + + const cccryptorcreate = (ident: string): InvocationListener => { + const hook = "CCCryptorCreate"; + return Interceptor.attach( + Module.getExportByName(null, hook), { + onEnter(args) { + + this.cccryptorcreate = {}; + + // args[0] Defines the basic operation: kCCEncrypt or kCCDecrypt. + op = args[0].toInt32() + this.cccryptorcreate.op = CCOperation[op]; + + // args[1] Defines the encryption algorithm. + alg = args[1].toInt32() + this.cccryptorcreate.alg = CCAlgorithm[alg].name; + + // args[2] A word of flags defining options. See discussion for the CCOptions type. + const option = args[2].toInt32(); + this.cccryptorcreate.options = CCOption[option]; + + // args[3] Raw key material, length keyLength bytes. + // args[4] Length of key material. Must be appropriate + // for the select algorithm. Some algorithms may + // provide for varying key lengths. + const keyPtr = args[3]; + this.cccryptorcreate.keyLength = args[4].toInt32(); + this.cccryptorcreate.key = arrayBufferToHex(keyPtr.readByteArray(this.cccryptorcreate.keyLength)); + + // args[5] Initialization vector, optional. Used for + // Cipher Block Chaining (CBC) mode. If present, + // must be the same length as the selected + // algorithm's block size. If CBC mode is + // selected (by the absence of any mode bits in + // the options flags) and no IV is present, a + // NULL (all zeroes) IV will be used. This is + // ignored if ECB mode is used or if a stream + // cipher algorithm is selected. + const ivPtr = args[5]; + this.cccryptorcreate.iv = arrayBufferToHex(ivPtr.readByteArray(CCAlgorithm[alg].blocksize)); + }, + onLeave(retval) { + fsend(ident, hook, this.cccryptorcreate); + } + }); + }; + + const cccryptorupdate = (ident: string): InvocationListener => { + const hook = "CCCryptorUpdate"; + return Interceptor.attach( + Module.getExportByName(null, hook), { + onEnter(args) { + this.cccryptorupdate = {}; + + // reset for the next operation. + dataOutBytes = ""; + + // args[1] Data to process, length dataInLength bytes. + const dataInPtr = args[1]; + + // args[2] Length of data to process. + this.dataInLength = args[2].toInt32(); + // args[3] Result is written here. Allocated by caller. + // Encryption and decryption can be performed + // "in-place", with the same buffer used for + // input and output. + this.dataOutPtr = args[3]; + + // args[4] The size of the dataOut buffer in bytes. + this.dataOutAvailable = args[4].toInt32(); + + const dataIn = arrayBufferToHex(dataInPtr.readByteArray(this.dataInLength)); + this.cccryptorupdate.dataIn = op ? dataIn : hexToString(dataIn); + }, + onLeave(retval) { + const blocksize = CCAlgorithm[alg].blocksize + // if the messsage is longer than 1 block then we need to + // remember everything before the final block + if (this.dataInLength > blocksize) { + // TODO: There is sometimes padding added to the end of this message + // someone please fix this in a pull request. it is super hacky. + dataOutBytes = arrayBufferToHex(this.dataOutPtr.readByteArray(this.dataOutAvailable)).split("000000")[0]; + this.cccryptorupdate.dataOut = dataOutBytes; + } + + fsend(ident, hook, this.cccryptorupdate); + } + }); + }; + + const cccryptorfinal = (ident: string): InvocationListener => { + const hook = "CCCryptorFinal" + return Interceptor.attach( + Module.getExportByName(null, hook), { + onEnter(args) { + + this.cccryptorfinal = {} + + // args[1] Result is written here. Allocated by caller. + // Encryption and decryption can be performed + // "in-place", with the same buffer used for + // input and output. + this.dataOutPtr = args[1]; + + // args[2] The size of the dataOut buffer in bytes. + this.dataOutAvailable = args[2].toInt32(); + }, + onLeave(retval) { + // var dataOutHex = arrayBufferToHex(this.dataOutPtr.readByteArray(this.dataOutAvailable)) + // this.cccryptorfinal.dataOut = op ? hexToString(dataOutHex) : dataOutHex + + // append the final block the any previous blocks that might exist + dataOutBytes += arrayBufferToHex(this.dataOutPtr.readByteArray(this.dataOutAvailable)); + this.cccryptorfinal.dataOut = this.op ? hexToString(dataOutBytes) : dataOutBytes; + + // this.cccryptorfinal.dataOut = dataOutBytes + + fsend(ident, hook, this.cccryptorfinal); + } + }); + }; + + export const disable = (): void => { + // if we already have a job registered then kill it + if (jobs.hasIdent(cryptoidentifier)) { + send(c.red(`Killing `) + `${cryptoidentifier}`); + jobs.kill(cryptoidentifier); + } + }; + + 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)}`); + return; + } + + const job: IJob = { + identifier: jobs.identifier(), + invocations: [], + type: "ios-crypto-monitor", + }; + + 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)); + + jobs.add(job); + }; +} \ No newline at end of file diff --git a/agent/src/ios/lib/helpers.ts b/agent/src/ios/lib/helpers.ts index 536a11d5..bfd636b1 100644 --- a/agent/src/ios/lib/helpers.ts +++ b/agent/src/ios/lib/helpers.ts @@ -130,3 +130,28 @@ export const getNSMainBundle = (): NSBundle => { const bundle = ObjC.classes.NSBundle; return bundle.mainBundle(); }; + +export const arrayBufferToHex = (arrayBuffer): string => { + if (typeof arrayBuffer !== 'object' || arrayBuffer === null || typeof arrayBuffer.byteLength !== 'number') { + throw new TypeError('Expected input to be an ArrayBuffer') + } + + const buffer = new Uint8Array(arrayBuffer) + let result = '' + let value + + for (const byte of buffer) { + value = byte.toString(16) + result += (value.length === 1 ? '0' + value : value) + } + + return result +}; + +export const hexToString = (hexx): string => { + const hex = hexx.toString(); // force conversion + let str = ''; + for (let i = 0; (i < hex.length && hex.substr(i, 2) !== '00'); i += 2) + str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); + return str; +} \ No newline at end of file diff --git a/agent/src/lib/helpers.ts b/agent/src/lib/helpers.ts index b2de8560..78ec99e5 100644 --- a/agent/src/lib/helpers.ts +++ b/agent/src/lib/helpers.ts @@ -31,9 +31,29 @@ export const qsend = (quiet: boolean, message: any): void => { } }; +// send a preformated dict +export const fsend = (ident: string, hook: string, message: any): void => { + send( + c.blackBright(`[${ident}] `) + + c.magenta(`[${hook}]`) + + printArgs(message) + ) + // send(header + printArgs(message)); +}; + // a small helper method to use util to dump export const debugDump = (o: any, depth: number = 2): void => { c.log(c.blackBright("\n[start debugDump]")); c.log(util.inspect(o, true, depth, true)); c.log(c.blackBright("[end debugDump]\n")); }; + +// a small helper method to format JSON nicely before printing +function printArgs(args: JSON) : string { + let printableString : string = " (\n" + for (const arg in args) { + printableString += ` ${c.blue(arg)} : ${args[arg]}\n`; + } + printableString += ")" + return printableString +} \ No newline at end of file diff --git a/agent/src/rpc/ios.ts b/agent/src/rpc/ios.ts index 24c38bb7..ac0070c4 100644 --- a/agent/src/rpc/ios.ts +++ b/agent/src/rpc/ios.ts @@ -5,6 +5,7 @@ import { credentialstorage } from "../ios/credentialstorage"; import { iosfilesystem } from "../ios/filesystem"; import { heap } from "../ios/heap"; import { hooking } from "../ios/hooking"; +import { ioscrypto } from "../ios/crypto"; import { iosjailbreak } from "../ios/jailbreak"; import { ioskeychain } from "../ios/keychain"; import { BundleType } from "../ios/lib/constants"; @@ -59,6 +60,10 @@ export const ios = { iosHookingWatchMethod: (selector: string, dargs: boolean, dbt: boolean, dret: boolean): void => hooking.watchMethod(selector, dargs, dbt, dret), + // ios crypto monitoring + iosCryptoDisable: (): void => ioscrypto.disable(), + iosCryptoMonitor: (): void => ioscrypto.monitor(), + // jailbreak detection iosJailbreakDisable: (): void => iosjailbreak.disable(), iosJailbreakEnable: (): void => iosjailbreak.enable(), @@ -83,7 +88,7 @@ export const ios = { iosBundlesGetFrameworks: (): IFramework[] => bundles.getBundles(BundleType.NSBundleFramework), // ios keychain - iosKeychainAdd: (account: string, service: string, data: string): boolean => + iosKeychainAdd: (account: string, service: string, data: string): boolean => ioskeychain.add(account, service, data), iosKeychainEmpty: (): void => ioskeychain.empty(), iosKeychainList: (smartDecode): IKeychainItem[] => ioskeychain.list(smartDecode), diff --git a/objection/commands/ios/crypto.py b/objection/commands/ios/crypto.py new file mode 100644 index 00000000..3a978ebe --- /dev/null +++ b/objection/commands/ios/crypto.py @@ -0,0 +1,24 @@ +from objection.state.connection import state_connection + +def ios_disable(args: list = None) -> None: + """ + Attempts to disable ios crypto monitoring. + + :param args: + :return: + """ + + api = state_connection.get_api() + api.ios_crypto_disable() + + +def ios_monitor(args: list = None) -> None: + """ + Attempts to enable ios crypto monitoring. + + :param args: + :return: + """ + + api = state_connection.get_api() + api.ios_crypto_monitor() diff --git a/objection/console/commands.py b/objection/console/commands.py index 21c79a50..8c117de3 100644 --- a/objection/console/commands.py +++ b/objection/console/commands.py @@ -21,6 +21,7 @@ from ..commands.ios import binary from ..commands.ios import bundles from ..commands.ios import cookies +from ..commands.ios import crypto as ios_crypto from ..commands.ios import generate as ios_generate from ..commands.ios import heap as ios_heap from ..commands.ios import hooking as ios_hooking @@ -736,6 +737,20 @@ 'exec': jailbreak.simulate }, } + }, + 'crypto': { + 'meta': 'Hooks for working with monitoring iOS CommonCrypto usage', + 'commands': { + 'disable': { + 'meta': 'Disable CommonCrypto monitor', + 'exec': ios_crypto.ios_disable + }, + 'monitor': { + 'meta': 'Monitor CommonCrypto operations', + 'exec': ios_crypto.ios_monitor + } + + } } } }, diff --git a/objection/console/helpfiles/ios.crypto.monitor.txt b/objection/console/helpfiles/ios.crypto.monitor.txt new file mode 100644 index 00000000..c62f1841 --- /dev/null +++ b/objection/console/helpfiles/ios.crypto.monitor.txt @@ -0,0 +1,15 @@ +Command: ios crypto monitor + +Usage: ios crypto monitor + +Hooks CommonCrypto to output information about cryptographic operation. Works best for AES with PKCS7 Padding. +Currently the following hooks are supported: + - SecRandomCopyBytes + - CCKeyDerivationPBKDF + - CCCrypt + - CCCryptorCreate + - CCCryptorUpdate + - CCCryptorFinal + +Examples: + ios crypto monitor \ No newline at end of file