HTTPS 中间人攻击及其防范
在之前的文章中,笔者简要介绍了一下 HTTPS 的工作原理,在扩展阅读中,笔者提到了中间人攻击(Man In The Middle Attack,简称 MITM)而在本文中,笔者将进一步解释什么是中间人攻击。
什么是中间人攻击
以下内容来自维基百科中的中间人攻击词条:
在密码学和计算机安全领域中,中间人攻击(Man-in-the-middle attack,缩写:MITM)是指攻击者与通讯的两端分别建立独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。在许多情况下这是很简单的(例如,在一个未加密的Wi-Fi 无线接入点的接受范围内的中间人攻击者,可以将自己作为一个中间人插入这个网络)。
简单来说攻击者就是一个介入通信的传话员,攻击者知道通信双方的所有通信内容,而且可以任意增加、删除、修改双方的通信内容,而双方对此并不知情。
而中间人攻击不仅仅局限于针对 HTTPS,对于开放性的连接,中间人攻击非常容易。比如在一个未加密的 Wi-Fi 网络中,一个攻击者可以很容易地将自己插入双方的通信之中以截取或者修改通信的内容。
一个通俗的例子
- 假设 Tom 想和 Jerry 交换一些秘密信息,然而 Tom 又不想跑到 Jerry 家里,于是 Tom 叫来了邮递员,给了邮递员一封信。信的内容是希望 Jerry 给 Tom 一个盒子(这个盒子有两把钥匙)和其中一把钥匙(另一把在 Jerry 手里)。
- 邮递员在拿到 Tom 给的信件以后,把 Tom 的信拆开看了一遍,了解到 Tom 希望 Jerry 给 Tom 一个有锁的盒子,又用另一个信封装了回去,并交给了 Jerry。
- Jerry 在收到 Tom 的信(实际已经被邮递员拆阅过了)之后,给了邮递员一个有锁的盒子和其中一把钥匙。
- 邮递员想知道他们的通信内容,于是他把 Jerry 给 Tom 的盒子换成了他自己的盒子,并附上了自己盒子中的一把钥匙,并在之后将自己的盒子交给了 Tom。
- Tom 在收到盒子之后,以为这个盒子是 Jerry 给他的,于是就把秘密的信件放进了盒子里,并把钥匙留下了,之后又交给了邮递员。
- 邮递员在拿到盒子之后,用自己的另一把钥匙打开盒子,看了里面的信件。之后将信件调换之后放进了 Jerry 给的盒子,交给了 Jerry。
- Jerry 在拿到邮递员给他的盒子之后,并不知道这个盒子里的信件其实已经被邮递员调换过了,所以 Jerry 认为盒子里的信件是来自 Tom 且未被修改过的。之后 Jerry 把回信放进了盒子里,又交给了邮递员。
- 邮递员再次调换盒子里的信件,交给了 Tom。
这就是一个典型中间人攻击的过程。在 HTTPS 中,Tom 就是客户端,Jerry 是服务端,而邮递员就是客户端和服务端之间的任何实体(包括代理服务器、路由器、反向代理服务器等等),两把钥匙分别是公钥和私钥。通信双方并不知道(且通常很难发觉)自己其实在和中间人通信而非直接和对方通信。在通信过程中,Tom 和 Jerry 并没有验证对方的身份,这就导致了邮递员可以任意查看、修改或者丢弃双方的通信内容。
HTTPS 如何防范中间人攻击
从上面的例子看起来,似乎任何在通信双方的实体都可以实施中间人攻击,那么 HTTPS 是如何防止中间人攻击的呢?要防止被中间人攻击,那么就要确保通信中的信息来自他声称的那个人,且没有被修改过。在现实中,有多种方式可以确定某个实体的身份,比如个人的签名 / 私章、组织的公章、甚至古时的信物。大部分情况下,只需要在信件最后盖上签上自己的名字或者盖上组织的公章,那么接收者就可以确定这封信件就来自于他所声称的那个人 / 组织。在二进制的世界中,可以使用数字签名来确保某段消息 / 某份文件确实是由他所声称的那个实体所发出来的。
在之前的文章中,我们介绍过非对称加密,其中公钥是公开的,而私钥只有拥有者知道。用私钥对某个文件 / 某段消息的散列值进行签名就像一个人亲手在信件最后签上了自己的名字一样,证明这份文件 / 这段消息确实来自私钥的拥有者(因为公钥是公开的,私钥只有拥有者知道,所以如果能用其公开的公钥解开数字签名,那就证明这条消息确实来自于他私钥的拥有者),这就可以确保消息是来自他所声称的那个实体。这样,在通信中,双方每次在写完消息之后,计算消息的散列值,并用自己的私钥加密生成数字签名,附在信件后面,接收者在收到消息和数字签名之后,先计算散列值,再使用对方的公钥解密数字签名中的散列值,进行对比,如果一致,就可以确保该消息确实是来自于对方,并且没有被篡改过。
不过有个问题,如果中间人在会话建立阶段把双方交换的真实公钥替换成自己的公钥了,那么中间人还是可以篡改消息的内容而双方并不知情。为了解决这个问题,需要找一个通信双方都信任的第三方来为双方确认身份。这就像大家都相信公证处,公证处拿着自己的公章为每一封信件都盖上了自己的章,证明这封信确实是由本人发出的,这样就算中间人可以替换掉通信双方消息的签名,也无法替换掉公证处的公章。这个公章,在二进制的世界里,就是数字证书,公证处就是 CA(数字证书认证机构)。
数字证书就是申请人将一些必要信息(包括公钥、姓名、电子邮件、有效期)等提供给 CA,CA 在通过各种手段确认申请人确实是他所声称的人之后,用自己的私钥对申请人所提供信息计算散列值进行加密,形成数字签名,附在证书最后,再将数字证书颁发给申请人,申请人就可以使用 CA 的证书向别人证明他自己的身份了。对方收到数字证书之后,只需要用 CA 的公钥解密证书最后的签名得到加密之前的散列值,再计算数字证书中信息的散列值,将两者进行对比,只要散列值一致,就证明这张数字证书是有效且未被篡改过的。
通信过程的安全性自下而上就是这样保证的:
- 双方通信内容的安全性是靠公钥加密、私钥解密来保证的,这一安全性由非对称加密的特性,即由公钥加密的信息只能使用对应的私钥才能解开来保证。由于私钥不会传递,只有拥有者知道,所以安全性就由公钥的正确性来保证。
- 公钥由对方在通信初始所提供,但是这时很容易被中间人替换掉,为了保证公钥的正确性,所以在发送公钥的时候也会提供对应的数字证书,用于验证这个公钥是对方的而不是中间人的。那么安全性就是由数字证书的正确性来保证了。
- 数字证书是由上级 CA 签发给个人 / 组织的,上级 CA 用自己的私钥给个人证书进行签名,保证证书中的公钥不被篡改,而接受者需要用上级 CA 证书中的公钥来解密个人数字证书中的数字签名来验证证书中的公钥是否是正确的。那么安全性就是由上级 CA 证书的正确性保证的了。
- 但是,上级 CA 证书也是由其上级 CA 签发的,这种信任关系一直到根证书。根证书没有上级 CA 为其签名,而是自签名的,也就是说,它自身为自身签名,保证正确性。所以根证书就是这个信任链最重要的部分。如果根证书泄露的话,其签名的所有证书及使用其签名的证书所签名的证书的安全性将不复存在。现在,安全性就是靠系统根证书的私钥不被泄露或者其公钥不被篡改来保证的了。
- 根证书不应该通过网络分发,因为通过网络分发的话,可能会被中间人攻击。一般根证书都通过操作系统或者浏览器分发,在操作系统中会内置很多根证书,但是最初的操作系统也不能通过网络分发,因为中间人可以修改操作系统中的根证书。所以要保证安全只能靠最原始的方法,当面交流。硬件厂商会和证书签发机构合作,在电脑、手机等设备出厂的时候在其操作系统中内置签发机构的根证书,再将这些设备分发出去,这样,这些设备的用户就可以安全地进行信息交换了。所以,安全性就依赖于这些设备在分发到消费者手中之前不会被恶意修改来保证了。
至此,整个信任链就建立起来了,只需要有一台设备上安装了可以信任的根证书,就可以用来分发更多安全的操作系统了。之后的所有信任链都是安全的了。
(题外话)这些设备在到消费者手里之前有没有被恶意修改,谁都不知道。密码学家想法设法想用程序而非人类(因为人类容易收到外界影响,是无法完全信任的)来保证安全,到最后还是离不开人类,而人类恰恰是这些精妙设计中最容易出现问题的一环。
SSLTrip 及 HSTS
HTTP 协议最初的时候是明文的,因为安全问题所以现在很多网站都在逐渐过渡到 HTTPS,然而对于大部分使用者来说,他们并不知道 HTTP 和 HTTPS 之间的区别,在浏览器输入地址的时候都是直接输入 www.example.com
而非 https://www.example.com
,在大部分情况下,如果一个网站启用了 HTTPS,服务器会将这个请求使用 301
或者 302
状态码以及一个 Location
头部将请求从 80 端口重定向至使用 HTTPS 的 443 端口。但是,如果中间人劫持了使用者的网络请求,那么中间人可以阻止客户端与服务器建立 HTTPS 连接,而是一直使用不安全的 HTTP 连接,而中间人则和服务器建立正常的 HTTPS 连接,让客户端以为自己正在和真实服务器通信。这种攻击手法称作 SSLTrip。
为了解决这个问题,IETF(互联网工程任务小组)引入了一个策略,叫做 HSTS (HTTP Strict Transport Security, HTTP 严格传输安全)。HSTS 的作用是强制客户端与服务端建立安全的 HTTPS 连接,而非不安全的 HTTP 连接。如果一个站点启用了 HSTS 策略,那么客户端在第一次与该站点建立连接之后,在未来的一段时间内(由一个 HTTP 头部控制,这个头部为:Strict-Transport-Security),客户端与该站点的所有连接都会直接使用 HTTPS,即使客户端访问的是 HTTP,也会直接在客户端重定向到 HTTPS 连接。
假设 https://example.com
的响应头部含有 Strict-Transport-Security: max-age=31536000; includeSubDomains
,这意味着:
- 在未来的 1 年时间里(即 31536000 秒中),只要浏览器向
example.com
或者其子域名发送请求,必须采用 HTTPS 来发起连接。即使用户在地址栏里写的是http://example.com
,那也直接重写为https://example.com
并直接发起 HTTPS 连接。 - 在接下去的一年中,如果服务器提供的 HTTPS 证书无效(不论是域名对不上还是自签名还是不在有效期内),用户都无法访问该站点。
如果站点没有启用 HSTS,用户可以忽略证书无效的警告,继续建立连接,而如果站点启用了 HSTS,那么用户即使想冒风险,浏览器也不会继续访问。
HSTS 可以很大程度上防止 SSLTrip 攻击,不过这样还是有个问题,那就是要启用 HSTS,浏览器至少要和服务器建立一次 HTTPS 连接,如果中间人一直阻止浏览器与服务器建立 HTTPS 连接,那么 HSTS 就失效了。解决这个问题有个办法,那就是将 HSTS 站点列表内置到浏览器中,这样只要浏览器离线判断该站点启用了 HSTS,就会跳过原先的 HTTP 重定向,直接发起 HTTPS 请求。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。