iOS安全笔记

这一篇文章, 加上里面链接的几篇文章(一个百度的, 两个阮一峰的), 全看完应该了解得差不多了

如何打造一个安全的App?这是每一个移动开发者必须面对的问题。在移动App开发领域,开发工程师对于安全方面的考虑普遍比较欠缺,而由于iOS平台的封闭性,遭遇到的安全问题相比于Android来说要少得多,这就导致了许多iOS开发人员对于安全性方面没有太多的深入,但对于一个合格的软件开发者来说,安全知识是必备知识之一。

对于未越狱的iOS设备来说,由于强大的沙箱和授权机制,以及Apple自己掌控的App Store, 基本上杜绝了恶意软件的入侵(非越狱)。但除系统安全之外,我们还是面临很多的安全问题:网络安全、数据安全等,每一项涉及也非常广,安全是非常大的课题,本人并非专业的安全专家,只是从开发者的角度,分析我们常遇到的各项安全问题,并提出通常的解决方法,与各位同学交流学习。

每一个软件工程师都有义务保护用户数据的隐私和安全。



首先是网络安全,OSI模型各层都会面临相应的网络安全问题,涉及宽广,而网络安全也是安全领域发展最为繁荣的领域。本文我们只是从移动应用开发角度,以尽量简单的方式,讲解HTTPS核心概念知识,以及在iOS平台上的实现。建议现在还在使用HTTP的应用都升级到HTTPS。

引读:互联网全站HTTPS的时代已经到来

 

1. HTTPS

其实HTTPS从最终的数据解析的角度,与HTTP没有任何的区别,HTTPS就是将HTTP协议数据包放到SSL/TSL层加密后,在TCP/IP层组成IP数据报去传输,以此保证传输数据的安全;而对于接收端,在SSL/TSL将接收的数据包解密之后,将数据传给HTTP协议层,就是普通的HTTP数据。HTTP和SSL/TSL都处于OSI模型的应用层。从HTTP切换到HTTPS是一个非常简单的过程,在做具体的切换操作之前,我们需要了解几个概念:



SSL/TSL

关于SSL/TSL,阮一峰的两篇博客文章做了很好的介绍:

简单的来说,SSL/TSL通过四次握手,主要交换三个信息:

  1. 数字证书:该证书包含了公钥等信息,一般是由服务器发给客户端,接收方通过验证这个证书是不是由信赖的CA签发,或者与本地的证书相对比,来判断证书是否可信;假如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证;
  2. 三个随机数:这三个随机数构成了后续通信过程中用来对数据进行对称加密解密的“对话密钥”

    首先客户端先发第一个随机数N1,然后服务器回了第二个随机数N2(这个过程同时把之前提到的证书发给客户端),这两个随机数都是明文的;而第三个随机数N3(这个随机数被称为Premaster secret),客户端用数字证书的公钥进行非对称加密,发给服务器;而服务器用只有自己知道的私钥来解密,获取第三个随机数。这样,服务端和客户端都有了三个随机数N1+N2+N3,然后两端就使用这三个随机数来生成“对话密钥”,在此之后的通信都是使用这个“对话密钥”来进行对称加密解密。因为这个过程中,服务端的私钥只用来解密第三个随机数,从来没有在网络中传输过,这样的话,只要私钥没有被泄露,那么数据就是安全的。

  3. 加密通信协议:就是双方商量使用哪一种加密方式,假如两者支持的加密方式不匹配,则无法进行通信;

有个常见的问题,关于随机数为什么要三个?只最后一个随机数N3不可以么?

这是由于SSL/TLS设计,就假设服务器不相信所有的客户端都能够提供完全随机数,假如某个客户端提供的随机数不随机的话,就大大增加了“对话密钥”被破解的风险,所以由三组随机数组成最后的随机数,保证了随机数的随机性,以此来保证每次生成的“对话密钥”安全性。



数字证书

数字证书是一个电子文档,其中包含了持有者的信息、公钥以及证明该证书有效的数字签名。而数字证书以及相关的公钥管理和验证等技术组成了PKI(公钥基础设施)规范体系。一般来说,数字证书是由数字证书认证机构(Certificate authority,即CA)来负责签发和管理,并承担PKI体系中公钥合法性的检验责任;数字证书的类型有很多,而HTTPS使用的是SSL证书。

怎么来验证数字证书是由CA签发的,而不是第三方伪造的呢? 在回答这个问题前,我们需要先了解CA的组织结构。首先,CA组织结构中,最顶层的就是根CA,根CA下可以授权给多个二级CA,而二级CA又可以授权多个三级CA,所以CA的组织结构是一个树结构。对于SSL证书市场来说,主要被Symantec(旗下有VeriSign和GeoTrust)、Comodo SSL、Go Daddy 和 GlobalSign 瓜分。 了解了CA的组织结构后,来看看数字证书的签发流程:


数字证书的签发流程

数字证书的签发机构CA,在接收到申请者的资料后进行核对并确定信息的真实有效,然后就会制作一份符合X.509标准的文件。证书中的证书内容包含的持有者信息和公钥等都是由申请者提供的,而数字签名则是CA机构对证书内容进行hash加密后得到的,而这个数字签名就是我们验证证书是否是有可信CA签发的数据。


数字证书的验证流程

假设上图证书是由证书签发机构CA1签发的。

1)接收端接到一份数字证书Cer1后,对证书的内容做Hash得到H1;

