Sdílet prostřednictvím


Přístupové objekty uživatelského režimu

Přístupové objekty uživatelského režimu (UMA) jsou sada DDI navržených pro bezpečný přístup k paměti uživatelského režimu z kódu v režimu jádra. Tyto identifikátory DDI řeší běžné chyby zabezpečení a chyby programování, ke kterým může dojít, když ovladače režimu jádra přistupují k paměti uživatelského režimu.

Kód v režimu jádra, který přistupuje k paměti uživatelského režimu a manipuluje s ním, bude brzy nutné použít technologii UMA.

Možné problémy při přístupu k paměti uživatelského režimu z režimu jádra

Když kód v režimu jádra potřebuje přístup k paměti v uživatelském režimu, vzniká několik problémů:

  • Aplikace v uživatelském režimu můžou předat škodlivé nebo neplatné ukazatele na kód režimu jádra. Nedostatek správného ověření může vést k poškození paměti, chybovému ukončení nebo ohrožení zabezpečení.

  • Kód v uživatelském režimu je vícevláknový. V důsledku toho mohou různá vlákna měnit stejnou paměť v uživatelském režimu mezi samostatným přístupem k režimu jádra, což může vést k poškození paměti jádra.

  • Vývojáři v režimu jádra často zapomenou zkontrolovat paměť v uživatelském režimu před přístupem, což je bezpečnostní problém.

  • Kompilátory předpokládají provádění s jedním vláknem a můžou optimalizovat to, co vypadá jako redundantní přístup k paměti. Programátoři, kteří si takové optimalizace neuvědomují, mohou psát nebezpečný kód.

Tyto problémy ilustrují následující fragmenty kódu.

Příklad 1: Možné poškození paměti kvůli vícevláknům v uživatelském režimu

Kód v režimu jádra, který potřebuje přístup k paměti v uživatelském režimu, musí být proveden v rámci bloku, aby se zajistilo, že je paměť platná. Následující fragment kódu ukazuje typický vzor pro přístup k paměti v uživatelském režimu:

// 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
    }
}

Tento fragment kódu nejprve testuje paměť, což je důležitý první, ale často přehlédnutelný krok.

Jeden problém, který může nastat v tomto kódu, je však způsoben multithreadingem v uživatelském režimu. Konkrétně se Ptr->Size může změnit po volání ExAllocatePool2 , ale před voláním RtlCopyMemory, což může vést k poškození paměti v jádru.

Příklad 2: Možné problémy kvůli optimalizaci kompilátoru

Pokus o vyřešení problému s více vlákny v příkladu 1 může být zkopírování Ptr->Size do místní proměnné před přidělením a kopírováním:

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 (…) {}
}

I když tento přístup zmírní problém způsobený multithreadingem, stále není bezpečný, protože kompilátor nezná více vláken, a proto předpokládá jedno vlákno provádění. Při optimalizaci může kompilátor zjistit, že již má na svém zásobníku kopii hodnoty, na kterou Ptr->Size ukazuje, a proto neprovedeme kopii do LocalSize.

Řešení přístupových prvků v uživatelském režimu

Rozhraní UMA řeší problémy, ke kterým došlo při přístupu k paměti uživatelského režimu z režimu jádra. UMA poskytuje:

  • Automatické sondování: Explicitní sonda (ProbeForRead/ProbeForWrite) se už nevyžaduje, protože všechny funkce UMA zajišťují bezpečnost adres.

  • Nestálý přístup: Všechny rozhraní UMA DDI používají nestálou sémantiku, aby se zabránilo optimalizaci kompilátoru.

  • Jednoduchost přenositelnosti: Komplexní sada DDI UMA usnadňuje zákazníkům portovat stávající kód, aby používal rozhraní UMA DDI a zajistil tak bezpečný a správný přístup k paměti v uživatelském režimu.

Příklad použití UMA DDI

Pomocí dříve definované struktury uživatelského režimu ukazuje následující fragment kódu, jak pomocí UMA bezpečně přistupovat k paměti v uživatelském režimu.

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 (…) {}
}

