在与其他机构调试接口的时候会遇到请求是通过HTTPS发送的,但是想通过抓包看实际发出去的是什么东西。但是HTTPS本身通过Wireshark或tcpdump等工具抓出来的包都是经过TLS加密的,我们如果才能正常将本次调试的时候发出的包进行解密呢? 本文的主要工作是在Java语言下,结合Wireshark或tcpdump工具完成HTTPS流量的解密。也有针对浏览器下的操作步骤,这个后续再讲。

HTTPS

HTTPS (全称:Hypertext Transfer Protocol Secure^ [5]^),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性^ [1]^。HTTPS 在HTTP 的基础下加入SSL,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。 HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP 之间)。这个系统提供了身份验证与加密通讯方法。它被广泛用于万维网上安全敏感的通讯,例如交易支付等方面^ [2]^

不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,带来了三大风险。

(1) 窃听风险(eavesdropping):第三方可以获知通信内容。

(2) 篡改风险(tampering):第三方可以修改通信内容。

(3) 冒充风险(pretending):第三方可以冒充他人身份参与通信。

SSL/TLS协议是为了解决这三大风险而设计的,希望达到:

(1) 所有信息都是加密传播,第三方无法窃听。

(2) 具有校验机制,一旦被篡改,通信双方会立刻发现。

(3) 配备身份证书,防止身份被冒充。

互联网是开放环境,通信双方都是未知身份,这为协议的设计带来了很大的难度。而且,协议还必须能够经受所有匪夷所思的攻击,这使得SSL/TLS协议变得异常复杂。

以上关于HTTPS的解释和SSL/TLS的作用来自百度和阮一峰的博客文章SSL/TLS协议运行机制的概述

SSL/TLS交互过程

HTTPS 在TCP连接三次握手后会进入秘钥交换的步骤,具体步骤流程可以阅读SSL/TLS协议运行机制的概述。 在此处简单描述一下,HTTPS的交互流程大概是:

  1. 客户端生成随机数1,并向服务端发送一个ClientHello请求,包含自己客户端支持加密方式
  2. 服务端接收到ClientHello后,也生成随机数2,确认好本次要使用的加密方式以及证书信息返回给客户端,这个过程称之为ServerHello
  3. 客户端接收到服务端的响应后,正常情况下会验证服务端响应的证书是否为可信机构颁发,域名是否与证书中的一致,如果不一致就会提示用户连接不安全,是否要继续访问。如果验证正常或者用户选择继续访问,客户端将会生成第三个随机数(pre-master key),取出证书中的公钥对随机数3进行加密(防止被窃听,只有客户端和服务端自己知道这个数是什么),并携带加密后的信息响应服务端;
  4. 服务端收到客户端的回应后,取出随机数3解密后,计算出秘钥,往后客户端和服务端的加密都通过该秘钥进行。

因为在第三步中客户端是使用了服务端的证书公钥进行加密的,能解密的只有服务端,客户端的是存在本地的,所以在网络通讯上杜绝了被窃听的可能。现在客户端和服务端都安全的获取到了随机数1,随机数2,随机数3,使用相同的计算逻辑,就能得到本次会话的“对话秘钥”或称为“主密钥”(master key)。这也是HTTPS安全的原因。

破题关键

从上面的HTTPS交互流程来分析,我们要是能获取到这个随机数3,然后计算出主密钥理论上就能够解密HTTPS的流量。从服务端来说一般是不好处理的,正常情况下服务器假设在别的机构,我们是无法访问的,所以我们只能从客户端下手。

在Java程序下,我们只要能通过技术手段拦截指定方法就能够拿到生成的随机数得到最终的主密钥。经过前期的一些调研, 我在github下找到了相关的工具jar包可以解决这个问题。extract-tls-secrets,再此感谢neykov的开源分享。

通过研读该项目的源码我大概摸清楚了技术细节。

  1. 通过javaagent的方式attach到在运行的线程上或者启动的时候添加javaagent参数,这样就无需对现有的程序做任何改动,只要有权限能访问到进程即可。
  2. 通过字节码技术javassist拦截指定的方法,主要是sun.security.ssl.SSLSessionImplcom.sun.net.ssl.internal.ssl.Handshakersun.security.ssl.SSLTrafficKeyDerivation
  3. 在相关方法调用前得到秘钥参数,在TLS握手协商完成后解出秘钥输出到文件中。

具体的使用方法有两种,我在此简单记录下(github仓库里面也有)

第一种: 运行jar包时指定javaagent

java -javaagent:~/Downloads/extract-tls-secrets-4.0.0.jar=/tmp/secrets.log -jar MyApp.jar

第二种:attach到一个正在运行的java进程上

-- 先获取进程列表
java -jar ~/Downloads/extract-tls-secrets-4.0.0.jar list
-- attach指定进程
java -jar ~/Downloads/extract-tls-secrets-4.0.0.jar <pid> /tmp/secrets.log

当正常attach到指定进程后,之后发送http都会将会话的秘钥信息保存下来,这个秘钥信息后续可以导入到wireshark中实现对捕获的流量进行解密。需要注意的是保存的秘钥信息是你在网络抓包信息期间attach到目标java进程产生的才行,不然对不上号是没有用的。

联合Wireshark进行包解密和分析

我们拿到捕获的包以后打开,里面的HTTPS的流量协议上都是显示TLS表示这是一个加密的数据。我们可以按照以下顺序导入秘钥信息:

  1. 点击编辑(Edit),选择首选项(Perferences)
  2. 选择打开菜单左侧的协议(Protocols),然后找到TLS协议。如果找不到TLS可能是你的Wireshark的版本与我的不一致,你可以继续找找SSL协议。找到后选中
  3. 选择后在右侧会显示相关配置,你应该能够看到一行 (Pre)-Master-Secret log filename 点击右边的Browser选择我们之前保存下来的秘钥文件,如果按照上面的步骤进行的话应该是"secrets.log"。找到该文件然后导入
  4. 点击确认后,回到wireshark就应该能够看到解密后的内容了。

image.png

参考资料