Debajo del cortador se encuentra la traducción de la parte inicial del documento Detección de la divulgación de la memoria del núcleo con emulación x86 y seguimiento de manchas ( Artículo Proyecto Cero ) por Mateusz Jurczyk .
En la parte traducida del documento:
- Específicos del lenguaje de programación C (como parte del problema de expansión de memoria)
- Los detalles del funcionamiento de los núcleos de Windows y Linux (como parte del problema de expansión de memoria)
- importancia de la divulgación de la memoria del núcleo y el impacto en la seguridad del sistema operativo
- Métodos y técnicas existentes para detectar y contrarrestar la divulgación de la memoria del núcleo.
Aunque el documento se centra en los mecanismos de comunicación entre el núcleo privilegiado del sistema operativo y las aplicaciones de usuario, la esencia del problema puede generalizarse para cualquier transferencia de datos entre diferentes dominios de seguridad: el hipervisor es la máquina invitada, el servicio del sistema privilegiado (daemon) es la aplicación GUI, el cliente de red es el servidor, etc. .

Introduccion
Una de las tareas de los sistemas operativos modernos es garantizar la separación de privilegios entre las aplicaciones del usuario y el núcleo del sistema operativo. En primer lugar, esto incluye el hecho de que la influencia de cada programa en el tiempo de ejecución debe estar limitada por una determinada política de seguridad y, en segundo lugar, que los programas solo pueden acceder a la información que se les permite leer. El segundo es difícil de proporcionar, dadas las propiedades del lenguaje C (el lenguaje de programación principal utilizado en el desarrollo del núcleo), lo que hace que sea extremadamente difícil transferir datos de forma segura entre diferentes dominios de seguridad.
Los sistemas operativos modernos que operan en plataformas x86 / x86-64 son multiproceso y utilizan un modelo cliente-servidor en el que las aplicaciones en modo de usuario (clientes) se ejecutan de forma independiente y llaman al kernel (servidor) del sistema operativo con la intención de trabajar con un recurso administrado por el sistema. El mecanismo utilizado por el código de modo de usuario ( anillo 3 ) para llamar a un conjunto predefinido de funciones del núcleo (anillo 0) se llama llamadas del sistema o (brevemente) llamadas al sistema. Una llamada típica del sistema se muestra en la Figura 1:

