FortiGuard Labs 脅威リサーチ

ドライバー署名の強制の改ざんの沈静化

投稿者 Omri Misgav | 2022年12月1日

はじめに

コード整合性は、15年以上前にMicrosoftが初めて導入した脅威保護機能です。x64ベース版のWindows上では、カーネルモードのドライバーはデジタル署名されており、メモリにロードされるたびに検証される必要があります。これは、ドライバー署名の強制(DSE)とも呼ばれています。管理者権限で実行された悪意あるソフトウェアによって、未署名のドライバーやシステムファイルがカーネルにロードされていないか、またはシステムファイルが変更されていないかを検知すると、オペレーティングシステムのセキュリティが向上します。

攻撃者は、こうした制限を切り抜けるために、有効なデジタル証明書(攻撃者に対して発行された、または盗難で入手した)を使用するか、実行時にDSEを無効化します。証明書の入手は主に実行上の課題ですが、改ざんは純粋に技術的な課題です。

ここ数年、この問題に全力で対処しているMicrosoftの取り組みや、Microsoftが提供しているソリューションの範囲にもかかわらず、この有名なDSE改ざん手法を利用した攻撃の件数は明らかに増加しています。そのため、フォーティネットは、この問題をより詳しく調査することにしました。

本ブログでは、フォーティネットによる調査結果を示しており、DSE改ざんのさらに2つの手法の詳細と、この攻撃対象領域が排除されないのであれば、セキュリティ担当者がこの問題にどのように対処すればよいのかを解説しています。

DSEの実装

GoogleのProject Zeroに携わるリサーチャー j00ruが以前に書いたブログに、Windows 7でのコード整合性の実装概要が記載されています。

ntoskrnl.exeは追加のカーネルライブラリCI.dll(コード整合性)と連携して機能します。オペレーティングシステムの初期化段階で、カーネルはnt!g_CiEnabledを設定し、CI.dllを初期化するCI!CiInitializeルーチンをnt!g_CiCallbacks構造へのポインターを使用して呼び出します。次にカーネルはCI!g_CiOptionsを設定し、カーネルに返される前にコールバックCI!CiValidateImageHeader、CI!CiValidateImageData、およびCI!CiQueryInformationのアドレスを入力します。

コールバックを使用するために、ntoskrnl.exe内に各コールバックのラッパー関数が存在します。ラッパー関数は、nt!g_CiEnabledがTRUEに設定されており、該当するコールバックがNULLではないことを確認したら、コールバックを呼び出します。

カーネルがドライバーをロードする際、実行はnt!MmLoadSystemImageからnt!MmCreateSectionにまで進み、最終的にnt!MiValidateImageHeaderルーチンにまで進みます。ここから、nt!SeValidateImageHeaderラッパーとnt!SeValidateImageDataラッパーが順番に呼び出されます。各コールバックは、成功するとゼロを、それ以外の場合はゼロ以外の値を返すことになっています。

図1:ドライバーロード時のコールスタック

書籍『Rootkits and Bootkits: Reversing Modern Malware and Next Generation Threats』(76~78ページ)に、Windows 8における実装の変更点が記載されています。nt!g_CiEnabled変数が削除されたため、DSEの状態はCI!g_CiOptionsによってのみ決まります。また、CI.dllで提供されるインタフェースにコールバックがさらに追加されています。この書籍には記載されていませんが、ヘッダー検証が成功した場合はデータ検証コールバックが呼び出されないという変更点もあります。

Windows 8.1では、コールバック構造のシンボル名はnt!SeCiCallbacksに変更されています。

カーネルモードの署名ポリシー

署名の暗号妥当性を除き、Microsoftは相互認証に対応しているCAのみが署名証明書を発行できるとしています。これにより、攻撃者が独自のCA証明書を各マシンに簡単にインストールできないようにしています。

Windows 10 Redstone(2016年8月)以降、ドライバー署名ポリシーが変更になり、Microsoft自体による2番目の署名が必要になりました。そのためには、開発者がWebポータルで署名済みバイナリをアップロードしてMicrosoftに送信します。攻撃者の観点では、このことはペイロードを単にセキュリティ担当者に配布することを意味し、セキュリティ担当者が通常必要としていることの逆のことです。この新たな対策で脅威アクターが阻止されなかったという文書化されたケースが、少なくとも1つあります。

