TLSのパケットキャプチャ方法


先日プロフェッショナルSSL/TLS を読み、 社内勉強会でTLSについて話した。 その際、ブラウザと各Webサイトの間で実際どのようにTLSのハンドシェイクがなされているかを説明するために、 ブラウザのTLS通信をパケットキャプチャしたのだが、うまくキャプチャするのに多少工夫したので、 そのことをこの記事では説明する。

(普段からLinuxデスクトップを利用しているので、Linuxを前提とする。)

背景

パケットキャプチャそのものはブラウザを実行するホスト上で tcpdump などを使えば容易に実行できるが、 ブラウザからのTLS通信をキャプチャする場合、次の問題がある。

方法

本題となるキャプチャ方法だが、ここでは以下の3つについて説明する。

  1. SSLKEYLOGFILEを活用し、パケット復号する方法
  2. netnsを活用し、特定プロセスのみを対象にしてパケットキャプチャする方法
  3. NFLOGを活用し、特定プロセスのみを対象にしてパケットキャプチャする方法

2番目と3番目に関しては、どちらかを選んで使えばよい。 (先日の社内勉強会のためにキャプチャしたときは、1+3の方法で実施した。)

SSLKEYLOGFILEを活用し、パケット復号する方法

SSLKEYLOGFILE で調べると説明しているところがたくさんあるので、細かい手順は省くが、要約すれば

ということになる。TLSクライアント機能を有する任意のソフトウェアで同じ手法が使えるわけではないが、FirefoxやGoogle Chromeなどの主要ブラウザでは使える方法であるらしい。(もちろん、ブラウザのバージョンやビルドオプション等にも依存するとは思うが)

軽く調べた感じだと、以下のような事情になっているらしい。

netnsを活用し、特定プロセスのみを対象にしてパケットキャプチャする方法

netns ことnetwork namespaceというLinuxの機能を使うことで、 Linuxカーネルのネットワークスタックは仮想的に分離できる。

異なるnetns間で、ルートテーブルやiptablesのようなネットワークの設定は別々に持つようになり、 プロセスはいずれかのnetnsの中で動作させられるので、 特定のプロセスを他とは異なるネットワーク設定のもとで動作させることができる。

まず、キャプチャ用に新しいnetns netns-pcap を作成する。

$ sudo ip netns add netns-pcap

ip netns exec を使い netns-pcap でプロセスを起動できるが、 異なるnetnsでは利用できるネットワークインターフェイスも異なり、 以下のように netns-pcap ではホスト外のネットワークに出ていくことができない。

$ sudo ip netns exec netns-pcap ping 1.1.1.1
ping: connect: ネットワークに届きません

$ sudo ip netns exec netns-pcap ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/sit 0.0.0.0 brd 0.0.0.0

これを解消するためには、外部ネットワークへの疎通性のあるインターフェイスを netns-pcap に移動する必要がある。 ただし、インターフェイスを特定のnetnsに移動すれば、そのnetns以外からは利用できなくなるため、 すでに利用しているものを移動するわけにはいかず、普段使っていないインターフェイスが必要となる。

都合よくそのようなインターフェイスがホストにあればよいが、 そうでない場合はLinuxのネットワークブリッジと仮想インターフェイスを作成して使うことになるだろう。

$ sudo ip link add dev veth-pcap0 type veth peer veth-pcap1

これで veth-pcap0, veth-pcap1 という2つのtype vethの仮想インターフェイスが作成される。 type vethのインターフェイスは、2つ1組のペアで存在し、 片方のインターフェイスから送信されたパケットが他方で受信されるようになっており、 以下のようにそれぞれ異なるnetnsに配置してnetns間での通信に使うことが多い。

$ sudo ip link set dev veth-pcap1 netns netns-pcap

$ sudo ip netns exec netns-pcap ip addr add 169.254.100.1/31 dev veth-pcap1

$ sudo ip netns exec netns-pcap ip link set up dev veth-pcap1

$ sudo ip netns exec netns-pcap ip route add default via 169.254.100.0

$ sudo ip addr add 169.254.100.0/31 dev veth-pcap0

$ sudo ip link set up dev veth-pcap0

ここまでで、仮想インターフェイスにアドレスを割り当て、 netns-pcap からの通信はveth-pcap1経由でveth-pcap0に送られるようになる。 (実際、veth-pcap0でパケットキャプチャしながら先程のようにnetns-pcap内でpingなど実行すればわかる。)

