ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

OpenSSLの脆弱性(CVE-2017-3733)に見られる仕様とcastの落とし穴

0. 短いまとめ

  • OpenSSL-1.1.0dに脆弱性(CVE-2017-3733)が見つかり、Encrypt-Then-Mac と renegotiation を組み合わせて crashさせることができました。
  • この脆弱性は、仕様の準拠不足や不適切な変数の cast などが原因でした。
  • TLS1.3ではこういう落とし穴が少なくなるよう機能の根本的な見直しが行われています。

1. はじめに

先週 OpenSSL-1.1.0d に対してセキュリティアップデートがあり、 Encrypt-Then-Mac renegotiation crash (CVE-2017-3733)という脆弱性(Severity: High)が公開されました。 対象となった 1.1.0 は、昨年2016年8月にリリースされたOpenSSLの新しいリリースブランチです。1.1.0ではAPIの大幅変更もあり、まだあまり普及していないため影響を受けた方は比較的少なかったのではと予想します。 しかし今回の脆弱性、その原因をよくよく探ってみるとなかなか趣深いものがあります。

そこで Encrypto-then-Macとは何か、Renegotiationとはどういうものか、はたまた何故Highにまで影響するような脆弱性になっちゃったのか、その仕組みを書いてみたいと思います。

2. MtE(Mac-then-Encrypt) や EtM(Encrypt-then-MAC) と AEAD(Authenticated Encryption with Associated Data)

インターネット上でセキュアな通信を行うには、暗号化によってデータの盗聴を防ぐ機密性の確保を行うだけでは不十分です。暗号化の有無に関わらずデータの改ざんを検知し完全性を確保することも必要です。 従来、改ざんを検知するにはデータのMAC(Message Authentication Code)を計算し、その値をデータに付与してチェックを行ってきました。

暗号化とMACの計算、どっちを先にやるのか。その手順の安全性に関して古くから議論が行われてきました。代表的には、MACを先に行うMtE(Mac-then-Encrypt)と暗号化を先に行うEtM(Encrypt-then-Mac)の2つのやり方が挙げられます。TLSSSLの時代から 、ブロック暗号(DES/AES)とCBCモードを利用する際にMACを先に行うMtE方式を採用してきました。しかしこの方式を利用していると、復号化してからデータのチェックを行うためパディングオラクル攻撃の対象となり、これまでソフトウェアの実装不備を突いた攻撃手法がいくつも公表されてきました。中でも2013年の Lucky Thirteen 攻撃CBCモードの実装不備を突いた非常に有名な攻撃手法です。

最近になっても2015年に amazon の s2n に対するLucky Microsecondsや、2016年もOpenSSLのAES-NIの実装不備をついたLuckyNegative20などの脆弱性が公表されています。このようにMtEの安全性を確保するソフトウェアの実装を行うためには、高度なセキュリティや計算機科学の知識と実装能力が必要とされます。個人的には素人が手を出せる領域ではないなと感じています。

そんななか、TLS1.2からAEAD(Authenticated Encryption with Associated Data)という暗号化手法が採用されました。これは内部的にEtMを使いつつも、同時に認証用の高速なMACも合わせて計算するといった方式で、その安全性は利用する対称暗号やMAC 方式に依存するということが数学的に証明されています。しかもAEADは、暗号対象となるデータ以外のデータ(平文のヘッダデータなど)の認証も合わせて行うこともできます。何よりAEADの中でAES-GCM方式は、Intel AES-NIやARMv8のAES拡張機能などハードウェア処理機能が提供されていて、他の方式より格段に高速な処理が実現できるといったメリットがあります。

簡単にMtE, EtM, AES-GCM(AEAD)の方式の違いを表したのが以下の図です。 f:id:jovi0608:20170220113615j:plain 現在のTLSでは、まずAES-GCMのAEAD暗号方式使った通信の利用を中心に考えて良いことは間違いないことでしょう。

3. RFC7366: Encrypt-then-MAC for TLS and DTLS

そうは言っても、まだ広く使われているAES-CBCはこのままでいいのか、TLS1.0や1.1もなんとかしないとあかん、ということから、TLSの暗号通信を従来のMtEからEtMに変更できる仕様 RFC7366: Encrypt-then-MAC for TLS and DTLS が2014年に標準化されました。MtEとEtM共に混在することができないことから、EtM用のCipherSuiteを別に用意するということも考えられたのですが、CipherSuiteの数が多くなりすぎるため、ハンドシェイクのClientHello/ServerHelloの拡張を使ってEtM方式の利用を合意する方式が採用されました。やり方としてはクライアントがEtMをサポートしていることを伝えるEtM拡張をClientHelloに付与し、ServerがEtM方式が可能なCipherSuiteを選択したらEtM拡張をServerHelloに付けてクライアントに返せば完了です。もしサーバがAEADなどEtMを必要としていない暗号方式を使う場合は、ServerHelloにEtM拡張を付けずに返します。簡単に書くと下図のようなやりとりです。 f:id:jovi0608:20170220113626j:plain この方式なら比較的簡単にEtM対応が可能になるだろうという見込みを持って仕様化されましたが、やっぱり今回みたいに落とし穴がありました。仕様はホント注意深く読み込まないといけません。

