OpenSSL、今度はChange Cipher Specメッセージ処理に脆弱性

2014年4月に致命的ともいうべき HeartBleed 脆弱性 (CVE-2014-0160) が発見され大騒ぎを引き起こした OpenSSL。それからたった 2 ヶ月しか経過していないにもかかわらず、新たな脆弱性 (CVE-2014-0224) が発見されました。発見したのは株式会社レピダムの菊池正史氏とのこと、素晴らしい!

どのような脆弱性

SSL通信では、本通信の前に証明書を検証したり、共有鍵を生成して交換したりします。これをSSLハンドシェークと呼び、下図の順でメッセージを交換します。

クライアント                  サーバ
==================            ==================
ClientHello         ------->
                    <-------  ServerHello
                    <-------  Certificate*
                    <-------  ServerKeyExchange*
                    <-------  CertificateRequest*
                    <-------  ServerHelloDone
Certificate*        ------->
ClientKeyExchange   ------->
CertificateVerify*  ------->
[ChangeCipherSpec]  ------->
Finished            ------->
                    <-------  [ChangeCipherSpec]
                    <-------  Finished
Application Data    <------>  Application Data

今回の脆弱性が発見されたのは、ChangeCipherSpec の箇所です。ChangeCipherSpec は、以降の暗号化通信で使用する暗号化方式を決定するためのメッセージです。ChangeCipherSpec は、必ず上記の位置で送受信される必要があるにもかかわらず、上記の位置以外でも受信できるようになっていたことが問題とのことです。ClientKeyExchange のセキュリティ・パラメータを受け取っていない状態で攻撃者から ChangeCipherSpec を送られると、暗号化鍵は空のパラメータから生成され、誰もが容易に推測できる状態になってしまうとのことです。

どのように修正されたか

Gitリポジトリ上でソースコードを確認してみました。ソースコードの取得は下記のコマンドで行えます。*1

$ git clone git://github.com/openssl/openssl.git
$ cd openssl
$ git diff a7c682fb6f692c9a3868777a7ff305784714c131 a91be10833e61bcdc9002de28489405101c52650
diff --git a/ssl/s3_clnt.c b/ssl/s3_clnt.c
index 5fc9069..34efff8 100644
--- a/ssl/s3_clnt.c
+++ b/ssl/s3_clnt.c
@@ -599,6 +599,7 @@ int ssl3_connect(SSL *s)
 		case SSL3_ST_CR_FINISHED_A:
 		case SSL3_ST_CR_FINISHED_B:
 
+			s->s3->flags |= SSL3_FLAGS_CCS_OK;
 			ret=ssl3_get_finished(s,SSL3_ST_CR_FINISHED_A,
 				SSL3_ST_CR_FINISHED_B);
 			if (ret <= 0) goto end;
@@ -1051,6 +1052,7 @@ int ssl3_get_server_hello(SSL *s)
 		SSLerr(SSL_F_SSL3_GET_SERVER_HELLO,SSL_R_ATTEMPT_TO_REUSE_SESSION_IN_DIFFERENT_CONTEXT);
 		goto f_err;
 		}
+	    s->s3->flags |= SSL3_FLAGS_CCS_OK;
 	    s->hit=1;
 	    }
 	else	/* a miss or crap from the other end */
diff --git a/ssl/s3_pkt.c b/ssl/s3_pkt.c
index 34eb2b4..fb9720f 100644
--- a/ssl/s3_pkt.c
+++ b/ssl/s3_pkt.c
@@ -1593,6 +1593,15 @@ start:
 			goto f_err;
 			}
 
+		if (!(s->s3->flags & SSL3_FLAGS_CCS_OK))
+			{
+			al=SSL_AD_UNEXPECTED_MESSAGE;
+			SSLerr(SSL_F_SSL3_READ_BYTES,SSL_R_CCS_RECEIVED_EARLY);
+			goto f_err;
+			}
+
+		s->s3->flags &= ~SSL3_FLAGS_CCS_OK;
+
 		rr->length=0;
 
 		if (s->msg_callback)
diff --git a/ssl/s3_srvr.c b/ssl/s3_srvr.c
index 72fd3e4..31bfe47 100644
--- a/ssl/s3_srvr.c
+++ b/ssl/s3_srvr.c
@@ -708,6 +708,7 @@ int ssl3_accept(SSL *s)
 		case SSL3_ST_SR_CERT_VRFY_A:
 		case SSL3_ST_SR_CERT_VRFY_B:
 
+			s->s3->flags |= SSL3_FLAGS_CCS_OK;
 			/* we should decide if we expected this one */
 			ret=ssl3_get_cert_verify(s);
 			if (ret <= 0) goto end;
@@ -735,6 +736,7 @@ int ssl3_accept(SSL *s)
 
 		case SSL3_ST_SR_FINISHED_A:
 		case SSL3_ST_SR_FINISHED_B:
+			s->s3->flags |= SSL3_FLAGS_CCS_OK;
 			ret=ssl3_get_finished(s,SSL3_ST_SR_FINISHED_A,
 				SSL3_ST_SR_FINISHED_B);
 			if (ret <= 0) goto end;
@@ -805,7 +807,10 @@ int ssl3_accept(SSL *s)
 				s->s3->tmp.next_state=SSL3_ST_SR_FINISHED_A;
 #else
 				if (s->s3->next_proto_neg_seen)
+					{
+					s->s3->flags |= SSL3_FLAGS_CCS_OK;
 					s->s3->tmp.next_state=SSL3_ST_SR_NEXT_PROTO_A;
+					}
 				else
 					s->s3->tmp.next_state=SSL3_ST_SR_FINISHED_A;
 #endif
diff --git a/ssl/ssl3.h b/ssl/ssl3.h
index 8bd201e..82dd76c 100644
--- a/ssl/ssl3.h
+++ b/ssl/ssl3.h
@@ -428,6 +428,7 @@ typedef struct ssl3_buffer_st
 #define TLS1_FLAGS_TLS_PADDING_BUG		0x0008
 #define TLS1_FLAGS_SKIP_CERT_VERIFY		0x0010
 #define TLS1_FLAGS_KEEP_HANDSHAKE		0x0020
+#define SSL3_FLAGS_CCS_OK			0x0080
  
 /* SSL3_FLAGS_SGC_RESTART_DONE is set when we
  * restart a handshake because of MS SGC and so prevents us

ChangeCipherSpecを受信してもよいタイミングであるかどうかを判定するためのフラグSSL3_FLAGS_CCS_OKを追加し、フラグがONになっていないときにChangeCipherSpecを受信するとエラーを返すように変更されたようです。

*1:gitプロトコルの代わりにhttpsプロトコルを使用することも可能です。