このままだと外部に対して 169.254.100.1 を送信元アドレスとした通信を送ることになってしまい、 応答を受け取れないので、以下のようにして応答を受け取る必要がある。

$ sudo iptables -t nat -A POSTROUTING -s 169.254.100.1 -j MASQUERADE

これで通信はできるようになる。 (できない場合は、インターフェイスのforwardの設定や、iptablesでDROPされていたりする可能性が高い。)

$ sudo ip netns exec netns-pcap ping 1.1.1.1 -c 3
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 バイト応答 送信元 1.1.1.1: icmp_seq=1 ttl=51 時間=5.54ミリ秒
64 バイト応答 送信元 1.1.1.1: icmp_seq=2 ttl=51 時間=5.35ミリ秒
64 バイト応答 送信元 1.1.1.1: icmp_seq=3 ttl=51 時間=8.63ミリ秒

--- 1.1.1.1 ping 統計 ---
送信パケット数 3, 受信パケット数 3, パケット損失 0%, 時間 2003ミリ秒
rtt 最小/平均/最大/mdev = 5.352/6.506/8.630/1.503ミリ秒

あとはブラウザを netns-pcap 内で立ち上げて、 veth-pcap0 に対してパケットキャプチャを実施すれば良い。 立ち上げたプロセスが子プロセスを作る場合も通常は同じnetns内に作成されるので、 それらも含めてキャプチャされる。

$ sudo ip netns exec netns-pcap sudo -u $USER SSLKEYLOGFILE=sklf google-chrome-stable --incognito
$ sudo tcpdump -i veth-pcap0 -w chrome.pcap

ただし、すでに同じユーザーがそのブラウザのプロセスを立ち上げている場合は、 新規にプロセスが生成されないこともあるようで、その場合ブラウザは netns-pcap では動作しないことになってしまう。

よってブラウザをすべて閉じた上で行うか、pcap用に別ユーザーを用意して行うのが良いと思われる。 (GUI環境で別ユーザーとしてブラウザを起動する場合、X Window System的にエラーになることも考えられる。 この場合は xhost +si:localuser:${別ユーザー名} のようなことをする必要があるかもしれない。)

この方法は若干設定が煩雑に感じるが、NFLOGによる方法と違いユーザーを分ける必要があるわけではない。

NFLOGを活用し、特定プロセスのみを対象にしてパケットキャプチャする方法

iptablesにはNFLOGというターゲットがあり、このターゲットを指定したルールにマッチしたパケットは、 netlinkソケット経由でユーザー空間プロセスからその情報を取得できるようになる。 (パケットはNFLOGターゲットを指定したルールにマッチしてもドロップされたりするわけではなく、そのまま後続のルールが評価される。)

tcpdumpはこのnflogをサポートしているため、 キャプチャ対象としたいパケットのみマッチするルールでNFLOGターゲットが指定されたものを、 iptablesで追加しておけば、そのパケットのみキャプチャすることが可能となる。 したがってtcpdumpで利用できるlibpcapのフィルタでは不可能な絞り込みが可能になる。

iptablesにはownerというマッチがあり、 これを使えば特定ユーザーのプロセスからのパケットのみにマッチさせることができる。 ただしこれはOUTPUTチェインでしか機能せず、プロセスが送信するパケットは取れても受信するものが取れないので、 CONNMARKターゲットおよびマッチと併用する。

ここ にある例と同じだが、

$ sudo iptables -D OUTPUT -m owner --uid-owner userpcap -j MARK --set-mark 1

$ sudo iptables -A INPUT -m connmark --mark 1 -j NFLOG --nflog-group 10

$ sudo iptables -A OUTPUT -m connmark --mark 1 -j NFLOG --nflog-group 10

のようにすることで、userpcap ユーザーのプロセスが送信したパケットと、それに対する応答がマッチする。 tcpdumpにnetlinkソケット経由でそれらをキャプチャさせるために、次のような指定をする。

sudo tcpdump -i nflog:10 -w chrome.pcap

nflog:1010--nflog-group 10 に対応する。)

あとは userpcap ユーザーとしてプロセスを起動すれば良いので

$ sudo -u userpcap SSLKEYLOGFILE=sklf firefox --private-window

のようにすれば良い。(この場合も前述のxhostコマンドによるXのアクセスコントロールの設定が必要かもしれない。)

この方法はownerマッチの都合からユーザーを分ける必要があるが、比較的準備が楽である。 また、ownerマッチ以外のマッチも利用できるので、 「特定のプロセスからの通信をキャプチャしたい」という今回のようなケース以外でも、 うまく活用する方法があるかもしれない。