iOS安全防护--在AFNetworking中实现 SSL pinning

我手头上的APP都是企业级证书的APP
而这些APP呢每一年都要被抓去做一次Pentest并进行enhance
pentest是penetration test的简写,渗透性测试的意思。

渗透测试 (penetration test)并没有一个标准的定义,国外一些安全组织达成共识的通用说法是:渗透测试是通过模拟恶意黑客的攻击方法,来评估计算机网络系统安全的一种评估方法。这个过程包括对系统的任何弱专点、技术缺陷或漏洞的主动分属析,这个分析是从一个攻击者可能存在的位置来进行的,并且从这个位置有条件主动利用安全漏洞。

我的亲儿子(我负责的其中一个APP)也在这两年pentest的修复中茁壮成长
收到pentest的报告其中有一项骂我说没有帮他做SSL pinning。。
下面就记录一下这个SSL pinning


其实我的APP很危险

HTTP:我有SSL/TLS的加持,恕我直言,在座的各位,都是垃圾
Charles:哦?是吗?那么棒?你帮我看看刚刚你的这个登录密码对不对?
Fiddle:Charles哥你负责在Mac的,Windows那边交给我
其实我的APP很危险,甚至被中间人攻击了都不知道🤷♀️

苍蝇不叮无缝的蛋

在你了解完http加上s的工作原理之后,以为非对称加密+对称加密就很安全了吗?
其实在客户端和服务器握手的第一步,中间人就能截获客户段请求,截获客户端发给服务器之后用来生成对称加密的钥匙的随机数(有点绕,如果很难理解的话先看懂上面的https工作原理),然后以客户段的名义跟服务器握手,以服务器的名义与客户端愉快的交流

中间人用的伪证书,这个证书可能就是从CA官方申请的,客户端一般都会信任这类证书,这就是可以进行中间人攻击的原因所在。

为什么伪造证书可以实现中间人攻击?

答案就在于用户让iOS系统信任了不应该信任的证书(安装了证书)。用户设置系统信任的证书,会作为锚点证书(Anchor Certificate)来验证其他证书。当返回的服务器证书是锚点证书,就被信任。
其实Charles和Fiddle就是中间人,而我们使用它们的时候必须要下载并信任它的证书,就是这个道理


其实我的APP也可以没那么危险

如果从CA申请证书打包到App中,把这个证书作为Anchor Certificate来保证证书链的唯一性和可信性。只相信app里面的锚点证书,也就只会验证通过由这些锚点证书签发的证书。这样就算被验证的证书是由系统其他信任的锚点证书签发的,也无法验证通过。
简单来说,将服务器的证书固定在客户端上,通过SSL证书绑定来验证服务器身份,防止应用被抓包

  • 解决办法思路:如果从CA申请证书打包到App中,把这个证书作为Anchor Certificate来保证证书链的唯一性和可信性。只相信app里面的锚点证书,也就只会验证通过由这些锚点证书签发的证书。这样就算被验证的证书是由系统其他信任的锚点证书签发的,也无法验证通过。

  • 做法:选择哪个证书打包到app里面,很多开发者会直接选择叶子证书。其实对于自建证书来说,选择哪一节点都是可行的。而对于由CA颁发的证书,则建议导入颁发该证书的CA机构证书或者是更上一级CA机构的证书,甚至可以是根证书。这是因为:

  1. 一般叶子证书的有效期都比较短
  2. 越往证书链的末端,证书越有可能变动;

准备工作

ssl pinning:我要上场啦
我用的是Objective-C语言

取到证书

客户端需要证书(Certification file), .cer格式的文件。可以跟服务器端索取。
如果他们给个.pem文件,要使用命令行转换:

openssl x509 -inform PEM -in name.pem -outform DER -out name.cer

如果给了个.crt文件,请这样转换:

openssl x509 -in name.crt -out name.cer -outform der

如果啥都不给你,你只能自己动手了:

openssl s_client -connect www.website.com:443 </dev/null 2>/dev/null | openssl x509 -outform DER > myWebsite.cer

好,我们拿到证书了。

把证书加进项目中

把生成的.cer证书摁住直接拖进你的项目相关文件中,记得勾选Copy items if needed和你的targets

image.png

AFSecurityPolicy

下面我们来分两个版本来说

  1. AFNetworking 3.0 以下版本使用AFHTTPRequestOperationManager
+(AFHTTPRequestOperationManager *)manager{
    static AFHTTPRequestOperationManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //1.创建manager对象
        NSURL *baseUrl = [NSURL URLWithString:@"你的访问的地址的domian"];
        manager = [[AFHTTPRequestOperationManager manager]initWithBaseURL:baseUrl];
        
        //2.设置接收的response类型
        [[manager responseSerializer]setAcceptableContentTypes:[NSSet setWithObjects:@"application/json",@"text/plain",@"text/html", nil]];
        
        //3.https证书配置
        //3.1 先将证书拖进项目
        //3.2 获取证书的路径
        NSString *certPath = [[NSBundle mainBundle] pathForResource:@"你的证书名字" ofType:@"cer"];
        //3.3 获取证书data
        NSData *certData = [NSData dataWithContentsOfFile:certPath];
        //3.4 创建AFN 中的securityPolicy
        AFSecurityPolicy *securitypolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
        //3.5 绑定证书
        [securitypolicy setPinnedCertificates:@[certData]];
        //3.6 是否允许无效证书
        [securitypolicy setAllowInvalidCertificates:NO];
        //3.7 是否需要验证域名
        /*
        validatesDomainName 是否需要验证域名,默认为YES;
        假如证书的域名与你请求的域名不一致,需把该项设置为NO;
        如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
        置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。
        因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;
        当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
        如置为NO,建议自己添加对应域名的校验逻辑。
         */
        [securitypolicy setValidatesDomainName:YES];
        
        //4. 上述securitypolicy设置为manager的securitypolicy
        manager.securityPolicy = securitypolicy;
        
    });
    return manager;
}

