2020年1月に公開されたMicrosoftのセキュリティ更新プログラムには、Windows CryptoAPI のなりすましの脆弱性「CVE-2020-0601」への対処が含まれていました。この脆弱性は、米国の国家安全保障局(NSA)によって警告されたもので、CryptoAPIの一部を構成する暗号化ライブラリの1つが楕円曲線暗号(Elliptic Curve Cryptography 、ECC)証明書を検証する方法に存在します。「CurveBall(カーブボール)」または「Chain of Fools」と呼ばれるこの脆弱性は、攻撃者に悪用された場合、偽のコード署名証明書を使用してファイルに署名し、信頼できる正当な送信元から送られたファイルに見せかける可能性があります。

公開から数日のうちには概念実証(PoC)が公開され始めたと同時に、この脆弱性に関係する楕円曲線暗号の概念に関する説明も見かけられるようになりました。

本ブログ記事ではそのどちらでもなく、アプリケーションがCryptoAPIを使用して電子証明書を処理する方式に存在する脆弱性について詳細を解説します。具体的には、Transport Layer Security(TLS)プロトコルで通信を行うアプリケーション環境における問題の脆弱性について、コードレベルでの発生原因を解析します。

なお、本脆弱性の前提となる電子証明書とECCについての概説を、補完資料としてまとめております。こちらもご参照ください。

■更新プログラムによって修正された箇所

攻撃者がこの脆弱性を利用する場合、まずECCに関するパラメータを操作し、攻撃者が作成した鍵を使用して証明書を生成します。これは通常、基点以外のパラメータを元の証明書が使用するパラメータのままにしておくことで実行されます。この新しく作成した「信頼できる」証明書と秘密鍵を使用して追加の証明書に署名し、作成された証明書とエンドエンティティ証明書として知られる追加の証明書を攻撃対象に提示します。攻撃対象は、攻撃者から提示された証明書とWindowsの証明書ストアに含まれる信頼できる証明書の組み合わせを使用して、証明書チェーンの検証を試みます。この検証プロセスに脆弱性が存在します。ここでは、パッチによって変更された部分について掘り下げ、Crypto APIのプロセスを詳しく解説します。

Windows CryptoAPIにはいくつかの異なるライブラリが含まれていますが、問題の脆弱性はcrypt32.dllに存在します。パッチ未適用バージョンのDLLファイル「10.0.18362.476」と、パッチ適用後のDLLのバージョン「10.0.18362.592」のバイナリdiffをBinDiffで確認すると、2つのファイル間のわずかな差異が確認できます。

図1:パッチ未適用DLLとパッチ適用後DLLをBinDiffで比較すると、わずかな差異が表示される

DLLの何が変わったのでしょうか。

上位レベルでは、新しいバージョンには5つの興味を惹く新しい関数が追加され、5つの既存の関数が変更されました。このように複数の新しい関数が追加された場合、新しい関数を利用できるように既存の関数が更新されていることがあります。幸いMicrosoftはほとんどのWindowsコンポーネントに対してデバッグシンボルを提供しているため、関数の名前を調べるだけで多くの情報を得ることができます。

まず、変更された関数の名前を見てみましょう。

図2:crypt32.dllの変更された関数名

「CCertObject」クラスのコンストラクタとデストラクタも少々変更されていますが、最大の変更は「ChainGetSubjectStatus()」と「CCertObjectCache :: FindKnownStoreFlags()」に確認できます。

次に、新しい関数についてです。

図3:crypt32.dllに加えられた新しい関数名

新しい関数のいくつかが目を引きますが、特にそのうちの1つが解析の開始点として利用できます。 「ChainLogMSRC54294Error()」関数は、脆弱性悪用の試みが発生した場合にWindowsイベントログを取るために追加された新しいロギング関数です。この関数の目的は、次のブロックによって確認できます。

図4:ChainLogMSRC54294Error()関数の目的が確認できるブロック

このブロックは、問題の脆弱性に関連するCVEを含む文字列をCveEventWriteと呼ばれる外部ライブラリ関数に渡します。これは、CVE関連イベントをWindowsイベントログに書き込む、比較的新しいイベント追跡のAPI関数です。