Figura 1: Ciclo de vida de la llamada al sistema.
Es muy importante evitar la pérdida inadvertida de contenido de la memoria del núcleo al interactuar con los programas en modo de usuario. Existe un riesgo significativo de revelar datos sensibles del núcleo. Los datos pueden transmitirse implícitamente en los parámetros de salida de las llamadas seguras (desde otros puntos de vista) del sistema.
La divulgación de la memoria del sistema privilegiado se produce cuando el núcleo del sistema operativo devuelve una región de memoria mayor (exceso) de lo necesario para almacenar la información correspondiente (contenida en el interior). A menudo, los bytes redundantes contienen datos que se poblaron en un contexto diferente, y luego la memoria no se inicializó previamente, lo que evitaría la propagación de información en nuevas estructuras de datos.
Específicos del lenguaje de programación C
En esta sección, veremos varios aspectos del lenguaje C que son más importantes para el problema de expansión de memoria.
Estado indefinido de variables no inicializadas
Las variables individuales de tipos simples (como char o int), así como los miembros de estructuras de datos (matrices, estructuras y uniones) permanecen en un estado indefinido hasta la primera inicialización (independientemente de si se colocan en la pila o en el montón). Citas relevantes de la especificación C11 (ISO / IEC 9899: 201x Comité Draft N1570, abril de 2011):
6.7.9 Inicialización
...
10 Si un objeto que tiene una duración de almacenamiento automático no se inicializa explícitamente, su valor es indeterminado .
7.22.3.4 La función malloc
...
2 La función malloc asigna espacio para un objeto cuyo tamaño se especifica por tamaño y cuyo valor es indeterminado .
7.22.3.5 La función realloc
...
2 La función realloc desasigna el objeto antiguo al que apunta ptr y devuelve un puntero a un nuevo objeto que tiene el tamaño especificado por tamaño. El contenido del nuevo objeto será el mismo que el del objeto anterior antes de la desasignación, hasta el menor de los tamaños nuevos y antiguos. Cualquier byte en el nuevo objeto más allá del tamaño del objeto antiguo tiene valores indeterminados .
La parte que se aplica al código del sistema es más relevante para los objetos ubicados en la pila, ya que el núcleo del sistema operativo generalmente tiene interfaces de asignación dinámica con su propia semántica (no necesariamente compatible con la biblioteca C estándar, como se describirá más adelante).
Hasta donde sabemos, ninguno de los tres compiladores de C más populares para Windows y Linux (compilador C / C ++ de Microsoft, gcc, LLVM) crea código que preinicializa las variables no inicializadas por el programador en la pila en modo Release-build (o su equivalente). Hay opciones de compilación para marcar marcos de pila con bytes especiales - marcadores (/ RTC en Microsoft Visual Studio, por ejemplo) pero no se usan en versiones de lanzamiento por razones de rendimiento. Como resultado, las variables no inicializadas en la pila heredan los valores antiguos de las áreas de memoria correspondientes.
Considere un ejemplo de una implementación estándar de una llamada ficticia del sistema de Windows que multiplica un entero de entrada por dos y devuelve el resultado de la multiplicación (Listado 1). Obviamente, en el caso especial (InputValue == 0), la variable OutputValue permanece sin inicializar y se copia de nuevo al cliente. Este error le permite abrir cuatro bytes de memoria de la pila del núcleo para cada llamada.
NTSTATUS NTAPI NtMultiplyByTwo(DWORD InputValue, LPDWORD OutputPointer) { DWORD OutputValue; if (InputValue != 0) { OutputValue = InputValue * 2; } *OutputPointer = OutputValue; return STATUS_SUCCESS; }
Listado de Código 1: Expandiendo la memoria a través de una variable local no inicializada.
Las fugas a través de una variable local no inicializada no son muy comunes en la práctica: por un lado, los compiladores modernos a menudo detectan y advierten sobre tales problemas, por otro lado, tales fugas son errores funcionales que pueden detectarse durante el desarrollo o las pruebas. Sin embargo, el segundo ejemplo (en el Listado 2) muestra que una fuga también puede ocurrir a través del campo de estructura.
En este caso, el campo de estructura reservada nunca se usa explícitamente en el código, pero todavía se copia de nuevo al modo de usuario y, por lo tanto, también expone cuatro bytes de memoria del núcleo al código de llamada. Este ejemplo muestra claramente que inicializar cada campo de cada estructura devuelta al cliente para todas las ramas de ejecución de código no es una tarea fácil. En muchos casos, la inicialización forzada parece ilógica, especialmente si este campo no juega ningún papel práctico. Pero es el hecho de que una variable no inicializada (o campo de estructura) en la pila (o en el montón) acepta el contenido de los datos previamente almacenados en esta área de memoria (en el contexto de otra operación), se encuentra en el corazón del problema de expansión de la memoria del núcleo.
typedef struct _SYSCALL_OUTPUT { DWORD Sum; DWORD Product; DWORD Reserved; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtArithOperations( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.Product = InputValue * 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; }
Listado 2: Memoria expansible a través de un campo de estructura reservada.
Alineación de estructuras y bytes de relleno.
Inicializar todos los campos de la estructura de salida es un buen comienzo para evitar expandir la memoria. Pero esto no es suficiente para garantizar que en la representación de bajo nivel no haya bytes sin inicializar. Volvamos a la especificación C11:
6.5.3.4 Los operadores sizeof y Alignof
...
4 [...] Cuando se aplica a un operando que tiene estructura o tipo de unión, el resultado es el número total de bytes en dicho objeto, incluidos los rellenos internos y finales .
6.2.8 Alineación de objetos
1 Los tipos de objetos completos tienen requisitos de alineación que imponen restricciones a las direcciones en las que se pueden asignar objetos de ese tipo . Una alineación es un valor entero integrado definido por la implementación que representa el número de bytes entre direcciones sucesivas a las que se puede asignar un objeto dado. [...]
6.7.2.1 Especificadores de estructura y unión
...
17 Puede haber relleno no identificado en el extremo de una estructura o unión.
Es decir, los compiladores de lenguaje C para arquitecturas x86 (-64) utilizan la alineación natural de campos de estructuras (que tienen un tipo primitivo): cada uno de estos campos está alineado por N bytes, donde N es el tamaño del campo. Además, las estructuras y combinaciones completas también se alinean cuando se declaran en una matriz, y se cumple el requisito de alineación de campos anidados. Para garantizar la alineación, los bytes de relleno implícitos se insertan en las estructuras cuando es necesario. Aunque no son accesibles directamente en el código fuente, estos bytes también heredan valores antiguos de las áreas de memoria y pueden transmitir información al modo de usuario.
En el ejemplo del Listado 3, la estructura SYSCALL_OUTPUT se devuelve al código de llamada. Contiene campos de 4 y 8 bytes, separados por 4 bytes de relleno, necesarios para que la dirección del campo LargeSum se convierta en un múltiplo de 8. A pesar de que ambos campos se inicializan correctamente, los bytes de relleno no se configuran explícitamente, lo que nuevamente conduce a la expansión de la memoria de la pila del núcleo. La ubicación específica de la estructura en la memoria se muestra en la Figura 2.
typedef struct _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.LargeSum = 0; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; }
Listado 3: Expandiendo la memoria alineando la estructura.

