分类 默认分类 下的文章

Windows 上解密 chrome 的 https 流量

https 其实是使用传输层的 SSL/TLS 对内容进行加密. SSL/TLS 首先使用非对称加密传输客户端产生的 pre-master secret key. 然后之后使使用对称加密传输内存, 对称加密的的算法使用之前双方都有的 pre-master secret key 作为秘钥. 如果知道了 pre-master secret key, 那么就能解密 https 的流量了. 下面是 windows 上使用 chrome 截取 pre-master secret key 并使用 wireshark 解密的例子.

  1. 设置环境变量 SSLKEYLOGFILE 的值是本地一个文件
    win_env.png
  2. 重新启动 chrome;
  3. 随便访问一个 https 网站, 查看上面设置的文件里是不是已经有 key 产生;
  4. 启动 wireshark, 设置 TLS 的 key 文件.

    菜单: Edit -> Preferences -> Protocols -> TLS -> (pre-)master secret key log file

    wins_key.png

  5. 使用 wireshark 抓包, 或者 wincap 抓包. 抓包后最好使用 wireshark filter 一下. 找到 http1.1 或者 http2 的内容, 就能看到原文了
    wins_wire.png

MAC 上解密 chrome 的 https 流量

Chrome 或者 Firefox 都支持: 如果设置了环境变量 SSLKEYLOGFILE, 就把 SSL/TLS 的 pre-master secret key 写到设置的文件里面去. 之后可以使用这个 pre-master secret key 文件在 wireshark 里面解密加密的流量.

  1. 设置 SSLKEYLOGFILE 环境变量
    可以简单在命令行使用 export 命令 (记得之后打开 Chrome 要在这个命令行)

    export SSLKEYLOGFILE=~/ssh_key.log
  2. 在同一个命令行窗口打开 chrome

    open /Applications/Google\ Chrome.app/
  3. 在 chrome 随便访问一个 https 的网站, 检查 ~/ssh_key.log 是不是有内容
  4. 打开 wireshark 拦截流量或者 使用 tcpdump 有针对性的拦截

    sudo tcpdump host 103.144.218.5 -w mydump.pcap 
  5. 打开 wireshark, 分析这个加密的流量.
    显示设置SSL/TLS 的 pre-master secret key log 文件:

菜单: preferences -> Protocols -> TLS (Wireshark 3.0 之前是 SSL):

Preferences.png

  1. 关于 key log 文件的格式:https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format

之后:

  1. 首先找到带有 data 的行
  2. 可以看 http 或 http 2 的数据
  3. 点击下面(3)的 Decrypted data, 其实里面看上去是乱码, 因为直接解密后还是压缩的, 所以是乱码;
  4. 点解下面(4)的 uncompressed entity body 可以看到加压后的明文
    xiaohui_pcap.png

Java ForkJoinPool 的 worker thread 是如何自我终止的?

最近研究一个 Java 应用程序的性能问题, 当观察该应用程序的线程的时候, 发现有很多 ForkJoinPool 的 worker 线程, 这种线程数量很大, 并从线程的编号来看, 该应用创建了非常多的这样线程, 然后它们又逐渐被销毁.

下面是一个 Thread dump 的截图, 从截图中可以看到, 这种线程基本处于 2 种状态: TERMINATED 和 TIMED_WAITING. 理论来讲, 应该还有 Runnable 状态的, 只是没有出现在我们的 thread dump 中.
bad_method.png

通过 Btrace 代码注入, 我们很容易找到了错误使用线程池的代码:
forkJoin.png

这段代码在一个 Stream 的代码块中, 使用 Executors.newWorkStealingPool() 创建了一个 ForkJoinPool. 意味着每次调用这个方法, 都要创建一个新的 ForkJoinPool. 然而这正是线程池本来设计要解决的问题: 不要重复创建线程, 要复用.

所以, 解决办法就是替换这个线程池的创建和使用方式, 使用应用生命周期内复用线程的方法.

虽然上面的问题解决了, 那么还有个疑惑的地方: 我们看到上面线程池的编号是一个非常大的数字, 说明它已经被创建了这么多线程池了, 可是我们现在活着的总的线程数才几千, 说明早期创建的线程池都已经消亡了. 从那些 worker 的状态: TERMINATED 来看, 这些线程正在死亡, 最终线程池也会消亡.

一般情况下, 想让一个线程池终止, 我们都是通过调用线程池的 shutdown() 方法或者 shutdownNow() 方法. 可是我们观察上面线程池创建的地方, 并没有任何对象引用这个线程池, 那么是哪里调用这个 shutdown() 或者 shutdownNow() 方法的呢? 看上去这是一个矛盾的问题: 没用人调用 shutdown() 或者 shutdownNow(), 但是这些智能的 worker 线程却在慢慢消亡.

为了查清 worker 线程智能消亡的问题, 我们写了如下的 Btrace 代码来查找 worker 线程消亡的原因:

import static org.openjdk.btrace.core.BTraceUtils.jstackStr;
import static org.openjdk.btrace.core.BTraceUtils.println;
import org.openjdk.btrace.core.annotations.BTrace;
import org.openjdk.btrace.core.annotations.OnMethod;

@BTrace 
public class ForkJoinPoolTrace {

    /**
     * 用来查找那里创建了 ForkJoinPool
    @OnMethod( clazz="/java\\.util\\.concurrent\\.ForkJoinPool/", method="/<init>/" )
    public static void createPool() {
        println(jstackStr());
    }**/
    
    @OnMethod( clazz="/java\\.util\\.concurrent\\.ForkJoinPool/", method="/shutdown/" )
    public static void shutdownPool() {
        println(jstackStr());
    }
    
    @OnMethod( clazz="/java\\.util\\.concurrent\\.ForkJoinPool/", method="/tryTerminate/" )
    public static void tryTerminate() {
        println(jstackStr());
    }
    
    @OnMethod( clazz="/java\\.util\\.concurrent\\.ForkJoinPool/", method="/shutdownNow/" )
    public static void shutdownPoolNow() {
        println(jstackStr());
    } 
}

最终找到了让 worker 线程消亡的原因: 是在 awaitWorker() 方法中使用了 tryTerminate() 方法.

java.util.concurrent.ForkJoinPool.tryTerminate(ForkJoinPool.java)
java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1807)
java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1693)
java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175)

其实终止的原因就是: 如果 worker 线程发现很久没有活可以干, 那么就 自我终止吧.
ForkJoinPool.awaitWork 方法:
whyT.png

可能有人会问, 线程消亡了, 那么创建的那些 ForkJoinPool 呢? 这些对象要靠 Thread 活着, 如果那些 Worker Thread 都终止了, 那么这些对象必然会被回收掉.