iOS HTTPS 防抓包原理与实现(Objective-C)
一、我们为什么要防抓包?
App 在跟服务器通信时,如果数据被第三方截获,就可能泄露密码、隐私、金融信息等。通过抓包工具(如 Charles、Fiddler、mitmproxy),攻击者可以在局域网内轻松窥探甚至修改你 App 的网络请求。所以我们需要保证 客户端只信任合法的服务器,拒绝任何中间人代理。
二、HTTPS 的基本保护机制
HTTPS = HTTP + TLS/SSL。它的核心保护:
-
加密:传输内容被加密,窃听者看到的是乱码。
-
认证:确保你连接的是真实的服务器,而不是假冒的。
-
完整性:防止数据被篡改。
但为什么开了 HTTPS,抓包工具依然能抓到明文内容呢?
三、抓包工具是如何工作的(中间人攻击原理)
当你给手机安装并信任了 Charles 的根证书后,抓包的流程如下:
-
你的 App 向
https://api.example.com发起请求。 -
请求被 Charles 代理截获,Charles 伪装成服务器,把自己的证书(由 Charles 根证书签发)返回给 App。
-
App 因为系统信任了 Charles 的根证书,就认为这个"假证书"是合法的,然后用它加密通信。
-
Charles 解密 App 发来的数据,再用真正的服务器证书去和
api.example.com建立连接。 -
最后,Charles 把响应原路返回。至此,明文数据就被完整拿到了。
核心问题 :iOS 系统的默认证书验证,只检查 证书链是不是由设备信任的根证书签发,并不检查证书的"唯一身份"。你只要导入一个自制的根证书,就可以随意伪造任意域名的证书。
四、防抓包的核心思路:证书固定(SSL Pinning)
要防止中间人攻击,必须锁定真正的服务器证书 ,拒绝其他假证书。这就是 SSL Pinning。
通俗解释:你第一次约会之前,对方给了你一张照片。系统默认的做法是:只要有警察(CA)担保说这个人就是他,你就信。但抓包工具相当于找了一个假扮者,警察却被你买通了(导入了假 CA)。SSL Pinning 相当于 你记住了对方脸上的痣、声音、指纹等唯一特征,即使有人担保,你也会仔细对比这些特征,不相符就立即识破。
SSL Pinning 有两种形式:
-
证书固定(Certificate Pinning) :把服务器的
.cer证书直接打包进 App,连接时与服务器返回的证书做 逐字节比对。 -
公钥固定(Public Key Pinning):比对接收到证书里的公钥,与预先存好的公钥是否一致。这种方式更灵活,因为证书更新时只要公钥不变,App 无需升级。
五、iOS 中的证书验证机制
iOS 的 TLS 验证通过 NSURLSession 或 NSURLConnection 的 **认证挑战(Authentication Challenge)**回调实现。当服务器返回证书后,系统会调用代理方法,让你有机会自定义验证逻辑。
关键方法(NSURLSessionDelegate):
objc
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler;
在这个方法里,我们可以拿到 challenge.protectionSpace.serverTrust 对象,提取服务器证书链,再和内置的证书做对比。如果不匹配,则拒绝连接。
六、在 Objective-C 中实现 SSL Pinning
下面以两种常见场景为例:原生 NSURLSession 和 AFNetworking。
1. 准备工作:将服务器证书放入 App
从服务器导出受信任的 .der 或 .cer 格式证书文件,拖入 Xcode 工程,确保在 Copy Bundle Resources 中。
2. 原生 NSURLSession 实现(证书固定)
objc
// 创建 NSURLSession 并设置代理
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
// 实现代理方法
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
// 只处理服务器信任认证
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
// 系统默认的证书链验证已通过,但我们还要进行自定义固定
// 从 bundle 中加载内置证书
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"your_server" ofType:@"cer"];
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
SecCertificateRef pinnedCert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);
// 设置要匹配的证书数组(可以放多个)
NSArray *pinnedCerts = @[(__bridge id)pinnedCert];
// 告诉系统只信任这些证书
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCerts);
// 关键:只使用锚点证书验证,不信任系统根证书
SecTrustSetAnchorCertificatesOnly(serverTrust, YES);
// 异步评估信任
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
SecTrustResultType result;
OSStatus status = SecTrustEvaluate(serverTrust, &result);
if (status == errSecSuccess && (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed)) {
// 信任验证成功
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
// 验证失败,拒绝连接
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
if (pinnedCert) CFRelease(pinnedCert);
});
} else {
// 其他认证方式使用默认处理
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
如果是公钥固定,则需要从证书中提取公钥,然后和服务器的公钥比对,代码稍微复杂,但原理相同。
3. 使用 AFNetworking 的 AFSecurityPolicy
AFNetworking 已经内置了 SSL Pinning 功能,只需配置即可。
objc
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate]; // 证书固定模式
// AFSSLPinningModePublicKey 为公钥固定模式
policy.validatesDomainName = YES; // 是否验证域名
policy.allowInvalidCertificates = NO; // 禁止无效证书
manager.securityPolicy = policy;
AFNetworking 会自动在内部处理 didReceiveChallenge,并将内置证书与服务器返回的证书链进行比对。你需要把 .cer 文件打包进 App,AFNetworking 会从 mainBundle 读取所有 .cer 文件作为内置证书。
七、加强防护:双向认证(客户端证书)
上面都是服务端认证,即 App 验证服务器。如果还想防止别人伪造客户端请求,可以启用双向认证:服务器要求客户端提供证书,确认是合法 App 在访问。
实现步骤:
-
生成客户端证书(通常为
.p12格式),打包进 App。 -
在
didReceiveChallenge中,当收到服务器的NSURLAuthenticationMethodClientCertificate挑战时,从 bundle 读取 p12 文件并创建身份(SecIdentityRef),构建NSURLCredential返回。
objc
// 加载 p12 文件并提取身份
NSString *p12Path = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"];
NSData *p12Data = [NSData dataWithContentsOfFile:p12Path];
NSDictionary *options = @{(__bridge id)kSecImportExportPassphrase : @"your_password"};
CFArrayRef items = NULL;
OSStatus status = SecPKCS12Import((__bridge CFDataRef)p12Data, (__bridge CFDictionaryRef)options, &items);
if (status == errSecSuccess) {
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
SecIdentityRef identity = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
// 创建证书数组
NSArray *certs = (__bridge NSArray *)CFDictionaryGetValue(identityDict, kSecImportItemCertChain);
NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identity
certificates:certs
persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
八、可能遇到的坑与绕过方式
-
证书过期:固定证书后,如果服务器证书更新且没有使用相同公钥,App 将无法连接。最佳实践是同时固定多张证书(当前和下一张),或使用公钥固定,保证兼容。
-
App 被反编译:硬编码的证书文件可以被提取替换,所以安全敏感的 App 还需配合代码混淆、运行时校验等。
-
利用 jailbreak 绕过 :攻击者可以通过修改系统 API(如 hook
SecTrustEvaluate)让验证直接通过。因此 SSL Pinning 不是绝对安全,但仍大幅提升了攻击门槛。 -
使用 OCSP/CRL 吊销检查:有些银行 App 还会实时查询证书是否被吊销,防止已泄漏的合法证书被滥用。
九、总结
-
HTTPS 默认的证书验证依赖系统根证书存储,容易因导入自签名 CA 而被抓包。
-
SSL Pinning(证书固定)将信任锚定到特定的证书或公钥,可有效防御中间人攻击。
-
iOS 中通过
NSURLSession的认证挑战回调,可灵活实现证书或公钥级别的验证。 -
结合双向认证、代码保护等多种手段,能构建更坚固的安全体系。