https简介
详谈HTTPS通信机制,HTTPS是如何进行安全通信的?
首先来看一种场景:小红发信息约小明放学后去电影。
正常的信息流动是这样的:
1.小红 -> 放学后去看电影吧? -> 小明
2.小明 -> 好,校门口等我。 -> 小红
但如果存在一个中间人把小红和小明的信息拦截,并做了修改,信息流变成了下面这样:
1.小红 -> 放学后去看电影吧?-> 中间人(你) -> 今天上课有点累,放学我就回家了,不要等我。 -> 小明
2.小明 -> 好吧,好好休息。 -> 中间人(你) -> 不去了,放学后我就回家。 -> 小明
小红以为小明不陪自己看电影,小明以为小红放学就回家了,而你就找了个理由约上小红去看电影了。
之所以会产生误解,是因为小红和小明没办法验证对方信息的真假,都以为收到了对方的正确信息,这是个典型的中间人攻击的例子。
HTTPS正是要解决这个问题,在HTTP传输层之上加了一个安全层(SSL或TLS协议实现),可以做到以下3点:
数据的保密性
校验双方身份的真实性
数据的完整性
下面来看https是如果解决这3个问题的。
数据的保密性
数据要保密,就需要对数据进行加密。加密算法可以分为2类,一类是对称加密算法,另一类是非对称加密算法。
对称加密算法,加密和解密使用相同的密钥,优点是加密速度快,缺点是如果密钥泄露的话就无法做到保密了。常见的对称加密算法有DES、AES等。
非对称加密算法,又叫公开密钥加密。需要有2个密钥,公钥和私钥,公钥向所有人公开,私钥不公开。用公钥加密的数据只有私钥才能解密;反之,用私钥加密的数据只有公钥才能解密。因为这种特性,非对称加密算法可以用来校验数字签名,下面会具体讲解。
很显然,仅使用对称加密算法是不现实的,互联网中通信的双方大多是临时建立的连接,不可能提前协商好密钥,而且密钥也要进行传输,无法保证密钥本身的安全性。
如果使用非对称加密,客户端向服务器发送数据是安全的,客户端用服务器的公钥进行加密,只有服务器用自己的私钥才能解密。但如果服务器用私钥对数据进行加密,则所有人都可以用公钥进行解密,这是不安全的。
HTTPS的解决方案是这样的:用非对称算法随机加密出一个对称密钥,然后双方用对称密钥进行通信。具体来说,就是客户端生成一个随机密钥,用服务器的公钥对这个密钥进行非对称加密,服务器用私钥进行解密,然后双方就用这个对称密钥来进行数据加密了。
校验双方身份的真实性
上面说了加密,保证了数据不能被他人读取,但通信的双方怎样校验对方的身份呢?HTTPS使用了数字证书,数字证书就是身份认证机构(Certificate Authority)盖在数字身份证上的一个章或印(或者说加在数字身份证上的一个签名),这一行为表示身份认证机构已认定这个人。证书的合法性可以向CA验证。
数字证书主要包含以下信息:
-
证书颁发机构
-
证书颁发机构签名
-
证书绑定的服务器域名
-
证书版本、有效期
-
签名使用的加密算法(非对称算法,如RSA)
-
公钥
客户端收到服务器的响应后,先向CA验证证书的合法性(根据证书的签名、绑定的域名等信息),如果校验不通过,浏览器会中止连接,向用户提示证书不安全。
需要提一点的是,证书的制作方法是公开的,任何人都可以自己制作证书,所以有些公司不向CA申请,比如12306。但自己制作的证书是得不到CA认证的,所以访问12306网站时,浏览器会有证书不合法的提示,如下图,只有用户选择信任该网站时,才能继续访问。
image数据的完整性
网络传输过程中需要经过很多中间节点,虽然数据无法被解密,但可能被篡改,那如何校验数据的完整性呢?通过校验数字签名,流程见下图:
image首先来了解下哈希算法,哈希算法能够将任意长度的字符串转化为固定长度的字符串,该过程不可逆,可用来作数据完整性校验。
服务器在发送报文之前做了3件的事:
-
用哈希算法对报文提取定长摘要
-
用私钥对摘要进行加密,作为数字签名
-
将数字签名附加到报文末尾发送给客户端
客户端接收到报文后:
-
用公钥对服务器的数字签名进行解密
-
用同样的算法重新计算出报文的数字签名
-
比较解密后的签名与自己计算的签名是否一致,如果不一致,说明数据被篡改过。
同样,客户端发送数据时,通过公钥加密报文摘要,服务器用私钥解密,用同样的方法校验数据的完整性。
HTTPS通信的大致过程
image客户端将自己支持的加密算法发送给服务器,请求服务器证书;
服务器选取一组加密算法,并将证书返回给客户端;
客户端校验证书合法性,生成随机对称密钥,用公钥加密后发送给服务器;
服务器用私钥解密出对称密钥,返回一个响应,HTTPS连接建立完成;
随后双方通过这个对称密钥进行安全的数据通信。
charles手机抓包
- mac电脑分享wifi热点,手机连接wifii,点击wifi设置代理。服务器地址是网络设置里本机ip,端口8888.
- 如果要访问https的话,则要打开charles->help->SSL Proxying->Install Charles Root Certificate on a Mobile Device or Remote Browser (本地电脑和模拟器的话选择其他选项。)
- 手机端打开safari,访问标红的地址,即会跳转下载并安装描述性文件,安装完成.
后到 【通用】->【关于本机】-> 【证书信任设置】,然后就启用完全信任刚才安装的证书
image
- mac设置代理
选择Proxy | Proxy Settings,弹出proxy设置选项卡,勾选Enabling transparent HTTP proxying
勾选Enable SSL Proxying,在Location部份选择add,按如下图添加,抓取任意站点、443端口的数据 (当然可以指定站点,端口号若不确定可以用*表任意)
image中间人攻击的情形,浅谈HTTPS抓包原理,为什么Charles能够抓取HTTPS报文?
抓取https包的时候,青花瓷会要求使用者 对抓包的设备(手机或其他设备),安装一个证书,安装这个证书的时候,其实是安装了一个根证书(允许颁发CA的一个证书机构的根证书),当你安装了该根证书之后,该证书机构颁发的其他证书,默认都会被你的系统所信任,这个就是青花瓷完成https抓包的一个重要前提!!
(如果不太了解证书信任链为什么会这样,可以看一下这个文章,关于iOS系统对https信任链的校验关系,
地址: http://www.jianshu.com/p/2cae04922e9c)
当客户端设置了代理,并且开始发出网络请求的时候,这个网络请求的校验过程就会变成这样
image- 当客户端Client对服务器Server发送请求(带着随机数和加密算法),由于青花瓷做了代理,请求被青花瓷拦截,处理(青花瓷的角色现在对于Client来说是服务器),青花瓷将客户端带的随机数和加密算法处理,然后返回自己的证书通过客户端校验,获取到客户端提交的请求参数等数据,
- 青花瓷作为客户端(自己去产生随机数和携带支持的加密算法)去请求刚刚Client想要请求的Server,然后,Server会和青花瓷完成上面讲的那个完整的校验,并且读取青花瓷带错来的具体请求,返回正常的数据结果.
- 青花瓷得到服务器数据的返回结果之后,开始继续和过程1中的Client以服务器的身份,去做处理,首先收到客户端的随机数和加密算法,自己生成一个随机数和选择一个客户端的加密算法,然后*********重要********** 青花瓷会返回一个伪造的CA证书(公钥和真实的server不一样,但是域名是一样的,或者说,除了域名是一致的,其他的都不是一致的,而且这个签发机构是青花瓷之前让你安装的根证书 签发的,所以,当返回这个证书的时候,你的客户端的信任链是可以完成的,会被系统信任),然后Client在这个伪造的证书(对于青花瓷和Client是真实证书(验证信任链和证书信息都通过了),但是和真实的域名对应的证书来看,是伪造证书)的基础上,和青花瓷通信,然后青花瓷再和Server通信,成了一个中间人的角色,这样,整个过程的数据传输,都被青花瓷给监听到了
在此,中间人攻击的过程 就完成了
反抓包
判断是否有网络代理(不推荐)
当进行网络请求的时候,客户端判断当前是否设置了代理,如果设置了代理,不允许进行访问,附带判断是否设置代理的代码:
+ (BOOL)getProxyStatus {
NSDictionary *proxySettings = NSMakeCollectable([(NSDictionary *)CFNetworkCopySystemProxySettings() autorelease]);
NSArray *proxies = NSMakeCollectable([(NSArray *)CFNetworkCopyProxiesForURL((CFURLRef)[NSURL URLWithString:@"http://www.baidu.com"], (CFDictionaryRef)proxySettings) autorelease]);
NSDictionary *settings = [proxies objectAtIndex:0];
NSLog(@"host=%@", [settings objectForKey:(NSString *)kCFProxyHostNameKey]);
NSLog(@"port=%@", [settings objectForKey:(NSString *)kCFProxyPortNumberKey]);
NSLog(@"type=%@", [settings objectForKey:(NSString *)kCFProxyTypeKey]);
if ([[settings objectForKey:(NSString *)kCFProxyTypeKey] isEqualToString:@"kCFProxyTypeNone"])
{
//没有设置代理
return NO;
}
else
{
//设置代理了
return YES;
}
}
结果:
没有代理:
[20383:1874008] type=kCFProxyTypeNone
(lldb) po proxies
<__NSArrayM 0x1c064e5e0>(
{
kCFProxyTypeKey = kCFProxyTypeNone;
}
)
设置了代理
[20391:1874714] type=kCFProxyTypeHTTP
(lldb) po proxies
<__NSArrayM 0x1c005f8c0>(
{
kCFProxyHostNameKey = “127.0.0.1”;
kCFProxyPortNumberKey = 1380;
kCFProxyTypeKey = kCFProxyTypeHTTP;
}
)
使用自签证书 ssl-pinning模式
客户端本地做证书校验,并且设置不仅仅校验公钥,设置完整的正式校验模式。
这么做为什么是安全的?了解HTTPS的人都知道,整个验证体系中,最核心的实际上是服务器的私钥。私钥永远,永远也不会离开服务器,或者以任何形式向外传输。私钥和公钥是配对的,如果事先在客户端预留了公钥,只要服务器端的公钥和预留的公钥一致,实际上就已经可以排除中间人攻击了。
AFSSLPinningModeCertificate采用该模式证书有过期时间,自签证书要自己更新证书特别要注意。
#import "HttpRequest.h"
#import "AFNetworking.h"
/**
* 是否开启https SSL 验证
*
* @return YES为开启,NO为关闭
*/
#define openHttpsSSL YES
/**
* SSL 证书名称,仅支持cer格式。“app.bishe.com.cer”,则填“app.bishe.com”
*/
#define certificate @"adn"
@implementation HttpRequest
+ (void)post:(NSString *)url params:(NSDictionary *)params success:(void (^)(id))success failure:(void (^)(NSError *))failure
{
//对应域名的校验我认为应该在url中去逻辑判断。--》冯龙腾写
//通过对url字符串的切割对子域名进行逻辑验证处理。
// 1.获得请求管理者
AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
// 2.申明返回的结果是text/html类型
mgr.responseSerializer = [AFHTTPResponseSerializer serializer];
mgr.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json",@"text/json",@"text/javascript",@"text/html",nil];
// 3.设置超时时间为10s
mgr.requestSerializer.timeoutInterval = 10;
// 加上这行代码,https ssl 验证。
if(openHttpsSSL)
{
[mgr setSecurityPolicy:[self customSecurityPolicy]];
}
// 4.发送POST请求
[mgr POST:url parameters:params
success:^(AFHTTPRequestOperation *operation, id responseObj) {
if (success) {
success(responseObj);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (failure) {
failure(error);
}
}];
}
+ (AFSecurityPolicy*)customSecurityPolicy
{
// /先导入证书
NSString *cerPath = [[NSBundle mainBundle] pathForResource:certificate ofType:@"cer"];//证书的路径
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
// AFSSLPinningModeNone:完全信任服务器证书;只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。
//AFSSLPinningModePublicKey:只比对服务器证书和本地证书的Public Key是否一致,如果一致则信任服 务器证书;
// AFSSLPinningModeCertificate:比对服务器证书和本地证书的所有内容,完全一致则信任服务器证书;
// 选择那种模式呢?
// AFSSLPinningModeCertificate:最安全的比对模式。但是也比较麻烦,因为证书是打包在APP中,如果服
务器证书改变或者到期,旧版本无法使用了,我们就需要用户更新APP来使用最新的证书。
// AFSSLPinningModePublicKey:只比对证书的Public Key,只要Public Key没有改变,证书的其他变动都不会影响使用。 如果你不能保证你的用户总是使用你的APP的最新版本,所以我们使用AFSSLPinningModePublicKey。
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
// allowInvalidCertificates 是否允许无效证书(不在证书信任链内所以是NO,也就是自建的证书),默认为NO
// 如果是需要验证自建证书,需要设置为YES
securityPolicy.allowInvalidCertificates = YES;
//validatesDomainName 是否需要验证域名,默认为YES;
//假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
//置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
//如置为NO,建议自己添加对应域名的校验逻辑。
securityPolicy.validatesDomainName = NO;
if (certData) {
securityPolicy.pinnedCertificates = @[certData];
}
return securityPolicy;
}
@end
https双向认证
HTTPS双向验证单向验证过程中,客户端会验证自己访问的服务器,服务器对来访的客户端身份不做任何限制。如果服务器需要限制客户端的身份,则可以选择开启服务端验证,这就是双向验证。从这个过程中我们不难发现,使用单向验证还是双向验证,是服务器决定的。一般而言,我们的服务器都是对所有客户端开放的,所以服务器默认都是使用单向验证。如果你使用的是Tomcat服务器,在配置文件server.xml中,配置Connector节点的clientAuth属性即可。若为true,则使用双向验证,若为false,则使用单向验证。如果你的服务,只允许特定的客户端访问,那就需要使用双向验证了。
image.png
下面直接上代码(单向认证和双向认证适用同一套代码,只要证书配置的对就可以):
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
// 设置超时时间
[manager.requestSerializer willChangeValueForKey:@"timeoutInterval"];
manager.requestSerializer.timeoutInterval = 30.f;
[manager.requestSerializer didChangeValueForKey:@"timeoutInterval"];
[manager.requestSerializer setValue:@"Content-Type" forHTTPHeaderField:@"application/json; charset=utf-8"];
[manager setSecurityPolicy:[self customSecurityPolicy]];
[self checkCredential:manager];
[manager POST:@"这里填你的https地址" parameters:nil progress:^(NSProgress * _Nonnull uploadProgress) {
NSLog(@"123");
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"成功:%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"失败:%@",error);
}];
- (AFSecurityPolicy*)customSecurityPolicy {
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
//获取证书路径
NSString * cerPath = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"];
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
NSSet *dataSet = [NSSet setWithArray:@[certData]];
[securityPolicy setAllowInvalidCertificates:YES];//是否允许使用自签名证书
[securityPolicy setPinnedCertificates:dataSet];//设置去匹配服务端证书验证的证书
[securityPolicy setValidatesDomainName:NO];//是否需要验证域名,默认YES
return securityPolicy;
}
//校验证书
- (void)checkCredential:(AFURLSessionManager *)manager
{
[manager setSessionDidBecomeInvalidBlock:^(NSURLSession * _Nonnull session, NSError * _Nonnull error) {
}];
__weak typeof(manager)weakManager = manager;
[manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession*session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing*_credential) {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__autoreleasing NSURLCredential *credential =nil;
NSLog(@"authenticationMethod=%@",challenge.protectionSpace.authenticationMethod);
//判断服务器要求客户端的接收认证挑战方式,如果是NSURLAuthenticationMethodServerTrust则表示去检验服务端证书是否合法,NSURLAuthenticationMethodClientCertificate则表示需要将客户端证书发送到服务端进行检验
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 基于客户端的安全策略来决定是否信任该服务器,不信任的话,也就没必要响应挑战
if([weakManager.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 创建挑战证书(注:挑战方式为UseCredential和PerformDefaultHandling都需要新建挑战证书)
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 确定挑战的方式
if (credential) {
//证书挑战 设计policy,none,则跑到这里
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else { //只有双向认证才会走这里
// client authentication
SecIdentityRef identity = NULL;
SecTrustRef trust = NULL;
NSString *p12 = [[NSBundle mainBundle] pathForResource:@"client"ofType:@"p12"];
NSFileManager *fileManager =[NSFileManager defaultManager];
if(![fileManager fileExistsAtPath:p12])
{
NSLog(@"client.p12:not exist");
}
else
{
NSData *PKCS12Data = [NSData dataWithContentsOfFile:p12];
if ([self extractIdentity:&identity andTrust:&trust fromPKCS12Data:PKCS12Data])
{
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate(identity, &certificate);
const void*certs[] = {certificate};
CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs,1,NULL);
credential =[NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
disposition =NSURLSessionAuthChallengeUseCredential;
}
}
}
*_credential = credential;
return disposition;
}];
}
//读取p12文件中的密码
- (BOOL)extractIdentity:(SecIdentityRef*)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
OSStatus securityError = errSecSuccess;
//client certificate password
NSDictionary*optionsDictionary = [NSDictionary dictionaryWithObject:@"123456"
forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data,(__bridge CFDictionaryRef)optionsDictionary,&items);
if(securityError == 0) {
CFDictionaryRef myIdentityAndTrust =CFArrayGetValueAtIndex(items,0);
const void*tempIdentity =NULL;
tempIdentity= CFDictionaryGetValue (myIdentityAndTrust,kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
const void*tempTrust =NULL;
tempTrust = CFDictionaryGetValue(myIdentityAndTrust,kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
} else {
NSLog(@"Failedwith error code %d",(int)securityError);
return NO;
}
return YES;
}
!bug
所有方法都是有漏洞的,iOS的app够安全,但是圈内依然有一群逆向工程师
-
方法1破解:在使用Class-Dump还是能够找到方法并且运行时替换或者直接hook方法进行修改返回逻辑,动态库注入方式,再使用企业签名把ipa包进行重新签名ios-app-signer,fir发布。
-
方法2破解:
- 一:在逆向工程师眼里也是很简单破解的,app砸壳,再显示包内容,依然可以直接把你证书放在青花瓷中使用。
网友评论