Figura 2: Representación de la estructura en la memoria con la alineación en mente.
Las fugas a través de las alineaciones son relativamente comunes, ya que muchos parámetros de salida de las llamadas al sistema están representados por estructuras. El problema es especialmente grave para las plataformas de 64 bits, donde el tamaño de los punteros, size_t y tipos similares aumenta de 4 a 8 bytes, lo que lleva a la aparición del relleno necesario para alinear los campos de tales estructuras.
Dado que los bytes de relleno no se pueden direccionar en el código fuente, es necesario usar memset o una función similar para restablecer el área de memoria completa de la estructura antes de inicializar cualquiera de sus campos y copiarlo en modo de usuario, por ejemplo:
memset(&OutputStruct, 0, sizeof(OutputStruct));
Sin embargo, Seacord RC en su libro "The CERT C Coding Standard, Second Edition: 98 Rules for Development Safe, Reliable and Secure Systems. Addison-Wesley Professional" 2014 afirma que esta no es una solución ideal porque los bytes de relleno ) aún puede ser derribado después de llamar a memset, por ejemplo, como un efecto secundario de las operaciones con campos adyacentes. La preocupación puede justificarse mediante la siguiente declaración en la especificación C:
6.2.6 Representaciones de tipos
6.2.6.1 General
...
6 Cuando un valor se almacena en un objeto de estructura o tipo de unión , incluso en un objeto miembro, los bytes de la representación del objeto que corresponden a los bytes de relleno toman valores no especificados . [...]
Sin embargo, en la práctica, ninguno de los compiladores de C que probamos leyó o escribió fuera de las áreas de memoria de los campos declarados explícitamente. Parece que esta opinión es compartida por los desarrolladores de sistemas operativos que usan memset.
Uniones y campos de diferentes tamaños.
Las uniones son otra construcción compleja del lenguaje C en el contexto de la comunicación con un código de llamada menos privilegiado. Considere cómo la especificación C11 describe la representación de uniones en la memoria:
6.2.5 Tipos
...
20 Se puede construir cualquier número de tipos derivados a partir de los tipos de objeto y función, de la siguiente manera: [...] Un tipo de unión describe un conjunto superpuesto de objetos miembros no vacíos , cada uno de los cuales tiene un nombre opcionalmente especificado y posiblemente un tipo distinto.
6.7.2.1 Especificadores de estructura y unión
...
6 Como se discutió en 6.2.5, una estructura es un tipo que consiste en una secuencia de miembros, cuyo almacenamiento se asigna en una secuencia ordenada, y una unión es un tipo que consiste en una secuencia de miembros cuyo almacenamiento se superpone .
...
16 El tamaño de un sindicato es suficiente para contener al mayor de sus miembros . El valor de como máximo uno de los miembros se puede almacenar en un objeto de unión en cualquier momento.
El problema es que si la unión consta de varios campos de diferentes tamaños y solo se inicializa explícitamente un campo de menor tamaño, los bytes restantes asignados para acomodar campos grandes permanecen sin inicializar. Veamos un ejemplo de un manejador de llamadas de sistema hipotético, que se muestra en el Listado 4, junto con la asignación de memoria de unión SYSCALL_OUTPUT que se muestra en la Figura 3.
typedef union _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; }
Listado de Código 4: Expandiendo la memoria inicializando parcialmente una unión.

