CCS Injection脆弱性(CVE-2014-0224)発見の経緯についての紹介
菊池です。CCS Injection脆弱性(CVE-2014-0224)発見の経緯について紹介します。
バグの簡単な解説
OpenSSLがハンドシェーク中に不適切な状態でChangeCipherSpecを受理してしまうのが今回のバグです。 このバグはOpenSSLの最初のリリースから存在していました。
通常のハンドシェークでは、右の図のような順序でメッセージを交換します(RFC5246 The Transport Layer Security (TLS) Protocol Version 1.2 §7.3より作成)。
ChangeCipherSpecは必ずこの位置で行うことになっています。OpenSSLもChangeCipherSpecをこのタイミングで送信しますが、受信は他のタイミングでも行うようになっていました。これを悪用することで、攻撃者が通信を解読・改ざん可能です。
発見の困難さ
このバグが16年の間発見されなかった原因は、TLS/SSLを実装したことのある経験者が十分にレビューできてなかったことにあります。 もし、TLS/SSLを実装したことがあれば、自分の実装で検査している項目をOpenSSLでも同様に検査しているか確認すればすぐに見つかるはずです。
他にもファジング等の手法も有効のように見えますが、以下の今までの歴史を見るにやはりTLS/SSLの実装の知識が必要だったことが分かります。
CVE-2004-0079
最初の発見のチャンスは2004年にCodenomiconが発見したCVE-2004-0079です。このときに以下の検査が追加されました。
Fix null-pointer assignment in do_change_cipher_spec() revealed
これにより、new_cipherが設定されているときだけChangeCipherSpecを受理するようなりました。しかし、残念なことに、new_cipherはServerHelloの段階で設定されるので、正しい実装になりませんでした。
CVE-2009-1386
次の発見のチャンスは2009年に発見されたCVE-2009-1386です。
DTLS: SegFault if ChangeCipherSpec is received before ClientHello
これはDTLSで、いきなりChangeCipherSpecを受け取った場合に落ちる問題が直りましたが、やはり正しい実装になりませんでした。
DTLS fragment retransmission bug
OpenSSLのバグ番号1950でもChangeCipherSpecの挙動が修正されました。
DTLS fragment retransmission bug
これによりDTLSにおいて、運悪くパケットの順番が入れ替わってChangeCipherSpecを受け取った場合の検査が追加され、DTLSでは正しい実装になりました。 チケットにあるように、未計算のマスターシークレットが暗号に使われ、これがランダムなメモリを読み出して、ハンドシェークが失敗すると分析されています。 このとき使われるランダムだと思われていたメモリは、実は空のバイト列でした。これに気付いていれば、攻撃の可能性が分かったはずです。
正しい実装の容易さ・困難さ
ChangeCipherSpecを正しく実装するのは実は容易なことです。上に挙げたフローの順番通りのメッセージだけを送信し受信すればよいだけです。ただし、少しだけ落とし穴があって、ChangeCipherSpecは他のハンドシェークのメッセージとは異なるレコードを使います。RFCにはその理由が以下のように書いてあります。
Note: To help avoid pipeline stalls, ChangeCipherSpec is an independent SSL Protocol content type, and is not actually an SSL handshake message.draft-ietf-tls-ssl-version3-00 §5.5より引用
個人的にはこの一文が今回の脆弱性の最大の原因ではないかと思っているのですが、これによると、ChangeCipherSpecが独立したレコードになっているのはパイプラインストールを防ぐためだそうです。しかし、実装してみれば分かりますが、ChangeCipherSpecの処理はTLS/SSLのハンドシェークの中で最も複雑な同期が必要な場所です。まず、ChangeCipherSpecを受け取る前に、ハンドシェークが正しい段階まで進んでいることを待つ必要があります。さらにハンドシェークはFinishedを受け取る前にChangeCipherSpecを受け取っていることを確認しなければなりません。
より正確に書くと、ChangeCipherSpecを受理するときには
- ハンドシェークが正しい段階まで進んでいること(Finishedを受信する直前の状態)
- ハンドシェークのフラグメントが全く残っていないこと
- 直後のメッセージがFinishedであること
を確認しなければいけません。さらに細かく注意すると、Alert attack にあるように
- アラートのフラグメントが全く残っていないこと(そもそもアラートのフラグメントは弾いてもよい)
- 同様にHeartbeatのフラグメントも残っていないこと
も確認すべきです。
RFCは次のように修正すべきです。
- ChangeCipherSpecが別のレコードになっているのは、他のハンドシェークのフラグメントと混ざったレコードで送られないようにするため。
- パイプラインストールを防ぐために、ChangeCipherSpecはサーバとクライアントの双方が送信する。
バグを発見するまでの過程
Heartbleedが公開されたときに、どのようにして次のバグを防ぐかが話題になっていました。ユニットテストを使うとか、各種アナライザーツールで検査するとか、フォークして綺麗に書き直してみるとか、APIも悪いとか、mallocを自作してはいけないとか、C言語はよくないから他の言語を使うべきとかいろいろありました。
他の言語の候補としてはATSが人気のようでしたが、自分の使い慣れているCoqでTLS/SSLを実装するとどうなるか考えていました。 プロトコルの安全性の証明などは大変ですし、実装の安全性にはたいして寄与しないので、見ただけで正しい実装をしているとすぐに確認できる程度のものを作ることを考えていました。
パーザとプリンタが正しく対になってることと、プリンタが自明に正しく実装されていることと、状態機械の動作が述語で表現できていることぐらいを目標にしていました。状態機械の一番複雑な部分がChangeCipherSpecにおける遷移なのは明らかだったので、そこだけ最初に考えてみて上に書いた条件を決定しました(一番複雑といっても、全く難しくはないです)。
次に、既存の実装が正しくこの条件をチェックしているかの確認を始めました。OpenSSL以外の実装はそれなりに検査をしていましたが、OpenSSLの実装はほとんど検査をしていないように見えました。その後、実際に攻略可能であることを確認しました。