老青菜

HTTPS MITM

2019-06-12

HTTPS MITM(Man in the middle attack),中间人攻击。也就是通讯双方中插入一个中间人,通讯双方的对方已经变成中间人了,而不是原本的对方。

简介

我们来看下中间人模式是如何工作的。

正常模式:        
A   <-------->   B

中间人模式:    
A   <-------->   C   <-------->  B

举个简单的例子:

  1. A访问B,发送消息 hello。
  2. 中间人C通过 ARP 欺骗、DNS 污染、代理等等技术,劫持了会话连接的建立。
  3. 中间人C收到A发送的 hello 消息,然后发送给B。
  4. 中间人C收到B的响应后,返回给A。

表面上是A和B在通信,其实这里有两个会话,A和C、B和C。对于中间人C来说,这些消息都是透明的。

HTTPS MITM

那么在 HTPPS 安全会话中,还会有中间人攻击吗?
答案是肯定的,比如说 Charles、mitmproxy,只不过这些抓包工具需要用户设置代理、手动信任根证书。当然也可以通过其他方式,比如说 ARP欺骗、DNS 污染等等手段,把请求重定向到攻击者服务器,从而实现中间人攻击。

通过之前的 HTTPS系列 文章,我们知道 HTTPS 会话前,会先进行 SSL/TLS 握手,校验证书,协商会话密钥。所以针对 HTTPS 的中间人攻击一般都是会话建立前的攻击。中间人模式的握手流程如下:

1. 中间人收到 ClientHello

中间人保存Client 随机数。伪造新的 Client 随机数,发送ClientHello给 Server。

2. 中间人收到 Server 的 ServerHello等消息

中间人收到 Server 的 ServerHelloServerCertificateServerKexExchangeCertificate RequestServerHelloDone这些消息,保存 Server 的随机数、证书、DH协商参数。

3. 中间人发送 ClientCertificate、ClientKeyExchange、ChangeCipherSpec、Finished

  • 发送ClientCertificate,携带真实的 Client 证书,可以在 Charle s的 SSL Proxying Setting 里的 Client Certificate 里配置;

  • 发送ClientKeyExchange,携带伪造的 Client DH 协商参数;

  • 和 Server 协商出预主密钥,计算出主密钥1

  • 发送ChangeCipherSpec,通知 Server 更改密码规范,发送Finished验证密钥。

4. 中间人收到 Server 的 Finished

  • 收到 Server 的Finished以后,发送ServerHello给 Client,携带伪造的 Server 随机数;

  • 发送ServerCertificate,携带伪造的Server证书

  • 发送ServerKeyExchange,携带伪造的 Server DH 协商参数;

  • 发送CertificateRequest,请求客户端证书;

  • 发送ServerHelloDone,问候结束。

5. 中间人收到 Client 的 ClientCertificate、ClientKeyExchange、ChangeCipherSpec、Finished

中间人收到这些消息,保存 Client DH 协商参数

6. 中间人发送 ChangeCipherSpec、Finished 消息

  • 和 Client 协商预主密钥,计算出主密钥2

  • 发送ChangeCipherSpec,通知 Client 更改密码规范,发送Finished验证密钥。

至此整个中间人模式的握手就完成了,可以看出,中间人具有双重身份,针对 Client,中间人作为Server,针对 Server,中间人作为 Client。最后中间人拥有两个MasterSecret(主密钥),主密钥1负责和 Server 的会话加解密,主密钥2负责和 Client 的会话加解密。

看到这里,可能会有点懵,接下来我们来用实际抓包来验证一下中间人攻击的流程。

Charles中间人

Charles为例,我们看一下 HTTPS 中实际中间人攻击的流程。启动WiresharkCharles,设置Charles监听端口,接着设置手机Wifi代理,然后我们打开手机浏览器,访问
https://www.laoqingcai.com/api/user

接着观察Wireshark(这里为了过滤无用的包,我加了 tls 过滤条件),最终结果如下图:

先解释一下几个IP:

192.168.0.76: 手机端内网IP
192.168.0.106: Charles代理服务器内网IP,
121.40.244.23: www.laoqingcai.com对应的服务器外网IP。

然后对照Wireshark数据包流图,分析实际的中间人握手流程如下:

  1. 第19条,Client 发起握手,发送Client Hello

  2. 第21条,中间人收到Client Hello,立即和 Server 握手,发送伪造的Client Hello

  3. 第21-33条,中间人扮演 Client 和 Server 握手、交换DH协商参数;到第33条,完成握手,互发Finished验证主密钥。这时候中间人和 Server 都已经计算出这次会话的主密钥1。

  4. 第39-47条,中间人扮演 Server 和 Client 握手、交换DH协商参数;到第47条,完成握手,互发Finished验证主密钥。这时候中间人和 Client 都已经计算出这次会话的主密钥2。

