Skip to content

Commit

Permalink
Monitor iOS CommonCrypto Usage (#430)
Browse files Browse the repository at this point in the history
* monitor CommonCrypto in real time

* typo

* improve output when job already registered

* fixed unpad

* attempt to convert dataOut to ascii on decrypt

* fixed output in CCCryptorUpdate

* cleaning up style with tslint

Co-authored-by: Corey Gagnon <corey.gagnon@salesforce.com>
  • Loading branch information
gagnonca and corey-gagnon committed Feb 17, 2021
1 parent 675a88f commit 746d08d
Show file tree
Hide file tree
Showing 7 changed files with 464 additions and 1 deletion.
359 changes: 359 additions & 0 deletions 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);
};
}
25 changes: 25 additions & 0 deletions agent/src/ios/lib/helpers.ts
Expand Up @@ -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;
}

0 comments on commit 746d08d

Please sign in to comment.