あるプロセスで特定のアクションの実行を許可する前に、そのプロセスを確実に特定しなければならない場合があります。それを確実に行う方法として、プロセスのAuthenticode署名を検証することが挙げられます。ユーザーモードDLLであるwintrustは、そのような機能に特化したAPIを提供します。

しかし、カーネルモードで信頼できる認証を行う必要がある場合はどうなるでしょう?そのような認証を行う理由としては、次の場合が考えられます。

  • プロセスにおける初期段階、障害、構成の問題などが原因で、お使いのアプリケーションのユーザーモード部が利用可能でない場合。
  • プロセスが検証されない場合にプロセスのアクションを回避できるようにするために、当該プロセスのアクションに対するインラインアクセス権を取得する必要がある場合。
  • ドライバのロード時にWindowsカーネルがドライバを検証するという従来通りの方法を使用する場合。これは明らかにカーネルモードで実行する必要がある。

これを行う方法については、これまでに多くのフォーラムで何回も質問されてきましたが、これを行うための共通した実装方法をネット上では見つけることができませんでした。

自分自身で実装することを推奨する人もいれば、OpenSSLのソースを各自のプロジェクトにインポートすることを推奨する人もいました。また、そのようなタスクをユーザーモードのコードへと委任すべきだと主張する人もいました。しかし、これらの選択肢にはいずれも、次に示すような大きな欠点があります。

  1. 複雑なASN1構造体の構文解析は、エラーを起こしやすくなります。
  2. 大量のソースコードをドライバにインポートすることは良いアイデアだとは言えません。なぜなら、OpenSSLでバクフィックスが発生するたびに、当該コードを再インポートする必要が生じるからです。
  3. ユーザーモードへの移行は有効でない場合があります。また、上述したように、ユーザーモードは常に利用可能であるとは限りません。

ファイルを認証する機能は、Microsoftのカーネルモードライブラリであるci.dll内に含まれています。

j00ruの調査によれば、ntoskrnlは、関数CiInitialize()を使用してCIモジュールを初期化します。

この関数は戻る際に、関数へのポインタを含む構造体をコールバックのリストで充填します。これらの関数やCI がエクスポートするその他の関数を使用して、実行中のプロセスやファイルの整合性および認証を確認できるならば、それはカーネルドライバにとってのゲームチェンジャーとなります。

ntoskernel.exe以外に、ci.dllにリンクしており、そのエクスポートを使用する2つのドライバを見つけました。


ci.dllにリンクしているドライバ(その1)


ci.dllにリンクしているドライバ(その2)

ドライバは、このモジュールにリンクすることで、CiValidateFileObject()のような関数を呼び出します。この関数は、その名前が示す通り、我々が探しているまさにそのものを実行する関数のようです。

本レポートでは、CI に光を当て、さらなる調査の基礎として役立つコード例と共に、その詳細を紹介します。

背景

ci.dllの詳細を理解するためには、次のテーマに関して精通していることをお勧めします。

調査

Windows 10上で、CIは次の関数をエクスポートします。


CIがエクスポートする関数

上述したように、関数CiInitialize() を呼び出すと、より多くの関数を含むg_CiCallbacksという名前の構造体が戻されます(詳細については[1]、[2]、[5]を参照のこと)。これらの関数のうちの1つであるCiValidateImageHeader()は、ドライバがその署名を検証するためにロードされる際に、ntoskernel.exeにより使用されます。


ロード時のドライバの署名検証を行うためのコールスタック

我々の調査では、エクスポートされた関数CiCheckSignedFile() と、同関数が対話するデータ構造体を利用します。後述するように、これらのデータ構造体は、他のCI 関数にも表れるため、それらの関数に対しても調査を拡張しています。

CiCheckSignedFile()

CiCheckSignedFile()は8つのパラメータを受け取りますが、それらのパラメータが何であるかは、この関数の名前からは明らかではありません。ただし、内部関数(例:MinCryptGetHashAlgorithmFromWinCertificate())を調べることにより、これらのパラメータを推測できます。


WIN_CERTIFICATE構造体のメンバーをチェックする

定数0x200の2を、WIN_CERTIFICATE構造体にとって典型的な数字として認識します。これは、4番目および5番目のパラメータを提供します。残りの入力パラメータも同様の方法で見つけることができます。なお、出力パラメータは、後述するように、入力パラメータとは完全に別の話になります。

リバースエンジニアリングの結果、関数CiCheckSignedFile()は次のパラメータを持つことが判明しました。