发送请求

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    AFHTTPRequestOperationManager *manager = [NetWorkManager manager];
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    
   [manager GET:@"https://www......."
      parameters:nil
         success:^(AFHTTPRequestOperation * _Nonnull operation, id  _Nullable responseObject) {
       NSLog(@"%@",responseObject);
   } failure:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nonnull error) {
       NSLog(@"%@",error);
   }];
}
  1. AFNetworking 3.0 以上版本移除了AFHTTPRequestOperationManager并且用AFHTTPSessionManager替代
+ (AFHTTPSessionManager *)manager
{
    static AFHTTPSessionManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        //1.创建manager对象
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        manager =  [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"你的访问的地址的domian"] sessionConfiguration:config];
        //2.设置接收的response类型
        [[manager responseSerializer]setAcceptableContentTypes:[NSSet setWithObjects:@"application/json",@"text/plain",@"text/html", nil]];
        
        //3.https 证书配置
        //3.1 将证书拖进项目
        //3.2 获取证书路径
        NSString *certPath = [[NSBundle mainBundle] pathForResource:@"你的证书名字" ofType:@"cer"];
        //3.3 获取证书data
        NSData *certData = [NSData dataWithContentsOfFile:certPath];
        //3.4 创建AFN 中的securityPolicy
        AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey withPinnedCertificates
                                                                                  :[[NSSet alloc] initWithObjects:certData,nil]];
        //3.5 这里就可以添加多个server证书
        NSSet *dataSet = [[NSSet alloc]initWithObjects:certData, nil];
        //3.6 绑定证书(不止一个证书)
        [securityPolicy setPinnedCertificates:dataSet];
        //3.7 是否允许无效证书
        [securityPolicy setAllowInvalidCertificates:NO];
        //3.8 是否需要验证域名
        /*
        validatesDomainName 是否需要验证域名,默认为YES;
        假如证书的域名与你请求的域名不一致,需把该项设置为NO;
        如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
        置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。
        因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;
        当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
        如置为NO,建议自己添加对应域名的校验逻辑。
         */
        [securityPolicy setValidatesDomainName:YES];

        manager.securityPolicy = securityPolicy;
    });
    return manager;
}

发送请求

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    AFHTTPSessionManager *manager = [NetworkManager manager];
    
    [manager POST:@"https://www....." parameters:nil headers:nil constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
        NSLog(@"formData:%@",formData);
    } progress:^(NSProgress * _Nonnull uploadProgress) {
        
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"responseObject:%@",responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"error:%@",error);
    }];

}

最关紧是设置 AFSSLPinningMode

AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey

AFSSLPinningModeNone: 完全信任;
AFSSLPinningModePublicKey:只校验服务器证书和本地证书的Public Key是否一致
AFSSLPinningModeCertificate:比对服务器证书和本地证书的所有内容,完全一致则信任服务器证书

选择那种模式呢?

AFSSLPinningMode:信任任何证书,没有安全性可言,是默认值。
AFSSLPinningModeCertificate:最安全的比对模式。但是也比较麻烦,因为证书是打包在APP中,如果服务器证书改变或者到期,旧版本无法使用了,我们就需要用户更新APP来使用最新的证书。
AFSSLPinningModePublicKey:只比对证书的Public Key,而一般更新服务器证书,公钥是不会变的,只要公钥没有改变,证书的其他变动都不会影响使用。

如果你不能保证你的用户总是使用你的APP的最新版本,所以我们使用AFSSLPinningModePublicKey

Charles对使用SSL Pinning前后抓包对比
证书固定前.png
证书固定后.png

亲测过的坑(泪目)

  1. 服务器的同事给了我.cer.crt的证书文件给我,但是cert pinning怎么都不起作用, 后来我用.crt 证书用命令行转换之后再导进项目就起作用了。。
openssl x509 -in name.crt -out name.cer -outform der
  1. 我尝试导出github的ssl证书再去试cert pinning但是始终不起作用,如果有大神知道的话麻烦不吝赐教🙏。

本文参考资料(感谢🙏)

AFNetworking + SSL Pinning
iOS 集成 SSL Pinning
如何使用SSL pinning来使你的iOS APP更加安全
App的中间人攻击
iOS AFNetworking框架HTTPS请求配置
iOS afnetworking最新版报错 没有AFHTTPRequestOperationManager类了
HTTPS 的工作原理

Android版的请顺着网线移步到Android逆开发--Volley/OkHttp SSL Pinning(证书固定)可以这样做

写作初心

梳理,积累,分享,交流

靴靴你能看到这里
下一篇见 ᕕ(ᐛ)ᕗ