Figura 3: Representación de la unión en memoria con alineación.
Resulta que el tamaño total de la unión SYSCALL_OUTPUT es de 8 bytes (debido al tamaño del campo LargeSum más grande). Sin embargo, la función establece solo el valor del campo más pequeño, dejando 4 bytes finales sin inicializar, lo que posteriormente conduce a una fuga en su aplicación cliente.
Una implementación segura solo debe establecer el campo Suma en el espacio de direcciones del usuario, y no copiar todo el objeto con áreas de memoria potencialmente no utilizadas. Otra solución de trabajo es llamar a la función memset para anular una copia de la unión en la memoria del kernel antes de configurar cualquiera de sus campos y transferirlo nuevamente al modo de usuario.
Tamaño inseguro de
Como se muestra en las dos secciones anteriores, el uso del operador sizeof puede contribuir directa o indirectamente a revelar la memoria del núcleo, haciendo que se copien más datos de los que se inicializaron previamente.
C no tiene el aparato necesario para transferir datos de forma segura desde el núcleo al espacio del usuario, o, más generalmente, entre contextos de seguridad diferentes. El lenguaje no contiene metadatos de tiempo de ejecución que pueden indicar explícitamente qué bytes se configuraron en cada estructura de datos que se utiliza para interactuar con el núcleo del sistema operativo. Como resultado, la responsabilidad recae en el programador, quien debe determinar qué partes de cada objeto deben pasarse al código de llamada. Si se hace correctamente, debe escribir una función de copia segura separada para cada estructura de salida utilizada en las llamadas al sistema. Lo que a su vez dará lugar a un aumento en el tamaño del código, un deterioro en su legibilidad y, en general, será una tarea tediosa y lenta.
Por otro lado, es conveniente y simple copiar toda el área de memoria del núcleo con una sola llamada de memoria y el argumento sizeof, y dejar que el cliente determine qué partes de la salida se utilizarán. Resulta que este enfoque se usa hoy en Windows y Linux. Y cuando se detecta un caso específico de fuga de información, el fabricante del sistema operativo proporciona y distribuye de inmediato un parche con una llamada de memset. Desafortunadamente, esto no resuelve el problema en el caso general.
Detalles del sistema operativo
Existen ciertas soluciones de diseño de kernel, métodos de programación y patrones de código que afectan la propensión del sistema operativo a las vulnerabilidades de expansión de memoria. Se consideran en las siguientes subsecciones.
Reutilizando memoria dinámica
Los asignadores actuales de memoria dinámica (tanto en modo de usuario como en modo de núcleo) están altamente optimizados, ya que su rendimiento tiene un impacto significativo en el rendimiento de todo el sistema. Una de las optimizaciones más importantes es la reutilización de la memoria: cuando se libera, la memoria correspondiente rara vez se descarta por completo, en cambio, se guarda en la lista de regiones listas para ser devueltas la próxima vez que se asigne. Para guardar los ciclos de la CPU, las áreas de memoria predeterminadas no se borran entre la desasignación y la nueva asignación. Como resultado de esto, resulta que dos partes no conectadas del núcleo funcionan con el mismo rango de memoria por un corto tiempo. Esto significa que la pérdida del contenido de la memoria dinámica del núcleo le permite revelar los datos de varios componentes del sistema operativo.
En los siguientes párrafos, damos una breve descripción de los asignadores utilizados en los núcleos de Windows y Linux, y sus cualidades más notables.
Ventanas
La función clave del administrador del grupo de kernel de Windows es ExAllocatePoolWithTag , que se puede llamar directamente o mediante uno de los shells disponibles: ExAllocatePool {∅, Ex, WithQuotaTag, WithTagPriority}. Ninguna de estas funciones vacía el contenido de la memoria devuelta, ya sea de forma predeterminada o mediante cualquier indicador de entrada. Por el contrario, todos tienen la siguiente advertencia en su respectiva documentación de MSDN:
Nota La memoria que asigna la función no está inicializada. Un controlador en modo kernel primero debe poner a cero esta memoria si va a hacerla visible para el software en modo usuario (para evitar fugas de contenido potencialmente privilegiado).
El código de llamada puede seleccionar uno de los seis tipos principales de grupos: NonPagedPool, NonPagedPoolNx, NonPagedPoolSession, NonPagedPoolSessionNx, PagedPool y PagedPoolSession. Cada uno de ellos tiene una región separada en el espacio de direcciones virtuales y, por lo tanto, las áreas de memoria asignadas solo se pueden reutilizar dentro del mismo tipo de grupo. La frecuencia de reutilización de piezas de memoria es muy alta, y las áreas cerradas generalmente se devuelven solo si no se encuentra un registro adecuado en las listas de búsqueda, o si la solicitud es tan grande que se requieren nuevas páginas de memoria. En otras palabras, actualmente no existen prácticamente factores que impidan la divulgación de la memoria de la agrupación en Windows, y casi todos estos errores se pueden utilizar para filtrar datos confidenciales de diferentes partes del núcleo.
Linux
El kernel de Linux tiene tres interfaces principales para asignar memoria dinámicamente:
- kmalloc : una función común utilizada para asignar bloques de memoria de tamaño arbitrario (continuo en el espacio de direcciones virtual y físico), utiliza la asignación de memoria de losa .
- kmem_cache_create y kmem_cache_alloc : un mecanismo especializado para asignar objetos de un tamaño fijo (estructuras, por ejemplo), también utiliza la asignación de memoria de losa .
- vmalloc es una función de asignación raramente utilizada que devuelve regiones cuya continuidad no está garantizada en el nivel de memoria física.
Estas funciones (por sí mismas) no garantizan que las regiones seleccionadas no contendrán datos antiguos (potencialmente confidenciales), lo que hace posible abrir la memoria del montón del núcleo. Sin embargo, hay varias formas en que el código de llamada puede solicitar memoria anulada:
- kmalloc tiene una función kzalloc analógico, lo que asegura que se borre la memoria devuelta.
- El indicador opcional __GFP_ZERO se puede pasar a kmalloc , kmem_cache_alloc y algunas otras funciones para lograr el mismo resultado.
- kmem_cache_create acepta un puntero a una función constructora opcional que se llama para preinicializar cada objeto antes de devolverlo al código de llamada. El constructor se puede implementar como una envoltura alrededor de un conjunto de memorias para poner a cero un área de memoria determinada.
Vemos la disponibilidad de estas opciones como condiciones favorables para la seguridad del kernel, ya que alientan a los desarrolladores a tomar decisiones informadas y les permiten simplemente trabajar con las funciones de asignación de memoria existentes en lugar de agregar llamadas de memoria adicionales después de cada asignación de memoria dinámica.
Matrices de tamaño fijo
El acceso a varios recursos del sistema operativo se puede obtener por sus nombres de prueba. La variedad de recursos con nombre en Windows es muy grande, por ejemplo: archivos y directorios, claves y valores de claves de registro, ventanas, fuentes y mucho más. Para algunos de ellos, la longitud del nombre es limitada y se expresa mediante una constante, como MAX_PATH (260) o LF_FACESIZE (32). En tales casos, los desarrolladores de kernel a menudo simplifican el código declarando los buffers de tamaño máximo y copiándolos como un todo (por ejemplo, usando la palabra clave sizeof) en lugar de trabajar solo con la parte correspondiente de la línea. Esto es especialmente útil si las cadenas son miembros de estructuras más grandes. Dichos objetos se pueden mover libremente en la memoria sin preocuparse por administrar punteros a la memoria dinámica.
Como era de esperar, las memorias intermedias grandes rara vez se usan por completo, y el espacio de almacenamiento restante a menudo no se vacía. Esto puede conducir a fugas particularmente graves de largas áreas contiguas de memoria del núcleo. En el ejemplo del Listado 5, la llamada del sistema usa la función RtlGetSystemPath para cargar la ruta del sistema en el búfer local, y si la llamada tiene éxito, los 260 bytes se pasan al llamante, independientemente de la longitud real de la línea.
NTSTATUS NTAPI NtGetSystemPath(PCHAR OutputPath) { CHAR SystemPath[MAX_PATH]; NTSTATUS Status; Status = RtlGetSystemPath(SystemPath, sizeof(SystemPath)); if (NT_SUCCESS(Status)) { RtlCopyMemory(OutputPath, SystemPath, sizeof(SystemPath)); } return Status; }
Listado 5: Expandiendo la memoria inicializando parcialmente el buffer de cadena.
La región de memoria copiada de nuevo al espacio de usuario en este ejemplo se muestra en la Figura 4.