NTSTATUS CiCheckSignedFile(
__In__ const PVOID digestBuffer,
__In__ int digestSize,
__In__ int digestIdentifier,
__In__ const LPWIN_CERTIFICATE winCert,
__In__ int sizeOfSecurityDirectory,
__Out__ PolicyInfo* policyInfoForSigner,
__Out__ LARGE_INTEGER* signingTime,
__Out__ PolicyInfo* policyInfoForTimestampingAuthority
);

この関数は次のように動作します。

  • 呼び出し側は、この関数にファイルダイジェスト(バッファとアルゴリズムタイプ)およびAuthenticode署名へのポインタを提供します。
  • この関数は、次のことを実行することで、当該署名およびダイジェストを検証します。
    ・ファイル署名に関して実行を繰り返すことで、指定のダイジェストアルゴリズムを使用している署名をフェッチします。
    ・署名(および証明書)を検証し、その中に現れているファイルダイジェストを取り出します。
    ・この取り出されダイジェストを、呼び出し側が提供したダイジェストと比較します。

  • ファイル署名の検証に加えて、この関数は、検証対象となった署名に関する各種の詳細情報を呼び出し側に提供します。

この関数の動作の最後の部分は、非常に興味深いものです。なぜなら、ファイルが適切に署名されていることを知るだけでは十分でないからです。誰がそれを署名したかを知る必要があります。この必要性に関しては、次のセクションで説明します。

PolicyInfo構造体

この時点で、CiCheckSignedFile() に対するすべての入力パラメータを取得し、この関数を呼び出せるようになりました。しかし、PolicyInfo構造体に関して、そのサイズ(Windows 10/x64上では0x30)以外には何も知りません。

出力パラメータの1つとなるために、この構造体が何らかの方法で署名者の身元に関するヒントを提供し、それを自身で取り出す手間を省いてくれると、期待していました。このため、この関数を呼び出した後、メモリを調べることで、どのようなデータでPolicyInfoが充填されるかを確認することにしました、メモリには、1つのアドレスと、いくつかの大きな数値が含まれているものと思われます。

この構造体には、内部関数MinCryptVerifyCertificateWithPolicy2()によって値が割り当てられます。


PolicyInfo構造体への値の割り当て

この関数内にある一部のコードは、値が特定の範囲を超えていないかどうかをチェックしているように思われます。証明書検証の文脈において、この範囲が、証明書の有効期間ではないかと疑っていましたが、それが正しかったことが判明します。


証明書の有効期間のチェック

この結果、次の構造体が生成されます。
typedef struct _PolicyInfo
{
int structSize;
NTSTATUS verificationStatus;
int flags;
PVOID someBuffer; // later known as certChainInfo;
FILETIME revocationTime;
FILETIME notBeforeTime;
FILETIME notAfterTime;
} PolicyInfo, *pPolicyInfo;

証明書の有効期間は興味深いものの、それは署名者の強固なIDを提供するものではありません。後述するように、ほとんどの情報は、次のセクションで紹介する構造体メンバーcertChainInfo内に存在しています。

CertChainInfoバッファ

PolicyInfoのメモリを調べた際に、それが構造体の外部にあるメモリロケーションを指していることを確認できました。この割り当てはI_MinCryptAddChainInfo()内で行われ、この関数名は当該バッファの目的に関するヒントを提供しています。

我々は、このメモリレイアウトを調べることで、このバッファの構造を明らかにしました。

  • 先頭の数バイトには、同バッファ内の各種の場所へのポインタが保存されます。
  • これらのポイントされた場所には、反復パターンと、バッファの内部にある別の場所へのポインタが保存されます。
  • これらの最後にポイントされた場所に、証明書の抜粋のように見えるテキストの一部を見つけました。

このバッファには、証明書のチェーン全体に関するデータが、構文解析後のフォーマット(副構造体内に整理されている)および生データフォーマット(証明書、キー、EKUからなるASN.1ブロブ)の両方で含まれています。

これにより、呼び出し側が、証明書の主体および発行者が誰であるか、証明書のチェーンの構成要素、および各証明書の作成に使用されたハッシュアルゴリズムをチェックすることなどが容易になります。

このバッファのフォーマットとバッファから導出した副構造体について詳いく説明するために、32ビットマシン上のそのメモリレイアウトを紹介します。32ビットマシンを使うと、アラインメント要件のために追加されるパディングバイトの数を減らせるため、クラッターを削減できます。下記の図は、Microsoftにより署名されているNotepad.exeで取得されたものです。


CertChainInfoバッファのメモリビュー

