Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Metody dostępu w trybie użytkownika (UMA) to zestaw interfejsów DDI zaprojektowanych do bezpiecznego dostępu do pamięci w trybie użytkownika i jej manipulacji z poziomu kodu w trybie jądra. Te identyfikatory DDI dotyczą typowych luk w zabezpieczeniach i błędów programowania, które mogą wystąpić, gdy sterowniki trybu jądra uzyskują dostęp do pamięci w trybie użytkownika.
Kod trybu jądra, który uzyskuje dostęp do pamięci w trybie użytkownika lub manipuluje nimi wkrótce będzie wymagany do korzystania z usługi UMA.
Możliwe problemy podczas uzyskiwania dostępu do pamięci trybu użytkownika z trybu jądra
Gdy kod trybu jądra musi uzyskać dostęp do pamięci w trybie użytkownika, pojawia się kilka wyzwań:
Aplikacje w trybie użytkownika mogą przekazywać złośliwe lub nieprawidłowe wskaźniki do kodu trybu jądra. Brak prawidłowej weryfikacji może prowadzić do uszkodzenia pamięci, awarii lub luk w zabezpieczeniach.
Kod trybu użytkownika jest wielowątkowy. W związku z tym różne wątki mogą modyfikować tę samą pamięć trybu użytkownika między oddzielnymi dostępami do trybu jądra, co może prowadzić do uszkodzenia pamięci jądra.
Deweloperzy trybu jądra często zapominają o sondowaniu pamięci w trybie użytkownika przed uzyskaniem do niej dostępu, co jest problemem z zabezpieczeniami.
Kompilatory zakładają wykonywanie w trybie jednowątkowym i mogą zoptymalizować to, co wydaje się nadmiernym dostępem do pamięci. Programiści nieświadomi takich optymalizacji mogą pisać niebezpieczny kod.
Poniższe fragmenty kodu ilustrują te problemy.
Przykład 1: Możliwe uszkodzenie pamięci z powodu wielowątkowości w trybie użytkownika
Kod, który działa w trybie jądra i musi uzyskać dostęp do pamięci w trybie użytkownika, powinien to zrobić w bloku __try/__except, aby upewnić się, że pamięć jest prawidłowa. Poniższy fragment kodu przedstawia typowy wzorzec uzyskiwania dostępu do pamięci w trybie użytkownika:
// 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
}
}
Ten fragment kodu najpierw sonduje pamięć, co jest ważnym, ale często pomijanym krokiem.
Jednak jednym z problemów, które mogą wystąpić w tym kodzie, jest wielowątkowość w trybie użytkownika. W szczególności Ptr->Size może ulec zmianie po wywołaniu funkcji ExAllocatePool2, ale przed wywołaniem funkcji RtlCopyMemory, co potencjalnie prowadzi do uszkodzenia pamięci w jądrze.
Przykład 2: Możliwe problemy z powodu optymalizacji kompilatora
Próba rozwiązania problemu z wielowątkowością w przykładzie 1 mogłoby polegać na skopiowaniu Ptr->Size do zmiennej lokalnej przed alokacją i kopiowaniem.
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 (…) {}
}
Chociaż takie podejście ogranicza problem spowodowany przez wielowątkowość, nadal nie jest bezpieczne, ponieważ kompilator nie zna wielu wątków, a tym samym zakłada jeden wątek wykonywania. W ramach optymalizacji kompilator może zauważyć, że na jego stosie znajduje się już kopia wartości, na którą wskazuje Ptr->Size, i dlatego nie wykonuje kopiowania do LocalSize.
Rozwiązanie dostępu w trybie użytkownika
Interfejs UMA rozwiązuje problemy występujące podczas uzyskiwania dostępu do pamięci trybu użytkownika z trybu jądra. Usługa UMA zapewnia:
Automatyczne sondowanie: Sondowanie jawne (ProbeForRead/ProbeForWrite) nie jest już wymagane, ponieważ wszystkie funkcje UMA zapewniają bezpieczeństwo adresów.
Nietrwały dostęp: wszystkie UMA DDI używają semantyki zmiennej, aby zapobiec optymalizacjom kompilatora.
Łatwość przenoszenia: kompleksowy zestaw UMA DDIs ułatwia klientom przeniesienie istniejącego kodu, aby korzystać z UMA DDIs, zapewniając bezpieczny i prawidłowy dostęp do pamięci użytkownika.
Przykład użycia UMA DDI
Korzystając z wcześniej zdefiniowanej struktury trybu użytkownika, poniższy fragment kodu pokazuje, jak bezpiecznie uzyskiwać dostęp do pamięci trybu użytkownika za pomocą usługi 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 (…) {}
}
Implementacja i użycie usługi UMA
Interfejs UMA jest dostarczany jako część zestawu sterowników systemu Windows (WDK):
- Deklaracje funkcji znajdują się w pliku nagłówka usermode_accessors.h .
- Implementacje funkcji znajdują się w bibliotece statycznej o nazwie umaccess.lib.
Narzędzie UMA działa we wszystkich wersjach systemu Windows, a nie tylko na najnowszej wersji. Aby uzyskać deklaracje i implementacje funkcji z biblioteki usermode_accessors.h i umaccess.lib, należy użyć najnowszego zestawu WDK. Wynikowy sterownik będzie działać prawidłowo w starszych wersjach systemu Windows.
Biblioteka Umaccess.lib zapewnia bezpieczną implementację na poziomie dolnym dla wszystkich interfejsów DDI. W przypadku wersji jądra systemu Windows obsługujących usługę UMA sterowniki będą miały wszystkie funkcje przekierowane do bezpieczniejszej wersji zaimplementowanej w ntoskrnl.exe.
Wszystkie funkcje dostępu w trybie użytkownika muszą być wykonywane w programie obsługi wyjątków ustrukturyzowanych (SEH) ze względu na potencjalne wyjątki podczas uzyskiwania dostępu do pamięci w trybie użytkownika.
Typy interfejsów dostępu DDI w trybie użytkownika
UMA zapewnia różne interfejsy DDI dla różnych typów dostępu do pamięci w trybie użytkownika. Większość tych identyfikatorów DDI jest dla podstawowych typów danych, takich jak BOOLEAN, ULONG i wskaźniki. Usługa UMA dodatkowo zapewnia interfejsy DDI na potrzeby dostępu do pamięci zbiorczej, pobierania długości ciągów i operacji z synchronizacją.
Ogólne identyfikatory DDI dla podstawowych typów danych
Usługa UMA udostępnia sześć wariantów funkcji do odczytywania i zapisywania prostych typów danych. Na przykład następujące funkcje są dostępne dla wartości typu Boolean:
| Nazwa funkcji | Description |
|---|---|
| ReadBooleanFromUser | Odczytaj wartość z pamięci trybu użytkownika. |
| ReadBooleanFromUserAcquire | Odczytaj wartość z pamięci w trybie użytkownika przy użyciu semantyki na potrzeby porządkowania pamięci. |
| ReadBooleanFromMode | Odczyt z pamięci w trybie użytkownika lub w trybie jądra na podstawie parametru trybu. |
| WriteBooleanToUser | Zapisz wartość w pamięci w trybie użytkownika. |
| WriteBooleanToUserRelease | Zapisz wartość w pamięci trybu użytkownika, stosując semantykę zwalniającą dla porządkowania pamięci. |
| WriteBooleanToMode | Zapisz w pamięci trybu użytkownika lub trybu jądra w zależności od parametru trybu. |
W przypadku funkcji ReadXxxFromUser parametr Source musi wskazywać wirtualną przestrzeń adresową (VAS) w trybie użytkownika. To samo dotyczy wersji ReadXxxFromMode, gdy Mode == UserMode.
W przypadku parametru ReadXxxFromMode, gdy Mode == KernelModeparametr Source musi wskazywać plik w systemie VAS w trybie jądra. Jeśli zdefiniowano definicję preprocesora DBG, operacja szybko kończy się niepowodzeniem z kodem FAST_FAIL_KERNEL_POINTER_EXPECTED.
W funkcjach WriteXxxToUser parametr Destination musi wskazywać na przestrzeń adresową VAS w trybie użytkownika. To samo dotyczy wersji WriteXxxToMode, gdy Mode == UserMode.
Interfejsy DDI do kopiowania i manipulacji pamięcią
Usługa UMA udostępnia funkcje kopiowania i przenoszenia pamięci między trybami użytkownika i jądra, w tym warianty dla kopii nietemporalnych i wyrównanych. Te funkcje są oznaczone adnotacjami wskazującymi potencjalne wyjątki SEH i wymagania IRQL (maksymalna APC_LEVEL).
Przykłady to CopyFromUser, CopyToMode i CopyFromUserToMode.
Makra, takie jak CopyFromModeAligned i CopyFromUserAligned, obejmują sprawdzanie wyrównania dla bezpieczeństwa przed wykonaniem operacji kopiowania.
Makra, takie jak CopyFromUserNonTemporal i CopyToModeNonTemporal, zapewniają kopie nietemporalne, które zapobiegają zanieczyszczeniu pamięci podręcznej.
Makrosy do odczytu/zapisu struktur
Makra programistyczne do odczytywania i zapisywania struktur pomiędzy trybami zapewniają zgodność typów i wyrównanie, wywołując funkcje pomocnicze z parametrami rozmiaru i trybu. Przykłady obejmują WriteStructToMode, ReadStructFromUser i ich wyrównane warianty.
Funkcje wypełniania i zerowania pamięci
DDI są udostępniane w celu wypełnienia lub wyzerowania pamięci w przestrzeniach adresowych trybu użytkownika lub systemu, z parametrami określającymi miejsce docelowe, długość, wartość wypełnienia i tryb. Te funkcje mają również adnotacje SEH i IRQL.
Przykłady obejmują FillUserMemory i ZeroModeMemory.
Operacje zakleszczone
Pakiet UMA obejmuje operacje z blokadą dostępu do pamięci niepodzielnej, które są niezbędne w przypadku manipulowania pamięcią bezpieczną wątkowo w środowiskach współbieżnych. DDI są dostępne zarówno dla wartości 32-bitowych, jak i 64-bitowych, z wersjami dedykowanymi dla pamięci użytkownika lub trybu systemowego.
Przykłady obejmują InterlockedCompareExchangeToUser, InterlockedOr64ToMode i InterlockedAndToUser.
Długość ciągu DDI
Funkcje umożliwiające bezpieczne określanie długości ciągów z pamięci użytkownika lub trybu są uwzględniane, obsługując zarówno ciągi ANSI, jak i ciągi szerokoznakowe. Te funkcje są zaprojektowane do zgłaszania wyjątków podczas niebezpiecznego dostępu do pamięci i są ograniczone przez IRQL.
Przykłady to StringLengthFromUser i WideStringLengthFromMode.
Metody dostępu do dużych liczb całkowitych i ciągów Unicode
UMA udostępnia DDI do odczytu i zapisu typów LARGE_INTEGER, ULARGE_INTEGER i UNICODE_STRING między pamięcią użytkownika a pamięcią trybu jądra. Warianty mają semantykę przejęcia i zwolnienia z parametrami trybu dla zapewnienia bezpieczeństwa i poprawności.
Przykłady obejmują ReadLargeIntegerFromUser, WriteUnicodeStringToMode i WriteULargeIntegerToUser.
Uzyskiwanie i wydawanie semantyki
W przypadku niektórych architektur, takich jak ARM, procesor CPU może zmienić kolejność dostępu do pamięci. Uniwersalne DDI mają implementację mechanizmu Acquire/Release, jeśli potrzebujesz gwarancji, że dostępy do pamięci nie są reorganizowane podczas dostępu w trybie użytkownika.
- Uzyskiwanie semantyki zapobiega zmienianiu kolejności obciążenia względem innych operacji pamięci.
- Semantyka zwalniania zapobiega zmianie kolejności operacji zapisu w pamięci względem innych operacji pamięci.
Przykłady semantyki uzyskiwania i wydawania w usłudze UMA obejmują readULongFromUserAcquire i WriteULongToUserRelease.
Aby uzyskać więcej informacji, zobacz Semantyka pozyskiwania i zwalniania.
Najlepsze rozwiązania
- Zawsze używaj identyfikatorów UMA DDI podczas uzyskiwania dostępu do pamięci trybu użytkownika z kodu jądra.
-
Obsłuż wyjątki za pomocą odpowiednich
__try/__exceptbloków. - Użyj trybowych identyfikatorów DDI, gdy kod może obsługiwać pamięć zarówno w trybie użytkownika, jak i jądra.
- Rozważ semantykę nabycia/zwolnienia kiedy kolejność pamięci jest ważna w twoim scenariuszu użycia.
- Sprawdź poprawność skopiowanych danych po skopiowaniu ich do pamięci jądra, aby zapewnić spójność.
Przyszła obsługa sprzętu
Metody dostępu w trybie użytkownika są przeznaczone do obsługi przyszłych funkcji zabezpieczeń sprzętowych, takich jak:
- SMAP (zapobieganie dostępowi w trybie nadzorcy): uniemożliwia dostęp kodu jądra do pamięci trybu użytkownika z wyjątkiem wyznaczonych funkcji, takich jak UMA DDI.
- ARM PAN (Nigdy dostęp uprzywilejowany): podobna ochrona w architekturach ARM.
Dzięki spójnym używaniu UMA DDI sterowniki będą zgodne z tymi ulepszeniami zabezpieczeń po włączeniu ich w przyszłych wersjach systemu Windows.