2)从签发该证书的机构CA1的数字证书中找到公钥,对证书上数字签名进行解密,得到证书Cer1签名的Hash摘要H2;

3)对比H1和H2,如相等,则表示证书没有被篡改。

4)但这个时候还是不知道CA是否是合法的,我们看到上图中有CA机构的数字证书,这个证书是公开的,所有人都可以获取到。而这个证书中的数字签名是上一级生成的,所以可以这样一直递归验证下去,直到根CA。根CA是自验证的,即他的数字签名是由自己的私钥来生成的。合法的根CA会被浏览器和操作系统加入到权威信任CA列表中,这样就完成了最终的验证。所以,一定要保护好自己环境(浏览器/操作系统)中根CA信任列表,信任了根CA就表示信任所有根CA下所有子级CA所签发的证书,不要随便添加根CA证书。

一般操作系统和浏览器只包含根CA机构的证书,而在配置Web服务器的HTTPS时,也会将配置整个证书链,所以整个校验流程是从最后的叶子节点证书开始,用父节点校验子节点,一层层校验整个证书链的可信性。

打个比喻:父(根CA数字证书)-子(CA数字证书)-孙(数字证书)三代人,假设父没有其他兄弟(相当于根CA机构是唯一的),假如子与父进行DNA亲子鉴定,检测DNA位点(即证书签名)相同,那就基本确定子是由父所生;孙与子一样。这样就能够确定孙肯定是源于父一脉,是父(根CA数字证书)的合法继承人。数字证书的验证就是基于同样的原理。



Basic Constraint校验漏洞

那是否不管多少层都可以这样一直信任下去呢?理论上是可行的,但会遇到一个问题。假设我从可信CA机构购买了一张证书,使用这张证书签发的证书是否也会被操作系统和浏览器信任呢?明显是不应该相信的,因为我并不是CA机构,假如我签发的证书也被信任的话,那我完全可以自己签发任何域名的证书来进行伪造攻击。这就是著名的Basic Constraint校验漏洞,X.509证书中的Basic Constraint包含了这是不是一个CA机构,以及有效证书路径的最大深度(即,这个CA还能否继续签发CA机构证书及其签发子CA证书的路径深度)。但在几年前,包括微软和Apple都爆出了没有正确校验这些信息的漏洞。

Basic Constraint信息请看下图:


Google Internet Authority G2

上图是Google Internet Authority G2的证书,该证书是个CA机构证书;路径深度为0,表示该证书无法再签发CA证书,只能签发客户证书(client certificate)。


google.com

上图是google.com的证书,这是个客户证书(client certificate),不可再签发子证书,所以由该证书签发的子证书是不会被信任的。

了解了上面关于SSL/TSL通信加密策略以及数字证书的概念之后,对HTTPS的安全机制就有了个初步的了解,下面我们看如何在iOS上实现对HTTPS的支持。



2. 实现支持HTTPS

首先,需要明确你使用HTTP/HTTPS的用途,因为OSX和iOS平台提供了多种API,来支持不同的用途,官方文档《Making HTTP and HTTPS Requests》有详细的说明,而文档《HTTPS Server Trust Evaluation》则详细讲解了HTTPS验证相关知识,这里就不多说了。本文主要讲解我们最常用的NSURLConnection支持HTTPS的实现(NSURLSession的实现方法类似,只是要求授权证明的回调不一样而已),以及怎么样使用AFNetworking这个非常流行的第三方库来支持HTTPS。本文假设你对HTTP以及NSURLConnection的接口有了足够的了解。

 

验证证书的API

相关的Api在Security Framework中,验证流程如下:

1). 第一步,先获取需要验证的信任对象(Trust Object)。这个Trust Object在不同的应用场景下获取的方式都不一样,对于NSURLConnection来说,是从delegate方法-connection:willSendRequestForAuthenticationChallenge:回调回来的参数challenge中获取([challenge.protectionSpace serverTrust])。

2). 使用系统默认验证方式验证Trust Object。SecTrustEvaluate会根据Trust Object的验证策略,一级一级往上,验证证书链上每一级数字签名的有效性(上一部分有讲解),从而评估证书的有效性。

3). 如第二步验证通过了,一般的安全要求下,就可以直接验证通过,进入到下一步:使用Trust Object生成一份凭证([NSURLCredential credentialForTrust:serverTrust]),传入challenge的sender中([challenge.sender useCredential:cred forAuthenticationChallenge:challenge])处理,建立连接。

4). 假如有更强的安全要求,可以继续对Trust Object进行更严格的验证。常用的方式是在本地导入证书,验证Trust Object与导入的证书是否匹配。更多的方法可以查看Enforcing Stricter Server Trust Evaluation,这一部分在讲解AFNetworking源码中会讲解到。

5). 假如验证失败,取消此次Challenge-Response Authentication验证流程,拒绝连接请求。

ps: 假如是自建证书的,则不使用第二步系统默认的验证方式,因为自建证书的根CA的数字签名未在操作系统的信任列表中。

iOS授权验证的API和流程大概了解了,下面,我们看看在NSURLConnection中的代码实现:

 

使用NSURLConnection支持HTTPS的实现

 

