次の方法で共有


ユーザー モード アクセサー

ユーザー モード アクセサー (UMA) は、カーネル モード コードからユーザー モード メモリに安全にアクセスして操作するように設計された DDI のセットです。 これらの DDI は、カーネル モード ドライバーがユーザー モード メモリにアクセスするときに発生する可能性がある一般的なセキュリティの脆弱性とプログラミング エラーに対処します。

ユーザー モード メモリにアクセス/操作するカーネル モード コードは、間もなく UMA を使用する必要があります。

カーネル モードからユーザー モード メモリにアクセスするときに発生する可能性のある問題

カーネル モード コードがユーザー モード メモリにアクセスする必要がある場合、いくつかの課題が発生します。

  • ユーザー モード アプリケーションは、カーネル モード コードに悪意のあるポインターまたは無効なポインターを渡す可能性があります。 適切な検証がないと、メモリの破損、クラッシュ、またはセキュリティの脆弱性につながる可能性があります。

  • ユーザー モード コードはマルチスレッドです。 その結果、異なるスレッドによって、個別のカーネル モード アクセス間で同じユーザー モード メモリが変更され、カーネル メモリが破損する可能性があります。

  • カーネル モードの開発者は、多くの場合、アクセスする前にユーザー モードメモリをプローブすることを忘れています。これはセキュリティの問題です。

  • コンパイラはシングル スレッド実行を想定しており、冗長なメモリ アクセスと思われるものを最適化する可能性があります。 このような最適化を知らされていないプログラマは、安全でないコードを記述できます。

次のコード スニペットは、これらの問題を示しています。

例 1: ユーザー モードでのマルチスレッドによるメモリ破損の可能性

ユーザー モード メモリにアクセスする必要があるカーネル モード コードは、メモリが有効であることを確認するために、 __try/__except ブロック内で行う必要があります。 次のコード スニペットは、ユーザー モード メモリにアクセスするための一般的なパターンを示しています。

// User-mode structure definition
typedef struct _StructWithData {
    ULONG Size;
    CHAR* Data[1];
} StructWithData;

// Kernel-mode call that accesses user-mode memory
void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);

        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, Ptr->Size);
        
        // Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr->Data, Ptr->Size);
    } __except (…) {
        // Handle exceptions
    }
}

このスニペットは、最初にメモリをプローブします。これは重要な最初のステップですが、見落とされることが多い手順です。

ただし、このコードで発生する可能性がある問題の 1 つは、ユーザー モードでのマルチスレッドによるものです。 具体的には、Ptr->Size の呼び出し後、RtlCopyMemory の呼び出しの前に、が変更される可能性があります。カーネルのメモリ破損につながる可能性があります。

例 2: コンパイラの最適化が原因で発生する可能性のある問題

例 1 のマルチスレッドの問題に対処するには、割り当てとコピーの前に Ptr->Size をローカル変数にコピーすることが考えられます。

void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);
        
        // Read Ptr->Size once to avoid possible memory change in user mode
        ULONG LocalSize = Ptr->Size;
        
        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr, LocalSize);
    } __except (…) {}
}

この方法では、マルチスレッドによって発生する問題が軽減されますが、コンパイラは複数のスレッドを認識していないため、1 つの実行スレッドを想定しているため、安全ではありません。 最適化として、コンパイラは、 Ptr->Size がスタック上を指す値のコピーを既に持っているため、 LocalSizeへのコピーを行わない可能性があります。

ユーザー モード アクセサー ソリューション

UMA インターフェイスは、カーネル モードからユーザー モード メモリにアクセスするときに発生する問題を解決します。 UMA は次の機能を提供します。

  • 自動プローブ: 明示的なプローブ (ProbeForRead/ProbeForWrite) は不要になりました。すべての UMA 関数によってアドレスの安全性が確保されるためです。

  • 揮発性アクセス: すべての UMA DDI は、コンパイラの最適化を防ぐために揮発性セマンティクスを使用します。

  • 移植性の容易さ: 包括的な UMA DDI セットにより、お客様は既存のコードを簡単に移植して UMA DDI を使用できるようになり、ユーザー モード メモリに安全かつ正しくアクセスできるようになります。