これにより次のことが分かります。

  • このバッファの先頭には、4バイトからなる数字が2つあります。1つは、 CertChainMember型の一連の構造体が存在する場所を伝えるアドレスであり、もう1つは、それらがそこにいくつあるかを示すカウンタ(2)です。
  • 最初のCertChainMemberは、アドレス0x89BF45C8(黒色の線で囲まれている部分)にあります。これを次のようにフォーマット化します。
  • CertChainMemberの末尾のアドレス0x89BF4688(青色の線で囲まれている部分)には、プレーンテキストで主体名があります。
  • アドレス0x89BF4699(オレンジの線で囲まれている部分)には、プレーンテキストで発行者名があります。
  • 赤色の矢印でポイントされているアドレス0x89BF46BEには、実際の証明書を含んでいるASN.1ブロブの先頭があります。このメモリは4バイトのグループごとにリトルエンディアン形式で表示されているため、証明書の先頭の2バイトは、実際には図に示されている通りの0x3131ではなく、0x3082になります。

typedef struct _CertChainMember
{
int digestIdetifier; // e.g. 0x800c for SHA256
int digestSize; // e.g. 0x20 for SHA256
BYTE digestBuffer[64]; // contains the digest itself
CertificatePartyName subjectName; // pointer to the subject name
CertificatePartyName issuerName; // pointer to the issuer name
Asn1BlobPtr certificate; // pointer to actual certificate in ASN.1
} CertChainMember, * pCertChainMember;

これが、先に構文解析済みのデータとして言及したものです。主体や発行者をフェッチするために、自分自身で証明書を構文解析する必要はありません。

本構造体内の末尾にあるバイトは、当該バッファ内にある別の場所をポイントしています。続く96バイトは、2番目のCertChainMemberを含んでいますが、この関数は可読性を損なわないように図中ではマークされていません。これには、チェーン内の次の証明書に関する情報が含まれています。

同様な一連のポインタと構造体が、公開鍵とEKU(Extended Key Usage)用に存在しています。言い換えれば、CIは、証明書から有益なビットをいくつか選び、それらを副構造体の形式で、呼び出し側が即座に利用できるようにします。ただし、呼び出し側がそれから別の何かを必要とする場合には、それには、構文解析されていない生のデータも含まれています。

注:PolicyInfoおよびCertChainInfoの両構造体の先頭には、各構造体のサイズが含まれています。これらの構造体はOSのバージョンにより拡張されているため、ユーザーは、他の構造体メンバーにアクセスしようとする前に、そのサイズを確認する必要があります。

CertChainInfo バッファの完全な分析と、各種の副構造体については、リポジトリ内のファイルci.hを参照してください。

CiFreePolicyInfo()

この関数は、PolicyInfoのcertChainInfoバッファを解放します。このバッファは、関数CiCheckSignedFile() や、構造体に値を割り当てるその他のCI 関数によって割り当てられます。また、この関数は、その他の構造体のメンバーをリセットします。この関数は、メモリリークを防ぐために呼び出す必要があります。


CiFreePolicyInfo()の実装

この関数は解放するメモリが存在するかどうかを内部的にチェックするため、PolicyInfoに値が割り当てられていない場合であっても、関数を安全に呼び出すことができます。

CiValidateFileObject()

先に見たように、CiCheckSignedFile() を使う場合、呼び出し側は、関数を呼び出す前に、いくつか作業を行う必要があります。呼び出し側は、この関数に署名の場所を提供するためには、ファイルのハッシュを計算し、PRを構文解析する必要があります。

一方、関数CiValidateFileObject() は、呼び出し側のためにこのような作業を実施します。この関数は、そのパラメータの一部をCiCheckSignedFile()と共有しているため、ユーザーは作業を一から始める必要はありません。

NTSTATUS CiValidateFileObject(
__In__ struct _FILE_OBJECT* fileObject,
__In__ int a2,
__In__ int a3,
__Out__ PolicyInfo* policyInfoForSigner,
__Out__ PolicyInfo* policyInfoForTimestampingAuthority,
__Out__ LARGE_INTEGER* signingTime,
__Out__ BYTE* digestBuffer,
__Out__ int* digestSize,
__Out__int* digestIdentifier
);

この関数は、ファイルをカーネルスペース内にマップし、その署名を取り出します。


CiValidateFileObject()によるシステムスペース内でのファイルのマッピング

また、同関数は、ファイルダイジェストを計算するほか、ユーザーが十分な長さの満杯でないバッファを提供した場合、この関数はバッファをこのダイジェストで完全に満たします。

注:この関数は最近のWindowsバージョンにのみ追加されているものであるため、今回の調査でこの関数を重点的には取り上げませんでした。今後も調査を続けた場合、この関数の検証ポリシーを解明することがあるかもしれません。