很明显,Charles扮演了中间人身份,和 Client、Server 都维持了会话。针对 Server,扮演了 Client,正常握手;针对 Client,扮演了 Server,使用自签名根证书,伪造 Server 证书,进行握手。

伪造Server Certificate

在和 Client 握手期间,中间人会用自签名根证书伪造 Server 证书,返回给 Client,具体如何生成我们可以参照 mitmproxy get_cert 源码(因为Charles不是开源的)。
从 Chrome、Wireshark ,我们可以看到造后的 Server 证书,颁发者都是自签名的Charles Proxy CA,并不是 DigiCert Global Root CA


如何防止

那么如何防止中间人攻击呢?其实也就是如何解决信任的问题,Clien t如何判断出是真实的 Server 还是中间人呢?
这里我从App层和协议层两个层面,总结了几个思路:

域名强校验

自定义证书验证逻辑,收到Server Certificate,校验证书域名。这是比较初级的保护手段,对于一般的攻击来说明没有什么作用。

SSL Pinning

加密固定技术,包含两种实现:

1. Certificate Pinning

证书固定,也就是把 Server 证书固定在客户端里,每一次请求收到 Server 证书的时候,对比证书是否一致,一致则认为是安全的,否则认为是不安全的。
这种方式有个很大的问题,就是证书如果如果更新了,那么客户端必须同步更新证书,需要强升,否则不能使用。

2. Certificate Public Key Pinning

证书公钥固定,原理类似,只不过是把 Server 证书的公钥固定在客户端,每一次请求收到 Server 证书的时候,对比证书的公钥是否一致。尽管 Server 证书更新了,依然可以保持公钥不变,所以不受影响。而且现在一般 Native App 都是配置多个公钥,完全可以应对证书替换的情况,所以是现在比较流行的解决方案,而且现在移动端的网络框架基本都支持,比如说OKHttpAFNetworkingAlamofire等等。

证书签名校验

参照SSL Pinning的思路,我们也可以自定义证书的签名校验算法,比如我们用 Server 证书的公钥、域名等等其他信息计算出一个摘要,然后用 Server 私钥计算出一个数字签名,这个数字签名就固定在客户端。
然后在 Server 证书校验的时候,用 Server 证书的公钥解密数字签名,得到一个摘要,然后我们再计算 Server 证书的摘要,对比是否一致。
这种方式其实和SSL Pinning里是类似的,增加了计算复杂度。

双向认证

SSL/TLS本身支持双向认证,即服务端需要校验客户端证书,服务端存储了签发客户端证书的根证书,客户端证书需要预先内置在 Native App 里,每次请求服务端都会请求客户端证书,来验证是否是收信任的客户端。其实和网银U盾的原理类似。
这种方案使用场景有限,适合隐私性比较强的应用。而且也不能完全避免MITM,现在Charles等等工具都支持设置Client Certificate了,可以在握手的时候,中间人传输设置好的客户端证书给 Server。

数字签名

最后我们说一下数字签名,数字签名是只有消息的发送者才能产生一段字符串,别人无法伪造(除非知知道你的签名算法),这段字符串也是对发送的消息的完整性的一个校验。
那么,我们就可以自定义签名算法,在每条消息加上数字签名,来保证数据的完整性。流程如下:

  1. Client 针对每个请求,计算消息的摘要,即URL Query参数和Post参数合在一起,进行md5或者sha256,然后截取固定位数,或者再加一些特定字符串。最后得出摘要。

  2. 使用AES对称加密,对消息摘要加密,得到签名。把签名和消息一起发送给 Server。

  3. Server 在统一的拦截器里处理,使用同样的算法计算消息的摘要,再计算签名,然后对比计算得到的签名和消息里的签名是否一致,即可判断消息完整性,也就是是否被篡改。

这样即使存在MITM,中间人看到消息,不知道数字签名算法,也无法篡改数据,也能很大的程度上限制攻击行为了。

Hook

上面说的几种思路,除了SSL/TLS协议层的双向认证,其他都是App应用层面的校验策略,那么攻击者完全逆向Native App,在二进制包Headers里的Load Command加入自己的动态库,通过Hook的方式,直接Hook掉校验 API 和签名的 API,无论是 Android 还是 iOS,越狱非越狱,都可以破解。
当然我们也可以逆向检测,比如说:

  1. 针对 iOS,我们可以检测二进制文件Headers里的LoadCommands是否包含异常的库;

  2. 针对 Andoid,简单的方式,可以通过apk的md5值判断是否被反编译,重新编译了。但是这只能检测出 apk 是否倍反编译过,无法检测 apk 以外的hook(比如说针对系统底层的 hook )。

这些都属于逆向工程领域的事情了。对于MITM,没有办法完全防止,我们能做的就是增加攻击和破解的复杂度。

参考链接

mitmproxy get_cert
google security ssl

Tags: HTTP
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章