Figura 4: Memoria de un búfer de línea parcialmente inicializado.
Una implementación segura solo debe devolver la ruta solicitada, y no todo el búfer utilizado para el almacenamiento. Este ejemplo demuestra una vez más cómo la estimación del tamaño de los datos con el operador sizeof (utilizado como parámetro para RtlCopyMemory) puede ser completamente incorrecta con respecto a la cantidad real de datos que el núcleo debe pasar al área de usuario.
Tamaño de salida de llamada del sistema arbitrario
La mayoría de las llamadas al sistema aceptan punteros a la salida en modo de usuario junto con el tamaño del búfer. En la mayoría de los casos, la información de tamaño solo debe usarse para determinar si el búfer proporcionado es suficiente para recibir la salida de la llamada del sistema. No utilice el tamaño completo del búfer de salida proporcionado para especificar la cantidad de memoria que se copiará. Sin embargo, vemos casos en los que el núcleo intentará utilizar cada byte del búfer de salida del usuario, sin contar la cantidad de datos reales que deben copiarse. Un ejemplo de este comportamiento se muestra en el Listado 6.
NTSTATUS NTAPI NtMagicValues(LPDWORD OutputPointer, DWORD OutputLength) { if (OutputLength < 3 * sizeof(DWORD)) { return STATUS_BUFFER_TOO_SMALL; } LPDWORD KernelBuffer = Allocate(OutputLength); KernelBuffer[0] = 0xdeadbeef; KernelBuffer[1] = 0xbadc0ffe; KernelBuffer[2] = 0xcafed00d; RtlCopyMemory(OutputPointer, KernelBuffer, OutputLength); Free(KernelBuffer); return STATUS_SUCCESS; }
Listado 6: Expandiendo la memoria a través de un buffer de salida de tamaño arbitrario.
El propósito de una llamada al sistema es proporcionar el código de llamada con tres valores especiales de 32 bits, que ocupan un total de 12 bytes. Aunque verificar el tamaño correcto del búfer al comienzo de la función es correcto, el uso del argumento OutputLength debería terminar allí. Sabiendo que el búfer de salida es lo suficientemente grande como para guardar el resultado, el núcleo puede asignar 12 bytes de memoria, llenarlo y copiar el contenido nuevamente al búfer en modo usuario proporcionado. En cambio, una llamada al sistema asigna un bloque de grupo (además, con una longitud controlada por el usuario) y copia toda la memoria asignada al espacio del usuario. Resulta que todos los bytes, excepto los primeros 12, no se inicializan y se abren por error al usuario, como se muestra en la Figura 5.

