知识点
参考
https://mp.weixin.qq.com/s/y12SCO7PMwNnKLuvrA2ijA 【iOS逆向】Instagram抓包小记
环境
抓包:mitmproxy
版本:419.0.0
重点是学习抓包。
使用之前老的方法已经不能够进行抓包了,所以来研究下新的抓包方案。
根据参考文章,
用 Frida hook NSBundle pathForResource:ofType:,运行时观察 App 加载了哪些资源文件:
运行的时候记得先暂时不要开抓包。
bash
var NSBundle = ObjC.classes.NSBundle['- pathForResource:ofType:'].implementation;
Interceptor.attach(NSBundle, {
onEnter: function (args) {
var source = ObjC.Object(args[2]);
console.log(`pathForResource: ${ObjC.Object(args[2])} ofType: ${ObjC.Object(args[3])}`);
},
onLeave: function (retval) {
}
})
输出如下
bash
muyang@Mac-Pro ins % frida -U -f com.burbn.instagram -l ssl.js
____
/ _ | Frida 16.7.19 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to iPhone (id=d4be7c38721a15dd242e268d99e4700afd6e1521)
Spawned `com.burbn.instagram`. Resuming main thread!
[iPhone::com.burbn.instagram ]-> pathForResource: SessionlessLatencyConfig ofType: json.gz
pathForResource: zh-Hans ofType: lproj
pathForResource: fbt_language_pack ofType: bin
pathForResource: ds ofType: g10p
pathForResource: metal_rt ofType: metallib
pathForResource: IGTabBarController ofType: nib
pathForResource: IGTabBar ofType: nib
pathForResource: IGMainAppScrollingContainerViewController ofType: nib
pathForResource: IGMainAppScrollingContainerView ofType: nib
pathForResource: CACerts ofType: plist
pathForResource: FacebookSDKStrings ofType: bundle
pathForResource: prepackaged_bloks_config ofType: json
重点关注 CACerts,在ins目录下可以找到该文件。