この情報を使用することによって、新しい関数への相互参照を調べ、このイベントログが記録された背景に関する詳細を探し出すことができます。この場合、「ChainLogMSRC54294Error()」への直接の参照は、「ChainGetSubjectStatus()」関数のみです。この関数は、更新された関数の中でも最も大きな変更が加えられたものです。

図5:ChainLogMSRC54294Error()への参照を確認するため、ChainGetSubjectStatus()関数を調べる

新しいロギング関数の呼び出しの周囲をざっと見てみると、「CryptVerifyCertificateSignatureEx ()」および「ChainComparePublicKeyParametersAndBytes()」の呼び出しの結果が特定のものであった場合、この関数が呼び出されていることがわかります。「ChainComparePublicKeyParametersAndBytes()」はパッチに追加された新しい関数の1つです。このように、「ChainGetSubjectStatus()」に焦点を当てることによって、関数の変更とそれが呼び出される経緯の両方を分析することができます。

■CryptoAPIの証明書検証の仕組み

「ChainGetSubjectStatus()」の役割を理解するため、最初に、通常プログラムがどのようにCryptoAPIを使用して証明書を処理するかを確認します。

今回、解析はPowershellのInvoke-Webrequestコマンドレット上で実行しましたが、他のTLSクライアントでも同様に動作すると考えられます。Powershellが最初にロードされるとき、明示的に信頼された証明書が含まれるシステム証明書ストアのハンドルが、「CertOpenStore」関数の呼び出しによって取得され、必要に応じて証明書ストアを使用できるようになります。これらの証明書ストアは、「コレクション(collection)」に追加され、1つの大きな統合された証明書ストアとしての役割を果たします。

Invoke-Webrequestなどのコマンドを使用してHTTPリクエストをTLS経由でサーバに送信すると、サーバはエンドエンティティ証明書と、証明書チェーンの検証に使用される追加の証明書を含む、TLS証明書ハンドシェイクメッセージを提示します。これらの証明書を受信すると、さらに「CertOpenStore ()」を呼び出すことにより「メモリ内」のストアが作成されます。受信した証明書は、「CertAddEncodedCertificateToStore()」関数によってこの新しいストアに追加されます。この関数は、証明書ストアへのハンドル、元のエンコードされた証明書へのポインタ、および証明書のASN.1構造体に対応するCERT_INFO構造体へのポインタを含む、CERT_CONTEXT構造体を作成します。

たとえば、以下は、エンドエンティティ証明書のために「CertAddEncodedCertificateToStore()」を呼び出した結果作成された構造体です。

図6:CertAddEncodedCertificateToStore()の呼び出しにより作成された構造体

このうちのいくつかは特に重要です。発行者(Issuer)は、この証明書の署名者に属する証明書の主体者(Subject)と完全に一致する必要があります。また、主体者公開鍵情報であるSubjectPublicKeyInfoは、同じ名前のASN.1構造体に含まれる正確な情報が含まれる構造体です。たとえば、アルゴリズム識別子のOIDが1.2.840.10045.2.1であることから、エンドエンティティ証明書の公開鍵に楕円曲線暗号が利用されていることがわかります。

図7:アルゴリズム識別子のOIDを示すエンドエンティティ証明書の公開鍵

また、パラメータの最初のバイトを調べることで、証明書が名前付き曲線あるいは明示的な曲線パラメータのどちらを提供しているかをすばやく識別することができます。

図8:パラメータの最初のバイトから、名前付き曲線か明示的な曲線パラメータかを判断できる

この場合、0x6はOIDのDERエンコードタグなので、名前付き曲線が指定されていることを意味します。明示的なecParametersが提供されていた場合、タグの値はシーケンスに対応する0x30になります。

受け取ったすべての証明書がメモリ内ストアに追加されると、Powershellが、エンドエンティティ証明書のための証明書チェーンコンテキストを構築するため、CERT_CONTEXT構造体で「CertGetCertificateChain()」関数を呼び出します。証明書チェーンのコンテキストには中間証明書もすべて、可能であれば信頼されるルート証明書も含まれます。

