CCCrypt is the one-shot CommonCrypto function that the majority of iOS apps reach for when they want AES or DES. Static analysis tells you the algorithm constant. It does not tell you the runtime key or the plaintext being encrypted. CCCrypt passes both as pointer arguments. Hook the function, read the pointers, you have the key, the IV, and the data. This collapses any symmetric encryption an app does - no matter how the key was derived - into a cleartext read.
The CCCrypt signature
1
2
3
4
5
6
7
8
9
10
11
12
13
CCCryptorStatus CCCrypt(
CCOperation op, // 0 = kCCEncrypt, 1 = kCCDecrypt
CCAlgorithm alg, // algorithm enum (see below)
CCOptions options, // kCCOptionPKCS7Padding=1, kCCOptionECBMode=2
const void *key, // raw key bytes
size_t keyLength, // key length in bytes (16=AES128, 32=AES256)
const void *iv, // IV pointer (NULL for ECB)
const void *dataIn, // input bytes
size_t dataInLength,
void *dataOut, // output buffer
size_t dataOutAvailable,
size_t *dataOutMoved
);
Every argument is available in args[] at onEnter. The key and plaintext are readable before the crypto operation executes.
Algorithm enum table
| Constant | Value | Notes |
|---|---|---|
kCCAlgorithmAES | 0 | AES, the common case |
kCCAlgorithmDES | 1 | 56-bit key, deprecated |
kCCAlgorithm3DES | 2 | 3 x 56-bit key |
kCCAlgorithmCAST | 3 | |
kCCAlgorithmRC4 | 4 | stream cipher |
kCCAlgorithmRC2 | 5 | |
kCCAlgorithmBlowfish | 6 |
Hooking CCCrypt for one-shot mode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const CCCrypt = Module.findExportByName('libcommonCrypto.dylib', 'CCCrypt');
Interceptor.attach(CCCrypt, {
onEnter: function(args) {
const op = args[0].toInt32(); // 0=encrypt, 1=decrypt
const algo = args[1].toInt32(); // kCCAlgorithmAES=0
const opts = args[2].toInt32(); // kCCOptionPKCS7Padding=1, ECB=2
const keyLen = args[4].toInt32();
const key = args[3].readByteArray(keyLen);
const ivPtr = args[5];
const iv = ivPtr.isNull() ? null : ivPtr.readByteArray(16);
const dataLen = args[7].toInt32();
const data = args[6].readByteArray(Math.min(dataLen, 512));
const hexKey = Array.from(new Uint8Array(key))
.map(b => b.toString(16).padStart(2, '0')).join('');
const hexIV = iv ? Array.from(new Uint8Array(iv))
.map(b => b.toString(16).padStart(2, '0')).join('') : '(null)';
console.log('[CCCrypt] op=' + op + ' algo=' + algo + ' opts=' + opts);
console.log('[CCCrypt] keyLen=' + keyLen + ' key=' + hexKey);
console.log('[CCCrypt] iv=' + hexIV);
console.log('[CCCrypt] dataLen=' + dataLen);
console.log('[CCCrypt] data=' + Array.from(new Uint8Array(data))
.map(b => b.toString(16).padStart(2, '0')).join(''));
}
});
Run this and trigger any crypto operation in VulnLabAppiOS. In CryptoViewController, the key vulnkey appears as 76756c6e6b6579 and the IV is 16 zero bytes (00000000000000000000000000000000).
On VulnLabAppiOS: what the hook surfaces
- Key:
vulnkey- a short, hardcoded ASCII string, 7 bytes, padded to 16 for AES-128 - IV:
00000000000000000000000000000000- all zeros - Algorithm:
0(AES) - Options:
1(PKCS7 padding), no ECB flag - so CBC mode with a zero IV
A zero IV under CBC with a static key means the first block of every message with the same content is identical ciphertext. Known-plaintext attack works trivially. With the key and IV extracted, decrypt any captured ciphertext offline:
1
2
3
4
5
6
from Crypto.Cipher import AES
key = b'vulnkey\x00\x00\x00\x00\x00\x00\x00\x00\x00' # padded to 16 bytes
iv = b'\x00' * 16
ct = bytes.fromhex('<captured ciphertext hex>')
cipher = AES.new(key, AES.MODE_CBC, iv)
print(cipher.decrypt(ct))
Streaming mode: CCCryptorCreate / CCCryptorUpdate / CCCryptorFinal
Apps that encrypt large payloads use the streaming API. Hook CCCryptorCreate for the key, CCCryptorUpdate for the chunks:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const CCCryptorCreate = Module.findExportByName('libcommonCrypto.dylib', 'CCCryptorCreate');
Interceptor.attach(CCCryptorCreate, {
onEnter: function(args) {
const op = args[0].toInt32();
const algo = args[1].toInt32();
const opts = args[2].toInt32();
const keyLen = args[4].toInt32();
const key = args[3].readByteArray(keyLen);
const ivPtr = args[5];
const iv = ivPtr.isNull() ? null : ivPtr.readByteArray(16);
const hexKey = Array.from(new Uint8Array(key))
.map(b => b.toString(16).padStart(2, '0')).join('');
console.log('[CCCryptorCreate] op=' + op + ' algo=' + algo + ' key=' + hexKey);
if (iv) console.log('[CCCryptorCreate] iv=' + Array.from(new Uint8Array(iv))
.map(b => b.toString(16).padStart(2, '0')).join(''));
}
});
const CCCryptorUpdate = Module.findExportByName('libcommonCrypto.dylib', 'CCCryptorUpdate');
Interceptor.attach(CCCryptorUpdate, {
onEnter: function(args) {
const dataLen = args[2].toInt32();
const data = args[1].readByteArray(Math.min(dataLen, 256));
console.log('[CCCryptorUpdate] plaintext chunk (' + dataLen + ' bytes)');
try {
// attempt UTF-8 read first
console.log(args[1].readUtf8String(dataLen));
} catch(e) {
console.log(Array.from(new Uint8Array(data))
.map(b => b.toString(16).padStart(2, '0')).join(''));
}
}
});
The onEnter of CCCryptorUpdate fires once per chunk with the plaintext before encryption (or ciphertext before decryption). For decrypt operations, op=1, the args[1] contains ciphertext and the output buffer after the call contains plaintext.
Decrypting captured ciphertext offline
Once the hook gives you the key and IV, any ciphertext captured from the network or from disk can be decrypted without the device:
1
2
3
4
5
6
7
# OpenSSL one-liner (hex key and IV from hook output)
echo '<ciphertext hex>' | xxd -r -p \
| openssl enc -d -aes-128-cbc \
-K 76756c6e6b657900000000000000000 \
-iv 00000000000000000000000000000000 \
-nosalt \
| xxd
The key and IV the hook printed are the only inputs required.
What to do when the function is not CCCrypt
Some apps wrap CommonCrypto in a C++ layer or use CryptoKit. For CryptoKit, hook AES.GCM.seal and AES.GCM.open at the Swift level:
1
2
3
4
// Swift-mangled name; use 'nm' or class-dump to find the actual symbol
const sealSym = Process.findModuleByName('CryptoKit')
.findExportByName('_$s9CryptoKit3AESO3GCMO4seal_5using5nonce12authenticatingQr_AA13SymmetricKeyV');
// Attach and read args
The mangled name changes per Swift toolchain version. Use nm Payload/VulnLabAppiOS.app/VulnLabAppiOS | grep -i 'aes.*seal\|seal.*aes' to find the exact symbol.
Closing
CCCrypt is the most common iOS crypto call. Its arguments are plaintext at hook time. The hook is four lines in Frida. Key extraction reduces any app-level symmetric encryption to a cleartext read.
Happy Hacking !!