// Now start the connection
NSURL * httpsURL = [NSURL URLWithString:@"https://www.google.com"];
self.connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:httpsURL] delegate:self];

    
//回调
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    //1)获取trust object
    SecTrustRef trust = challenge.protectionSpace.serverTrust;
    SecTrustResultType result;
    
    //2)SecTrustEvaluate对trust进行验证
    OSStatus status = SecTrustEvaluate(trust, &result);
    if (status == errSecSuccess &&
        (result == kSecTrustResultProceed ||
        result == kSecTrustResultUnspecified)) {
        
        //3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
        NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
        [challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
        
    } else {
    
        //5)验证失败,取消这次验证流程
        [challenge.sender cancelAuthenticationChallenge:challenge];
        
  }
}

 

上面是代码是通过系统默认验证流程来验证证书的。假如我们是自建证书的呢?这样Trust Object里面服务器的证书因为不是可信任的CA签发的,所以直接使用SecTrustEvaluate进行验证是不会成功。又或者,即使服务器返回的证书是信任CA签发的,又如何确定这证书就是我们想要的特定证书?这就需要先在本地导入证书,设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),再调用SecTrustEvaluate来验证。代码如下

 

//先导入证书
NSString * cerPath = ...; //证书的路径
NSData * cerData = [NSData dataWithContentsOfFile:cerPath];
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(cerData));
self.trustedCertificates = @[CFBridgingRelease(certificate)];

//回调
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    //1)获取trust object
    SecTrustRef trust = challenge.protectionSpace.serverTrust;
    SecTrustResultType result;

    //注意:这里将之前导入的证书设置成下面验证的Trust Object的anchor certificate
    SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);

    //2)SecTrustEvaluate会查找前面SecTrustSetAnchorCertificates设置的证书或者系统默认提供的证书,对trust进行验证
    OSStatus status = SecTrustEvaluate(trust, &result);
    if (status == errSecSuccess &&
        (result == kSecTrustResultProceed ||
        result == kSecTrustResultUnspecified)) {
        
        //3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
        NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
        [challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
        
    } else {
    
        //5)验证失败,取消这次验证流程
        [challenge.sender cancelAuthenticationChallenge:challenge];
        
  }
}

 

建议采用本地导入证书的方式验证证书,来保证足够的安全性。更多的验证方法,请查看官方文档《HTTPS Server Trust Evaluation》

 

使用AFNetworking来支持HTTPS

AFNetworking是iOS/OSX开发最流行的第三方开源库之一,其作者是非常著名的iOS/OSX开发者Mattt Thompson,其博客NSHipster也是iOS/OSX开发者学习和开阔技术视野的好地方。AFNetworking已经将上面的逻辑代码封装好,甚至更完善,在AFSecurityPolicy文件中,有兴趣可以阅读这个模块的代码;

AFNetworking上配置对HTTPS的支持非常简单:

 

NSURL * url = [NSURL URLWithString:@"https://www.google.com"];
AFHTTPRequestOperationManager * requestOperationManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:url];
dispatch_queue_t requestQueue = dispatch_create_serial_queue_for_name("kRequestCompletionQueue");
requestOperationManager.completionQueue = requestQueue;

AFSecurityPolicy * securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];

//allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
//如果是需要验证自建证书,需要设置为YES
securityPolicy.allowInvalidCertificates = YES;

//validatesDomainName 是否需要验证域名,默认为YES;
//假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
//置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
//如置为NO,建议自己添加对应域名的校验逻辑。
securityPolicy.validatesDomainName = YES;

//validatesCertificateChain 是否验证整个证书链,默认为YES
//设置为YES,会将服务器返回的Trust Object上的证书链与本地导入的证书进行对比,这就意味着,假如你的证书链是这样的:
//GeoTrust Global CA 
//    Google Internet Authority G2
//        *.google.com
//那么,除了导入*.google.com之外,还需要导入证书链上所有的CA证书(GeoTrust Global CA, Google Internet Authority G2);
//如是自建证书的时候,可以设置为YES,增强安全性;假如是信任的CA所签发的证书,则建议关闭该验证,因为整个证书链一一比对是完全没有必要(请查看源代码);
securityPolicy.validatesCertificateChain = NO;

requestOperationManager.securityPolicy = securityPolicy;

 

这就是AFNetworking的支持HTTPS的主要配置说明,AFHTTPSessionManager与之基本一致,就不重复了。



3. 总结

虽然HTTPS相比于HTTP来说,会有一定的性能上的劣势,但对于网络飞速发展,移动设备的性能成倍增长的今天,安全才是我们更应该去考虑的。全网HTTPS并不是那么遥远。

下一篇准备讲内存数据安全和持久化数据的安全,敬请期待。

 

======第二篇===

 

本文分为以下五节:

  1. 中间人攻击:介绍中间人攻击常见方法,并模拟了一个简单的中间人攻击;
  2. 校验证书的正确姿势:介绍校验证书的一些误区,并讨论了正确校验方式;
  3. ATS:讨论下 iOS 9.0 新发布的的特性App Transport Security
  4. 调试SSL/TLS:讨论使用Wireshark进行SSL/TLS调试的方法;
  5. 后记

其中第1节“中间人”是比较常见基础的知识,网上也可以找到相关的资料,如果对中间人攻击已经有了足够的了解,可以跳过。后面几节则是个人在iOS方面的实践总结,除了一些与系统相关的特性外,大部分都是系统无关的通用知识,并且每一章节都比较独立,所以可以直接跳到感兴趣的地方阅读。



1. 中间人攻击

关于HTTPS,我经常会提到的就是中间人攻击,那究竟什么是中间人攻击呢?中间人攻击,即所谓的Man-in-the-middle attack(MITM),顾名思义,就是攻击者插入到原本直接通信的双方,让双方以为还在直接跟对方通讯,但实际上双方的通信对方已变成了中间人,信息已经是被中间人获取或篡改。


