バッファー処理

おそらく、ドライバー内での最も一般的なエラーは、バッファーが無効であるか小さすぎる場合のバッファー処理に関するものです。 これらのエラーにより、バッファー オーバーフローやシステム クラッシュが発生する可能性があり、システムのセキュリティ侵害をもたらします。

ドライバーの観点からは、バッファーには次の 2 つの種類があります。

  • ページ バッファー。メモリ内に存在する場合と存在しない場合があります。

  • 非ページ バッファー。メモリ内に常駐している必要があります。

もちろん、無効なアドレスはページでも非ページでもありませんが、オペレーティング システムは、このようなバッファーによって発生するページ フォールトの解決を始めるとき、無効なアドレスを "標準" アドレス範囲 (ページ カーネル アドレス、非ページ カーネル アドレス、またはユーザー アドレス) のいずれかに分離して、適切な種類のエラーを発生させます。 バッファー エラーは常に、バグ チェック (PAGE_FAULT_IN_NONPAGED_AREA など) または例外 (STATUS_ACCESS_VIOLATION など) によって処理されます。 バグ チェックの場合、システムは動作を停止します。 例外の場合は、スタック ベースの例外ハンドラーが呼び出され、そのいずれでも例外が処理されない場合は、バグ チェックが呼び出されます。

いずれにしても、ドライバーでバグ チェックが発生する原因となる、アプリケーション プログラムによって呼び出される可能性があるすべてのアクセス パスは、ドライバー内でのセキュリティ違反です。 これにより、アプリケーションでシステム全体に対するサービス拒否攻撃を引き起こすことができます。

この領域での最も一般的な問題の 1 つは、ドライバー作成者が動作環境についてあまりにも多くのことを想定していることです。 そこに盛り込むべき項目として、以下のようなものがあります:

  • アドレスで上位ビットが設定されていることのチェック。 これは、システムが Boot.ini ファイルで /3GB オプションを設定することによって 4 ギガバイト チューニング (4GT) を使用している x86 ベースのコンピューターでは機能しません。 その場合、ユーザー モード アドレスでは、アドレス空間の 3 番目のギガバイト (GB) の上位ビットが設定されます。

  • アドレスを検証するための ProbeForReadProbeForWrite の使用。 これにより、プローブの時点でアドレスが有効なユーザー モード アドレスであることは保証されますが、プローブ操作の後も有効なままであることを要求するものは何もありません。 したがって、この手法では、再現できない定期的なクラッシュにつながる可能性がある微妙な競合状態が発生します。 ProbeForReadProbeForWrite の呼び出しは、それとは別の理由、つまりアドレスがユーザー モード アドレスかどうか、そしてバッファーの長さがユーザー アドレス範囲内にあることを検証するために必要ですです。 プローブを省略すると、ユーザーは有効なカーネル モード アドレスを渡すことができ、これは、__try および __except ブロック (構造化例外処理) によってキャッチされず、大きなセキュリティ ホールが開きます。 そのため、ProbeForReadProbeForWrite の呼び出しでは、アラインメント、およびユーザー モード アドレスと長さがユーザー アドレス範囲内にあることを、保証する必要があります。 ただし、アクセスから保護するには、__try と __except ブロックが必要です。

    ProbeForRead では、メモリ アドレスが有効かどうかではなく、アドレスと長さが可能なユーザー モード のアドレス範囲内 (たとえば、4GT ではないシステムの場合は 2 GB 未満) にあることだけが検証されます。 これに対し、ProbeForWrite では、指定された長さの各ページの最初のバイトにアクセスして、それらが有効なメモリ アドレスであることの検証が試みられます。

  • メモリ マネージャーの関数 (MmIsAddressValid など) に依存した、アドレスが有効であることの確認。 プローブ関数と同様に、これによって再現不可能なクラッシュにつながる可能性のある競合状態が発生します。

  • 構造化例外処理の不使用。 コンパイラ内の __try 関数と __except 関数は、例外処理にオペレーティング システム レベルのサポートを使います。 カーネル レベルの例外は、ExRaiseStatus または関連する関数の 1 つを呼び出すことによってスローされます。 ドライバーで、例外が発生する可能性のある呼び出しに対して構造化例外処理を使わないと、バグ チェック (通常は KMODE_EXCEPTION_NOT_HANDLED) につながります。

    エラーの発生が予想されないコードに対して構造化例外処理を使うのは間違いであることに注意してください。 これを行うと、それがなければ検出される実際のバグを覆い隠すだけです。 __try と __except ラッパーをルーチンの最上位のディスパッチ レベルに配置することは、この問題の正しい解決策ではありませんが、ドライバーの作成者によって試みられる Reflex ソリューションである場合があります。

  • ユーザー メモリの内容が安定していることへの依存。 たとえば、ドライバーがユーザー モード メモリの場所に値を書き込み、後から同じルーチンでそのメモリの場所を参照するとします。 悪意のあるアプリケーションがそのメモリをアクティブに変更し、その結果、ドライバーがクラッシュする可能性があります。

ファイル システムの場合は、通常、ユーザー バッファーへの直接アクセスに依存するため (METHOD_NEITHER 転送方法)、これらの問題は特に深刻です。 このようなドライバーはユーザー バッファーを直接操作するので、オペレーティング システム レベルのクラッシュを回避するため、バッファー処理の予防的な方法を組み込む必要があります。 高速 I/O は常に生メモリ ポインターを渡すので、高速 I/O がサポートされている場合、ドライバーは同様の問題に対して保護する必要があります。

WDK には、FASTFAT および CDFS ファイル システムのサンプル コードでのバッファー検証の例が多数含まれています。次はその例です。

  • fastfat\deviosup.c の FatLockUserBuffer 関数は、MmProbeAndLockPages を使ってユーザー バッファーの背後にある物理ページをロックダウンし、FatMapUserBufferMmGetSystemAddressForMdlSafe を使ってロックダウンされたページの仮想マッピングを作成します。

  • fastfat\fsctl.c の FatGetVolumeBitmap 関数は、ProbeForReadProbeForWrite を使って、最適化 API でユーザー バッファーを検証します。

  • cdfs\read.c の CdCommonRead 関数は、コードを囲む __try と __except を使って、ユーザー バッファーを 0 にします。 CdCommonRead のサンプル コードでは、try と except キーワードが使われていることに注意してください。 WDK 環境では、コンパイラ拡張機能の __try と __except に関して C のこれらのキーワードが定義されています。 __try は C++ のキーワードであって、C のキーワードではなく、カーネル ドライバーに対して有効ではない C++ の例外処理形式を提供するため、C++ のコードを使うユーザーは、ネイティブ コンパイラ型を使って例外を適切に処理する必要があります。