Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Los accesores de modo de usuario (UMA) son un conjunto de DDIs diseñados para acceder y manipular de forma segura la memoria del modo de usuario desde el código de modo kernel. Estos DDIs abordan vulnerabilidades de seguridad comunes y errores de programación que pueden producirse cuando los controladores en modo kernel acceden a la memoria del modo de usuario.
El código en modo kernel que accede o manipula la memoria en modo de usuario pronto será necesario para usar UMA.
Posibles problemas al acceder a la memoria en modo de usuario desde el modo kernel
Cuando el código en modo kernel necesita acceder a la memoria en modo de usuario, surgen varios desafíos:
Las aplicaciones en modo usuario pueden pasar punteros malintencionados o no válidos al código en modo kernel. La falta de validación adecuada puede provocar daños en la memoria, bloqueos o vulnerabilidades de seguridad.
El código en modo de usuario es multiproceso. Como resultado, los subprocesos diferentes podrían modificar la misma memoria en modo de usuario entre accesos independientes en modo kernel, lo que posiblemente provocaría un daño en la memoria del kernel.
A menudo, los desarrolladores en modo kernel olvidan sondear la memoria en modo de usuario antes de acceder a ella, que es un problema de seguridad.
Los compiladores asumen la ejecución monohilo y podrían optimizar lo que parecen ser accesos redundantes a la memoria. Los programadores que no saben estas optimizaciones pueden escribir código no seguro.
Los fragmentos de código siguientes muestran estos problemas.
Ejemplo 1: Posible daños en la memoria debido a la multithreading en modo de usuario
El código en modo kernel que necesita acceder a la memoria en modo de usuario debe hacerlo dentro de un __try/__except bloque para asegurarse de que la memoria es válida. El siguiente fragmento de código muestra un patrón típico para acceder a la memoria en modo de usuario:
// 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
}
}
Este fragmento de código sondea primero la memoria, que es un paso importante, pero que se pasa por alto con frecuencia.
Sin embargo, un problema que puede producirse en este código se debe a la multithreading en modo de usuario. En concreto, Ptr->Size puede cambiar después de la llamada a ExAllocatePool2 , pero antes de la llamada a RtlCopyMemory, lo que podría provocar daños en la memoria en el kernel.
Ejemplo 2: Posibles problemas debidos a optimizaciones del compilador
Un intento de solucionar el problema de multithreading en el ejemplo 1 podría ser copiar Ptr->Size en una variable local antes de realizar la asignación y la copia.
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 (…) {}
}
Aunque este enfoque mitiga el problema causado por la multithreading, todavía no es seguro porque el compilador no es consciente de varios subprocesos y, por tanto, supone un único subproceso de ejecución. Como optimización, el compilador puede ver que ya tiene una copia del valor al que Ptr->Size apunta en su pila y, por lo tanto, no realiza la copia en LocalSize.
Solución de accesores en modo de usuario
La interfaz UMA resuelve los problemas detectados al acceder a la memoria en modo de usuario desde el modo kernel. UMA proporciona:
Sondeo automático: el sondeo explícito (ProbeForRead/ProbeForWrite) ya no es necesario, ya que todas las funciones de UMA garantizan la seguridad de las direcciones.
Acceso volátil: todos los DDIs de UMA usan semántica volátil para evitar optimizaciones del compilador.
Facilidad de portabilidad: el conjunto completo de DDIs de UMA facilita a los clientes portar su código existente para usar DDIs de UMA, lo que garantiza que se accede a la memoria de modo usuario de forma segura y correcta.
Ejemplo de uso de DDI de UMA
Con la estructura del modo de usuario definida anteriormente, el siguiente fragmento de código muestra cómo usar UMA para acceder a la memoria en modo de usuario de forma segura.
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 (…) {}
}
Implementación y uso de UMA
La interfaz UMA se distribuye como parte del Kit de controladores de Windows (WDK):
- Las declaraciones de función se encuentran en el archivo de encabezado usermode_accessors.h .
- Las implementaciones de función se encuentran en una biblioteca estática denominada umaccess.lib.
UMA funciona en todas las versiones de Windows, no solo en la versión más reciente. Debe utilizar el WDK más reciente para obtener las declaraciones e implementaciones de función de usermode_accessors.h y del umaccess.lib, respectivamente. El controlador resultante se ejecutará correctamente en versiones anteriores de Windows.
Umaccess.lib proporciona una implementación segura y de nivel descendente para todos los DDIs. En versiones compatibles con UMA del kernel de Windows, los controladores tendrán todas sus funciones redirigidas a una versión más segura implementada en ntoskrnl.exe.
Todas las funciones de acceso en el modo usuario deben ejecutarse dentro de un controlador de excepciones estructurado (SEH) debido a las posibles excepciones al acceder a la memoria de modo usuario.
Tipos de DDIs de acceso en modo de usuario
UMA proporciona varias DDIs para distintos tipos de acceso a memoria en modo de usuario. La mayoría de estas DDIs son para tipos de datos fundamentales, como BOOLEAN, ULONG y punteros. Además, UMA proporciona DDIs para el acceso a memoria masiva, la recuperación de longitud de cadena y las operaciones interbloqueadas.
Identificadores de datos genéricos para tipos de datos fundamentales
UMA proporciona seis variantes de función para leer y escribir tipos de datos simples. Por ejemplo, las siguientes funciones están disponibles para los valores BOOLEAN:
| Nombre de función | Description |
|---|---|
| ReadBooleanFromUser (Leer valor booleano del usuario) | Lee un valor de la memoria en modo de usuario. |
| ReadBooleanFromUserAcquire | Lea un valor de la memoria en modo de usuario con semántica de adquisición para la ordenación de memoria. |
| ReadBooleanFromMode | Lee desde la memoria en modo de usuario o en modo núcleo en función de un parámetro de modo. |
| WriteBooleanToUser | Escriba un valor en la memoria en modo de usuario. |
| WriteBooleanToUserRelease | Escriba un valor en la memoria en modo usuario con semántica de liberación para el orden de memoria. |
| WriteBooleanToMode | Escriba en modo de usuario o en memoria en modo kernel en función de un parámetro de modo. |
Para las funciones ReadXxxFromUser , el parámetro Source debe apuntar al espacio de direcciones virtuales (VAS) en modo de usuario. Lo mismo sucede en las versiones ReadXxxFromMode cuando Mode == UserMode.
Para ReadXxxFromMode, cuando Mode == KernelMode, el parámetro Source debe apuntar al VAS en modo núcleo. Si se define la definición de preprocesador DBG, se produce un error rápido en la operación con el código FAST_FAIL_KERNEL_POINTER_EXPECTED.
En las funciones WriteXxxToUser , el parámetro Destination debe apuntar al VAS en modo de usuario. Lo mismo ocurre en las versiones de WriteXxxToMode cuando Mode == UserMode.
DDIs de manipulación de copia y memoria
UMA proporciona funciones para copiar y mover la memoria entre los modos de usuario y kernel, incluidas las variantes para copias no temporales y alineadas. Estas funciones se marcan con anotaciones que indican las posibles excepciones de SEH y los requisitos IRQL (máximo APC_LEVEL).
Algunos ejemplos son CopyFromUser, CopyToMode y CopyFromUserToMode.
Las macros como CopyFromModeAligned y CopyFromUserAligned incluyen sondeos de alineación para la seguridad antes de realizar la operación de copia.
Las macros como CopyFromUserNonTemporal y CopyToModeNonTemporal proporcionan copias no temporales que evitan la contaminación de la memoria caché.
Estructura de macros de lectura y escritura
Las macros para leer y escribir estructuras entre modos garantizan la compatibilidad y alineación de tipos, llamando a funciones auxiliares con parámetros de tamaño y modo. Algunos ejemplos son WriteStructToMode, ReadStructFromUser y sus variantes alineadas.
Funciones de relleno y memoria cero
Se proporcionan DDIs para rellenar o borrar memoria en espacios de direcciones de usuario y modo, con parámetros que especifican el destino, la longitud, el valor de relleno y el modo. Estas funciones también llevan anotaciones SEH e IRQL.
Algunos ejemplos son FillUserMemory y ZeroModeMemory.
Operaciones interbloqueadas
UMA incluye operaciones interbloqueadas para el acceso a memoria atómica, que son esenciales para las manipulaciones de memoria seguras para subprocesos en entornos simultáneos. Los DDIs se proporcionan para los valores de 32 y 64 bits, con versiones destinadas a la memoria de usuario o modo.
Algunos ejemplos son InterlockedCompareExchangeToUser, InterlockedOr64ToMode e InterlockedAndToUser.
DDIs de longitud de cadena
Se incluyen funciones para determinar las longitudes de cadena de forma segura desde la memoria de usuario o modo, que admiten cadenas ANSI y de caracteres anchos. Estas funciones están diseñadas para generar excepciones en el acceso a memoria no segura y están restringidas por IRQL.
Algunos ejemplos son StringLengthFromUser y WideStringLengthFromMode.
Accesores para enteros grandes y cadenas de Unicode
UMA proporciona DDIs para leer y escribir los tipos LARGE_INTEGER, ULARGE_INTEGER y UNICODE_STRING entre la memoria del usuario y la memoria en modo kernel. Las variantes tienen semántica de adquisición y liberación con parámetros de modo para la seguridad y corrección.
Algunos ejemplos son ReadLargeIntegerFromUser, WriteUnicodeStringToMode y WriteULargeIntegerToUser.
Semántica de adquisición y liberación
En algunas arquitecturas, como ARM, la CPU puede reordenar los accesos a memoria. Todos los DDIs genéricos tienen una implementación de Acquire/Release si necesita una garantía de que los accesos a memoria no se reordenan para el acceso en modo de usuario.
- La semántica de adquisición impide la reordenación de la carga en relación con otras operaciones de memoria.
- La semántica de liberación impide la reordenación del almacenamiento en relación con otras operaciones de memoria.
Entre los ejemplos de semántica de adquisición y liberación de UMA se incluyen ReadULongFromUserAcquire y WriteULongToUserRelease.
Para obtener más información, consulte Adquisición y semántica de liberación.
procedimientos recomendados
- Use siempre DDIs de UMA al acceder a la memoria en modo de usuario desde el código kernel.
-
Controle excepciones con bloques adecuados
__try/__except. - Utilice DDIs basadas en modo cuando su código pueda gestionar tanto la memoria en modo de usuario como en modo de núcleo.
- Considere la posibilidad de adquirir o liberar la semántica cuando el orden de memoria es importante para su caso de uso.
- Valide los datos copiados después de copiarlos en la memoria del kernel para garantizar la coherencia.
Compatibilidad con hardware futuro
Los descriptores de acceso en modo de usuario están diseñados para admitir futuras características de seguridad de hardware, como:
- SMAP (Supervisor Mode Access Prevention): impide que el código de kernel acceda a la memoria en modo de usuario, excepto a través de funciones designadas como DDIs de UMA.
- ARM PAN (Acceso privilegiado Nunca): protección similar en arquitecturas ARM.
Mediante el uso de DDIs de UMA de forma coherente, los controladores serán compatibles con estas mejoras de seguridad cuando estén habilitadas en versiones futuras de Windows.