MITM

当然,本文并不是科普性文章,本节就针对HTTPS攻击,特别是HTTPS在App这一应用场景下的常见的攻击手段进行分析讨论。

由前文我们知道,HTTPS在建立了TCP连接之后,会进行SSL握手(SSL Handshake)来校验证书,协商加密协议和对称加密的密钥,之后就会使用协商好的密钥来进行传输。所以HTTPS攻击一般分为SSL连接建立前的攻击,以及HTTPS传输过程中的攻击;

常见的HTTPS中间人攻击,首先需要结合ARP、DNS欺骗等技术,来对会话进行拦截,

 

1.1 SSL证书欺骗攻击

此类攻击较为简单常见。首先通过ARP欺骗、DNS劫持甚至网关劫持等等,将客户端的访问重定向到攻击者的机器,让客户端机器与攻击者机器建立HTTPS连接(使用伪造证书),而攻击者机器再跟服务端连接。这样用户在客户端看到的是相同域名的网站,但浏览器会提示证书不可信,用户不点击继续浏览就能避免被劫持的。所以这是最简单的攻击方式,也是最容易识别的攻击方式。

此类攻击有个经典的工具:SSLSniff。SSLSniff是大神Moxie Marlinspike开发的工具,该工具一开始是设计用于上一篇文章中提到的Basic Constaints 漏洞的,这类系统级别的漏洞,基本上可以让你不知不觉;现在的操作系统和浏览器基本修复了这一漏洞。但也可以使用SSLSniff来伪造证书实现钓鱼攻击。


MITM-Sniff

防范措施:

钓鱼类攻击,App直接调用系统API创建的HTTPS连接(NSURLConnection)一般不会受到影响,只使用默认的系统校验,只要系统之前没有信任相关的伪造证书,校验就直接失败,不会SSL握手成功;但如果是使用WebView浏览网页,需要在UIWebView中加入较强的授权校验,禁止用户在校验失败的情况下继续访问。

 

1.2 SSL剥离攻击(SSLStrip

SSL剥离,即将HTTPS连接降级到HTTP连接。假如客户端直接访问HTTPS的URL,攻击者是没办法直接进行降级的,因为HTTPS与HTTP虽然都是TCP连接,但HTTPS在传输HTTP数据之前,需要在进行了SSL握手,并协商传输密钥用来后续的加密传输;假如客户端与攻击者进行SSL握手,而攻击者无法提供可信任的证书来让客户端验证通过进行连接,所以客户端的系统会判断为SSL握手失败,断开连接。

该攻击方式主要是利用用户并不会每次都直接在浏览器上输入https://xxx.xxx.com来访问网站,或者有些网站并非全网HTTPS,而是只在需要进行敏感数据传输时才使用HTTPS的漏洞。中间人攻击者在劫持了客户端与服务端的HTTP会话后,将HTTP页面里面所有的https://超链接都换成http://,用户在点击相应的链接时,是使用HTTP协议来进行访问;这样,就算服务器对相应的URL只支持HTTPS链接,但中间人一样可以和服务建立HTTPS连接之后,将数据使用HTTP协议转发给客户端,实现会话劫持。

这种攻击手段更让人难以提防,因为它使用HTTP,不会让浏览器出现HTTPS证书不可信的警告,而且用户很少会去看浏览器上的URL是https://还是http://。特别是App的WebView中,应用一般会把URL隐藏掉,用户根本无法直接查看到URL出现异常。


MITM-Sniff

防范措施:

该种攻击方式同样无法劫持App内的HTTPS连接会话,因为App中传入请求的URL参数是固定带有https://的;但在WebView中打开网页同样需要注意,在非全网HTTPS的网站,建议对WebView中打开的URL做检查,检查应该使用https://的URL是否被篡改为http://;也建议服务端在配置HTTPS服务时,加上“HTTP Strict Transport Security”配置项。

参考:【流量劫持】躲避HSTS的HTTPS劫持

 

1.3 针对SSL算法进行攻击

上述两种方式,技术含量较低,而且一般只能影响 WebApp,而很难攻击到 Native App , 所以高阶的 Hacker,会直接针对SSL算法相关漏洞进行攻击,期间会使用很多的密码学相关手段。由于本人非专业安全相关人员,没有多少相关实践经验,所以本节不会深入讲解相关的攻击原理和手段,有兴趣的同学可以查看以下拓展阅读:

 

防范措施:

这类攻击手段是利用SSL算法的相关漏洞,所以最好的防范措施就是对服务端 SSL/TLS 的配置进行升级:

  • 只支持尽量高版本的TLS(最低TLSv1);
  • 禁用一些已爆出安全隐患的加密方法;
  • 使用2048位的数字证书;

 

1.4 模拟最简单的攻击

经过上述几种攻击方式的说明之后,我们来模拟下最简单的中间人攻击。

中间人攻击步骤方式的上文已经说过了,流量劫持相关操作不是本文重点,可以参考流量劫持是如何产生的?, 本例直接使用Charles来做代理,对流量进行劫持。并使用SSL代理来模拟下对iPhone设备HTTPS请求的中间人攻击,让大家在思考理解中间人攻击方式的同时,了解在开发中如何防范类似的攻击。

 

1) Charles设置代理

在Charles中开启并设置HTTP代理和SSL代理,Menu -> Proxy -> Proxy Setting,设置如图:

