WiresharkでSSL通信の中身を覗いてみる

OpenSSLの脆弱性「Heartbleed」が世間を賑わせていますが、色々と乗り遅れてしまった感があるので、ゆるゆると落ち穂拾いをしようかと思います。

Heartbleedで秘密鍵を手に入れたらSSL通信の中身全部見えちゃうじゃん!! という事態になっていますが、なんとなく理論的にそうだろうなと分かるもののイマイチ具体的な手順が分からない。
というわけで今回のテーマとして、手元にサーバの秘密鍵と、SSL通信をパケットキャプチャしたpcapファイルがあるときに、Wiresharkでどんな感じでSSL通信を「ほどく」のか……という具体的な手順を、ハマり所を含めてまとめておこうかと思います。

というか、私自身がハマったので自分用メモですな。なおこの文書では"SSL"とだけ記述し、TLSは無視しています。

前提条件

とりあえず以下のような感じの検証環境で試しました。

  IPアドレス 説明
ホストOS 192.168.56.1 手元のWindows7。ブラウザでゲストOSへhttps接続する
ゲストOS 192.168.56.102 CentOSApache+mod_sslを立てて、httpsサーバになる
tcpdumpWireshark

まずtcpdumpの予備知識をいくつかまとめておきます。

tcpdumpコマンドでパケットキャプチャする際に、キャプチャ内容をファイル出力してWiresharkで読み込めるようにするには、-wオプションを利用してpcapファイルとして出力します。なおこの際、パケットを途中で切り捨てられないように、-s 0を合わせて指定するのがよく使われる手法です。

つまり、具体的なコマンドラインは次のようになります。

# tcpdump -i eth1 -s 0 -w <filename> port 443
オプション 意味
-i eth1 インタフェースeth1をキャプチャ
-s 0 パケットから取り出すバイト数。0指定で最大値(65535)を意味する
-w キャプチャをにpcap形式で出力
port 443 ポート443をキャプチャ

パケットキャプチャの手順

具体的には、以下のような手順でhttpsSSL通信のパケットキャプチャをして、pcapファイルを出力してみましょう。

  1. ゲストOSでApacheを起動
  2. ゲストOSでtcpdumpを実行し、SSL通信のキャプチャ開始
  3. ホストOSから、WebブラウザでゲストOSのhttpsページを閲覧
  4. ゲストOSでtcpdumpをCtrl+Cで終了
  5. ゲストOSからpcapファイルをscpなどで持ってきて、ホストOSのWiresharkで閲覧
ゲストOSのhttpsページ

ここでは、以下のようなCGIhttps://192.168.56.102/cgi-bin/test.cgi に置いています。

#!/bin/sh

echo "Content-type: text/plain"
echo
echo "This is a test message."
date

実行すると、以下のようにテストメッセージと現時刻が出るだけの簡単なものですね。

Wiresharkの設定と操作

まずはじめに、Wireshark秘密鍵を設定せずにSSL通信をキャプチャするとどうなるかを確認しておきましょう。先述の/cgi-bin/test.cgiにアクセスした通信のキャプチャが以下です。

見て分かる通り、暗号化されていて通信内容は何も分かりません。分かるのはTCPレベルでのSrcIPとDstIPだけです。HTTP通信丸ごとを暗号化しているので、レスポンスボディ等だけでなく、リクエストヘッダも分からないことに注意しましょう(つまりボディ部だけでなく、どのページにアクセスしたかすら、httpsでは分からない)。

秘密鍵の登録

Wiresharkでは、通信先ごとに秘密鍵を事前に登録しておくことで、SSL通信の中身を見ることができます。登録するには、メニューの[Edit]→[Preferences]から、Preferencesウィンドウの[Protocols]→[SSL]を選択します。

[RSA keys list]の[Edit]をクリックします。

[SSL Decrypt]のProfile設定画面となるので、[New]をクリックします。

この入力ボックスはちょっと分かりにくいのですが、以下のように入力します。

項目 入力値
IP address 対象のIPアドレス
Port 対象のポート番号。普通は443でしょう
Protocol ここはhttpsではなくhttpと記述します
Key File 秘密鍵ファイル。クリックするとファイル選択ダイアログが開きます
Password 鍵ファイルのパスフレーズ。パスを付けていないならば、空のままにしておく

こうして[OK]をクリックすれば、暗号化されていた通信の中身が見えるようになります。

上図では、先ほど見えなかったGETリクエストと、そのレスポンスボディが見えているのが分かります。

Follow SSL Stream

Wiresharkで便利な機能に[Follow TCP Stream]がありますが、秘密鍵の登録をすることで[Follow SSL Stream]で流れを追うことができるようになります。

対象のパケットを右クリックし、[Follow SSL Stream]を選びます。

このように、SSL通信の流れをそのまま見ることができるので大変便利です。

ありがちな失敗例

ハマり所をいくつか紹介します。

暗号化が解けずにそのままEncryptedで出ちゃう

