Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Средства доступа в пользовательском режиме (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.