この関数は、CiCheckSignedFile()に比べてより厳格なポリシーを使用していることにご注意ください。これは、同関数は、CiCheckSignedFile() が承認したPEの検証に失敗する可能性があることを意味します。これは、現時点では解明していない、パラメータ2およびパラメータ3の値により影響を受ける可能性があります。

Githubリポジトリ

PE署名の検証にci.dllが利用されることを実証するために、Githubリポジトリに関する情報を捕捉することにしました。

このリポジトリには、調査を実施するための簡単なドライバが含まれています。このドライバは次のことを実行します。

  • 新しいプロセスの通知を行うためのコールバックを登録します。
  • 本レポートで紹介したci.dll 関数を使用して、すべての新規プロセスのPR署名を検証しようとします。
  • ファイルの署名の検証に成功した場合、ドライバは、出力されたPolicyInfo構造体を構文解析することにより、署名する証明書とその詳細情報を取り出します。

お客様におかれましては、このリポジトリを使用してCI における初期動作を理解するための実験を行った後、調査を拡張することをお勧めします。

ci.dllとのリンク

本レポートの締めくくりとして、この文書化されていないライブラリとのリンクを行う手順を説明します。これは、CIの利用におけるドライでテクニカルな側面のように思われるかもしれませんが、この手順が重要であることを理解しており、お客様がより多くの関数を使用して調査を拡張する場合には、同じ手順を通過する必要があると考えています。

特定のDLLとリンクする場合、ユーザーは通常、ベンダーにより提供される重要なライブラリを使用するはずです。我々の場合、Microsoftから提供された.libファイルは存在しなかったため、それを自分自身で生成する必要がありました。同ファイルは、生成された後、リンカーへの入力としてプロジェクトのプロパティに追加する必要があります。.libファイルの生成に必要となる手順を下記に示します。

64ビットの場合
  • dumpbinユーティリティを使用して、当該DLLからエクスポートされた関数を取得します。
    dumpbin /EXPORTS c:\windows\system32\ci.dll
  • 次のコマンドを実行して.defファイルを作成します。
    LIBRARY ci.dll
    EXPORTS
    CiCheckSignedFile
    CiFreePolicyInfo
    CiValidateFileObject
  • libユーティリティを使用して.libファイルを作成します。
    lib /def:ci.def /machine:x64 /out:ci.lib
32ビットの場合

32ビット環境では、関数が引数の合計(バイト単位)を反映するため、状況はややトリッキーになります。次に例を示します。

CiFreePolicyInfo@4

ただし、ci.dllは、これを使わずに関数をエクスポートするため、必要なのは、このような変換を行う.libファイルを作成することだけです。これを行うために、 [3] および [4]を使用しました。

  • 上記の62ビットの説明におけるステップ1および2の説明に従って、.defファイルを作成します。
  • 同じ署名とダミーの本体を持つ関数スタブを使用してC++ファイルを作成します。基本的に、ベンダーが各自のコードから関数をエクスポートする際に行う作業を模倣します。次に例を示します。
    extern “C” __declspec(dllexport)
    PVOID _stdcall CiFreePolicyInfo(PVOID policyInfoPtr)
    {
    return nullptr;
    }
  • 同ファイルをコンパイルしてOBJファイルを作成します。
  • 今回は、このOBJ ファイルと共にlibユーティリティを使用して.libファイルを作成します。
    Lib /def:ci.def /machine:x86 /out:ci.lib

Githubリポジトリには、このスタブ用のコードが含まれています。

要約

本ブログ記事では、CI APIのサブグループの使い方を紹介しました。これにより、自分自身で実装することなしに、カーネルモードでAuthenticode署名の検証が行えるようになります。

この記事が、このDLLのさらなる調査につながることを希望します。

参考文献

[1] Microsoft Windows FIPS 140 Validation Security Policy Document
[2] windows-driver-signing-bypass-by-derusbi
[3] how-to-create-32-bit-import-libraries
[4] Q131313: HOWTO: Create 32-bit Import Libraries Without .OBJs or Source
[5] j00ru’s blog about CI

ホワイトペーパー「すべての組織が狙われている」

企業、組織がどんなにセキュリティを強固にしてもハッカーが悪用できる脆弱性は必ず存在します。侵入されることが避けられないことを受け入れ、新たな対策を立てる必要があります。本書で、なぜ避けられないのか、どのように対処するのかをご覧ください。
https://www.cybereason.co.jp/product-documents/input/?post_id=606

ホワイトペーパー「すべての組織が狙われている」