秘密鍵Wiresharkに登録しているのに、通信内容が暗号化されたままとなることがあります。

上図では、SSLでApplication Dataが流れているところまでは分かりますが、肝心のデータが暗号化されたままになっています。

Encrypted Application Data: e127673d4e5fb04ce09ebaed1702a6dd8e2eaeeed12852f4...

これは、SSL通信においてメッセージを実際に暗号化している共通鍵が取得できていないためです。その原因は、「SSLキャッシュが効いているから」ということになります。

ここで、まずSSL通信のシーケンスを押さえておきましょう。通常は下図のようになっています(この図を描くだけで疲れちゃった)。

押さえておくべきポイントは、ClientKeyExchangeです。SSLでは公開鍵暗号方式を利用しているというのは一般的によく知られていますが、HTTP通信すべての内容を公開鍵暗号方式で暗号化しているわけではありません。

公開鍵暗号法方式での暗号化は「重い」処理のため、じゃんじゃかトラヒックが流れるメッセージ全体を暗号化するのはとても大変です。ですからSSLではHTTP通信自体は(高速な)共通鍵暗号方式で暗号化し、その共通鍵のやりとりのみに、公開鍵暗号方式での暗号化を適用しています。そのため、この形式をハイブリット方式と呼ぶこともあります。

実際に共通鍵をやりとりしている様子は、Wiresharkでも確認できます。ClientKeyExchangeのパケットをクリックし、[Secure Sockets Layer]→[Client Key Exchange]→[RSA Encrypted PreMaster Secret]と追っていけば、Encrypted PreMasterを見ることができます。

これが実際に通信を暗号化している共通鍵の元です(Preと付いていることから分かるように、本当はこのPreMasterからさらに鍵を作っていくのですが、そこは省略します)。

さて、このようにSSLでは通信を開始する前に鍵交換をするわけですが、一つ一つのリクエスト全てに対して鍵交換をするのはコスト的によろしくありません。そのため、SSLキャッシュを導入し、いったん作った鍵はしばらく使い回すのが普通です。

ここまでの解説で、なぜさっき上手く復号できなかったのかが分かりましたでしょうか。

SSLでは、はじめにクライアントで生成した共通鍵(の元)を受け渡しするので、セッションの一番はじめからキャプチャしていないと、暗号化された通信を復号することができません。つまり、以下のような手順でキャプチャすると上手く行きません。

  1. httpsWebブラウザで繋いでみて、よし、OK
  2. ではtcpdumpでキャプチャ開始
  3. よし取れた、Wiresharkで中身を……あれ、出ない?

もう一度、先ほど復号できなかったパケットキャプチャを見てみます。

ここで通信のやりとりを見ると、Client HelloとServer Helloはありますが、Client Key Exchangeがありません。そのためこの通信だけをキャプチャしても、メッセージのやりとりを復号するための共通鍵が不明のため中身を見ることができないわけです。SSL通信を復号するには、キャッシュに乗ってしまった後ではダメで、「キャッシュに乗る前の一番最初から、通信全体を丸ごと」取得する必要があります。

なおSSLキャッシュは、Apacheならば、ssl.confに以下のように設定されています(もちろん環境により色々違うでしょう。これはCentOShttpdのデフォルト値です)。

SSLSessionCache         shmcb:/var/cache/mod_ssl/scache(512000)
SSLSessionCacheTimeout  300

学習・勉強用のサーバならば、SSLキャッシュは切ってしまった方が良いかもしれません。ApacheSSLキャッシュを無効にするには、SSLSessionCacheディレクティブに"none"を指定します。

SSLSessionCache  none

実験環境ならこの方が良いかもしれません。

Encrypt Alertっていうのが出てる! 何かのエラー?

前述の「キャッシュに乗ってしまった後」からパケットキャプチャした場合、誤解しやすいのがこの事例です。

上図のように、コネクション切断時に「Encrypted Alert」と表示されるため、何か問題があるのでは!? と考えてしまいます。WiresharkでこのEncrypted Alertが出るのは、セッションの途中やキャッシュが残っている状態でキャプチャを開始して、前述の共通鍵が入手できず通信の中身が分からないときです。

さて、SSLでは通信切断時はAlertプロコトルのClose Notifyで通知します。つまりタネあかしをすると、先ほどの謎のAlertは、単なるClose Notifyのパケットです。

セッションの一番はじめからキャプチャしていないと共通鍵が入手できないため、これがClose Notifyとまでは分からず、「Alertプロトコルの何か」であることしか分かりません。結果として、Wireshark上には「Encrypted Alert」と表示されます。

Alertと出るのでちょっとドキッとしますが、SSLでは切断時には「AlertプロトコルでClose Notifyを投げる」ので、Alertが出るのは正常です。ちょっとネーミングが良くない気がしますね。

(2014/04/18追記) 最初から通信全体丸ごとキャプチャしているのに、Diffie-Hellmanってのが出ててEncryptされたまなんですけど