Figura 5: Memoria de búfer de tamaño arbitrario.
El esquema discutido en esta sección es especialmente común para Windows. :
- , Windows, . , .
- . , , . , ( — ) .
, . , , .
,
, . , Windows .
, , . , : AddressSanitizer , PageHeap Special Pool . , , - . , . , , , , , . , ( ).
, , , . , .
, API
API, Windows (Win32/User32 API). API , , , . , , , , . .
, . , . , , , . , , .
, , . , KASLR (Kernel Address Space Layout Randomization ), . : Windows, Hacking Team 2015 ( Juan Vazquez. Revisiting an Info Leak ) (derandomize) win32k.sys, . , Matt Tait' Google Project Zero ( Kernel-mode ASLR leak via uninitialized memory returned to usermode by NtGdiGetTextMetrics ) MS15-080 (CVE-2015-2433).
(/) , , (control flow), : , , , , StackGuard Linux /GS Windows . , . , , .
(/)
(/) , , , : , , , . , , . . , ( , ) , , .

Microsoft Windows
2015 Windows. 2015 Matt Tait win32k!NtGdiGetTextMetrics. Windows Hacking Team. , , , 0-day Windows.
2015, WanderingGlitch (HP Zero Day Initiative) ( Acknowledgments – 2015 ). Ruxcon 2016 ( ) "Leaking Windows Kernel Pointers" .
, 2017 fanxiaocao pjf IceSword Lab (Qihoo 360) "Automatically Discovering Windows Kernel Information Leak Vulnerabilities" , , 14 2017 (8 ). Bochspwn Reloaded, , . VMware (Bochs) . , Bochspwn Reloaded, .
, , 2010-2011 , win32k: "Challenge: On 32bit Windows7, explain where the upper 16bits of eax come from after a call to NtUserRegisterClassExWOW()" "Subtle information disclosure in WIN32K.SYS syscall return values" . Windows 8, 2015 Matt Tait , : Google Project Zero Bug Tracker .
( ), , 2017 - Windows -, : Joseph Bialek — "Anyone notice my change to the Windows IO Manager to generically kill a class of info disclosure? BufferedIO output buffer is always zero'd" . , IOCTL- .
, Visual Studio 15.5 POD- , "= {0}", . , padding- () .
Linux
Windows, Linux , 2010 . , ( ) ( ) . , Windows Linux , — , .
, Linux . "Linux kernel vulnerabilities: State-of-the-art defenses and open problems" 2010 2011 28 . 2017- "Securing software systems by preventing information leaks" Lu K. 59 , 2013- 2016-. . : Rosenberg Oberheide 25 , Linux 2009-2010 , . Linux c grsecurity / PaX-hardened . Vasiliy Kulikov 25 2010-2011 , Coccinelle . , Mathias Krause 21 2013 50 .
, , Linux. — -Wuninitialized ( gcc, LLVM), . kmemcheck , Valgrind' . , . , KernelAddressSANitizer KernelMemorySANitizer . KMSAN syzkaller ( ) 19 , .
Linux. 2014 — 2016 Peir´o Coccinelle , Linux 3.12: "Detecting stack based kernel information leaks" International Joint Conference SOCO14-CISIS14-ICEUTE14, pages 321–331 (Springer, 2014) "An analysis on the impact and detection of kernel stack infoleaks" Logic Journal of the IGPL. , . 2016- Lu UniSan — , , : , . , 20% (350 1800), 19 Linux Android.
— (multi-variant program execution), , . , . , KASLR, -, . , 2006 DieHard: probabilistic memory safety for unsafe languages, 2017 — BUDDY: Securing software systems by preventing information leaks. John North "Identifying Memory Address Disclosures" 2015- . , SafeInit (Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities) , , . , , , Linux.
, . , : , . , , - , . .
CONFIG_PAGE_POISONING CONFIG_DEBUG_SLAB, -. -, . , , , Linux.
grsecurity / PaX . , PAX_MEMORY_SANITIZE , slab , ( — ). , PAX_MEMORY_STRUCTLEAK , ( ), . padding- (), 100% . , — PAX_MEMORY_STACKLEAK, . , , . (Kernel Self Protection Project) STACKLEAK .
Linux:
Secure deallocation, Chow , 2005Chow, Jim and Pfaff, Ben and Garfinkel, Tal and Rosenblum, Mendel. Shredding Your Garbage: Reducing Data Lifetime Through Secure Deallocation. In USENIX Security Symposium, pages 22–22, 2005.
, , ( ) . Linux .
Split Kernel, Kurmus Zippel, 2014Kurmus, Anil and Zippel, Robby. A tale of two kernels: Towards ending kernel hardening wars with split kernel. In Proceedings of the 2014 ACM SIGSAC Conference on Computer and Communications Security, pages 1366–1377. ACM, 2014.
, .
SafeInit, Milburn , 2017Milburn, Alyssa and Bos, Herbert and Giuffrida, Cristiano. SafeInit: Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities. In Proceedings of the 2017 Annual Network and Distributed System Security Symposium (NDSS)(San Diego, CA), 2017.
, , .
UniSan, Lu , 2016Lu, Kangjie and Song, Chengyu and Kim, Taesoo and Lee, Wenke. UniSan: Proactive kernel memory initialization to eliminate data leakages. In Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security, pages 920–932. ACM, 2016.
SafeInit , , , , .
, Linux .
( )
, , ( ). : (), , , , ( - ) . , . , , .
, :
- Bochspwn Reloaded – detection with software x86 emulation
- Windows bug reproduction techniques
- Alternative detection methods
- Other data sinks
- Future work
- Other system instrumentation schemes
, :) , .