カーネルパッチの保護

2005年に初めて導入されたKPP(PatchGuard)は、Windowsのx64エディションが備えている機能であり、カーネルへのパッチ適用を阻止します。「カーネルへのパッチ適用」とは、ntoskrnl.exeのコード、ならびに他の重要なシステムドライバーやデータ構造(SSDT、IDT、GDTなど)のコードの修正のことです。

KPPによって、これらの保護領域が修正されていないことが定期的にチェックされます。修正が検知されると、BSODがトリガーされて、基本的にシステムが停止されます。PatchGuardは新規Windowsリリースのたびに更新されるため、全バージョンに通用する迂回手法を攻撃者が開発することが非常に難しくなっています。

ntoskrnl.exeのコールバック構造およびCI!g_CiOptions変数は、それぞれWindows 8以降およびWindows 8.1以降でPatchGuardによって保護されています。

活発なDSE改ざん

j00ruはnt!g_CiEnabledやCI!g_CiOptionsなどのプライベートシンボルを上書きするのが比較的難しくなると結論を下していますが、これはまさに攻撃者が選択してきた方向です。悪名高いTurla APTはこのような手法を開発したことで知られており、セキュリティリサーチャーはTurla APTのコードをリバースエンジニアリングして公開しています。

これらのプライベートシンボルは単純なパターン照合で検知でき、すべてのWindowsバージョンでほぼ同じです。攻撃者は、フラグを上書きしたら、未署名のドライバーをロードする手順にすばやく進みます。ロード手順が完了したら、フラグを元の状態に戻します。PatchGuardによって適用される制限では、このような瞬時の変更は阻止されないうえ、存続期間が短いため検知されません。

攻撃者が書き込みプリミティブを取得するために用いる一般的な手法は、ユーザーとカーネルモードコードの間のセキュリティ境界を取り除く、脆弱性(または単に不適切に作成されたコード)が含まれたサードパーティ製ドライバーを利用するというものです。攻撃者は通常、標的のマシン上で実行されているドライバーを利用するのではなく、このような脆弱性が含まれたドライバーを利用します。考慮の末の妥協点は、まずユーザーモードで管理権限を取得して、パッチの適用後も移植可能で持続可能な機能を持つというものです。

Microsoftのソリューション

この重大な攻撃パスを踏まえ、Microsoftでは3つの方法でこの問題に対処しています。

  1. 攻撃ベクトルの低減:即時のソリューション。ドライバーブロックリストを使用して書き込みプリミティブの取得を困難にします。
  2. 攻撃対象領域の縮小:中期的なソリューション。カーネルデータ保護を使用してCI!g_CiOptions変数に対する変更を阻止します。
  3. 攻撃対象領域の排除:長期的なソリューション。HyperVisor-protected Code Integrity(HVCI)によってまず検証することなく、コードがカーネル内で実行されるのを阻止します。

仮想化ベースのセキュリティ

VBSでは、ハードウェア仮想化機能を使用することで、メモリのセキュア領域を作成して通常のオペレーティングシステムから分離します。Windowsでは、この「仮想セキュアモード」を使用して多数のセキュリティソリューションをホストすることができ、これにより、オペレーティングシステム内の脆弱性に対する保護を大幅に高めて、保護を侵害するように試みる悪意のあるエクスプロイトの使用を阻止します。このアーキテクチャは、Windows 10の初期リリースで初めて導入されました。

VBS環境では、権限は仮想信頼レベル(VTL)に従って設定されます。通常のNTカーネルはVTL0と呼ばれる仮想環境で実行されるのに対して、セキュアなカーネルはVTL1と呼ばれる環境で実行されます。

仮想メモリ管理では、Secondary Level Address Translation(SLAT)のページテーブルが使用されます。プロセッサが、ゲスト仮想アドレス(GVA)と呼ばれる初期仮想アドレスを、ゲスト物理アドレス(GPA)と呼ばれる中間の物理アドレスに変換します。この変換は、ゲストOSページテーブルによって引き続き管理されます。ハイパーバイザーによって保持されているSLATページテーブルを使用して、プロセッサによって中間の物理アドレスをマシン物理アドレス(ホスト物理アドレス(HPA)またはシステム物理アドレス(SPA)とも呼ばれます)に変換する必要があります。

