技術めいた何か

社会人になってしまった

Challenge CVE-2020-13777に応募しました!

はじめに

編集履歴:

  • 2020/07/03 18:23: 誤字脱字の修正
  • 2020/07/03 13:25: 初版公開

本記事及び、企画「Challenge CVE-2020-13777」へ提供した解説文章・PoCは情報通信産業に関わる人の情報セキュリティのリテラシーの向上に貢献することを目的としています。 修正パッチが広く配布されている脆弱性であるCVE-2020-13777について、修正パッチ配布後に開発元によって公開された脆弱性の情報を元に技術的な検証を行った結果を啓蒙活動の一環として公開しています。 それにより、脆弱性について情報通信産業に関わる人に広く認知されることを期待しています。

なお、作成し公開したPoCは技術的な検証を目的に作成されたもので、実在のサーバーに対してCVE-2020-13777を用いた攻撃する能力は無く、また攻撃を意図して作成したものではありません。 このPoCは主催者が配布・解析を許可したパケット情報をまとめたファイル(pcapファイル)を解析するように設計しており、外部のサーバーとの通信は一切行わずオフラインで動作します。

こんにちは、@prprhytです。 先日開催されたChallenge CVE-2020-13777に応募しました。
Challenge CVE-2020-13777は、Ohtsuさん(@jovi0608)が主催する 先日情報が公開されたGnuTLSの脆弱性であるCVE-2020-13777についてTLS1.3に関係する部分の解説とPoCの公募をする企画です。

企画の詳細や結果の詳細についてはOhtsuさんのブログを参照してください。

告知・募集:
https://jovi0608.hatenablog.com/entry/2020/06/13/104905

Ohtsuさんのフォローブログ(結果と提出した解答へのフォロー):

jovi0608.hatenablog.com

解答:

https://gist.github.com/prprhyt/548ba3148f3b1bbfa5c20edde60d6b75

PoCのソースコード:

github.com

選考の結果提出課題を高く評価していただきまして、嬉しいことに主催者であるOhtsuさんからの正解賞(Amazonギフト1万円)と協賛のVさん(@voluntas)からVさん賞(Amazonギフト3万円とお好きなラムダノート本1冊)を頂きました。改めてありがとうございます。 特に、Vさんからは告知にあった額面より大幅にブーストしていただいた上、好きなラムダノート本として「プロフェッショナル SSL/TLS」をいただきました! 改めて、企画していただいたOhtsuさんと協賛のVさんにこの場を借りてお礼を申し上げます。 (あと、自分で言うのは少し恥ずかしいですが気合を入れて解説を書いたおかげかお2人からお褒めの言葉をいただきました。)

応募にあたって課題の進め方

注意: 以降の文章はOhtsuさんのフォローブログを読んでいる前提で書いています。 よって、セッション再開時の認証の説明など技術的な解説の多くは省略してあります。予めご了承ください。

CVE-2020-13777のTLS1.3に関係する部分の解説とPoCについての詳細はOhtsuさんのフォローブログを見ていただくとして、ここでは問題2を例に実際に手をどう動かして課題を進めたかについて共有しようと思います。 ただし、僕は課題への対処の仕方は十人十色だと思っていて課題の進め方に正解があるとは思っていません。 なのでこういうやり方もあるのだな、くらいに思ってもらえると幸いです。

公募の問題は次の通りです。

以下のレポジトリにある pcap データを使って次の問題1,2に回答してください。
pcap データにはGnuTLS-3.6.13のサーバ(192.168.100.23:5556)に対するTLS1.3の接続データが2つ含まれています。1回目は新規TLS1.3接続、続く2回目は 0-RTT のTLS1.3再接続です。

https://github.com/shigeki/challenge_CVE-2020-13777 1. pcap中のTLS1.3 ClientHelloデータだけ使って、CVE-2020-13777によってTLS1.3のMITMが可能であることを証明してください。
2. pcap中の暗号化されたTLS1.3 の 0-RTTアプリケーションデータをCVE-2020-13777によって復号し、アプリケーションデータの平文を取得してください。

引用: https://jovi0608.hatenablog.com/entry/2020/06/13/104905

さて、本題ですが僕は課題2を次の手順で進めました。

  1. CVE-2020-13777のIssueに上がっている動作を再現できる環境の構築
  2. 配布されたpcapを眺める
  3. ドキュメントとプログラムの調査とメモの作成
  4. PoCの実装
  5. PoCの実装で行き詰まった箇所があれば2や3に戻る

また、調査やPoCの実装に使用したツールは次の通りです。

  • Wireshark: 配布されたpcapを眺めるのに使用
  • ブラウザ・Google: RFCを読んだり参考になりそうな情報の検索をするのに利用
  • テキストエディタ: メモ用
  • Python3.7: PoCの実装に使用