HTTP代理设置,注意记住端口号为:8888

Charles HTTP Proxy

SSL代理设置,在Locations上可以设置想要进行SSL代理的域名,这里以百度的百付宝*.baifubao.com为模拟对象。

Charles SSL Proxy

 

2) 在iPhone端设置HTTP代理

在Mac上获取当前机器的IP地址:

ifconfig en0:

ifconfig

还有一个简单的方法,按住option+点击顶部菜单栏的WiFi网络图标:

ifconfig

可以看到当前电脑的IP地址为:192.168.199.249

将iPhone连接到与电脑相同的WiFi,在iPhone设置中:无线局域网 -> 已连接WiFi右边的Info详情图标 -> HTTP代理 -> 手动 -> 设置HTTP代理:

ifconfig

设置完成之后,打开Safari随便访问一个网页,初次设置代理的话,Charles会弹出一个iPhone请求代理的确认框,点击Allow即可。然后在Charles上就可以看到iPhone上的HTTP请求了。为了避免Mac上的请求过多影响对被代理iPhone上HTTP请求的查看和调试,可以在Charles取消Mac的代理:Menu -> Proxy -> 取消勾选Mac OS X Proxy 即可。

假如你访问的是被代理的目标 URL http://www.baifubao.com 则打不开网页。因为iPhone的HTTPS请求已经被Charles拦截,但iPhone无法信任Charles的证书,所以SSL Handshake失败,无法建立HTTPS连接。

SSLHandshake

 

3) 伪造证书欺骗

在被代理的iPhone上打开Safari,访问http://www.charlesproxy.com/getssl,会弹出安装描述符文件的界面,该描述文件包含了Charles根证书:

Charles Root Cer

注意:这个Charles证书是内置在Charles中的,可以在菜单Help -> SSL Proxying可以直接保存和安装证书。安装后的描述文件可以在iPhone设备的设置 -> 通用 -> 描述文件进行查看和管理。

“安装”完成之后,就会将Charles根证书加入系统可信任证书列表中,使用该证书签发的子证书也会被系统信任。Charles会为之前SSL代理设置中配置的域名生成对应的SSL证书,这样伪造证书的证书就实现了欺骗。可以使用Mac SSL代理查看下:

Baidu Cer

 

4) 结果验证

下载百度App,然后登录账号,在我 -> 我的钱包,就会访问百付宝:

Proxy Success

看到已成功获取到HTTPS请求包的内容。从这里,我们可以猜测出该App是使用系统默认的校验方式:系统信任了这个中间人服务器返回的SSL证书,App就信任了这一校验,SSL握手成功;而没有对服务器证书进行本地对比校验。这是当下非常多App存在的安全隐患。

这个简单的SSL代理模拟了简单钓鱼式的中间人攻击,大家应该都基本明白了这种攻击方式的所针对的漏洞,以及防范这种攻击方法的措施:

  • 不要随意连入公共场合内的WiFi,或者使用未知代理服务器
  • 不要安装不可信或突然出现的描述文件,信任伪造的证书;
  • App内部需对服务器证书进行单独的对比校验,确认证书不是伪造的;



2. 校验证书的正确姿势

上一节对中间人攻击进行了简单介绍,本节就上一节我们遇到的安全隐患问题,来讨论下在App中,应该怎么校验服务器返回的SSL证书,来保证HTTPS通信的安全。上一篇文章《iOS安全系列之一:HTTPS》有对基本校验过程相关代码进行讲解,本文不会赘述这些细节,而是主要讨论校验证书中几个重要的点:

 

2.1 域名验证

前不久,iOS上最知名的网络开源库AFNetworking爆出HTTPS校验漏洞,该漏洞是因为其校验策略模块 AFSecurityPolicy 内的参数 validatesDomainName 默认为NO,这会导致校验证书的时候不会校验这个证书对应的域名。即请求返回的服务器证书,只要是可信任CA机构签发的,都会校验通过,这是非常严重的漏洞。该漏洞已在v2.5.2版本中修复,对应Git版本号3e631b203dd95bb82dfbcc2c47a2d84b59d1eeb4

这个漏洞以及AFNetworking的相关源码会让很多人以为系统的默认校验是不校验证书对应域名的,实际上并非如此。这里AFNetworking确有画蛇添足之嫌。首先我们查看下系统的默认校验策略:

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    //1)获取trust object
    SecTrustRef trust = challenge.protectionSpace.serverTrust;

    //获取默认的校验策略
    CFArrayRef defaultPolicies = NULL;
    SecTrustCopyPolicies(serverTrust, &defaultPolicies);
    NSLog(@"Default Trust Policies: %@", (__bridge id)defaultPolicies);

    //...
}

打印默认校验策略信息:

    5 : <CFString 0x197814dc0 [0x196ea5fa0]>{contents = "ValidRoot"} = <CFBoolean 0x196ea6340 [0x196ea5fa0]>{value = true}
    6 : <CFString 0x197814b20 [0x196ea5fa0]>{contents = "SSLHostname"} = <CFString 0x170226b60 [0x196ea5fa0]>{contents = "xxx.xxx.com"}
    8 : <CFString 0x197814da0 [0x196ea5fa0]>{contents = "ValidLeaf"} = <CFBoolean 0x196ea6340 [0x196ea5fa0]>{value = true}

从打印信息来看,系统的默认校验策略中已包含了域名校验。然后再看AFSecurityPolicy中相关源码:

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }

    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    //...
}

