TLSのパケットキャプチャ方法
先日プロフェッショナルSSL/TLS を読み、 社内勉強会でTLSについて話した。 その際、ブラウザと各Webサイトの間で実際どのようにTLSのハンドシェイクがなされているかを説明するために、 ブラウザのTLS通信をパケットキャプチャしたのだが、うまくキャプチャするのに多少工夫したので、 そのことをこの記事では説明する。
(普段からLinuxデスクトップを利用しているので、Linuxを前提とする。)
背景
パケットキャプチャそのものはブラウザを実行するホスト上で tcpdump
などを使えば容易に実行できるが、
ブラウザからのTLS通信をキャプチャする場合、次の問題がある。
- 暗号化されているため、中身が確認できない
- 特にTLS 1.3では、1.2以前は平文だったようなハンドシェイク途中のメッセージも暗号化されているものがある
- TLSのハンドシェイクそのものだけでなく、上位プロトコル(HTTPなど)の内容も確認したい場合は、復号が必須
- 関心のあるTLSのセッションだけを選択してキャプチャすることが難しい
- 送信先IPアドレスは特定困難
- WebサイトのIPアドレスはDNSラウンドロビン等で複数から選んで使っている可能性があって、変動する
- サイト内の一部のコンテンツがサブドメインにあったりCDNを利用していて、送信先アドレスがまちまち
- キャプチャに使うホスト上で、関係ないプログラムも同じポートやプロトコルを使うため、判別が困難
- TCP 443宛やTLSプロトコルを使っているという条件では、キャプチャしたいブラウザ以外からの通信が混ざる
- 送信先IPアドレスは特定困難
方法
本題となるキャプチャ方法だが、ここでは以下の3つについて説明する。
- SSLKEYLOGFILEを活用し、パケット復号する方法
- netnsを活用し、特定プロセスのみを対象にしてパケットキャプチャする方法
- NFLOGを活用し、特定プロセスのみを対象にしてパケットキャプチャする方法
2番目と3番目に関しては、どちらかを選んで使えばよい。 (先日の社内勉強会のためにキャプチャしたときは、1+3の方法で実施した。)
SSLKEYLOGFILEを活用し、パケット復号する方法
SSLKEYLOGFILE
で調べると説明しているところがたくさんあるので、細かい手順は省くが、要約すれば
- ブラウザを
SSLKEYLOGFILE
環境変数にファイルパスを指定した状態で起動する - すると
SSLKEYLOGFILE
で指定されたパスのファイルにTLSセッションごとの鍵に関するパラメタが記録される - このファイルをパケットキャプチャと合わせてWiresharkに読み込ませると、TLSセッションを復号してくれる
- 設定箇所: 編集(Edit) -> 設定(Preferences) -> Protocols -> TLS -> (Pre)-Master-Secret log filename
ということになる。TLSクライアント機能を有する任意のソフトウェアで同じ手法が使えるわけではないが、FirefoxやGoogle Chromeなどの主要ブラウザでは使える方法であるらしい。(もちろん、ブラウザのバージョンやビルドオプション等にも依存するとは思うが)
軽く調べた感じだと、以下のような事情になっているらしい。
- SSLのライブラリであるNSSには、デバッグ用にTLSセッションを複合するのに必要な鍵情報をSSL key log fileとして吐き出す機能がある
SSLKEYLOGFILE
環境変数が設定されているとき、それが示すファイルパスに定められたフォーマットでpremaster secret等を出力する- よってNSSをバックエンドにするソフトウェアではこの方法が使えるはず
- TLSに関するソフトウェアのいくつかが、このSSL key log fileをサポートしている
- キャプチャしたパケット解析に広く使われるWiresharkでは、このSSL key log fileを使ってパケットの復号をする機能をサポートしている
- ちなみに、NSSを使うソフトウェアだけでなく、OpenSSLなども同じフォーマットのSSL key logを受け取るコールバック関数がある
- NSSのように
SSLKEYLOGFILE
環境変数をライブラリ側で読み取ってファイルに吐き出すところまではやってくれなさそう
- NSSのように
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:10
の 10
は --nflog-group 10
に対応する。)
あとは userpcap
ユーザーとしてプロセスを起動すれば良いので
$ sudo -u userpcap SSLKEYLOGFILE=sklf firefox --private-window
のようにすれば良い。(この場合も前述のxhostコマンドによるXのアクセスコントロールの設定が必要かもしれない。)
この方法はownerマッチの都合からユーザーを分ける必要があるが、比較的準備が楽である。 また、ownerマッチ以外のマッチも利用できるので、 「特定のプロセスからの通信をキャプチャしたい」という今回のようなケース以外でも、 うまく活用する方法があるかもしれない。