図2:通常のPTEとSLAT PTEの違いを表示

x86アーキテクチャでは、SLAT PTE(ページテーブルエントリ)が通常のPTEとは異なる方法で権限を処理します。書き込み / 読み取り権限は別個です。実行権限は明示的に設定する必要があり、リング0(カーネルモード)にのみ付与できます。ハイパーバイザーは、SLATページテーブルを使用してVTL間に分離を適用し、VTL1でアクセスできるようにします。そのため、セキュアなカーネルがこれを使用してVBS機能を実装し、ハイパーバイザー自体が機能そのもののコードやロジックを実装することがありません。

HyperVisor-protected Code Integrity

HVCI(元々の名称はDevice Guard)はVBSの導入と同時にリリースされました。このHVCIは、整合性適用のもう一つの層です。

新しいドライバーがロードされると、セキュアなカーネルもトリガーされて、コード整合性ライブラリSKCI.dll(Secure Kernel Code Integrity)の独自インスタンスを使用します。デジタル署名が検証されて、Secure World(VTL1)の現在のポリシー内で許可されているかどうかがチェックされます。これが行われた後にのみ、対応するGPAのSLATページテーブルに実行可能権限と書き込み不可権限が適用されます。その結果、NTカーネル(VTL0)では、セキュアなカーネルをプロセスに使用せずには、以前にロードされたコードの修正や新しいコードの実行を行うことができなくなります。

図3:Windows 10のSKCI.dllでのSkciInializeとそのCiOptions変数の逆アセンブリ

カーネルデータ保護

カーネルデータ保護(KDP)は、Windowsカーネル(OSコード自体)で実行されるドライバーとソフトウェアをデータ駆動型の攻撃から保護することを目的としており、Windows 10 20H1で初めて導入されました。

KDPを使用すると、カーネルモードで実行されているソフトウェアによって、読み取り専用メモリを静的に保護する(メモリイメージのセクション)ことも、動的に保護する(一回だけ初期化できるプールメモリ)ことも可能です。KDPは、ハイパーバイザーによって適用されるSLATページテーブルを使用して、保護対象のメモリ領域に対応しているGPAに対してVTL1で書き込み保護のみを確立します。この方法により、NTカーネル(VTL0)で実行されるソフトウェアは、メモリを変更するために必要となる権限を持つことができません。

KDPでは、保護対象領域のGVA範囲マッピングの変換方法は適用されません。開発者のブログに基づくと、KDPによって現在、保護対象のメモリ領域が適切なGPAに変換されていることは定期的にしか検証されていません。

Windows 11以降では、静的なKDP(MmProtectDriverSection API)を使用して強化するためにCI.dllが選択されており、関連するすべてのCIポリシー変数が「CiPolicy」という名前の個別セクションに配置されます。

図4:Windows 11のCI.dllでのCiInitializePolicyとCiPolicyセクションの逆アセンブリ

ドライバーのブロックリスト

ドライバーのブロックリストは、Windows Defender Application Control(WDAC)またはHVCIのポリシーに基づいて適用されます。さまざまな発行物によると、複数のサードパーティセキュリティ製品ベンダーもこの手法を採用しています。最新のブロックリストはこちらを参照してください。

ブロックリストに登録すると、攻撃者によるカーネルの書き込みプリミティブへの簡単なアクセスが拒否されます。この手法は非常に効果的ですが、プロアクティブな対策ではありません。以前に検知されたドライバーにのみ対処できます。そのため、この減災策はドライバー内のゼロデイ脆弱性に対しては効果がなく、セキュリティ担当者はこのようなゼロデイ攻撃を絶えず追跡する必要があるため、常に攻撃者の後手に回ることになります。

攻撃者の新たなノウハウ

前述の内容から、HVCIを有効にしていない場合は、コードのパッチ適用なしにDSEを改ざんできることがお分かりいただけるでしょう。