UMA DDI の使用例

前に定義したユーザー モード構造を使用して、次のコード スニペットは、UMA を使用してユーザー モード メモリに安全にアクセスする方法を示しています。

void MySysCall(StructWithData* Ptr) {
    __try {

        // This UMA call probes the passed user-mode memory and does a
        // volatile read of Ptr->Size to ensure it isn't optimized away by the compiler.
        ULONG LocalSize = ReadULongFromUser(&Ptr->Size);
        
        // Allocate memory in the kernel.
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //This UMA call safely copies UM data into the KM heap allocation.
        CopyFromUser(&LocalData, Ptr, LocalSize);
        
        // To be safe, set LocalData->Size to be LocalSize, which was the value used
        // to make the pool allocation just in case LocalData->Size was changed.
        ((StructWithData*)LocalData)->Size = LocalSize;

    } __except (…) {}
}

UMA の実装と使用方法

UMA インターフェイスは、Windows Driver Kit (WDK) の一部として出荷されます。

  • 関数宣言は 、usermode_accessors.h ヘッダー ファイルにあります。
  • 関数の実装は、 umaccess.lib という名前のスタティック ライブラリにあります。

UMA は、最新バージョンだけでなく、すべてのバージョンの Windows で動作します。 usermode_accessors.humaccess.lib から関数の宣言と実装をそれぞれ取得するには、最新の WDK を使用する必要があります。 結果として得られるドライバーは、古いバージョンの Windows で正常に動作します。

Umaccess.lib は、すべての DDI に対して安全で下位レベルの実装を提供します。 Windows カーネルの UMA 対応バージョンでは、ドライバーのすべての機能が 、ntoskrnl.exeに実装された安全なバージョンにリダイレクトされます。

すべてのユーザー モード アクセサー関数は、ユーザー モード メモリにアクセスするときに例外が発生する可能性があるため、構造化例外ハンドラー (SEH) 内で実行する必要があります。

ユーザー モード アクセサー DDI の種類

UMA は、さまざまな種類のユーザー モード メモリ アクセスに対してさまざまな DDI を提供します。 これらの DDI のほとんどは、BOOLEAN、ULONG、ポインターなどの基本的なデータ型用です。 さらに、UMA は、一括メモリ アクセス、文字列長の取得、およびインターロック操作用の DDI を提供します。

基本的なデータ型のジェネリックDDI

UMA には、単純なデータ型を読み書きするための 6 つの関数バリアントが用意されています。 たとえば、ブール値には次の関数を使用できます。

関数名 Description
ユーザーからブール値を読み取る ユーザー モード メモリから値を読み取ります。
ReadBooleanFromUserAcquire メモリ順序付けの取得セマンティクスを使用して、ユーザー モード メモリから値を読み取ります。
ReadBooleanFromMode モード パラメーターに基づいて、ユーザー モードまたはカーネル モードのメモリから読み取ります。
WriteBooleanToUser ユーザー モード メモリに値を書き込みます。
WriteBooleanToUserRelease メモリ順序付けのリリース セマンティクスを使用して、ユーザー モード メモリに値を書き込みます。
WriteBooleanToMode mode パラメーターに基づいて、ユーザー モードまたはカーネル モード メモリに書き込みます。

XxxFromUser 関数の場合Source パラメーターはユーザー モード仮想アドレス空間 (VAS) を指す必要があります。 ReadXxxFromMode バージョンでも同じことが当てはまりますMode == UserMode

ReadXxxFromMode の場合、Mode == KernelMode場合、Source パラメーターはカーネル モード VAS をポイントする必要があります。 プリプロセッサ定義 DBG が定義されている場合、操作は即座に FAST_FAIL_KERNEL_POINTER_EXPECTED コードで失敗します。

WriteXxxToUser 関数では、Destination パラメーターがユーザー モード VAS を指している必要があります。 の際のバージョンでも同様です。

コピーとメモリ操作の DDI

UMA には、ユーザー モードとカーネル モードの間でメモリをコピーおよび移動するための関数が用意されています。これには、非一時コピーとアラインコピーのバリアントが含まれます。 これらの関数は、潜在的な SEH 例外と IRQL 要件 (最大APC_LEVEL) を示す注釈でマークされます。

