Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
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/__exceptbloků - 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.