4 Renegotiation

今回の脆弱性は、EtMとRenegotiationを組み合わせたものです。ここではTLSのRenegotiationについて簡単に書いてみます。

TLSは、最初ハンドシェイクを行った後に再度ハンドシェイク(Renegotiation)を行うことができます。2回目以降は既にハンドシェイクが完了しているので暗号化通信上でRenegotiationが行われます。 これが必要なのは、当初サーバ認証でTLSの通信を行っている後にクライアント認証が必要なリソースにアクセスすることが必要になった場合などです。サーバからの合図でRenegotiationを開始し、クライアント証明書のチェックを行うことによって、サー バ認証のTLS接続後もクライアント認証にシームレスに移行することが可能になります。 f:id:jovi0608:20170220113632j:plain 他の用途として、長時間TLSの通信を行っている時に対称暗号の鍵をアップデートをする際にもRenegotiationを使うことがあります。Renegotiation自体は一見何ら問題ないように見えますが、Renegotiation前後で同一のセキュリティが確保できているか、 処理コストが高いのでDoSっぽいことをやられる恐れはないかとか、これまでもRenegotiationを踏み台にした攻撃手法もいくつか公表されたこともあり、その利用価値は次第に小さくなってきています。

今回の脆弱性は、MtEの実装でRenegotiation時の挙動をちゃんと対処できなかったことが原因でした。やっぱりRenegotiation機能はTLSの状態を非常に複雑にし、いろんな落とし穴の一因になっていると言われても仕方ないでしょう。

5. CVE-2017-3733

5.1 CVE-2017-3733 脆弱性の再現

まずは今回の脆弱性を再現させてみましょう。OpenSSL-1.1.0では default でEtM拡張が有効になっています。今回の脆弱性修正パッチから探ると、最初のハンドシェイクでAEADを利用しRenegotiationでEtMを使った暗号に変更すると crash してしまうようです。OpenSSLの s_clientコマンドでは Renegotiation をサポートしていますが、その際暗号方式を変えることができないので少し改造してみます。

下記パッチを使うと s_client で接続後、標準入力で S を入れると AES128-SHAで Renegotiation を行うようになります。脆弱性のある 1.1.0dを使うとクライアントが先に crashしてしまうので修正された1.1.0eの s_client にパッチを当ててみます。

--- a/apps/s_client.c
+++ b/apps/s_client.c
@@ -2440,6 +2440,12 @@ int s_client_main(int argc, char **argv)
                 SSL_renegotiate(con);
                 cbuf_len = 0;
             }
+            if ((!c_ign_eof) && (cbuf[0] == 'S' && cmdletters)) {
+                BIO_printf(bio_err, "RENEGOTIATING for CVE-2017-3733\n");
+                SSL_set_cipher_list(con, "AES128-SHA");
+                SSL_renegotiate(con);
+                cbuf_len = 0;
+            }

先に OpenSSL-1.1.0dでTLSサーバを立ち上げておき、このクライントで接続します。AES128-GCM-SHA256(AEAD)で接続してからコマンドSを入力してAES128-SHAにRenegotiationしてみましょう。

~/openssl-1.1.0e$ ./apps/openssl s_client -connect localhost:8443 -cipher AES128-GCM-SHA256
CONNECTED(00000003)
(中略)
    Extended master secret: yes
---
S
RENEGOTIATING for CVE-2017-3733
(中略)
write:errno=104

なんかエラー出てます。サーバ側がどうなっているのか見てみます。

~/openssl-1.1.0d$ ./apps/openssl s_server -cert ~/tmp/certs/server.cert -key ~/tmp/certs/server.key -accept 8443
Using default temp DH parameters
ACCEPT
(中略)
CIPHER is AES128-GCM-SHA256
Secure Renegotiation IS supported
ssl/record/ssl3_record.c:352: OpenSSL internal error: assertion failed: mac_size <= EVP_MAX_MD_SIZE
Aborted (core dumped)

うわっ、エラー吐いてTLSサーバが abort しています。たった一つのTLSセッションでTLSサーバを落とすことができました。

5.2 CVE-2017-3733 の原因

なんでこんなことになってしまったのか、その原因を探ってみましょう。

OpenSSL-1.1.0dのEtM実装ではサーバは ClientHello のEtM拡張と選択するCipherSuiteを見てEtMを使うか判断し、EtM拡張付きのServerHelloを返すと共にEtM利用のFlag(TLS1_FLAGS_ENCRYPT_THEN_MAC)を立てます。