CACerts.plist 是什么?
这是 Instagram 自带的 CA 根证书信任列表(Certificate Authority Root Certificates)。
具体来说:
内容
-
• 一个 plist 数组 ,包含了 143 个 DER 编码的 X.509 CA 根证书
-
• 里面的证书包括各种知名 CA,例如:
-
• COMODO RSA / ECC Certification Authority
-
• GlobalSign Root CA
-
• Telia Root CA
-
• Buypass Class 2 Root CA
-
• Go Daddy Root Certificate Authority
-
• ......等等
作用:SSL Certificate Pinning(证书锁定)
Instagram 不依赖系统的根证书信任链 ,而是自己打包了一套受信任的 CA 根证书。这是典型的 SSL/TLS Certificate Pinning 策略:
对逆向/抓包的意义
这就是为什么 直接装 Charles/mitmproxy 证书抓不了 Instagram 的包 的原因之一------App 内部有自己的信任锚点(Trust Anchor)。要抓包 Instagram,通常需要:
-
• 用 Frida hook 掉 SSL 验证逻辑
-
• 或者 直接 patch 这个
CACerts.plist,把你的抓包工具的 CA 证书加进去 -
• 或者 hook
SecTrustEvaluate等系统 API 让验证永远返回成功
你 Frida hook 到的 pathForResource: CACerts ofType: plist 就是 Instagram 在启动时加载这个自定义信任列表的过程。
定位
但是我们不用文章接下去的方法,因为我测试是没有成功的,并且 文章说的 X509_verify_cert 我测试的时候如果证书没过是没有调用的。所以去其他的方法。
既然我们已经拿到了证书名称,接着我们去询问Ai,开发的过程中
反重力 opus4.6 模型
bash
CACerts.plist 一般怎么传递校验的,写一段代码看下
接下来是Ai的回答
好的,下面是 Instagram 这种 App 使用 CACerts.plist 进行 SSL Certificate Pinning 的典型实现流程:
1. 加载阶段(启动时执行一次)
bash
// IGCertificatePinningManager.m
@interface IGCertificatePinningManager : NSObject
@property (nonatomic, strong) NSArray<SecCertificateRef> *pinnedCACerts;
+ (instancetype)sharedInstance;
@end
@implementation IGCertificatePinningManager
+ (instancetype)sharedInstance {
static IGCertificatePinningManager *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
[self loadPinnedCertificates]; // 启动时加载一次
}
return self;
}
- (void)loadPinnedCertificates {
// ========== 这就是你 hook 到的那一次调用 ==========
NSString *path = [[NSBundle mainBundle] pathForResource:@"CACerts" ofType:@"plist"];
// 读取 plist ------ 得到一个 NSArray<NSData *>(143个 DER 编码的证书)
NSArray<NSData *> *certDataArray = [NSArray arrayWithContentsOfFile:path];
// 把 NSData 转换成 SecCertificateRef 对象,缓存到内存
NSMutableArray *certs = [NSMutableArray array];
for (NSData *certData in certDataArray) {
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);
if (cert) {
[certs addObject:(__bridge id)cert];
CFRelease(cert);
}
}
self.pinnedCACerts = [certs copy]; // 缓存,后续不再读文件
NSLog(@"Loaded %lu pinned CA certificates", (unsigned long)self.pinnedCACerts.count);
}
@end
2. 验证阶段(每次 HTTPS 请求都执行)
bash
// NSURLSession 的认证挑战代理方法
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
// ========== 核心:用自带的 CA 列表替代系统默认的信任锚点 ==========
NSArray *pinnedCerts = [IGCertificatePinningManager sharedInstance].pinnedCACerts;
// 设置自定义锚点证书(只信任这 143 个 CA)
OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCerts);
// 关键!设为 YES = 只信任我们提供的锚点;设为 NO = 同时也信任系统证书
SecTrustSetAnchorCertificatesOnly(serverTrust, true);
// 执行证书链验证
CFErrorRef error = NULL;
bool trusted = SecTrustEvaluateWithError(serverTrust, &error);
if (trusted) {
// ✅ 服务器证书链可以追溯到我们 CACerts.plist 中的某个 CA
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
// ❌ 验证失败 ------ 证书链不在我们的信任列表里
// 比如你用 Charles 代理,Charles 的 CA 不在这 143 个里面,就会走到这里
NSLog(@"SSL Pinning 验证失败: %@", error);
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
3. 整个流程图
bash
App 启动
│
▼
pathForResource:@"CACerts" ofType:@"plist" ← 你 hook 到的就是这一步(只调用1次)
│
▼
读取 plist → 143 个 NSData (DER 证书)
│
▼
转换为 SecCertificateRef 数组 → 缓存到单例内存
│
╔══════════════════════════════════════════════════╗
║ 之后每次 HTTPS 请求(循环多次): ║
║ ║
║ 服务器返回证书 → SecTrustRef ║
║ │ ║
║ ▼ ║
║ SecTrustSetAnchorCertificates(trust, 缓存的CA) ║
║ │ ║
║ ▼ ║
║ SecTrustSetAnchorCertificatesOnly(trust, true) ║
║ │ ║
║ ▼ ║
║ SecTrustEvaluateWithError() ║
║ │ ║
║ ┌──┴──┐ ║
║ ▼ ▼ ║
║ ✅信任 ❌拒绝(如 Charles/mitmproxy 的证书) ║
╚══════════════════════════════════════════════════╝
4. 绕过方式(Frida hook 关键点)
bash
// 方式1:hook SecTrustEvaluateWithError,让它永远返回 true
Interceptor.attach(Module.findExportByName(null, "SecTrustEvaluateWithError"), {
onLeave: function(retval) {
retval.replace(0x1); // 返回 true,表示信任
}
});
// 方式2:hook SecTrustSetAnchorCertificates,阻止设置自定义锚点
Interceptor.attach(Module.findExportByName(null, "SecTrustSetAnchorCertificates"), {
onEnter: function(args) {
// 不设置自定义锚点,让系统用默认信任存储(包含你安装的 Charles CA)
},
onLeave: function(retval) {
retval.replace(0x0); // 返回 errSecSuccess
}
});
// 方式3:hook SecTrustSetAnchorCertificatesOnly,允许系统证书
Interceptor.attach(Module.findExportByName(null, "SecTrustSetAnchorCertificatesOnly"), {
onEnter: function(args) {
args[1] = ptr(0x0); // 改为 false,同时信任系统证书
}
});
核心思路 :两个关键 API 是 SecTrustSetAnchorCertificates(设置自定义 CA 列表)和 SecTrustSetAnchorCertificatesOnly(是否排斥系统证书)。只要 hook 掉这两个中的任意一个,就能让系统同时信任你安装的 Charles/mitmproxy 证书。
但是上面的frida代码并不能过掉代理校验,我们去打开IDA分析其他的方法看有无。
从上面的关键API入手。搜索 SecTrustSetAnchorCertificates 定位到如下函数
bash
// attributes: thunk
OSStatus __cdecl SecTrustSetAnchorCertificates(SecTrustRef trust, CFArrayRef anchorCertificates)
{
return _SecTrustSetAnchorCertificates(trust, anchorCertificates);
}
查看交叉引用。定位到 sub_46B8D4 这个函数

该函数有什么特点了?有许多验证相关的字符串。下面的 sub_760CDC 没有这些字符串。

可以询问AI sub_46B8D4 函数的作用是什么。
5.sub_46B8D4 --- MNSCertificateVerifier 核心证书验证函数
这个函数位于 FBSharedFramework 中,是 Instagram/Facebook 自研的证书验证器,函数签名大致为:
bash
int64_t MNSCertificateVerifier_verify(void *self, CFArrayRef certChain, CFStringRef hostname)
执行流程
1. CRL 吊销检查(0x46B918 - 0x46B968)
-
• 从证书链取第一个证书(叶子证书),提取序列号
-
• 调用
MNSCertificateVerifierAppleCrlUtil处理序列号 -
• 检查
self+224处的 CFSet(吊销列表),如果证书序列号在集合中 → 返回错误"Certificate is revoked."
2. 创建 SecTrust 并做标准验证(0x46B978 - 0x46B9B8)
-
•
SecPolicyCreateSSL(true, hostname)--- 创建 SSL 策略,绑定域名 -
•
SecTrustCreateWithCertificates(certChain, policy, &trust)--- 创建信任对象 -
• 如果
self+24(一个 flag)为 true,会走一段特殊逻辑:调用SecTrustSetAnchorCertificates设置自定义锚点证书,然后SecTrustSetAnchorCertificatesOnly(trust, false)允许系统证书也参与验证 -
•
SecTrustEvaluateWithError(trust, &error)--- 执行标准 iOS 证书链验证
3. 域名白名单匹配(0x46B9E4 - 0x46BA4C)
-
• 将 hostname 转为 C 字符串
-
• 遍历
self+32处的一个数组(最多 24 个条目,每条 64 字节),逐一与 hostname 比较 -
• 支持两种匹配:精确
strcmp和通配符sub_51C1F4(可能是 wildcard match) -
• 如果 hostname 不在白名单中(index > 0x17),直接跳过 pinning 检查,返回成功
4. 过期时间检查(0x46BA7C)
-
• 如果
self+16有一个 CFDate,检查当前时间是否已超过该日期 -
• 超过则跳过 pinning,返回成功(相当于 pinning 有"有效期")
5. 证书 Pin 验证 --- 核心(0x46BA80 - 0x46BAF8)
-
• 遍历信任链中的每个证书
-
• 对每个证书调用
sub_3AF0B0(很可能是计算证书的 SHA256 指纹) -
• 将指纹与
off_2218550处存储的 18 个预置 pin 值(144 / 8 = 18 个指针)逐一比较 -
• 如果任一证书的指纹匹配任一 pin → 验证通过,返回 0(成功)
-
• 如果
self+24flag 为 true,还会尝试用证书数据重新构造证书再比较一次 -
• 全部不匹配 → 返回错误
"Pinning validation failed."
两个调用者
sub_3B8158 --- TLS 握手阶段的证书验证回调
-
• 先做一次快速检查(
sub_51BCCC),如果通过就跳过 -
• 否则调用
sub_46B8D4做完整验证 -
• 验证失败返回
0xFFFFFFFF(-1),成功返回 0
sub_130E120 --- TLS 1.3 CertificateVerify 签名验证
-
• 先验证签名(
SecKeyVerifySignature),支持 ECDSA-SHA256/384/512 和 RSA-PSS-SHA256 -
• 签名验证通过后,再调用
sub_46B8D4做证书 pinning 检查 -
• 这是 Fizz(Facebook 的 TLS 1.3 实现)的验证路径
关键结论
这个函数就是 Instagram SSL Pinning 的核心。要绕过它,有三个切入点:
bash
Interceptor.attach(Module.findBaseAddress("FBSharedFramework").add(0x46b8d4), {
onEnter: function(args) {
// 不设置自定义锚点,让系统用默认信任存储(包含你安装的 Charles CA)
},
onLeave: function(retval) {
console.log("0x46b8d4",retval)
retval.replace(0x0); // 返回 errSecSuccess
}
});
这样就可以抓到包了。
后续如果更新。
思路一:从字符串入手(最常用)
bash
"Pinning validation failed"
"Certificate is revoked"
"verify/trust init failed"
这个函数里有 "Pinning validation failed." 和 "Certificate is revoked.",非常明显。
思路二:从 API 调用入手
SSL 验证绕不开这几个系统 API:
-
•
SecTrustEvaluateWithError -
•
SecTrustCreateWithCertificates -
•
SecTrustSetAnchorCertificates -
•
SecPolicyCreateSSL
在 IDA 里对这些函数按 X 查交叉引用,看谁调用了它们。