为什么 CCCrypt 是 iOS 加密的"总开关"
iOS App 的对称加密(AES/DES/3DES)绝大多数最终汇聚到系统库 libcommonCrypto 的 CCCrypt 函数——无论上层封装多花哨(自研 SDK、三方库),真正干活的都是它。hook 住这一个函数,密钥、IV、明文、密文全部现形。
CCCryptorStatus CCCrypt(
CCOperation op, // kCCEncrypt=0 / kCCDecrypt=1
CCAlgorithm alg, // kCCAlgorithmAES=0 / DES=1 / 3DES=2
CCOptions options, // kCCOptionPKCS7Padding=1 / ECBMode=2
const void *key, // ← 密钥
size_t keyLength,
const void *iv, // ← IV
const void *dataIn, // ← 明文(加密时)
size_t dataInLength,
void *dataOut, // ← 密文输出
size_t dataOutAvailable,
size_t *dataOutMoved
);参数设计得太"贴心"了——加密所需的一切都在参数里。
Frida hook 完整脚本
var CCCrypt = Module.findExportByName('libcommonCrypto.dylib', 'CCCrypt');
Interceptor.attach(CCCrypt, {
onEnter: function(args) {
this.op = args[0].toInt32();
this.alg = args[1].toInt32();
this.options = args[2].toInt32();
this.keyLen = args[4].toInt32();
this.dataInLen = args[6].toInt32();
this.dataOut = args[7];
var opStr = this.op === 0 ? '加密' : '解密';
var algStr = {0: 'AES', 1: 'DES', 2: '3DES'}[this.alg] || this.alg;
var modeStr = (this.options & 2) ? 'ECB' : 'CBC';
// 密钥和 IV 转 hex
this.key = hexdump(args[3], {length: this.keyLen, header: false, ansi: false});
this.iv = args[5].isNull() ? 'null' : hexdump(args[5], {length: 16, header: false, ansi: false});
// 输入数据
this.dataIn = args[6].readUtf8String(this.dataInLen) || '(二进制)';
console.log(`\n[CCCrypt] ${opStr} ${algStr} ${modeStr}`);
console.log(' key: ' + this.key.replace(/\n/g, ''));
console.log(' iv: ' + this.iv.replace(/\n/g, ''));
console.log(' in: ' + String(this.dataIn).substring(0, 500));
},
onLeave: function(retval) {
// 输出数据(加密后的密文/解密后的明文)
try {
var out = this.dataOut.readUtf8String(this.dataInLen);
console.log(' out: ' + String(out).substring(0, 500));
} catch(e) {
// 二进制输出就打 hex
}
// 调用栈:看谁调的加密
console.log(' stack:\n' + Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).slice(0, 5).join('\n'));
}
});输出怎么读
[CCCrypt] 加密 AES CBC
key: 31323334353637383930616263646566 ← "1234567890abcdef" 的 hex
iv: 30313233343536373839616263646566
in: {"username":"test","timestamp":1787968800}
out: <二进制密文>
stack:
0x100abcdef TargetApp!-[CryptoUtils aesEncrypt:]
0x100123456 TargetApp!-[SignManager makeSign]一次 hook 拿到:算法(AES-CBC)、密钥明文、IV、输入输出、调用方。密钥直接就是硬编码的 1234567890abcdef——这种案例在真实逆向中占比高得惊人。
精髓一:从调用栈定位上层逻辑
CCCrypt 的日志里最有价值的不是密钥,是调用栈。DebugSymbol.fromAddress 解析出的符号告诉你:
- 哪个类哪个方法发起的加密(
CryptoUtils aesEncrypt:) - 顺着栈帧往上就是业务逻辑(
SignManager makeSign)
拿着这些符号去 Hopper 里跳过去,加密的上层组装逻辑(密钥从哪来、明文怎么拼)一目了然。
精髓二:CCCryptorCreate 系列也要覆盖
部分 App 不用一次性的 CCCrypt,而用分段式 API:
CCCryptorCreate → CCCryptorUpdate(可多次) → CCCryptorFinal → CCCryptorRelease这种要 hook CCCryptorCreate(拿算法/密钥/IV)+ CCCryptorUpdate(拿数据流)。脚本结构类似,注意用 cryptor 句柄关联同一次加密的多段数据。
精髓三:不用 CCCrypt 的怎么办
两套常见例外:
- 自研加密/SO 内实现:金融 App 把 AES 实现在自己的 SO 里(防的就是 CCCrypt hook)。识别:hook 无输出但流量确实加密。对策:IDA 里搜 AES 特征常量(S 盒
0x63, 0x7c, 0x77...)定位自研实现,hook 其输入输出 - CryptoKit/CommonCrypto 的 Swift 封装:
AES.GCM.seal等新 API 底层还是 libcommonCrypto,但 GCM 模式走CCCryptorGCM系列函数,hook 点要换成它们
总结
- CCCrypt 是 iOS 对称加密的总开关,参数里密钥 IV 明文全有
- 调用栈是黄金信息,直接指路上层组装逻辑
- 分段式 API 要 hook Create+Update 组合
- 自研加密搜 AES S 盒常量定位,GCM 换 hook CCCryptorGCM
交流微信:run1255