なお、課題開始時点でのTLS1.3に関する知識は次のとおりでした。

  • おおまかな特徴やTLS1.3の暗号理論における安全性についての記事を読んだことがある。
    また、TLS1.3への攻撃の一部について概要を知っている。
  • プロトコル的な観点についてはなんとなくTLS1.2までとTLS1.3の違いを知っているが詳細は知らない
    • フルハンドシェイクが2-RTT->1-RTTになった。条件を満たせば0-RTTハンドシェイクが使えるとか。上記の記事にある概要は知っているくらい。
    • 0-RTT時の認証についてはあまり詳しくない。
    • 完全に忘れていたのですが、TLS1.3のPSKやセッション再開について2年前にセキュリティ・キャンプ全国大会2018 TLS1.3/暗号ゼミの応募課題のために少し調べていたことがわかりました。ただし、このときは理解できていなかった(この記事を書いている最中に当時の応募用紙を見直したらかなり間違いがあったので)

では、次の節から手順ごとにやったことを説明します。

ちなみに余談ですが、問題1については次のように進めました。

  1. PSK(Pre-shared key)をセッション再開に用いるという情報をもとにMITMのシーケンス図の下書きをする
    (参照: RFC 8446 Section 2.2: https://tools.ietf.org/html/rfc8446#section-2.2)
  2. 問題2を解いてPSKの導出やkey schedulingついて理解をする
  3. 下書きをと問題2を解く過程で得た情報をもとに提出用の解説を書く

1. CVE-2020-13777のIssueに上がっている動作を再現できる環境の構築

GnuTLSのGitLab上のCVE-2020-13777に関するIssueを見ながら報告されている動作を再現できる環境を構築します。 該当のIssue: https://gitlab.com/gnutls/gnutls/-/issues/1011
以下の環境を構築しました。

インストールするものは次の通りです。

  • GnuTLS 3.6.13
  • OpenSSL 1.1.1g (21 Apr 2020)

GnuTLSのバイナリを入手する方法はソースコードからのビルドや各パッケージマネージャーから入手するなどがあります。 各OSやディストリビューションによって手順が異なるので手順は割愛します。

2. 配布されたpcapをながめる

TLS1.3の仕様であるRFC 8446の2.2節によるとセッション再開に用いる情報であるPre-Shared Keyはセッション再開時のCHLOに含まれるようです。つまり、セッション再開時のCHLOのどこかにPSKに関する情報が含まれているはずです。
参照: RFC 8446 Section 2.2: https://tools.ietf.org/html/rfc8446#section-2.2

その情報を頭に入れた状態で、Wiresharkで配布されたpcapファイルを眺めました。 1つめのClient Hello(CHLO)をみてもセッション再開に関係しそうな情報は特になさそうです。 2つめのCHLOを見ます。すると、画像のようにPSK Identity->Identityの先頭の16byteが0になっているのに気づきます。 怪しいですよね。

cve-2020-13777-resumption
図:配布されたpcapファイルの再開時のCHLOの中身

ここで正常なセッション再開のCHLOパケットを見てみます。 正常なセッション再開のCHLOパケットは修正済みのGnuTLSサーバー(ver 3.6.14以降)でGnuTLSの該当IssueStep to Reproduceの手順を参考に同じサーバーに対してセッション再開をすることで入手できます。 なお、逆に3.6.13以下で手順通りに実行するとCVE-2020-13777の認証バイパスが再現します。

nomal-resumption
図:正常な再開時のCHLOの中身

正常なCHLOを見ると先頭16byteが0になっているわけではありません。 では、手元でCVE-2020-13777の認証バイパスを再現した場合のセッション再開時のCHLOはどうでしょうか。次に画像を示します。

cve-2020-13777-resumption
図:手元でCVE-2020-13777を再現したセッション再開時のCHLOの中身

確認すると、配布されたpcapのセッション再開時のCHLOパケットと同じように先頭16byteが0になっていることがわかります。

3. ドキュメントとプログラムの調査とメモの作成

ここで、おもむろにGnuTLSの暗号化されたセションチケットの書式を確認します。 20200613055805
図:GnuTLSのチケット書式

図の引用元: https://jovi0608.hatenablog.com/entry/2020/06/13/104905

図の左側のテーブルの一番上の項目を見てください。 Key Name(16bytes)となっています。 これらの情報から次の仮説を立てます。

  1. 暗号化されたセッションチケットはセッション再開時のPSK Identityに含まれている。
  2. そして、セッションチケットの暗号化/復号鍵は構造体で定義されており、それがall-0で初期化されるため、Key name(16bytes)や暗号化のkey, MACのkeyも0になっている。

この仮説に基づいて調査をします。 セッションチケットを復号できたとすると、攻撃者が入手できるのは図:GnuTLSのチケット書式の右側のテーブルにあるGnuTLSのチケットデータの中身の書式にある情報です。

順番に項目を見てresumption_master_secretnonceがあるのが気になりました。名前からして秘密情報にあたりそうです。ここで、resumption_master_secretnonceが何に使われているのかRFC 8446から確認します。 次の式を見ます。

psk=HKDF-Expand-Label(resumption_master_secret,
                        "resumption", nonce, Hash.length)

参考:RFC8446 4.6.1 https://tools.ietf.org/html/rfc8446#page-75 を基に作成

Ohtsuさんフォローブログを読んだ方はもうおわかりだと思いますが、この式から、resumption_master_secretnonceはセッション再開時の認証や各暗号化鍵/復号鍵の鍵導出に使うPSKの導出に使用されていることがわかりました。

次にPSKからの鍵導出についてTLS1.3のKey Schedulingの図を確認します。

Screen Shot 2020-06-17 at 18 05 36
図:TLS1.3のKey Scheduling

図の引用元: RFC8446 7.1 https://tools.ietf.org/html/rfc8446#page-93

これで、PSKからどのようにPSKから各暗号化鍵/復号鍵を導出しているかわかりました。

また、RFC 8446 section 7.3より、この図の中で0-RTT Application dataの暗号化/復号鍵, ivの基となる情報に該当するのは client_early_traffic_secretであることがわかりました。

参照:RFC 8446 section 7.3 https://tools.ietf.org/html/rfc8446#section-7.3

次の節ではこの節まででわかった情報をもとに配布されたpcap中の0-RTT Application dataを復号するPoCの実装をしていきます。

4. PoCの実装

PoCの実装に入りました。 さて、3節で次のように仮定しました。

  1. 暗号化されたセッションチケットはセッション再開時のPSK Identityに含まれている。
  2. そして、セッションチケットの暗号化/復号鍵は構造体で定義されており、それがall-0で初期化されるため、Key name(16bytes)や暗号化のkey, MACのkeyも0になっている。

この仮定にもとづいて実装を進めました。(そして、結果的にこの仮定は正しかったです)
なお、実装時もわからない箇所が出たら繰り返し調査をする方針で進めました。

0-RTT Application dataの復号までの流れは次のとおりです。

  1. 暗号化されたセッションチケットの復号, MACの検証
  2. セッションチケットに含まれるresumption_master_secretとnonceからPSKを導出
  3. TLS 1.3のKey Schedulingに従ってclient_early_traffic_secureを導出
  4. client_early_traffic_secureから0-RTT Application dataの復号鍵とivを導出し、ivからnonceを導出
  5. 0-RTT Application dataを復号, MACの検証

詳細はOhtsuさんのフォローブログやPoCのソースコードを参照してください。 なお、ソースコード中には各処理についてコメントしてあります。

以下にPoCで各処理を実装する上で特筆すべき点や再度調査を行ったものについて書きました。 主に実装依存のもので、RFC 8446に定義されていないもの中心です。 なお、ソースコードの検索についてはgitlabの検索+ローカルにgit cloneした上で、git grepでやっていました。

1. 暗号化されたセッションチケットの復号, MACの検証 のMACの計算手順

セッションチケットが実装依存であり、確認した範囲ではRFC 8446に具体的な記載がなかったのでMACの算出方法についてはGnuTLSの実装を参照しました。 次の手順で各値を入力することで比較用のMACを計算できます。 GnuTLSの実装を見たところ、ちゃんとEncrypt-then-MACのようです。

  1. HMACでハッシュ関数SHA1(図:GnuTLSのチケット書式より)に指定する
  2. HMACの鍵を入力(all-0で16byte)
  3. セッションチケットのKey Nameを入力
  4. セッションチケットのIVを入力
  5. セッションチケットのdata length(暗号化部分の長さ)
  6. セッションチケットdata(暗号化された状態のデータ)を入力
  7. MACを計算する

参考: https://gitlab.com/gnutls/gnutls/-/blob/1d4615aa650dad1c01452d46396c0307304b0245/lib/ext/session_ticket.c#L155

余談ですが、GnuTLSでのセッションチケット復号時のMACの計算関数の呼び出し箇所を見ると ダイジェストを受け取る変数名がcmacとなっており、そのせいで最初見たときはMACの方式がCMACなのかと勘違いしかけました。ハッシュ関数の指定があるのでCMACだとおかしいと思いながらコードを読み進めてHMACだと確認しました。 (おそらくcalculated MACの略のつもりなのでしょうが紛らわしかったです...)

2以降で使用するHKDFのハッシュ関数の決定方法

HKDFで使用するハッシュ関数の指定方法について確認をしたところ、GnuTLSでは図:GnuTLSのチケット書式の右側GnuTLSのチケットデータの中身の書式の先頭2byteのprf_idが該当することがわかりました。

参照: https://gitlab.com/gnutls/gnutls/-/blob/d00638997fa269a975095d852633b48b2b64fbf9/lib/ext/pre_shared_key.c#L53

なお、上記のリンク先で呼び出している_tls13_expand_secret2RFC 8446でいうとHKDF-Expand-Label()にあたります。

参照: https://gitlab.com/gnutls/gnutls/-/blob/d00638997fa269a975095d852633b48b2b64fbf9/lib/secrets.c#L122

そして、配布されたpcapの再開時のCHLOではprf_idは7(10進数)です。 prf_id==7の場合の対応するハッシュ関数を調べたところ、定数GNUTLS_MAC_SHA384に7が割り当てられていることがわかりました。 そして、定数名からprf_id==7に対応するハッシュ関数はSHA384であることがわかりました。

なお、調査手順は次のとおりでした

  1. ciphersuiteが定義されていそうなソースファイル(lib/algorithms/ciphersuites.c)をみてみる
  2. ハッシュ関数に関するものもいくつか参照されていることがわかった
  3. 試しにgit grep GNUTLS_MAC_SHA256 する
  4. gnutls/devel/libdane-latest-x86_64.abi が検索にかかり、具体的な値を紐付けていそうなので確認する
  5. 7に該当するのがGNUTLS_MAC_SHA384だとわかる

参照:
lib/algorithms/ciphersuites.c: https://gitlab.com/gnutls/gnutls/-/blob/e48290a51da19288986bd7aaca265ea62b054dc8/lib/algorithms/ciphersuites.c#L407
gnutls/devel/libdane-latest-x86_64.abi: https://gitlab.com/gnutls/gnutls/-/blob/e48290a51da19288986bd7aaca265ea62b054dc8/devel/libdane-latest-x86_64.abi#L304

4. TLS 1.3のKey Schedulingに従ってclient_early_traffic_secureを導出

当初、うまくclient_early_traffic_secureが算出できなかったのですが、 以下のような誤りをしていたのが原因でした。

client_early_traffic_secure = Derive-Secret(Early Secret, "c e traffic", ClientHello)

とすべきところを以下のようにしていました。

client_early_traffic_secure = Derive-Secret(Early Secret, "c e traffic", "")

Key Schedulingの図を見直すことで解決しました。

5. 0-RTT Application dataを復号, MACの検証

AEADのAssociated(additional_data)の計算方法
0-RTT Application dataの暗号化には1つ前のセッションのCipher Suiteが選択されます。 そして、1つ前のCHLOを見ると先頭にCipher SuiteのTLS_AES_256_GCM_SHA384(0x1302)が選択されていました。 そのことから、AES_256_GCMで暗号化されていて、MACの計算にはSHA384が使用されていると仮定しました。

そして、AEADの暗号化/復号に必要なAssociated(additional data)の算出方法は次の通りです。

additional_data = TLSCiphertext.opaque_type ||
                        TLSCiphertext.legacy_record_version ||
                        TLSCiphertext.length

参照: https://tools.ietf.org/html/rfc8446#page-81

よって、AEADのassociate=(Early dataが入っているTLSレコードレイヤーのヘッダ+暗号文の長さ) であることがわかりました。

PoCの実装・調査の記録については以上です。 余談ですが、実行結果は次の通りです(ASCII, HEX)。

python3 main.py
b"Let's study TLS with Professional SSL/TLS!\n\n\x17"
4c6574277320737475647920544c5320776974682050726f66657373696f6e616c2053534c2f544c53210a0a17

実はお好きなラムダノート本にProfessional SSL/TLSを選んだ理由の1つはこれです。 (もう一つの理由は読んだことがなかったからです。あと評判が良いので。)

最後に

TLS1.3の再接続性まわり、特に0-RTT周りは課題に取り組む前は雰囲気でしかわかっていなかったのですが、この機会に楽しく理解を深めることができました。 改めまして、企画を主催していただいたOhtsuさん(@jovi0608)、協賛のVさん(@voluntas)にお礼を申し上げます。 今後ですが、OhtsuさんからプロフェッショナルSSL/TLSの書評をお願いされたので、時間を見つけて読み進めようと思います。(今年は修論もあるので纏まった時間をとるのが難しいかもですが...) また、Vさんからは頂いた賞金で経済を回すように言われたので経済を回していくぞ〜という気持ちです。 やっていくぞ💪