具体的には、「CertGetCertificateChain()」が証明書チェーンの配列であるCERT_CHAIN_CONTEXT 構造体と、チェーンの有効性に関するデータを含む信頼ステータスの構造体を返します。これは主に「CCertChainEngine :: GetChainContext()」関数内で処理され、最終的に証明書チェーンの検証ステータスが含まれるCERT_CHAIN_CONTEXT構造体を返します。

【CCertChainEngine :: GetChainContext()】は、【CCertChainEngine :: CreateChainContextFrom PathGraph()】を呼び出し、これによってシステム証明書ストアから、以前に作成されたコレクションを含む新しいコレクションを最初に作成します。次に、コレクション(つまり、コレクションに属するすべてのストア)からエンドエンティティ証明書を見つけ、エンドエンティティ証明書のCERT_INFO構造体を使用して「ChainCreateCertObject()」を呼び出します。「 ChainCreateCertObject()」は、作成キャッシュの中に既存のCCertObjectがないか確認しますが、見つからない場合は、新しいCCertObjectがインスタンス化されます。

CcertObjectのコンストラクタは、次の処理を行います。多数のフィールドをイニシャライズし、署名ハッシュ、鍵識別子などのさまざまなプロパティをCERT_INFO構造体からコピーします。そして、証明書拡張情報で設定されたポリシー情報項目を探し、CryptoAPIがサポートしていない「重要(critical)」と指定された拡張情報がないかを確認します。

次に、コンストラクタは「ChainGetIssuerMatchInfo()」を呼び出し、証明書の署名に使用される鍵を識別するCERT_AUTHORITY_KEY_ID_INFO構造体を取得します。そして、証明書が自己署名されているかどうか、また、公開鍵に脆弱と判断される長さのRSAアルゴリズムが使用されていないかを確認します。

エンドエンティティ証明書のためのCCertObjectが作成されると、CChainPathObjectが作成されます。 CChainPathObjectの初期化が実行された後、コンストラクタは「CChainPathObject :: FindAndAddIssuers()」を呼び出し、続いて「CChainPathObject :: FindAndAddIssuersFromStore ByMatchType()」が呼び出されることによって、エンドエンティティ証明書の発行者と一致する証明書を探します。通常は、エンドエンティティ証明書の発行者のハッシュをもとに一致する主体者のハッシュを探します。メモリ内ストアに追加の証明書が見つかった場合(つまり、エンドエンティティ証明書とともに送信された証明書)、「ChainCreateCertObject()」が再度呼び出されますが、この度は、エンドエンティティ証明書発行者の証明書のCERT_INFO構造体が使用されます。

このプロセスは、「自己署名証明書」(つまり、ルート証明書)に辿り着くまで繰り返され、そして「ChainGetSelfSignedStatus()」が呼び出されます。ただし最初に、主体者と発行者が一致するかを確認し、一致した場合は、証明書のCERT_INFOのCERT_PUBLIC_KEY_INFO構造体が提供する公開鍵と鍵アルゴリズム情報を使い、「CryptVerifyCertificateSignatureEx()」を呼び出して、証明書の署名を検証します。

検証のほとんどは、「I_CryptCNGVerifyEncodedSignature()」関数内で実行されます。証明書が名前付き曲線ではなく明示的な楕円曲線のパラメータを提供する場合、関数「I_ConvertPublicKeyInfoToCNGECCKeyBlobFull()」が呼び出され、曲線パラメータを含むBCRYPT_ECCFULLKEY_BLOB構造体を作成します。この構造体は正式には文書化されていませんが、Windows SDKの bcrypt.hヘッダに存在します。これらのパラメータは、次に「CNGECCVerifyEncodedSignature()」の呼び出しで使用され、「CNGECCVerifyEncoded Signature()」は、bcryptライブラリ関数「BCryptVerifySignature()」を呼び出して、パラメータと署名によって実際の検証を実行します。