这其实也是很多开发者在处理异常与默认逻辑分支时会犯的错误,这段逻辑推荐实现方式是:

//取代validatesDomainName,默认为NO,就是系统默认行为
@property (nonatomic, assign) BOOL skipDomainNameValidation;

//校验
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    if (self.skipDomainNameValidation) {
        NSMutableArray *policies = [NSMutableArray array];
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
        SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
    }

    //...
}

从代码上看,逻辑是否变得更清晰了?而且也表明系统默认的校验方式是会验证域名的。实际上调用SecTrustSetPolicies来重新设置校验策略,主要是用于使用IP进行HTTPS请求,或者一个证书用于多个域名的场景;在这些场景下,服务器证书上的域名和请求域名(可能是IP,也可能是其他域名)就会出现不一致,导致校验不通过;这就需要重新设置下校验策略,把这个证书对应的域名设置下。详细说明请查看官方文档:《Overriding TLS Chain Validation Correctly》

 

2.2 校验证书链?

上一篇文章介绍系统验证SSL证书的方法和流程时,不是已经说明了会对证书链进行层层校验,以保证证书的可信么?为什么还需要讨论这一问题?其实本节要讨论的是AFNetworkingvalidatesCertificateChain的问题。

先说明下结果:在AFNetworking最新发布的V2.6.0,已经将该特性去掉了。相关的讨论:SSL Pinning: What Should Be Certificate Chain Validation Expected Behavior?#2744

AFNetworking中实现的验证证书链,是将App本地打包好的证书与服务器返回的证书链进行数据上的一一对比,只有打包到App的证书中包含了服务器返回的证书链上的所有证书,校验才会通过。如google的SSL证书:

Google Cer Chain

开启validatesCertificateChain后请求https://google.com,需要将GeoTrust Global CA、Google Internet Authority G2和google.com的证书都导入App中才能验证通过。请回忆下上一篇文章关于证书链的可信任机制,会发现这是完全没有必要的;证书链的验证,主要由三部分来保证证书的可信:叶子证书是对应HTTPS请求域名的证书,根证书是被系统信任的证书,以及这个证书链之间都是层层签发可信任链;证书之所以能成立,本质是基于信任链,这样任何一个节点证书加上域名校验(CA机构不会为不同的对不同的用户签发相同域名的证书),就确定一条唯一可信证书链,所以不需要每个节点都验证。

 

2.3打包证书校验

那是否就不需要在App中打包证书进行验证了呢?

这时需要想想为什么伪造证书是可以实现中间人攻击的?答案就在于用户让系统信任了不应该信任的证书。用户设置系统信任的证书,会作为锚点证书(Anchor Certificate)来验证其他证书,当返回的服务器证书是锚点证书或者是基于该证书签发的证书(可以是多个层级)都会被信任。这就是基于信任链校验方式的最大弱点。我们不能完全相信系统的校验,因为系统的校验依赖的证书的源很可能被污染了。这就需要选取一个节点证书,打包到App中,作为Anchor Certificate来保证证书链的唯一性和可信性。

所以还是需要App本地打包证书,使用SecTrustSetAnchorCertificates(SecTrustRef trust, CFArrayRef anchorCertificates)来设置Anchor Certificate进行校验。需要注意的是,官方文档《Certificate, Key, and Trust Services Reference》针对传入的 Anchor Certificates 有说明:

IMPORTANT

Calling this function without also calling SecTrustSetAnchorCertificatesOnly disables the trusting of any anchors other than the ones specified by this function call.

也就是说,单纯调用SecTrustSetAnchorCertificates方法后不调用SecTrustSetAnchorCertificatesOnly来验证证书,则只会相信SecTrustSetAnchorCertificates传入的证书,而不会信任其他锚点证书。关于这一点,SecTrustSetAnchorCertificatesOnly方法参数讲解中也有说明:

anchorCertificatesOnly:

If true, disables trusting any anchors other than the ones passed in with the SecTrustSetAnchorCertificates function. If false, the built-in anchor certificates are also trusted. If SecTrustSetAnchorCertificates is called and SecTrustSetAnchorCertificatesOnly is not called, only the anchors explicitly passed in are trusted.

只相信传入的锚点证书,也就只会验证通过由这些锚点证书签发的证书。这样就算被验证的证书是由系统其他信任的锚点证书签发的,也无法验证通过。

最后一个问题:选择证书链的哪一节点作为锚点证书打包到App中?很多开发者会直接选择叶子证书。其实对于自建证书来说,选择哪一节点都是可行的。而对于由CA颁发的证书,则建议导入颁发该证书的CA机构证书或者是更上一级CA机构的证书,甚至可以是根证书。这是因为:

1) 一般叶子证书的有效期都比较短,Google和Baidu官网证书的有效期也就几个月;而App由于是客户端,需要一定的向后兼容,稍疏于检查,今天发布,过两天证书就过期了。

2) 越往证书链的末端,证书越有可能变动;比如叶子证书由特定域名(aaa.bbb.com)改为通配域名(*.bbb.com)等等。短期内的变动,重新部署后,有可能旧版本App更新不及时而出现无法访问的问题。

因此使用CA机构证书是比较合适的,至于哪一级CA机构证书,并没有完全的定论,你可以自己评估选择。



3. ATS

在本文发表的时间(2015-09-03),大部分的iOS开发同学应该升级到iOS9了,在iOS9下进行HTTP/HTTPS请求时会遇到如下错误:

