Бөлісу құралы:


Методы доступа в режиме пользователя

Средства доступа в пользовательском режиме (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.