手法1:「ページのスワップ」

CI!CiOptionsへの書き込みが不可能になったからといって、CI!CiOptionsの値を変更できなくなるわけではありあせん。変数には仮想アドレスから引き続きアクセスでき、毎回、物理アドレスへの変換プロセスが実行されます。そのため、代わりに変換結果を変更します。

KDP保護対象のページから独自のページに物理ページをスワップして、メモリの制御を完全に取り戻します。GPAをスワップした場合、PTE(ページテーブルエントリ)内のPFN(ページフレーム番号)が変更されるだけで、基本的に別のポインターであるだけです。

特定の仮想アドレスに対してPTEの仮想アドレスを計算して、すべてのページテーブルを毎回トラバーサルするのを回避することができます。ページテーブルは、「PTE空間」と呼ばれるページング構造を管理するためにWindowsカーネルが使用する、仮想メモリの領域内にあります。PTEベースは、Windows 10 Redstone以降は、KASLR(Kernel Address Space Layout Randomization)によってランダム化されます。以前の調査で、これを確実に見つける方法を紹介しました。

この方法を行うには、書き込みプリミティブに加えて、カーネル読み取りプリミティブとメモリ割り当てプリミティブが必要です。以下に、C疑似コードを使用したステップバイステップの実装を示します。

図5:ページスワップ用のC疑似コード

ユーザー空間のページを使用できるため、カーネルメモリ割り当てプリミティブが冗長になります。CI.dllの場合、ページをコピーするのではなく、変数のデフォルト値を使用することが可能です。その結果、必要なごく少数のPET読み取りだけが残るため、カーネル空間からの読み取りの数が最小限にまで大幅に抑えられます。

手法2:「コールバックのスワップ」

ページのスワップにはカーネル読み取りプリミティブも必要であるため、KDPによってDSE改ざんのハードルが高くなったように見えます。CI.dllとntoskrnl.exeがどのように統合されているのかを再確認してから、「なぜこの段階に至ってもCI.dllを使用するのか。CI!CiValidateImageHeaderの代わりに、独自のコールバックを使用してみよう」と考えました。

図6:コールバックのスワップの概念図

フォーティネットのPoCは、カーネル空間のデータを読み取らずに、必要なすべてのアドレスを見つけることが実現可能であることを実証しています。以下に、全般の詳細を示します。

まず、ntoskrnl.exeでコールバック構造を見つけます。見つかった構造はパラメータとしてCI!CiInitializeへ渡されるため、この呼び出しからそのアドレスを取得できます。カーネルは関数を1回だけ呼び出すため、そのインポートテーブルエントリを使用するCALL命令またはJMP命令を探します。呼び出しサイトを見つけたら、「データ」セクション内の初期化されていないメモリを指しているパラメータのレジスタの割り当てに戻ります。

図7:Windows 11のntoskrnl.exeでのSepInitializeCodeIntegrityとSeCiCallbacksの逆アセンブリ

次に、使用する置換コールバック関数を探します。パラメータをとらずにゼロを返す関数が必要です。ntoskrnl.exeでは、FsRtlSyncVolumesやZwFlushInstructionCacheなど、この手法に適したエクスポート済み関数がいくつかあるため、GetProcAddressへの単純な呼び出しだけで、これを行うことができます。

最後に、元に戻す最初のコールバック関数を見つけます。コールバックはCI!CipInitializeによって構造内に設定されるため、構造にすべてのコールバックへの参照が設定されます。すべてのWindowsビルドに渡って、すべてのコールバックが1つの基本コードブロック内に設定されます。図8に示すように、命令のこのパターンを探して、lea命令からオフセットを抽出します。このオフセットが実際に関数に至っていることを検証するために、PEの例外ディレクトリをトラバースして、同じ開始アドレスを持つRUNTIME_FUNCTIONエントリを探します。

図8:Windows 11のCI.dllでのCipInitializeの逆アセンブリ

ntoskrnl.exeはコールバック構造では選択されていないため、KDPの保護は「意図的に」迂回されます。コールバックを変更する別の利点として、文書化されたクエリシステム情報APIでは、改ざんが可視化されないことがあります。