Request failed: Error Domain=NSURLErrorDomain Code=-1022 "The resource could not be loaded because the App Transport Security policy requires the use of a secure connection." UserInfo=0x7fbb4a158f00 {NSUnderlyingError=0x7fbb4a1141c0 "The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.", NSErrorFailingURLStringKey=http://api.xxx.com/mobile, NSErrorFailingURLKey=http://api.xxx.com/mobile, NSLocalizedDescription=The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.}

这是iOS9中一个重大的更新:App Transport Security,简称ATS。ATS对使用NSURLConnection, CFURL, 或NSURLSession 等 APIs 进行网络请求的行为作了一系列的强制要求,反逼服务器配置,以提高网络数据传输的安全性:

These are the App Transport Security requirements:

1) The server must support at least Transport Layer Security (TLS) protocol version 1.2.

2) Connection ciphers are limited to those that provide forward secrecy (see the list of ciphers below.)

3) Certificates must be signed using a SHA256 or better signature hash algorithm, with either a 2048 bit or greater RSA key or a 256 bit or greater Elliptic-Curve (ECC) key. Invalid certificates result in a hard failure and no connection.

ATS要求运行在iOS9的App,需将HTTP连接升级到HTTPS,并且TLS版本不得低于v1.2;而且规定了支持的加密套件(Cipher Suite)和证书签名的哈希算法;如果想要向前兼容的话,可以通过设置Info.plist来降低校验强度,具体可以看这篇文章:Configuring App Transport Security Exceptions in iOS 9 and OSX 10.11

本人升级到iOS9 GM版,从App Store上下载了一些并没有完全支持ATS的应用,使用起来也完全没有问题,估计iOS系统对使用低于SDK9编译的App做了兼容,这方面也是符合预期的,毕竟ATS的影响实在太大,基本上没有任何的App能够幸免,比如图片下载一般使用HTTP,而不会使用HTTPS。所以建议可以暂时使用NSAllowsArbitraryLoads来取消ATS的限制,后续慢慢完善对ATS的支持。

日益复杂脆弱的网络难以保证用户的数据安全,因此Apple才在iOS9上强推ATS,反向逼迫服务端升级,以提供更安全的网络环境。建议开发者不要简单地将ATS禁用,而应该升级服务器的配置支持ATS,为用户提供更安全的服务。

 

4. 调试SSL/TLS

开发一个新的App,通常终端和后端先协商好了具体业务逻辑的通信协议,后端和终端按照协议实现逻辑之后,就进入联调阶段,第一次联调往往会回到很多问题,包括数据格式不对,缺少基础字段等;假如是基于HTTPS的网络请求,则很可能由于后台配置问题,导致遇到如CFNetwork SSLHandshake failed (-9824)这类握手失败的错误。面对这类SSL错误,该如何来解决呢?根据本人经验,主要是分两步:

4.1 错误码

这会不会太简单了?其实最简单的往往是最有效的。SSL相关错误码可以在<Security/SecureTransport.h>中找到。上面-9824的错误,对应的是errSSLPeerHandshakeFail = -9824, /* handshake failure */,其他常见的错误码还有:

//...

    /* fatal errors detected by peer */
    errSSLPeerUnexpectedMsg     = -9819,    /* unexpected message received */
    errSSLPeerBadRecordMac      = -9820,    /* bad MAC */
    errSSLPeerDecryptionFail    = -9821,    /* decryption failed */
    errSSLPeerRecordOverflow    = -9822,    /* record overflow */
    errSSLPeerDecompressFail    = -9823,    /* decompression failure */
    errSSLPeerHandshakeFail     = -9824,    /* handshake failure */
    errSSLPeerBadCert           = -9825,    /* misc. bad certificate */
    errSSLPeerUnsupportedCert   = -9826,    /* bad unsupported cert format */
    errSSLPeerCertRevoked       = -9827,    /* certificate revoked */
    errSSLPeerCertExpired       = -9828,    /* certificate expired */
    errSSLPeerCertUnknown       = -9829,    /* unknown certificate */
    errSSLIllegalParam          = -9830,    /* illegal parameter */
    errSSLPeerUnknownCA         = -9831,    /* unknown Cert Authority */
    errSSLPeerAccessDenied      = -9832,    /* access denied */


    /* more errors detected by us */
    errSSLHostNameMismatch      = -9843,    /* peer host name mismatch */
    errSSLConnectionRefused     = -9844,    /* peer dropped connection before responding */
    errSSLDecryptionFail        = -9845,    /* decryption failure */
    errSSLBadRecordMac          = -9846,    /* bad MAC */
    errSSLRecordOverflow        = -9847,    /* record overflow */
    errSSLBadConfiguration      = -9848,    /* configuration error */

    //...

但靠错误码只能判断大概的情况,很多时候并不能明确知道到底是什么原因导致的,所以最直观的,还是需要抓包分析。

4.2 抓包分析

在这一阶段,使用Charles来抓包是没有用的,因为Charles是作为HTTP代理工作的,它会抓取代理的网络报文,然后将报文组合成HTTP/HTTPS协议包,对于HTTP调试非常方便,但由于细节的缺失,没办法使用它来分析SSL相关错误。所以我们需要使用上古神器Wireshark。

关于Wireshark就不再多介绍了,网上已经有很多相关介绍和抓包教程,如《Mac OS X上使用Wireshark抓包》等,基本上可以很快上手。下面我们就以适配iOS9的ATS为例,来说下如何进行抓包分析,找出因为不支持ATS导致SSL握手失败问题。