Implementace a využití UMA

Rozhraní UMA se dodává jako součást sady Windows Driver Kit (WDK):

  • Deklarace funkce jsou nalezeny v souboru hlavičky usermode_accessors.h .
  • Implementace funkcí se nacházejí ve statické knihovně s názvem umaccess.lib.

UMA funguje ve všech verzích Windows, nejen na nejnovější verzi. K získání deklarací funkcí a implementací z usermode_accessors.h a umaccess.lib je potřeba použít nejnovější sadu WDK. Výsledný ovladač bude fungovat správně ve starších verzích Windows.

Umaccess.lib poskytuje bezpečnou, nižší implementaci pro všechny DDI. Ve verzích jádra Windows s podporou UMA budou ovladače mít všechny své funkce přesměrovány na bezpečnější verzi implementovanou v ntoskrnl.exe.

Všechny funkce přístupového objektu v uživatelském režimu se musí spouštět v rámci strukturované obslužné rutiny výjimek (SEH) kvůli potenciálním výjimkám při přístupu k paměti v uživatelském režimu.

Typy přístupových funkcí DDI v režimu uživatele

Technologie UMA poskytuje různé DDI pro různé typy přístupu k paměti v uživatelském režimu. Většina těchto DDI je určená pro základní datové typy, jako jsou BOOLEAN, ULONG a ukazatele. Kromě toho UMA poskytuje DDI pro hromadný přístup k paměti, načtení délky řetězců a vzájemně uzamčené operace.

Obecné DDI pro základní datové typy

UMA poskytuje šest variant funkcí pro čtení a zápis jednoduchých datových typů. Například pro hodnoty BOOLEAN jsou k dispozici následující funkce:

Název funkce Description
ReadBooleanFromUser Čtení hodnoty z paměti v uživatelském režimu
ReadBooleanFromUserAcquire Čtení hodnoty z paměti v uživatelském režimu s získáním sémantiky pro řazení paměti.
ReadBooleanFromMode Čtení z paměti v uživatelském režimu nebo v režimu jádra na základě parametru režimu.
WriteBooleanToUser Zapište hodnotu do paměti v uživatelském režimu.
WriteBooleanToUserRelease Zapište hodnotu do paměti v uživatelském režimu s uvolňovací sémantikou pro řazení paměti.
WriteBooleanToMode Zápis do paměti v uživatelském režimu nebo v režimu jádra na základě parametru režimu.

Pro funkce ReadXxxFromUser musí parametr Source odkazovat na virtuální adresní prostor v uživatelském režimu (VAS). Totéž platí ve verzích ReadXxxFromMode při Mode == UserMode.

Pro čteníXxxFromMode, když Mode == KernelModemusí parametr Source odkazovat na VAS v režimu jádra. Pokud je definice DBG preprocesoru definovaná, operace rychle selže s kódem FAST_FAIL_KERNEL_POINTER_EXPECTED.

Ve funkcích WriteXxxToUser musí parametr Destination odkazovat do virtuálního adresního prostoru (VAS) v uživatelském režimu. Totéž platí ve verzích WriteXxxToMode , když Mode == UserMode.

Rozhraní DDI pro manipulaci s kopírováním a pamětí

UMA poskytuje funkce pro kopírování a přesouvání paměti mezi režimy uživatele a jádra, včetně variant pro netemporální a zarovnané kopie. Tyto funkce jsou označené poznámkami označujícími potenciální výjimky SEH a požadavky IRQL (maximální APC_LEVEL).

Mezi příklady patří CopyFromUser, CopyToMode a CopyFromUserToMode.

Makra, jako je CopyFromModeAligned a CopyFromUserAligned , zahrnují před provedením operace kopírování zarovnání pro testování bezpečnosti.

Makra, jako je CopyFromUserNonTemporal a CopyToModeNonTemporal, poskytují netemporální kopie, které zabraňují znečištění mezipaměti.

Makra pro čtení/zápis struktury