アドレス解決には追加のコード行がいくつか必要になる可能性がありますが、アドレス解決はユーザー空間で行われるため、現在の有名なDSE改ざん手法の場合と同じように、カーネル書き込みプリミティブのみ必要です。PoCではカーネル空間に8バイト(64ビットポインターのサイズ)の書き込みを使用していますが、代わりにCI.dllでコールバックのターゲットを見つけて、この値を減らすことができます。このブログの作成中に、TrustedSecのAdam Chesterが本件に関する独自の調査結果を公開しており、その中でAdam Chesterは、作成したバイナリシグネチャをスキャンして元のコールバックを見つけるという別の手法を選択しています。

結論

減災策

HVCIはすべての改ざん手法に対応しており、ドライバーのロード時に独自の検証が実行されます。HVCIは長年に渡って提供されていますが、新しいWindowsシステム上でデフォルトで有効化されるようになったのはごく最近です。これを踏まえて、代替案を1つ検討してみました。

ドライバーのロード中にDSEの状態を確認する方法がないかを検討しました。さらに、手順をブロックする方法がないかを検討しました。DSEの状態を可視化する方法をこの時点で明確にしておく必要があります。セキュリティ担当者は、攻撃者が内部変数を見つけるために使用するのと同じ戦略をそのまま利用できます。結局のところ、この方法が確実であることが証明されています。

改ざんされた状態が続くのは瞬時だけであるため、実行を開始するときにはシステムの状態は有効になっていると想定するのが妥当です。この時点で、内部変数のコピーが保持されます。

ドライバーのロードを検知するための選択肢は、いくつかあります。

  1. NtLoadDriver APIでユーザー空間にフックを配置する。
  2. レジストリコールバックを使用して、ドライバーのレジストリキーパスに対する操作を監視する。
  3. ドライバーファイルのセクションの作成に、ファイルシステムのミニフィルターコールバック(IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION)を使用する。

ドライバーのロードの一環としてコールバックがトリガーされたかを把握するには、現在のプロセスがSYSTEMであり、コールスタックの起点がntoskrnl.exeで、他のドライバーではないことを確認する必要があります。

ドライバーのロードが検知されると、変数に変更がないかが確認されてDSEの改ざんが検知されます。この時点で、エラー状態コードを発行して入出力要求をブロックするか、変数を保存された状態に戻すだけで、阻止することが可能です。

終わりに

このブログでは、Microsoftが実行時にDSEをどのように保護しているのか、その試みを説明し、DSEを改ざんして未署名のドライバーをロードする2つの新たな手法について解説しました。

HVCIは最も堅牢なソリューションを提供しますが、実行時にDSEの改ざんを検知して防ぐ、HVCI以外の手法についても説明しました。攻撃者と同じ前提を用いると、他の既存の保護を使用するよりも成功を収めることができます。

カーネルモードでコードを実行するという手法は、ハイパーバイザー、UEFI、SMMなどのマシン上で上位の権限を持つ要素を侵害したり、エンドポイントセキュリティ製品を侵害したりする足掛かりとなるため、攻撃者にとって依然として魅力的です。ドライバーのブロックリスト登録のために、TTPに変化が生じて、パッチが適用されていない脆弱性に対してカーネルのワンデイエクスプロイトを攻撃者がより頻繁に使用するようになる可能性があります。

ハードウェアを活用したセキュリティ機能がより一般的になっており、こうしたセキュリティ機能を利用するというMicrosoftの取り組みと連動して、脅威アクターによるDSE改ざんの利用は近い将来になくなる可能性が高いです。ただし、コンフィギュレーションミスやレガシーシステムは今後もある程度残るため、この攻撃経路がまったく使用されなくなるということはありません。そのため、このブログで提案しているソリューションを採用することを強くお勧めします。

フォーティネットのソリューション

FortiEDRは、図9に示すように、悪意のあるアクティビティを特定する実行後防止エンジンを使用して、これらの手法を検知およびブロックします。事前知識や特別なコンフィグレーションは一切不要で、すぐに活用できます。

図9:DSE改ざんが発生したら、該当するドライバーロードをFortiEDRがブロック