たとえば、 CopyFromUserCopyToModeCopyFromUserToMode などがあります

CopyFromModeAlignedCopyFromUserAligned などのマクロには、コピー操作を実行する前の安全のための配置プローブが含まれます。

CopyFromUserNonTemporalCopyToModeNonTemporal などのマクロは、キャッシュの汚染を回避する非一時コピーを提供します。

構造体の読み取り/書き込みマクロ

モード間の構造体の読み取りと書き込みのマクロにより、型の互換性と配置が保証され、サイズとモードのパラメーターを持つヘルパー関数が呼び出されます。 たとえば、 WriteStructToModeReadStructFromUser、およびアラインされたバリアントがあります。

メモリを埋める関数とゼロクリア関数

DDI は、ユーザーまたはモードのアドレス空間内のメモリを埋めるかゼロにするために提供され、宛先、長さ、埋める値、およびモードを指定するパラメーターを使用します。 これらの関数には、SEH と IRQL の注釈も含まれます。

たとえば、 FillUserMemoryZeroModeMemory などがあります

連動操作

UMA には、アトミック メモリ アクセス用のインターロック操作が含まれています。これは、同時実行環境でのスレッド セーフなメモリ操作に不可欠です。 DDI は 32 ビットと 64 ビットの両方の値に対して提供され、バージョンはユーザーまたはモード のメモリを対象とします。

例としては 、InterlockedCompareExchangeToUserInterlockedOr64ToModeInterlockedAndToUser などがあります

文字列長のDDI

ユーザーまたはモード のメモリから文字列の長さを安全に判断する関数が含まれており、ANSI 文字列とワイド文字列の両方をサポートします。 これらの関数は、安全でないメモリ アクセスで例外を発生させるように設計されており、IRQL に制約があります。

たとえば、 StringLengthFromUserWideStringLengthFromMode などがあります

大きな整数および Unicode 文字列アクセサー

UMA は、ユーザー メモリとモード メモリの間のLARGE_INTEGER、ULARGE_INTEGER、およびUNICODE_STRING型の読み取りと書き込みを行う DDI を提供します。 バリアントには、安全性と正確性のためのモード パラメーターを持つ取得セマンティクスと解放セマンティクスがあります。

たとえば、 ReadLargeIntegerFromUserWriteUnicodeStringToModeWriteULargeIntegerToUser などがあります

セマンティクスの取得とリリース

ARM などの一部のアーキテクチャでは、CPU はメモリ アクセスの順序を変更できます。 ユーザー モード アクセスでメモリ アクセスが並べ替えられていないことを保証する必要がある場合、ジェネリック DDI はすべて Acquire/Release 実装を持ちます。

  • 取得セマンティクスにより、他のメモリ操作に対する負荷の順序が変更されないようにします。
  • リリースセマンティクスは、ストアを他のメモリ操作と比較して並べ替えることを防ぎます。

UMA の取得セマンティクスとリリース セマンティクスの例には、 ReadULongFromUserAcquireWriteULongToUserRelease があります。

詳細については、「 セマンティックの取得とリリース」を参照してください。

ベスト プラクティス

  • カーネル コードからユーザー モード メモリにアクセスするときは、常に UMA DDI を使用します。
  • 適切な ブロックで__try/__exceptします。
  • コードがユーザー モードとカーネル モードの両方のメモリを処理する可能性がある場合は、モード ベースの DDI を使用します。
  • ユース ケースでメモリの順序付けが重要な場合は、取得/解放セマンティクスを検討してください。
  • 一貫性を確保するために、コピーしたデータをカーネル メモリにコピーした後で検証します。

今後のハードウェア サポート

ユーザー モード アクセサーは、次のような将来のハードウェア セキュリティ機能をサポートするように設計されています。

  • SMAP (Supervisor Mode Access Prevention): カーネル コードが UMA DDI などの指定された関数を介してユーザー モード メモリにアクセスできないようにします。
  • ARM PAN (特権アクセスなし): ARM アーキテクチャでの同様の保護。

UMA DDI を一貫して使用することで、ドライバーは将来の Windows バージョンで有効になると、これらのセキュリティ強化と互換性があります。