Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Средства доступа в пользовательском режиме (UMA) — это набор DDIs, назначенный для безопасного доступа и изменения памяти в пользовательском режиме из кода в режиме ядра. Эти DDIs устраняют распространенные уязвимости безопасности и ошибки программирования, которые могут возникать при доступе драйверов в режиме ядра к памяти пользовательского режима.
Код в режиме ядра, который обращается к памяти пользовательского режима и управляет ими, скоро потребуется использовать 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
}
}
Этот фрагмент исследует память в первую очередь, что является важным, но часто упускаемым из виду шагом.
Однако одна из проблем, которая может возникнуть в этом коде, связана с многопоточностью в пользовательском режиме. В частности, Ptr->Size может измениться после вызова ExAllocatePool2, но до вызова 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 (…) {}
}
Хотя этот подход устраняет проблему, вызванную многопоточным способом, он по-прежнему не является безопасным, так как компилятор не знает о нескольких потоках и, следовательно, предполагает один поток выполнения. В качестве оптимизации компилятор может увидеть, что Ptr->Size уже имеет на своём стеке копию значения, на которое он указывает, и поэтому не выполняет копирование в LocalSize.
Решение для функций доступа в пользовательском режиме
Интерфейс UMA решает проблемы, возникающие при доступе к памяти пользовательского режима из режима ядра. UMA предоставляет:
Автоматическая проверка: явная проверка (ProbeForRead/ProbeForWrite) больше не требуется, так как все функции UMA обеспечивают безопасность адресов.
Переменный доступ: все DDIS UMA используют переменную семантику для предотвращения оптимизации компилятора.
Простота переносимости. Комплексный набор DDIS UMA упрощает перенос существующего кода для использования UMA DDIs, обеспечивая безопасный и правильный доступ к памяти в режиме пользователя.
Пример использования 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 (WDK):
- Объявления функций находятся в файле заголовка usermode_accessors.h .
- Реализации функций находятся в статической библиотеке с именем umaccess.lib.
UMA работает на всех версиях Windows, а не только на последних версиях. Для получения объявлений и реализаций функций необходимо использовать последнюю версию WDK из usermode_accessors.h и umaccess.lib соответственно. Полученный драйвер будет работать в более ранних версиях Windows.
Umaccess.lib предоставляет безопасную реализацию нижнего уровня для всех DDIs. В версиях ядра Windows с поддержкой UMA драйверы будут перенаправлены на более безопасную версию, реализованную в ntoskrnl.exe.
Все функции доступа в режиме пользователя должны выполняться в структурированном обработчике исключений (SEH) из-за потенциальных исключений при доступе к памяти пользовательского режима.
Типы DDI акцессора в пользовательском режиме
UMA предоставляет различные DDIs для различных типов доступа к памяти в пользовательском режиме. Большинство этих DDis предназначены для основных типов данных, таких как BOOLEAN, ULONG и указатели. Кроме того, UMA предоставляет DDIs для пакетного доступа к памяти, извлечения длины строки и взаимоблокирующих операций.
Универсальные DDI для базовых типов данных
UMA предоставляет шесть вариантов функций для чтения и записи простых типов данных. Например, для значений BOOLEAN доступны следующие функции:
| Имя функции | Description |
|---|---|
| ReadBooleanFromUser | Чтение значения из памяти в пользовательском режиме. |
| ReadBooleanFromUserAcquire | Чтение значения из памяти в пользовательском режиме с семантикой для упорядочивания памяти. |
| ReadBooleanFromMode | Чтение из памяти в пользовательском режиме или режиме ядра в зависимости от параметра режима. |
| WriteBooleanToUser | Запись значения в память в режиме пользователя. |
| WriteBooleanToUserRelease | Запись значения в память в пользовательском режиме с семантикой освобождения для упорядочивания памяти. |
| WriteBooleanToMode | Запись в память в пользовательском режиме или в режиме ядра на основе параметра режима. |
Для функций ReadXxxFromUser параметр Source должен указывать на виртуальное адресное пространство в режиме пользователя (VAS). То же самое верно в версиях ReadXxxFromMode , когда Mode == UserMode.
При чтенииXxxFromModeMode == KernelMode параметр Source должен указывать на режим ядра. Если определено определение препроцессора DBG, операция немедленно завершается с кодом FAST_FAIL_KERNEL_POINTER_EXPECTED.
В функциях WriteXxxToUser параметр Destination должен указывать в пространство адресов пользователя (VAS). То же самое верно в версиях WriteXxxToMode , когда Mode == UserMode.
DDIS для копирования и обработки памяти
UMA предоставляет функции для копирования и перемещения памяти между режимами пользователя и ядра, включая варианты для неустраченных и выровненных копий. Эти функции помечены заметками, указывающими на потенциальные исключения SEH и требования IRQL (макс. APC_LEVEL).
Примерами являются CopyFromUser, CopyToMode и CopyFromUserToMode.
Макросы, такие как CopyFromModeAligned и CopyFromUserAligned, включают проверку выравнивания для безопасности перед выполнением операции копирования.
Макросы, такие как CopyFromUserNonTemporal и CopyToModeNonTemporal, предоставляют нентемпоральные копии, которые помогают избежать загрязнения кэша.
Структура макросов чтения и записи
Макросы для чтения и записи структур между режимами обеспечивают совместимость типов и выравнивание, вызывая вспомогательные функции с параметрами размера и режима. Примеры включают WriteStructToMode, ReadStructFromUser и их выровненные варианты.
Функции заполнения и обнуления памяти
DDIs предоставляются для заполнения памяти или её обнуления в адресных пространствах пользователя или режима с параметрами, определяющими место назначения, длину, значение заполнения и режим. Эти функции также содержат аннотации SEH и IRQL.
Примеры включают FillUserMemory и ZeroModeMemory.
Взаимосвязанные операции
UMA включает в себя заблокированные операции для доступа к атомарной памяти, необходимые для операций с потокобезопасной памятью в параллельных средах. DDIs предоставляются для 32-разрядных и 64-разрядных значений с версиями, предназначенными для памяти пользователя или режима.
Примерами являются InterlockedCompareExchangeToUser, InterlockedOr64ToMode и InterlockedAndToUser.
Длина строки DDIs
Функции для безопасного определения длины строк из пользовательской или системной памяти включены, поддерживающие как строки ANSI, так и строки с широкими символами. Эти функции предназначены для создания исключений при небезопасном доступе к памяти и ограничены IRQL.
Примеры: StringLengthFromUser и WideStringLengthFromMode.
Крупные целочисленные и строковые методы доступа Юникода
UMA предоставляет DDIs для чтения и записи типов LARGE_INTEGER, ULARGE_INTEGER и UNICODE_STRING между памятью пользователя и памятью режима ядра. Варианты имеют семантику захвата и освобождения с параметрами режима работы для обеспечения безопасности и корректности.
Примеры: ReadLargeIntegerFromUser, WriteUnicodeStringToMode и WriteULargeIntegerToUser.
Семантика захвата и освобождения
В некоторых архитектурах, таких как ARM, ЦП может изменить порядок доступа к памяти. Универсальные DDIs все имеют реализацию Acquire/Release, если вам нужна гарантия того, что доступ к памяти не переупорядочен для доступа в пользовательском режиме.
- Получение семантики предотвращает переупорядочение нагрузки относительно других операций памяти.
- Семантика выпуска предотвращает переупорядочение хранилища относительно других операций памяти.
Примеры семантики acquire и release в UMA включают ReadULongFromUserAcquire и WriteULongToUserRelease.
Дополнительные сведения см. в разделе "Семантика захвата и освобождения".
Лучшие практики
- Всегда используйте UMA DDIs при доступе к памяти в пользовательском режиме из кода ядра.
-
Обрабатывайте исключения с соответствующими
__try/__exceptблоками. - Используйте режимные DDIs, когда код может обрабатывать как пользовательскую, так и ядровую память.
- Учитывайте семантику захвата/освобождения, когда порядок доступа к памяти важен для вашего варианта использования.
- Проверьте скопированные данные после копирования в память ядра, чтобы обеспечить согласованность.
Поддержка будущих аппаратных возможностей
Методы доступа в режиме пользователя предназначены для поддержки будущих аппаратных функций безопасности, таких как:
- SMAP (защита доступа в режиме руководителя): запрещает код ядра получать доступ к памяти в пользовательском режиме, за исключением назначенных функций, таких как DDIS UMA.
- ARM PAN (Privileged Access Never): аналогичная защита в архитектурах ARM.
Благодаря согласованному использованию DDIS UMA драйверы будут совместимы с этими улучшениями безопасности при включении в будущих версиях Windows.