你好,我是胜辉。
在上节课里,我们对TLS的整体的知识体系做了总览性的介绍,然后回顾了两个实际的案例,从中领略了TLS握手的奥妙。我们也知道了,TLS握手的信息量还是很大的,稍有差池就可能引发问题。我们只有对这些知识有深刻的理解,才能更准确地展开排查。
不过,也正因为这种种严苛的条件,TLS才足够安全,因为满足了这些前提条件后,真正的数据传送就令人十分放心了。除非你能调动超级计算机或者拥有三体人的智慧,要不然一个TLS连接里面的加密数据,你是真的没有办法破解的。
可话说回来,如果排查工作确实需要我们解开密文,查看应用层信息,那我们又该如何做到呢?
所以在这节课里,我会带你学习TLS解密的技术要点,以及背后的技术原理,最后进行实战演练,让加密不再神秘。好了,让我们开始吧。
在上节课里我们已经了解到,TLS是结合了对称加密和非对称加密这两大类算法的优点,而密码套件是四种主要加密算法的组合。那么这些概念,跟我们的日常工作又有着什么样的交集呢?
下面这个证书,是我在访问站点https://sharkfesteurope.wireshark.org的时候获取到的,我们来仔细读一下这里面的内容,看看哪些是跟我们学过的TLS知识相关的。
我把图中的很多关键信息做了标记,希望可以帮助你更好地理解。
从上到下,我们了解了这张证书所在的证书链,然后是证书名称、身份验证和签名算法、有效期。不过,看完这个证书,你可能也发现了一个小问题:站点名称跟证书名称不一致?这两个不匹配,浏览器为啥不报错呢?
其实,这里的站点名称跟证书实际上是匹配的,但它匹配的不是Common Name,而是另外一个概念:SAN。
TLS证书为了支持更多的域名,设计了一个扩展选项Subject Alternative Name,简称 SAN,它就包含有多个域名。比如还是这张证书,它的SAN里的域名里就有wireshark.org、sni.cloudflaressl.com,还有跟这次访问的站点名直接相关的*.wireshark.org。这个是通配符域名,就意味着sharkfesteurope.wireshark.org也被支持了。SAN列表如下:
这里也有一个小的注意点:通配符证书只能支持一级域名,比如*.wireshark.org证书可以支持以下域名:
但不支持这样的域名:
然后我们再来温习一下密码套件。在这张证书里,我们能看出它用到的密码套件是什么了吗?下面我们来解读一下。
密钥交换算法是什么呢?这在证书里看不出来,需要根据握手协商的结果来判定。不过,我们也可以有个初步的判断。如果这次通信用的TLS版本是1.3,那么就是DHE或者ECDHE这样的“前向加密”的密钥交换算法了。结尾的E是Ephemeral,意思是“短时间的”,也就是密钥是每次会话临时生成的。
补充:稍后我会介绍什么是前向加密。
身份验证和签名算法呢?就是证书里明确写着的ECDSA,其中EC就是Elliptic Curve的缩写,也就是椭圆曲线算法,它可以用更短的密钥达到跟RSA同样的密码强度。后面跟着的SHA-256是哈希摘要算法,证书内容用这个SHA-256算法做了哈希摘要,然后用ECDSA算法对摘要值做了签名,这样的话,客户端就可以验证这张证书的内容有没有被篡改了。
对称加密算法又是什么呢?在证书这里看不出来,因为它也是通过握手协商出来的。当然,用OpenSSL或者curl命令就可以观察到,我们稍后演示。
最后是完整性校验算法了。其实在2里面已经提过了,是SHA-256。
我们用OpenSSL命令,可以直接观测到这次TLS里协商出来的密码套件:
$ openssl s_client -connect sharkfesteurope.wireshark.org:443
......
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
......
原来,这里用的就是最新的TLS1.3版本,密码套件是TLS_AES_256_GCM_SHA384。你有没有发现它跟TLS1.2的那些密码套件相比还有一个区别呢?比如跟下面这个TLS1.2的密码套件比较一下:
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
相比之下,TLS1.3的套件TLS_AES_256_GCM_SHA384是不是少了两个算法:身份验证和签名算法,还有密钥交换算法。不过,身份验证和签名算法倒是可以从证书里看到,它是ECDSA。那密钥交换算法又到哪里去了呢?
其实,这是因为TLS1.3只允许前向加密(PFS)的密钥交换算法了,所以使用静态密钥的RSA已经被排除了,它默认使用的是DHE和ECDHE,所以就不写在密码套件名称里了。
那么在这里,就又涉及了一个新的概念:前向加密。
前向加密又称为“完美前向加密”,它的英文就是Forward Secrecy和Perfect Forward Secrecy。
虽然我前面刚提到TLS1.3去掉了RSA,不过你不要误会了,TLS1.3只是把RSA从密钥交换算法中排除了,但证书签名算法还是可以用RSA的。
为什么TLS1.3强制要求前向加密呢?这是因为,如果在密钥交换的时候用非前向加密的算法(比如RSA),那么一旦黑客取得了服务端私钥,并且抓取了历史上的TLS密文,他就可以用这个私钥和抓包文件,把这些TLS会话的对称密钥给还原出来,从而破解所有这些密文。因为可以把之前的密文都破解,RSA就不属于“前向”加密。
人们发现,要解决这个问题的关键,就要做到:每次参与协商对称密钥时的非对称密钥都不一样。这样的话,即使黑客破解了其中一次会话的密钥,也无法用这个密钥破解其他会话。
我们可以用一个例子来帮助理解“前向加密”。假设我们不断地生成一个个的保险箱,相当于一个个的TLS加密报文,如果每个箱子用同样的锁,那么一旦其中一把锁被破解,所有的保险箱都可以被打开了。用上“前向加密”锁之后,每次新的保险箱都用不同的锁,那么即使一把锁被破解,损失的只是一个保险箱,其他的箱子依旧安全。
TLS只是一套协议,主要是“动动嘴皮子”,具体的活当然还是代码来干。目前应用最为广泛的SSL/TLS实现可能就是OpenSSL了,它既是一个开发库,也是一个命令行工具的名称。另外,NSS和GnuTLS也是开源的TLS实现。应用程序会基于这些TLS库来实现TLS加解密功能。
有没有觉得这个很像OSI的分层模型?业务代码工作在应用层,TLS库工作在表示层和会话层,两层之间有交互也有解耦,起到了很好的协同的效果。
学习完TLS加密原理,我们就要进入动手环节了,也就是期待已久的TLS抓包解密,让秘密不再是秘密。
这里说的客户端,包括了Chrome、Firefox等浏览器,也包括curl这样的命令行工具。我在上节课里提过,为了把TLS解密,我们需要完成几个前提条件。其实这些前提条件就是下面这三件事:
补充:如果是Mac又不想改动全局配置,那么你可以在terminal中的
export SSLKEYLOGFILE=
路径,然后执行open "/Applications/Google\ Chrome.app"
,这时Chrome就继承了这个shell父进程的环境变量,而terminal退出后,这个环境变量就自动卸除了。
在做完这三件事之后,我们用Wireshark打开抓包文件,就能看到解密后的报文了,比如HTTP请求和响应,还有TLS的控制信息,都会展示为明文。
比如,在默认情况下,我们看到的会是密文:
补充:抓包示例文件已经上传至Gitee,建议结合抓包文件和文稿一起学习。
而配置解密步骤之后,看到的就是明文了:
好了,你可以看到明文了,很多应用层的信息都可以辅助你做排查了,是不是有点小小的激动?
那么这背后的原理是什么呢?
其实是这样的:浏览器在启动过程中会尝试读取SSLKEYLOGFILE这个环境变量。如果存在这个变量,而它指向的又是一个有效的文件,那么浏览器就会做最为关键的事情了:它去调用TLS库,让TLS库把访问HTTPS站点的过程中的key信息导出到SSLKEYLOGFILE文件中。我画了一张示意图供你参考:
整个过程倒是不难理解,不过你可能会好奇:为什么这个日志文件有这么强大的能力,能解密TLS?然后又不免担心,如果这个文件“被坏人利用了”,该怎么办?
所以我们还需要近距离地认识一下SSLKEYLOGFILE。
这个文件之所以能够解密TLS,最关键的是,TLS库把密钥交换阶段的核心信息Master secret导出到了这个文件中。基于这个信息,Wireshark就可以还原出当时的对称密钥,从而破解密文。
我们先来认识一下SSLKEYLOGFILE的格式。它是由很多条记录组成的,对于TLS1.2来说,每一行是一条记录,每一条记录由3个部分组成,中间用空格分隔,也就是下面这样:
<Label1> <ClientRandom1> <Secret1>
<Label2> <ClientRandom2> <Secret2>
这三个部分的具体含义是这样的。
Label:是对这条记录的描述,对于TLS1.2来说,这个值一般是CLIENT_RANDOM。另外,RSA也是一个可能的值,但是前面说过,因为RSA算法在密钥交换方面不是前向加密的,所以已经不推荐使用了。所以如果你在日志文件里看到RSA,可能要小心一点,说明你的TLS不是前向加密的,所以并不是很安全。
ClientRandom:这个部分就是客户端生成的随机数,随机数原始长度为32字节,但在这里是用16进制表示的,每个字节用16进制表示就会成为2个字符,所以就变成了64个字符的字符串。我们在抓包文件里也能看到它,因为在密钥交换算法的设计中,ClientRandom就是要在网络上公开传输的。
Secret:这就是Master secret,也就是通过它可以生成对称密钥。Master secret固定是48字节,也是十六进制表示的关系,成为96个字节的字符串。你应该明白了,这个Master secret就是最为关键的信息了,也正是黑客苦苦寻求的东西。它是万万不能在网络上传输的,自然也不可能在抓包文件里看到它,只有TLS库才能导出它。
补充:TLS1.3的格式会很不一样,具体细节你可以参考这里的链接。
我们来看一个TLS1.2的KEYLOGFILE的具体的例子:
CLIENT_RANDOM 770c2c73ef1ab58dda9360a94587e5f8b0a80c0b1abf628ddd7b55a118ec18ec bea2c01c5b6f9c577e8ba251c8f262adf33c5aa31a238d464a9c56dbd1bf30cf55cbf14e6175102fa1db9b8a0183a721
补充:这个日志文件也已经上传至Gitee,你可以按照前面介绍的3个步骤,结合上面的抓包文件和这个日志文件,自己来观察解密前后的区别。
在输出这个key信息的时候,我们也做了对应的抓包,现在看一下抓包文件。我们选中Client Hello报文,点开TLS详情部分,继续点开TLSv1.2 Record Layer -> Handshake Protocol -> Random,你看看它是不是就是前面日志文件里,第二列的值(开头是770c)?
SSLKEYLOGFILE日志文件格式我们了解了,接下来了解Wireshark是怎么跟它协同工作,解开密文的。
在TLS1.2的SSLKEYLOGFILE中,每条记录的第一列是CLIENT_RANDOM这个字符串,第二列是这个client random的值,Wireshark就是通过它,找到对应的TLS会话(你可以理解为是TCP流)。就像上图所示,通过这个随机数,就找到了这条KEYLOG记录对应的TLS会话了。
那么接下来,Wireshark就知道真正的Master secret在哪里了:它就是前面匹配了这个客户端随机数的记录的第三列,也就是那96个字节的字符串。
由于在抓包文件里就有ECDHE密钥交换算法所需要的各种参数,结合这里的Master secret,Wireshark就可以解析出对称密钥,从而把密文解密了!
现在回答一下前面的问题:如果这个文件“被坏人利用了”,怎么办?
通过前面的学习,我们知道了:要想破解密文,既要有抓包文件,也要有SSLKEYLOGFILE日志文件,两者结合才能解密。而且不要忘了,即使你有抓包文件和日志文件,要是没抓到TLS握手阶段的报文,也还是不能解密,因为缺少了客户端随机数、加密算法参数等信息,Master secret对你也是无法下嘴的美食。
其实,上面的客户端做解密的过程,网络上已经有很多资料了,但是接下来要介绍的“服务端做TLS解密”这个话题,却鲜有人讨论。要知道,TLS是双方的加密任务,但是我们一边倒地关心客户端如何解密,却对服务端的解密不闻不问,这又是什么道理呢?
我觉得可能有这么几个原因:
其实,在软件架构上,服务端和客户端也是类似的,也是基于TLS库来构建TLS加解密的能力的。
就我们eBay的情况来说,在我们的软件负载均衡方案中,第七层的部分是基于Envoy实现的。我们在把一套新系统搬上生产环境之前有一系列的考量,就像体检的检查表一样逐一校验。我们发现Envoy的体检就有一项“不合格”:Envoy不提供对TLS流量进行抓包解密的功能。
补充:Envoy是硅谷的共享出行公司Lyft于2016年发起的开源项目,可以认为是云原生时代的Nginx。
在不少情况下,这个抓包解密特性对排查很有用。像一些商业产品比如Netscaler就是可以一边抓包,一边导出TLS key。就跟我们在客户端做解密类似,我们结合这两个文件,就可以在Wireshark中既观察到TCP行为,也读到应用层信息了。
你可能会问:“Envoy可以输出Web日志呀,把各种HTTP性能指标、访问头部,甚至包括用了什么Cipher,都输出到文件,这样还不香吗?”
其实,我在开篇词和第4讲中都提到过网络排查的两大鸿沟。
而包括Envoy在内的反向代理和LB软件,虽然也都提供了应用层日志,但跟实际的网络行为还有距离,这就是“应用现象跟网络现象之间的鸿沟”:日志是日志,网络是网络。如果日志里说某个HTTP请求耗时很长,你是无法知道网络上到底什么问题导致了这么长的耗时,是丢包引起了重传?还是没有丢包,纯粹是传输速度慢呢?
为了跨越第一个鸿沟,我们选择做tcpdump抓包。但是,如果抓取到的TLS密文无法被解密,就无法知道这些究竟是应用层的什么信息,这个鸿沟依然没有被跨越。
当然,我们可以选择“妥协”,采用一些灵活的策略来开展抓包分析。比如,可以把抓包分析的重心转移到TCP和TLS本身的层面,而不再关心其承载的应用层信息。但是“隔靴搔痒”总让排查工作不是特别“痛快”,我们犹豫许久之后,还是决定把这个解密的特性实现!
这并不是一件容易的事情。不过通过调研,我们发现,其实在服务端启用跟客户端类似的TLS解密功能,技术上是可行的,其中最为关键的信息就在2017年一位Wireshark开发工程师的演讲中:
Applications using OpenSSL 1.1.1 or BoringSSL d28f59c27bac (2015-11-19) can be configured to dump keys: void SSLCTXsetkeylogcallback(SSL CTX ∗ctx, void (∗cb)(const SSL ∗ssl, const char ∗line));
也就是说,只要我们使用这个BoringSSL(是谷歌Fork自OpenSSL的项目)的 SSLCTXsetkeylogcallback() 回调函数,就可以把TLS信息导出来,于是我们信心大增。核心突破就是要把这个BoringSSL的回调函数给用起来。
具体来说,我们需要做这样的几件事:
第二点,其实就是一种接口方式,比如SSLKEYLOGFILE环境变量就是一种,我们也可以选择API接口,或者某种别的接口。总之,只要让程序(这里是Envoy)知道:它需要去叫BoringSSL这个小弟去办点事情,整个功能就可以运作起来了。你可以参考这张示意图:
“体检通过”!我们在服务端Envoy上也可以做到方便的TLS解密了。其实,不仅实现了这个具体的需求本身,也实现了我们作为技术工作者,对“自我实现”的需求。
这个特性的主要开发者张博已经提交了PR,相信不久之后我们就能在正式版的Envoy里用上这个特性了。
实现Envoy的TLS抓包解密的具体的做法跟客户端解密的步骤差不多:
然后到这里,这里我们还需要搞清楚几个问题。
一个字的答案:行。只要设置好前面提及的3件事:
这时候你访问HTTPS站点时,在Wireshark里看到的就直接是解密好的信息了!因为Wireshark已经能从SSLKEYLOGFILE里读取到密钥信息,同时又在实时地抓取到TLS密文,这种解密工作是可以实时进行的。
当然,这里还有一个小的注意点,我们在第二个问题里展开。
有同学就遇到这个问题:重启浏览器后,在Wireshark里马上就能看到HTTP数据包,确实能解密。但是停止抓包之后,再启动抓包,看到的又变成了TLS密文了。必须得重启浏览器才行。这是为何呢?
表面上看,这似乎又是一个“重启大法”的问题,但本质上呢?
我们知道,密文是用对称密钥加密的。而对称密钥的生成,是在TLS握手阶段完成的。我们前面提到过,Wireshark(也包括其他需要读取SSLKEYLOGFILE的程序)正是根据第二列的客户端随机数,来找到抓包文件中的TLS session,然后运用第三列的Master secret来获取到对称密钥的。
抓包停止后,新的HTTPS请求所触发的TLS握手就不会被抓取到。这也就意味着,Wireshark没有抓取到客户端随机数这个关键信息,尽管SSLKEYLOGFILE里依然在输出着一行行的key信息,但是Wireshark已经不知道用哪个Master secret了。自然,解密就无从做起。
而在浏览器重启后,事实上造成了TLS的重新握手,此时就又可以抓取到客户端随机数了,这样,解密工作就可以恢复。你看,这其实跟第3讲中,没抓到TCP握手报文就无法知道Window Scale参数这个问题差不多,也是关于握手的,只不过这次是TLS握手。“技术是相通的”,这句话真不是随便说说。
这节课,我们通过对一张真实的TLS证书的解读,复习了各个加密算法在现实场景中的实现。你也需要重点掌握以下知识点:
之后就是这节课的核心了:如何做到对抓包文件进行解密。这里又分客户端和服务端两个不同场景,你也需要重点关注。
首先,在客户端做抓包解密,需要做三件事:
这样,我们就能在Wireshark里直接读取到应用层信息了。
而在服务端抓包解密,就要依托于软件实现了,但是有些软件并没有提供这种功能,比如Envoy。借助底层BoringSSL库的接口,eBay流量管理团队实现了对这个接口的调用,我们也可以在Envoy上完成抓包解密了。
另外,你还要知道Wireshark能解读出密文的原理:
最后,给你留两道思考题:
欢迎你把答案分享到留言区,我们一起交流、进步。
评论