署名の検証が成功すると、「CCertIssuerList :: AddIssuer()」関数が呼び出され、これによって最終的に新しい「CChainPathObject」が作成されます。エンドエンティティ証明書と自己署名証明書のCChainPathObjectsが、次に「CCertIssuerList :: CreateElement()」の呼び出しに使用され、最初に初期化を実行してから、2つのCChainPathObjectsで「ChainGetSubjectStatus ()」を呼び出します。

「ChainGetSubjectStatus()」は最初に、各CChainPathObjectsに関連付けられたCCertObjectsを使用して「ChainGetMatchInfoStatus()」を呼び出すことによって、2つの主体者が同じであるかどうかをチェックします。 次に、エンドエンティティ証明書に関連付けられたCCertObjectのフラグをテストします。

図9:ChainGetMatchInfoStatus()を呼び出し、エンドエンティティ証明書に関連付けられたCCertObjectsのフラグをテストする

このフラグは最初CCertObjectが作成されたときは0に設定されています。そして、自己署名証明書が有効なエンドエンティティ証明書の発行者であることを確認するために、「CryptVerifyCertificateSignatureEx()」が呼び出されます。

図10:検証のためCryptVerifyCertificateSignatureEx()が呼び出される

署名が有効であるとみなされると、CERT_ISSUER_PUBLIC_KEY_MD5_HASHプロパティがエンドエンティティ証明書に追加され、前述のフラグが3に設定されます。

図11:CERT_ISSUER_PUBLIC_KEY_MD5_HASHプロパティがエンドエンティティ証明書に追加される

「CCertIssuerList :: AddIssuer()」が戻り、他のすべての関数が元の「CChainPathObject :: FindAndAddIssuers()」呼び出しに戻ると、前述のフラグがチェックされ、設定されている場合、「CChainPathObject :: FindAndAddIssuersFromStoreByMatchType()」が再度呼び出されます。

今度は、検索のパラメータが含まれるCERT_STORE_PROV_FIND_INFO構造体が「FindElementInCollectionStore()」に提供され、システムの証明書信頼リストがあるコレクションストアが検索されます。「FindElementInCollectionStore()」はコレクション内のストアを反復処理し、自己署名証明書の公開鍵と一致するMD5ハッシュを持つ証明書を各ストアで検索します。

公開鍵ハッシュがシステムストアで見つかった証明書と一致した場合、「ChainCreateCertObject ()」がもう一度呼び出されます。今度は、システムストアから取得した証明書のCERT_CONTEXT構造体を使用して新しいCCertObjectオブジェクトを、証明書が「既知のストア」​​にあるものと一致したという事実によって、フラグを適用するプロセスに作成します。その後、オブジェクトは発行者オブジェクトとしてCCertObjectCacheに追加され、発行者リストに追加されます。これにより、再度新しいCChainPathObjectが作成され、自己署名証明書とストアから取得した信頼できる証明書のCChainPathObjectsによって「CCertIssuerList :: CreateElement()」の呼び出しが実行されます。

「ChainGetSubjectStatus()」が呼び出されると、前述のフラグが設定され、自己署名証明書に既に設定済みであるCERT_ISSUER_PUBLIC_KEY_MD5_HASHが、信頼できるストアからの証明書の公開鍵のMD5ハッシュと照合されます。

図12:自己署名証明書のCERT_ISSUER_PUBLIC_KEY_MD5_HASHを、信頼できるストアからの証明書の公開鍵のMD5ハッシュと照合する

一致した場合、提供された自己署名証明書は、信頼できるストアから取得した証明書に対しそれ以上検証されることはありません。これは、公開鍵ハッシュが証明書ストアから取得した証明書のものと完全に一致したという事実により、提供された証明書が信頼できる自己署名証明書であるとみなされたことを意味します。さらに、提供された自己署名証明書によって、エンドエンティティ証明書の署名の検証に成功しています。攻撃者が、信頼できるストアからの証明書つまりルート証明書のものと全く同じ公開鍵を備えた証明書を提供し、それが明示的に定義された楕円曲線パラメータを使用して作成されていた場合、エンドエンティティ証明書の署名は、正規のルート証明書によって署名されたものと同等の信頼をもつことになります。