还记得SSL握手过程么?不记得可以重温下这篇文章:图解SSL/TLS协议。我们也来看看Wireshark上抓取到的包来直观学习正常的SSL握手流程:

Wireshark SSL Handshake

上图是一个标准的HTTPS请求抓取的包:

1) 在TCP三次握手成功之后,客户端发起SSL的Client Hello(No.68帧),传递随机数(Random),和客户端支持的加密套件(Cipher Suites)、压缩方法、签名算法等信息; 如下图所示,这是Client Hello所携带的信息,可以展开来看相关的详情:

Client Hello

2) 服务器从Client Hello中匹配支持的加密套件(Cipher Suites)、压缩算法和签名算法,和服务器新生成的一个随机数返回给客户端,这就是Server Hello(No.70帧)。 下图就是对1)中Client Hello的回应,由图可以看出,服务端匹配的Cipher Suite是TLS_DHE_RSA_WITH_AES_256_CBC_SHA256:

Server Hello

3) 服务器同时会将证书发给客户端(No.73帧);有时候抓取的包只有Client HelloServer Hello,而没有再发送证书的,这是SSL/TLS的Session重用了:由于新建立一个SSL/TLS Session的成本太高,所以之前有建立SSL/TLS连接Session的话,客户端会保存Session ID,在下一次请求时在Client Hello中带上,服务端验证有效之后,就会成功重用Sesssion。

注:关于重用TLS Session,在特定场景下会引发严重的问题:当App只针对了代码中发起的HTTPS请求做了本地证书校验,而WebView中发起的HTTPS请求并没有做本地证书校验,可能会出现App内代码发起的请求直接重用WebView中建立的HTTPS链接,导致中间人可以实现短暂的绕过攻击。

拓展阅读:


4) 客户端确认证书有效,则会生产最后一个随机数(Premaster secret),并使用证书的公钥RSA加密这个随机数,发回给服务端。为了更高的安全性,会改为Diffie-Hellman算法(简称DH算法);采用DH算法,最后一个随机数(Premaster secret)是不需要传递的,客户端和服务端交换参数之后就可以算出。Client Key Exchange(No. 75帧);

5) 接下来双方都会发送Change Cipher Spec通知对方,接下来的所有消息都会使用签名约定好的密钥进行加密通信。

6) 最后是双方的Finished Message(即Encrypted Handshake Message, No. 77、79帧),这个消息是最终的校验,里面包含了握手过程中的Session Key等信息,如果对方能够解密这个消息则表示握手成功,结束整个SSL Handshake流程。

掌握了SSL/TLS握手流程之后,调试SSL/TLS就会变得非常简单,只需要看在哪个环节报错(Alert),就可以基本推断出相关的错误。

相关SSL/TLS接口信息,请查看:RFC5246以及SSL/TLS in Detail

下面就列举下调试适配ATS过程中遇到的主要问题:


1) 加密套件(Cipher Suite)等参数无法匹配:加密套件不匹配是最常见的握手失败的例子。

在ATS中,可接受的加密套件有包括:

TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA

但往往很多服务器的HTTPS配置很久没有升级,没办法支持这些Cipher Suite;客户端发送Client Hello给服务端,带上支持加密套件参数;服务端查看这些参数,发现一个都不支持,则直接返回Handshake Failure的信息。如下图:

Handshake Failure

一般在接受到客户端发送的Client Hello后返回Handshake Failure,都是因为服务端无法匹配客户端SSL握手参数。至于是不是加密套件这个参数匹配的问题,建议抓取取消ATS了的正常HTTPS请求包进行对比,找出具体不匹配的参数。


2) SSL/TLS版本过低,这个也非常常见,但一般会被上一个参数不匹配的错误所掩盖。因为大多数SSL/TLS版本低的服务器HTTPS配置支持的加密套件等参数版本也比较低,而SSL/TLS版本是客户端收到Server Hello之后才验证的,但前面握手失败就走不到这一步了。所以加密套件(Cipher Suite)等参数无法匹配支持,一般也就意味着服务端SSL/TLS版本过低。


3) 证书链配置错误:在开发过程中,本人遇到过证书链没有按照顺序进行配置的问题,也遇到过只配置了叶子证书的问题。对于这些问题,可以直接查看SSL握手过程中,服务端返回的Certificate包:

SSL Certificate error

上图可以看到证书链Certificates只有一个,这是典型的配置错误。

PS:使用Wireshark进行抓包的时候,有时候由于一些HTTPS请求的SSL/TLS版本号太低,Wireshark没办法辨认其是SSL包,而是显示为TCP;此时可以手动来Decode:选择对应的TCP数据帧,右键 -》Decode As -》Transport 选择SSL -》Apply既可。

Wireshark Decode



5. 后记

这个时代,安全重要么?这是我曾常疑惑的。90%以上的大众对安全没有切实的概念,即使安全上了春晚,过了热潮一切又重归原样。特别最近换工作到保险金融类公司,安全问题更是触目惊心。一直相信,人如同一个圆,你知道的越多,学的越深,接触的越广,圆就越大,越知道自己的渺小,越懂得敬畏。

这世界永远不会缺少矛和盾,没有“Mission Impossible”,不是么?


版权所有,转载请保留Jaminzzhang署名

posted @ 2016-01-21 18:04  $walker  阅读(565)  评论(0编辑  收藏  举报