Makra pro čtení a zápis struktur mezi režimy zajišťují kompatibilitu typů a zarovnání, volání pomocných funkcí s parametry velikosti a režimu. Mezi příklady patří WriteStructToMode, ReadStructFromUser a jejich zarovnané varianty.

Funkce pro vyplnění a nulování paměti

DDI jsou k dispozici k vyplnění nebo vynulování paměti v uživatelských nebo režimových adresních prostorech s parametry určujícími cíl, délku, hodnotu vyplnění a režim. Tyto funkce také obsahují poznámky SEH a IRQL.

Mezi příklady patří FillUserMemory a ZeroModeMemory.

Zamknuté operace

UMA zahrnuje vzájemně uzamčené operace pro přístup k atomické paměti, které jsou nezbytné pro manipulaci s pamětí bezpečnou pro přístup z více vláken v souběžných prostředích. DDI jsou k dispozici pro 32bitové i 64bitové hodnoty s verzemi, které cílí na paměť uživatele nebo režimu.

Mezi příklady patří InterlockedCompareExchangeToUser, InterlockedOr64ToMode a InterlockedAndToUser.

Délka řetězce DDI

Funkce pro bezpečné určování délek řetězců z uživatelské paměti nebo paměti režimu jsou zahrnuty a podporují jak ANSI řetězce, tak řetězce s širokými znaky. Tyto funkce jsou navržené tak, aby vyvolaly výjimky při nebezpečném přístupu k paměti a jsou omezené technologií IRQL.

Mezi příklady patří StringLengthFromUser a WideStringLengthFromMode.

Velké celé číslo a přístupové objekty řetězce Unicode

UMA poskytuje DDIs pro čtení a zápis typů LARGE_INTEGER, ULARGE_INTEGER a UNICODE_STRING mezi pamětí uživatele a režimovou pamětí. Varianty mají získávací a uvolňovací sémantiku s parametry režimu pro bezpečnost a správnost.

Mezi příklady patří ReadLargeIntegerFromUser, WriteUnicodeStringToMode a WriteULargeIntegerToUser.

Získání a uvolnění sémantiky

V některých architekturách, jako je ARM, může procesor změnit pořadí přístupu k paměti. Tato obecná rozhraní DDI mají implementaci získání/uvolnění, pokud potřebujete záruku, že přístupy k paměti nebudou pro přístup v uživatelském režimu přeuspořádány.

  • Získání sémantiky brání změna pořadí zatížení vzhledem k jiným operacím paměti.
  • Sémantika vydávání verzí zabraňuje změnu pořadí úložiště vzhledem k jiným operacím paměti.

Příklady sémantiky akvizice a uvolnění v UMA zahrnují ReadULongFromUserAcquire a WriteULongToUserRelease.

Další informace naleznete v tématu Získání a vydání sémantiky.

Osvědčené postupy

  • Při přístupu k paměti uživatelského režimu z kódu jádra vždy používejte rozhraní UMA DDI.
  • Ošetřete výjimky pomocí příslušných __try/__except bloků
  • Použijte rozhraní DDI založená na režimu , pokud váš kód může zpracovávat paměť v uživatelském režimu i v režimu jádra.
  • Pokud je pro váš případ použití důležité řazení paměti, zvažte sémantiku získání/uvolnění.
  • Ověřte kopírovaná data po zkopírování do paměti jádra, abyste zajistili konzistenci.

Podpora budoucího hardwaru

Přístupové objekty uživatelského režimu jsou navržené tak, aby podporovaly budoucí funkce hardwarového zabezpečení, jako jsou:

  • SMAP (Ochrana přístupu v režimu správce): Zabraňuje kódu jádra v přístupu k paměti v uživatelském režimu s výjimkou určených funkcí, jako jsou DDI UMA.
  • ARM PAN (Privileged Access Never): Podobná ochrana v architekturách ARM.

Díky konzistentnímu používání rozhraní UMA DDI budou ovladače kompatibilní s těmito vylepšeními zabezpečení, pokud jsou povoleny v budoucích verzích Windows.