話がややこしくなるのでここまで敢えて触れませんでしたが、実はSSLキャッシュが効いていないケースで通信最初から丸ごとキャプチャしていても、通信内容がEncryptされたままで見えないことがあります。

例えば以下の通信は、きちんとKey Exchangeからキャプチャしていますが、Application Dataの中身はEncryptedされたままでGETリクエストなどを見ることができません。

これは何故でしょうか?

鍵交換からキャプチャしていても通信内容が復号できないワケは、Client Key Exchangeすなわち鍵交換のアルゴリズムにあります。

上図は、先ほどの通信のClient Key Exchangeのパケットを見たものです。ここでは、Diffie-Hellman(DH)という形式で鍵交換をしていることが分かります。

SSLのように、公開鍵暗号方式で暗号化した通信路上で共通鍵の交換をおこなう通信では、「過去の通信内容を丸ごとキャプチャして保存している人が、あとで秘密鍵を手に入れた場合、過去の通信内容を簡単に復号できてしまう」という点に気を付ける必要があります。このような事態(というか、今回のHeartbleed問題で現実になりましたね)に備えるには、鍵の生成を、第三者が盗聴したときに計算困難な形で行う手法が用いられます。ディフィー・ヘルマン(DH; Diffie-Hellman)鍵交換はこの計算困難なアルゴリズムです。

DH鍵交換では、まず「素数p」と「pを法とした原始根g」を両者で共有します。この2つの値pとgは、第三者に知られても構いません。なお実用上は、gは2か5で決め打ちとし、そこから適当な素数を決めるのが普通です。

両者はそれぞれ勝手に乱数を生成し、その乱数でgのべき乗を計算してpを法とした剰余を取ります(n = g^x mod p)。これが実際に交換する値n,mです。値の交換後、鍵Kは、相手からもらった値に自分の乱数をべき乗することで計算できます(K = m^x mod p)。

こうすると、たとえ通信路上でmとnが盗まれても、第三者にはxとyが分からないので、Kを計算することは多項式時間では不可能なため計算困難です。そのためpが十分に大きい値ならば、安全と言えるでしょう。

さて、数式を離れて元に戻ります(DH鍵交換は中間者攻撃できるという大きな欠点があるのですが、今回は説明省略)。SSLでは、鍵交換するアルゴリズムは複数あり、それはWebサーバ側やWebブラウザ側で「提示し合って」決めます。今まで見てきた例はInternetExplorerを使いましたが、IEはどうもDHを嫌うようで、RSAが選択されます(たぶん。私のWin7 + IE11ではそうでした)。一方、FirefoxはDHを優先して選択するようです(たぶん。この辺ちょっと調査不足)。先ほどのDH鍵交換のパケットキャプチャも、Firefoxでブラウズしたものです。

つまり、このブログで例示した実験をする際には、暗号通信の中身を全部見たいでしょうからIEを使うと良いでしょう。あるいは、Webサーバ側でDHを使わないようにするのも一つの手です。/etc/httpd/conf.d/ssl.confで、SSLCipherSuiteディレクティブに以下のように「kRSA」のみを指定すると、DH鍵交換を使わないように強制できます(これはあくまで実験用の設定例ですので、外部公開するWebサーバに使うもんじゃありません。念のため)。

(ssl.confのSSLCipherSuiteで、kRSAのみを指定した)

SSLCipherSuite kRSA

こうすれば、私の環境ではFirefoxであってもDH鍵交換を使わなくなったため直接通信内容が見えるようになりました。

なお、ここで指定しているSSLCipherSuiteの実際の値は、opensslのciphersコマンドで-vオプションに文字列を与えることにより確認することができます。

$ openssl ciphers -v 'kRSA'
AES256-GCM-SHA384       TLSv1.2 Kx=RSA      Au=RSA  Enc=AESGCM(256) Mac=AEAD
AES256-SHA256           TLSv1.2 Kx=RSA      Au=RSA  Enc=AES(256)  Mac=SHA256
AES256-SHA              SSLv3 Kx=RSA      Au=RSA  Enc=AES(256)  Mac=SHA1
CAMELLIA256-SHA         SSLv3 Kx=RSA      Au=RSA  Enc=Camellia(256) Mac=SHA1
DES-CBC3-SHA            SSLv3 Kx=RSA      Au=RSA  Enc=3DES(168) Mac=SHA1
DES-CBC3-MD5            SSLv2 Kx=RSA      Au=RSA  Enc=3DES(168) Mac=MD5
AES128-GCM-SHA256       TLSv1.2 Kx=RSA      Au=RSA  Enc=AESGCM(128) Mac=AEAD
AES128-SHA256           TLSv1.2 Kx=RSA      Au=RSA  Enc=AES(128)  Mac=SHA256
AES128-SHA              SSLv3 Kx=RSA      Au=RSA  Enc=AES(128)  Mac=SHA1
SEED-SHA                SSLv3 Kx=RSA      Au=RSA  Enc=SEED(128) Mac=SHA1
....(省略)....

参考リンク