バッファの処理
ドライバー内で最も一般的なエラーの 1 つは、バッファーが無効または小さすぎるバッファー処理に関連します。 これらのエラーにより、バッファーオーバーフローが発生したり、システムクラッシュが発生したりして、システムのセキュリティが損なわれる可能性があります。 この記事では、バッファー処理に関する一般的な問題とその回避方法について説明します。 また、適切なバッファー処理手法を示す WDK サンプル コードも識別します。
バッファーの種類と無効なアドレス
ドライバーの観点からは、バッファーには次の 2 種類のいずれかが含まれます。
ページ バッファー。メモリ内に存在する場合と存在しない場合があります。
非ページ バッファー。メモリ内に常駐する必要があります。
無効なメモリ アドレスはページングも非ページもされません。 オペレーティング システムが正しくないバッファー処理によって発生したページ エラーを解決するには、次の手順を実行します。
無効なアドレスは、"標準" アドレス範囲 (ページングされたカーネル アドレス、非ページ カーネル アドレス、またはユーザー アドレス) のいずれかに分離されます。
適切な種類のエラーが発生します。 システムは常に、PAGE_FAULT_IN_NONPAGED_AREAなどのバグ チェック、または STATUS_ACCESS_VIOLATION などの例外によってバッファー エラーを処理します。 エラーがバグ チェックの場合、システムは操作を停止します。 例外の場合、システムはスタック ベースの例外ハンドラーを呼び出します。 例外ハンドラーのいずれも例外を処理しない場合、システムはバグ チェックを呼び出します。
アプリケーション プログラムが呼び出すことができるアクセス パスに関係なく、ドライバーがバグ チェックにつながる原因となるアクセス パスは、ドライバー内のセキュリティ違反です。 このような違反により、アプリケーションはシステム全体にサービス拒否攻撃を引き起こすことができます。
一般的な前提条件と間違い
この領域での最も一般的な問題の 1 つは、ドライバー作成者が動作環境についてあまりにも多くのことを想定していることです。 いくつかの一般的な前提と間違いは次のとおりです。
ドライバーは、アドレスに上位ビットが設定されているかどうかを確認するだけです。 固定ビット パターンに依存してアドレスの種類を特定すると、すべてのシステムまたはシナリオで機能するわけではありません。 たとえば、システムが ギガバイト チューニング (4GT) を使用している場合、このチェックは x86 ベースのコンピューターでは機能しません。 4GT が使用されている場合、ユーザー モード アドレスは、アドレス空間の 3 番目のギガバイトの上位ビットを設定します。
ProbeForRead と ProbeForWrite のみを使用してアドレスを検証するドライバー。 これらの呼び出しにより、プローブ時にアドレスが有効なユーザー モード アドレスであることが保証されます。 ただし、プローブ操作後もこのアドレスが有効なままであるという保証はありません。 したがって、この手法では、再現できない定期的なクラッシュにつながる可能性がある微妙な競合状態が発生します。
ProbeForRead と ProbeForWrite 呼び出しは引き続き必要です。 ドライバーがプローブを省略した場合、ユーザーは有効なカーネル モード アドレスを渡すことができます。
__try
と__except
ブロック (構造化例外処理) ではキャッチされないため、大きなセキュリティ ホールが開きます。要するに、プローブと構造化の両方の例外処理が必要です。
プローブでは、アドレスがユーザー モード アドレスであり、バッファーの長さがユーザー アドレス範囲内にあることを検証します。
__try/__except
は、アクセスから保護をブロックします。
ProbeForRead では、メモリ アドレスが有効かどうかではなく、アドレスと長さが可能なユーザー モード のアドレス範囲内 (たとえば、4GT ではないシステムの場合は 2 GB 未満) にあることだけが検証されます。 これに対し、 ProbeForWrite は、指定された長さの各ページの最初のバイトにアクセスして、これらのバイトが有効なメモリ アドレスであることを確認しようとします。
アドレスが有効であることを確認するために、 MmIsAddressValid などのメモリ マネージャー関数に依存するドライバー。 プローブ機能について説明したように、この状況では、再現不可能なクラッシュにつながる可能性のある競合状態が発生します。
ドライバーが構造化例外処理を使用できない。 コンパイラ内の
__try/except
関数は、例外処理にオペレーティング システム レベルのサポートを使用します。 カーネル レベルの例外は、 ExRaiseStatus または関連する関数の 1 つを呼び出してシステムにスローされます。 例外を発生させる可能性のある呼び出しに関して構造化例外処理を使用できないドライバーは、バグ チェック (通常はKMODE_EXCEPTION_NOT_HANDLED) につながります。エラーが発生することが予想されないコードに対して構造化例外処理を使用するのは間違いです。 この使用は、それ以外の場合に見つかる実際のバグを隠すだけです。
__try/__except
ラッパーをルーチンの最上位ディスパッチ レベルに配置することは、この問題の正しい解決策ではありません。ただし、ドライバーライターが試した反射ソリューションである場合もあります。ユーザー メモリの内容が安定していることを前提とするドライバー。 たとえば、ドライバーがユーザー モードのメモリ位置に値を書き込み、その後、そのメモリの場所を参照する同じルーチンで後でとします。 悪意のあるアプリケーションは、書き込み後にそのメモリを積極的に変更し、その結果、ドライバーがクラッシュする可能性があります。
ファイル システムの場合、通常、ファイル システムはユーザー バッファー (METHOD_NEITHER 転送方法) に直接アクセスすることに依存するため、これらの問題は深刻です。 このようなドライバーはユーザー バッファーを直接操作するので、オペレーティング システム レベルのクラッシュを回避するため、バッファー処理の予防的な方法を組み込む必要があります。 高速 I/O は常に生メモリ ポインターを渡すので、高速 I/O がサポートされている場合、ドライバーは同様の問題に対して保護する必要があります。
バッファー処理のサンプル コード
WDK には、 fastfat および CDFS ファイル システム ドライバーのバッファー検証の例が多数含まれています サンプル コードは次のとおりです。
fastfat\deviosup.c の FatLockUserBuffer 関数は、MmProbeAndLockPages を使用してユーザー バッファーの背後にある物理ページをロックダウンし、 FatMapUserBuffer でmmGetSystemAddressForMdlSafeをロックダウンするページの仮想マッピングを作成します。
fastfat\fsctl.c の FatGetVolumeBitmap 関数は、ProbeForRead と ProbeForWrite を使用して最適化 API のユーザー バッファーを検証します。
cdfs\read.c の CdCommonRead 関数は、コードの
__try
と__except
を使用してユーザー バッファーを 0 にします。 CdCommonReadのサンプル コードは、try
キーワードとexcept
キーワードを使用するように見えます。 WDK 環境では、C のこれらのキーワードは、コンパイラ拡張機能の__try
と__except
の観点から定義されます。 C++ コードを使用する場合は、ネイティブ コンパイラ型を使用して例外を適切に処理する必要があります。__try
は C++ キーワードですが、C キーワードではなく、カーネル ドライバーでは無効な形式の C++ 例外処理を提供するためです。