すべての関数が「CCertChainEngine :: CreateChainContextFromPathGraph ()」に戻ると、この信頼が明確に示されます。証明書パス内の各オブジェクト(つまり、エンドエンティティ証明書と細工されたルート証明書)は、現在の時刻と失効ステータスに関する証明書の有効性など、追加のチェックを実行します。次に、「CChainPathObject :: CreateChainContextFromPath()」を呼び出してCERT_CHAIN_CONTEXT構造体を作成し、その中のCERT_TRUST_STATUS構造体は、証明書チェーンの有効性を反映するように設定されます。

「CertGetCertificateChain()」関数が戻ると、メインアプリケーションはCERT_CHAIN_CONTEXT構造体内に含まれる証明書チェーンの検証ステータスを確認し、攻撃シナリオにおいて有効と表示されます。

図13:CERT_CHAIN_CONTEXT構造体内に含まれる証明書チェーンのステータスの確認
■結論

以上のCVE-2020-0601の解析結果を要約すると、次の点が明らかになりました。

  • エンドエンティティ証明書の署名は、細工されたルート証明書と、証明書に含まれる楕円曲線パラメータを使用して検証される
  • 細工されたルート証明書の署名は、証明書に含まれる楕円曲線パラメータを使用して、自己署名証明書として検証される
  • 細工されたルート証明書に一致する証明書はシステム証明書ストアにある。公開鍵のハッシュは、細工されたルート証明書と正規のルート証明書で同一である
  • 細工されたルート証明書と正規のルート証明書の公開鍵のハッシュをチェックし、一致した場合、細工されたルート証明書はそれ以上検証されないため、細工されたエンドエンティティ証明書の検証が成功する

Microsoftの更新プログラムによってどのように脆弱性が解決されたでしょうか。図5を見ると、新しい関数「ChainComparePublicKeyParametersAndBytes()」の呼び出しが追加されており、これによって、発行者と信頼されたルート証明書の公開鍵ハッシュの単純な比較であった部分が置き換えられました。これにより、信頼されたルート証明書とエンドエンティティ証明書の署名を検証するため実際に使用された証明書の、公開鍵のパラメータとバイトを比較します。

比較の結果検証が失敗した場合、「CryptVerifySignatureEx()」が呼び出され、実際の信頼されたルート証明書、パラメータなどすべてを使用してエンドエンティティ証明書の署名を再検証します。これにより、実際の信頼された証明書のものとは異なる暗号化パラメータを持つ巧妙に細工されたルート証明書を見つけ出すことができる、つまり、脆弱性が解決されたことになります。

■トレンドマイクロの対策

CurveBallの脆弱性「CVE-2020-0601」を利用した攻撃の被害にあわないようにするため、企業および個人ユーザは、Microsoftが公開している最新の更新プログラムを迅速に適用してください。また、「セキュリティ対策度診断ツール」を使用して、脆弱性のリスクがあるかどうかを確認できます。

法人向け総合エンドポイントセキュリティ「Trend Micro Apex One™ 」、総合サーバセキュリティ「Trend Micro™ Deep Security™ 」では以下の DPIルールにより本記事内で挙げた脆弱性を利用する攻撃を検出します。

  • 1010130-Microsoft
    Windows CryptoAPI Spoofing Vulnerability (CVE-2020-0601)
  • 1010132-Microsoft
    Windows CryptoAPI Spoofing Vulnerability (CVE-2020-0601) – 1

ネットワーク脅威防御ソリューション「Trend Micro™ TippingPoint® 」では、以下のMainlineDV filter により本記事内で挙げた脆弱性を利用する攻撃を検出します。

  • 36956: HTTP: Microsoft Windows CryptoAPI Spoofing Vulnerability

参考記事:

翻訳: 室賀 美和(Core Technology Marketing, Trend Micro™ Research)



Source link

You must be logged in to post a comment.