Windows CryptoAPI における重大なスプーフィング(なりすまし)の脆弱性の悪用
編集・協力:Tricia Howard
エグゼクティブサマリー
Akamai Security Intelligence Group は先日、National Security Agency(国家安全保障局、NSA)と National Cyber Security Center(国家サイバー・セキュリティ・センター、NCSC)が Microsoft に開示した Windows CryptoAPI の重大な脆弱性を分析しました。
この脆弱性、いわゆる CVE-2022-34689の CVSS スコアは 7.5 で、2022 年 8 月のパッチで修正されていますが、一般に公開されたのは 2022 年 10 月の Patch Tuesday でした。
Microsoft によると、この脆弱性を悪用することで、攻撃者は正規のエントリーへのなりすましが可能になります。
バグの根本原因は、MD5 ベースの証明書キャッシュ・インデックス・キーでは衝突は発生しないという前提です。2009 年以降、MD5 の耐衝突性は 形骸化しています。
攻撃フローは二段構えです。第 1 段階では、正規の証明書をリクエストし、その証明書を改変して、その改変版を被害者に供給します。第 2 段階では、改変された正規の証明書と MD5 が衝突する新しい証明書を作成し、その新しい証明書を使用して、元の証明書の対象者のアイデンティティになりすまします。
私たちは、出回っているアプリケーションの中で、このなりすまし攻撃の標的となる方法で CryptoAPI を使用しているものがないか検索しました。これまでに、Chrome の旧バージョン(v48 以前)と Chromium ベースのアプリケーションで悪用の可能性があることがわかりました。私たちは、これ以外にも多くの標的が存在すると考えており、調査を継続しています。
パッチが適用されているのはデータセンターの目に見えるデバイスの 1% 未満であり、残りのデバイスはこの脆弱性に対して無防備な状態です。
このブログ記事では、想定される攻撃フローの詳細な説明とその結果を解説するとともに、完全な攻撃の流れを再現する 概念実証(POC) を試みます。また、脆弱性のある CryptoAPI ライブラリのバージョンを検知する OSQuery も提供します。
背景
3 か月前、 2022 年 10 月の Patch Tuesday 分析で、私たちは、Windows CryptoAPI における重大なスプーフィング(なりすまし)の脆弱性(CVE-2022-34689)の基礎的な解説を共有しました。Microsoft によると、この脆弱性を悪用することで、攻撃者は「ターゲット証明書のアイデンティティになりすまし、認証やコード署名などのアクションを実行」できるようになります。
CryptoAPI は、Windows の実質的な API として、暗号化に関するあらゆる処理に使用されています。特に、証明書の読み取りと解析、さらに検証済みの認証局(CA)との照合まで、証明書の処理に多用されます。ブラウザーも TLS 証明書の検証に CryptoAPI を使用しており、その検証の結果がブラウザーにロックアイコンとして表示されています。
しかし、証明書の検証はブラウザーだけでなく、他の TLS クライアントでも発生します。たとえば、PowerShell Web 認証、curl、wget、FTP マネージャー、EDR など、他にも多くのアプリケーションでも使用されます。さらに、コード署名証明書は実行可能ファイルとライブラリで検証され、ドライバー署名証明書はドライバーの読み込み時に検証されます。証明書の検証プロセスにおける脆弱性が攻撃者にとって格好の標的になることは自明の理です。アイデンティティをマスクするだけで、重要なセキュリティ保護を回避できるからです。
国家安全保証局が CryptoAPI の脆弱性を開示するのは、これが初めてではありません。2020 年には、CurveBall(CVE-2020-0601)を発見して開示しました。攻撃者が CurveBallまたは CVE-2022-34689 を悪用すると、ID が乗っ取られます。 CurveBall は多くのアプリケーションに感染しましたが、CVE-2022-34689 は感染の前提条件が多いため、攻撃の標的範囲は限定的でした。
脆弱性の詳細
私たちは脆弱性を分析するために、まずパッチが適用されたコードの特定を試みました。一般的なバイナリ差分ツールの BinDiff を使用し、CryptoAPI に対するさまざまなコード変更を確認しました。crypt32.dll では、 CreateChainContextFromPathGraph という 1 つの関数のみが変更されていました。この関数では、その一部として 2 つの証明書を比較します。1 つは入力として受信したもの、もう 1 つは受信するアプリケーションの証明書キャッシュにあるものです(このキャッシュについては後述します)。
変更内容を検証した結果、 memcmp チェックが 2 つの場所の関数に追加されていることがわかりました(図 1)。
パッチ前は、関数は MD5 サムプリントのみに基づいて、受信した証明書がすでにキャッシュにあるかどうか(つまり検証済みかどうか)を確認します。パッチ後は、 memcmp が追加され、2 つの証明書の実際の内容が完全に一致するかどうかを確認するようになります。
この時点で、理論的には、攻撃者が被害者の証明書キャッシュにすでに存在する証明書の MD5 と衝突する悪性の証明書を提示すると、脆弱性チェックを通過することができ、悪性の証明書が信頼されるという結果になります(図 2)。
CryptoAPI の証明書キャッシュ
CryptoAPI は、キャッシュを使用して最終証明書を受信することで、パフォーマンスと効率を高めます。このメカニズムはデフォルトで無効になっています。有効にするためには、アプリケーション開発者が Windows API 関数の CertGetCertificateChainに特定のパラメーターを渡す必要がありますが、これが最終的に脆弱なコードになります(図 3)。
CertGetCertificateChain は、複数の興味深いパラメーターを受信します。
hChainEngine — 証明書を検証する方法の制御に使用する設定可能なパラメーター
pCertContext — 入力証明書のコンテキストであり、WinAPI 関数の CertCreateCertificateContext による入力証明書を使用して構築されたデータ構造
dwFlags — さらに詳細な設定を指定するフラグ
ppChainContext — (他のフィールドとともに)信頼状況(つまりチェーンの検証判断)を格納する出力オブジェクト
最終証明書のキャッシュメカニズムを有効にするためには、開発者は CERT_CHAIN_CACHE_END_CERT フラグを dwFlagsで設定するか、チェーンエンジンを作成して CERT_CHAIN_CACHE_END_CERT フラグを dwFlags フィールドで設定します。
キャッシュの実装および使用方法を理解するためには、キャッシュから証明書を取得する FindIssuerObject 関数を参照します。大まかに言うと、この関数は次のように動作します。
MD5 サムプリントの下 4 桁のバイトに基づいて、キャッシュ内で入力証明書のバケットインデックスを計算します。
キャッシュに存在する場合、関数はキャッシュされた証明書と入力証明書の MD5 サムプリント全体を比較します。
サムプリントが一致する(キャッシュヒット)する場合、入力証明書は信頼された状態で返されます。それ以降、 アプリケーションは、キャッシュされた証明書ではなく、入力証明書の属性(公開鍵、発行者など)を使用します。
サムプリントが一致しない場合(キャッシュミス)、バケットの次の証明書の MD5 サムプリントを比較し、このプロセスを繰り返します。
Microsoft はキャッシュ証明書の正当性を基本的に信頼しているため、キャッシュで最終証明書を検出した後は、追加の正当性チェックを実行しません。これ自体は、業務上、妥当な判断だと言えます。しかし、コードは、MD5 サムプリントが一致すれば、2 つの証明書は同一だと判断します。これは悪用につながる間違った判断であり、そのためにパッチが発行されています。
この仮説を実証するために、私たちは CertGetCertificateChain を使用する簡単なアプリケーションを記述し、crypt32.dll の証明書検証フローをデバッグしました。WinDbg を使用してシミュレーションしたシナリオでは、(自己署名の)証明書とキャッシュにすでにある正規の証明書の MD5 サムプリントを一致させます。その結果、図 4 のように、私たちが自作した証明書は信頼されました。
このように、1 つのチェックを通過するだけで、Windows に悪性の証明書を正規の証明書と信じさせることができました。
この脆弱性はどのように悪用されるのか
規定の MD5 値と完全に一致する MD5 サムプリントを使用して証明を構築することを原像攻撃と言います。これは、現在でも計算的に実行するのは不可能です。しかし、選択した 2 つのプレフィックスを持つ 2 つの証明書を効率的に生成し、最終的に同じ MD5 サムプリントを与えることは可能です。これを、選択プレフィックス衝突攻撃と言います。
この方法を選択すると、被害者のアプリケーションに 2 つの証明書を提供しなければなりません。1 つ目の証明書は、正常に署名され、検証され、キャッシュに保存されます(これを「改変されたターゲット証明書」と言います)。この証明書は、選択プレフィックス衝突攻撃を誘発するように生成されます。もう 1 つの証明書(いわゆる「悪性証明書」)は、偽のアイデンティティを格納しています。この証明書は、1 つ目の証明書の MD5 サムプリントと衝突します(図 5)。
MD5 衝突による証明書スプーフィング(なりすまし)
MD5 衝突が登場したのは約 14 年前です。ビヨンセが「シングル・レディース」をリリースし、オバマ氏が大統領に初当選したこの年に MD5 衝突が SSL 証明書のスプーフィング(なりすまし)に初めて使用されました。この最初の攻撃と、先ほど紹介したシナリオには、大きな違いが 1 つあります。最初の攻撃では、MD5 の 署名が標的でしたが、現在の脆弱性で標的になるのは、MD5 の サムプリントです。この違いを理解しましょう。
RFC 5280のセクション 4.1 によると、証明書は ASN.1 シーケンスであり、2 つのセクションがあります(図 6)。
tbsCertificate (あるいは「署名対象の」証明書) — これは、アイデンティティに関するすべての詳細を格納する部分です(対象者、公開鍵、シリアル番号、EKU など)。つまり、署名される部分です。
signatureAlgorithm および signatureValue — これらのフィールドは TBS の署名で構成されます。
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING }
図 6:証明書を定義する ASN.1 シーケンス
つまり、証明書の 署名 は、証明書内に格納される構造であり、証明書の TBS 部分のみに署名します。一方、証明書の サムプリント は、(署名を含む)証明書 全体 のハッシュです。
そのため、証明書を無効にせずに、TBS の外部にある証明書の任意の部分を改変すると、 署名を変更せずに、サムプリントを改変することになります。構文解析ツールが署名を正しく解析し、TBS が変更されていなければ、証明書全体の構造は変更されているにもかかわらず、証明書は有効で署名済みとみなされます(図 7)。
MD5 選択プレフィックス衝突 — 概要
A と B という同じ長さの 2 つの任意の項目があります。そして、以下のように、そこから C と D を効率的に計算できるとします。
MD5(A || C) = MD5(B || D) |
|| は文字列の連結を示します。
さらに、最終の MD5 結果が同一になるだけでなく、C または D の追加後に MD5 の内部状態も同一になります。そのため、サフィックス E にかかわらず、
MD5(A || C || E) = MD5(B || D || E) |
(同じサフィックス E を両辺に追加した場合)となります。
衝突ブロックのための余地を確保
攻撃者は、有効に見える証明書を生成する一方で、衝突ブロックのための余地も確保する必要があります(上記の C と D の文字列)。こうすることで、(同じ MD5 サムプリントを持つ)悪性証明書を自作できます。その流れを具体的に見ていきましょう。
RFC 5280セクション 4.1.1.2 によると、 signatureAlgorithm の構造は以下のようになります。
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
ここで、 RFC 3279に基づく RSA アルゴリズムのパラメーターフィールドは、「SHALL be the ASN.1 type NULL」となります。つまり、RSA は署名パラメーターを使用せず、NULL 値を取るからです。CryptoAPI は RSA 署名でこのフィールドを無視できるでしょうか?
このフィールドに(衝突ブロックのための準備として)プレースホルダーバイトを挿入するために、ASN.1 タイプを NULL から BIT STRING に変更してみました。これを CryptoAPI と OpenSSL でテストしたところ、 正常に動作し、 証明書は有効とみなされました。TBS を改変していないため、署名は変更も破損もしていません(もちろん、MD5 サムプリントも変化なしです)。
証明書の MD5 サムプリントの衝突
これらの結果を踏まえて、既存の署名済み証明書を操作し、悪性の証明書の MD5 サムプリントと衝突させるレシピを作成します。
Web サイトの TLS 証明書など、正規の RSA 署名済みの最終証明書を用意します(これを「ターゲット証明書」とします)。
証明書の TBS 部分の各種フィールド(対象者、拡張子、EKU、公開鍵など)を改変し、悪性の証明書を作成します。注:悪性の証明書は不正に署名されているため、署名は変更しません。ここで重要なのは、公開鍵を改変することです。そうすることで、攻撃者は悪性証明書として署名できます。
次に、2 つの証明書の パラメーター フィールド( signatureAlgorithm フィールド内)を改変し、MD5 衝突ブロックを挿入する余地を確保します(上記の C と D)。この場合、2 つの証明書と同じオフセットから開始します。
MD5 衝突ブロックを配置する位置で、2 つの証明書を短縮します。
MD5 の選択プレフィックス衝突計算を実行し、結果を証明書にコピーします。
正規の署名の値(上記のサフィックス E)を 2 つの不完全な証明書に連結します。
実際の例
MD5 衝突を理解したところで、この CVE を実際のターゲットで悪用してみましょう。チェックした多数のアプリケーションの中で、脆弱性のターゲットとして、Chrome v48 を特定しました(このアプリケーションが脆弱な理由は、CERT_CHAIN_CACHE_END_CERT フラグを CertGetCertificateChainに渡すからです)。これ以降の他の Chromium ベースのアプリケーションも、この CVE に対して脆弱です。
この脆弱性を悪用するためには、まず、同じ MD5 サムプリントを持つ 2 つの証明書を作成する必要があり、そのために HashClash を使用します(図 8)。
次に、改変したターゲット証明書を Chrome のキャッシュに組み込む方法を模索します。秘密鍵を知らずに証明書を提供するのは不可能なので、これは難題でした。
TLS 1.2 では、関連する 2 つの検証段階があります。
Server Key Exchange メッセージ - このメッセージは、証明書によって署名されるため、証明書の秘密鍵を知っているユーザーしか作成できません。
Server Handshake Finished メッセージ - このメッセージには、以前のすべてのハンドシェイクメッセージの改ざん防止のための検証が含まれています
(TLS 1.3 はまた異なりますが、ここでは説明しません)。
先ほど説明したように、攻撃の第 1 段階では、改変した証明書を Chrome の最終証明書のキャッシュに組み込むことが目的でした。
そのため、Python スクリプトをプロキシとして使用し、中間マシン(MITM)攻撃を仕掛けます。
悪性の MITM サーバーが実際のサーバーとトークし、TLS ハンドシェイクの 1 つ目のメッセージを被害者に送信します。
Server Certificate メッセージで、悪性の MITM サーバーは実際のサーバーのメッセージを改変し、実際のターゲット証明書を改変した証明書と置き換えます。
Server Key Exchange メッセージは、変更なしで送信できます。
悪性のサーバーは、ハンドシェイクが実際に改ざんされているため、Server Handshake Finished メッセージを単純に送信できません。そのため、接続を終了ます。
Server Key Exchange メッセージを検証するために、Chrome は CryptoAPI で改変された証明書を読み込む必要があります。その証明書がキャッシュに組み込まれます。Chrome は、破損した接続を TLS セキュリティ問題として処理しません。単なる無作為なネットワーク問題として解決します。Chrome は再接続を試みます。このとき、メッセージを実際の Web サイトからリダイレクトする代わりに、悪性のサーバーが悪性証明書とともに Web サイトを供給します。Chrome は、証明書がすでにキャッシュにあるとみなして、完全な検証プロセスをスキップします。その結果、正規の Microsoft Web サイトに対するシームレスな正規のサイト訪問に見えます(図 9 と図 10)。この悪用全体の流れは 動画で確認できます。
検出
私たちは、OSQuery を提供して、脆弱なライブラリである crypt32.dll の脆弱なバージョンを検知します(図 11)。 Akamai Guardicore Segmentation のお客様は、このクエリーと知見機能を併用して脆弱なアセットを検索できます。
アセットが脆弱だと判断するためには、未パッチの crypt32.dll バージョンを検知し、脆弱なアプリケーションを実行していることが条件となります(これまでに私たちが脆弱だと判断したのは Chrome v48 のみです)。
WITH product_version AS (
WITH os_minor AS (
WITH os_major AS (
SELECT substr(product_version, 0, instr(product_version, ".")) as os_major, substr(product_version, instr(product_version, ".")+1) as no_os_major_substr
FROM file
WHERE path = "c:\windows\system32\crypt32.dll"
)
SELECT substr(no_os_major_substr, instr(no_os_major_substr, ".")+1) as no_os_minor_substr, substr(no_os_major_substr, 0, instr(no_os_major_substr, ".")) as os_minor, os_major
FROM os_major
)
SELECT
CAST(substr(no_os_minor_substr, instr(no_os_minor_substr, ".")+1) AS INTEGER) AS product_minor,
CAST(substr(no_os_minor_substr, 0, instr(no_os_minor_substr, ".")) AS INTEGER) AS product_major,
CAST(os_minor AS INTEGER) AS os_minor,
CAST(os_major AS INTEGER) AS os_major
FROM os_minor
)
SELECT
CASE
WHEN os_major = 6 AND os_minor = 3 THEN "not supported"
WHEN (
(product_major = 20348 AND product_minor >= 887)
OR
(product_major = 17763 AND product_minor >= 3287)
OR
(product_major = 14393 AND product_minor >= 5291)
OR
(product_major >= 19041 AND product_minor >= 1889)
)
THEN
"patched"
ELSE
"not patched"
END is_patched
FROM product_version
結論
証明書は、オンラインでのアイデンティティ検証で重要な役割を果たします。そのため、この脆弱性は攻撃者にとって格好の標的になります。しかし、重大としてマークされながら、この脆弱性の CVSS スコアはわずか 7.5 です。これは、脆弱性の前提条件を満たす脆弱なアプリケーションと Windows コンポーネントの範囲が限定されるからだと考えられます。
とは言え、この API を使用して脆弱性に晒されるコードは依然として多く、Windows 7 など、サポートが終了した Windows にもパッチを確実に適用する必要があります。
Akamai では、Microsoft がリリースする最新のセキュリティパッチを Windows サーバーとエンドポイントに適用することをお勧めします。開発者には、この脆弱性を緩和するための別の方法があります。他の WinAPI を使用し、証明書を使用する前に有効性をダブルチェックします。たとえば、 CertVerifyCertificateChainPolicy などです。最終証明書のキャッシュを使用しないアプリケーションに脆弱性はありません。
Akamai の PoC コードは GitHub リポジトリで入手できます。 Akamai Security Intelligence Group の最新の出版物は、X(旧 Twitter)アカウントでも確認できます。