最初のハンドシェイクでは、 Change Cipher Spec(CCS)の送受信が行われるまで平文通信です。CCSによりEtMの暗号化開始はサーバ・クライアント共に同期が取れていて問題ありません。 ところが Renegotiation は暗号化通信上で行われるハンドシェイクです。暗号化通信上でこのClientHelo/ServerHelloの送受信タイミングでEtM利用のFlagが立ってしまったらどうなるでしょうか?

本来は CCS の送受信のタイミングで暗号方式が変わります、このタイミングでEtMの利用を開始するのは早すぎるのです。

先の脆弱性の再現例では最初のハンドシェイクは AES-GCM でした。サーバ側は EtMのフラグがOnになっているのでAES-GCMで暗号化されたデータをEtM方式で復号化しようとします。まずMACチェックを行いますが、AES-GCMはMACを使いません。本来ありえないAEADのEtMの復号処理、その時点でそのTLSセッションの処理は止まってしまいます。 f:id:jovi0608:20170220113639j:plain 普通1つのTLSセッションのエラーがサーバ全体に波及することはありません。そこにはもう一つ落とし穴がありました。

5.3 int -> unsigned int へ、castの悲劇

じゃこのエラー時、どんな処理がされるのでしょうか? 該当するコードは以下のところです。

    if (SSL_USE_ETM(s) && s->read_hash) {
        unsigned char *mac;
        mac_size = EVP_MD_CTX_size(s->read_hash);
        OPENSSL_assert(mac_size <= EVP_MAX_MD_SIZE);

SSL_USE_ETMが有効化されているのでmac_sizeを取得しに行きます。AES128-GCM-SHA256の場合はAEADなのでMACが定義されておらず mac_size に -1 が返ります。

現状のTLSではMACの最大はSHA512の64バイト、 -1 <= 64 だから assert 問題ないです。しかし、

    short version;
    unsigned mac_size;
    unsigned int num_recs = 0;

あー、mac_sizeは unsigned にキャストされています。 -1 は、4294967295(=232-1) です。AES-GCMのMACサイズはなんと4Gバイト超の巨大な値とみなされます。

OPENSSL_assert(4294967295 <= 64);

これで assert チェックにひっかかり、しかもOPENSSL_assert は abort() まで行きます。 TLSサーバは見事ここで crash です。 この脆弱性は、RedHatのエンジニアからの報告だったようですが、よく見つけたものです。

5.4 修正方法

根本的な問題は、ClientHello/ServerHelloの送受信時にEtM利用を開始したことでした。そこで修正はCCSの送受信時にREAD/WRITEの2つのEtM利用のフラグを使うようにしました。 https://github.com/openssl/openssl/commit/4ad93618d26a3ea23d36ad5498ff4f59eff3a4d2 f:id:jovi0608:20170220113646j:plain 実はこれ、RFC7366の仕様にちゃんと注意事項として書いてありました。

3.1.  Rehandshake Issues
   (中略)
   If an upgrade from MAC-then-encrypt to encrypt-then-MAC is negotiated
   as per the second line in the table above, then the change will take
   place in the first message that follows the Change Cipher Spec (CCS)
   message.

「再ハンドシェイク時のEtMの切り替えはCCS後に変更を行うこと」まさにこれです。もう言い訳ききません。

OpenSSL-1.1.0eでは、今回の破壊的な結果を引き起こした unsigned 変数のキャストやOPENSSL_assert()の処理も修正されました。 https://github.com/openssl/openssl/commit/60747ea22f8b25b2a7e54e7fe4ad47dfe8f93383

実は master の OpenSSL-1.1.1-dev では、 mac_size をちゃんと int で受けて範囲チェックを行い、 size_t にキャストするよう変更されていました。 そのためエラーは発生するものの crash まで行くことはありません。最新ブランチには地道なコードの見直しがちゃんとされているようです。

6. TLS1.3とOpenSSL-1.1.1

OpenSSL-1.1.0では default で使えるようになっているEtM拡張ですが、BoringSSLやNSSで実装する動きはまだありません。すなわちChromeFirefoxなどのブラウザーでのサポート見込みはありません。 TLS1.2でAES-GCMやChaCha20-Poly1305などAEADが使えるようになっているので、わざわざ対応する必要はないということでしょう。

次期TLS1.3では根本的な機能の見直しが行われており、今回の要因となったTLSの機能を廃止・変更しています。

  • Renegotiationを廃止して Post-handshakeを新設。
  • Change Cipher Spec を廃止して、鍵交換後は即暗号化開始。
  • CBCモードの利用廃止、CipherSuiteはAEADのみ利用可に。

よってTLS1.3ではEtM自体が意味のない機能になっています。OpenSSL-1.1.1ではTLS1.3が実装されており、近く正式リリースされるのではないかと期待されています。OpenSSLの開発者が所属する akamai では、4月にTLS1.3を rollout するようです。 TLS1.3の仕様化完了とOpenSSL-1.1.1のリリースが待ち遠しいです。