Instituto de Tecnología de Massachusetts. Conferencia Curso # 6.858. "Seguridad de los sistemas informáticos". Nikolai Zeldovich, James Mickens. Año 2014
Computer Systems Security es un curso sobre el desarrollo e implementación de sistemas informáticos seguros. Las conferencias cubren modelos de amenazas, ataques que comprometen la seguridad y técnicas de seguridad basadas en trabajos científicos recientes. Los temas incluyen seguridad del sistema operativo (SO), características, gestión del flujo de información, seguridad del idioma, protocolos de red, seguridad de hardware y seguridad de aplicaciones web.
Lección 1: "Introducción: modelos de amenaza"
Parte 1 /
Parte 2 /
Parte 3Lección 2: "Control de ataques de hackers"
Parte 1 /
Parte 2 /
Parte 3Lección 3: “Desbordamientos del búfer: exploits y protección”
Parte 1 /
Parte 2 /
Parte 3 Bienvenido a la conferencia sobre exploits para desbordamientos de búfer. Hoy terminaremos la discusión sobre los
límites de
Baggy y luego pasaremos a otros métodos de protección de desbordamiento de búfer.

A continuación, hablaremos sobre los materiales impresos de la conferencia de hoy, que están dedicados a la
Programación orientada al retorno a
ciegas (BROP) : programación orientada hacia atrás y ciega. Esta es una técnica de explotación que puede llevarse a cabo incluso si el atacante no tiene el binario objetivo. Estas hazañas están destinadas a destruir "canarios" en las pilas de sistemas de 64 bits. Entonces, si eras como yo cuando leí estos materiales por primera vez, deberías haber sentido la película de Christopher Nolan. ¡Fue solo una explosión cerebral!
Vamos a considerar cómo funcionan estos gadgets correctamente. Por lo tanto, espero que al final de la conferencia pueda comprender todas estas altas tecnologías descritas en los materiales de la conferencia. Pero primero, como dije, terminaremos la discusión sobre los
límites de
Baggy . Considere un ejemplo muy simple.
Supongamos que vamos a asignar un puntero
p y le asignaremos un tamaño de 44 bytes. Suponga también que el tamaño de la ranura es de 16 bytes.
¿Qué sucede cuando asignamos la función
malloc ? Ya sabe que en este caso el sistema de
límites Baggy intentará complementar esta distribución con un logaritmo
2n . Entonces, para nuestro puntero de 44 bytes, se asignarán 64 bytes de memoria. Pero el tamaño de la ranura es de 16 bytes, por lo que crearemos 64/16 = 4 tablas de límites de 16 bytes cada una. Cada una de estas entradas se colocará en el registro de distribución de tamaños.
A continuación, asigne otro puntero
char * q = p + 60 . Vemos que este valor está fuera de límites porque el tamaño de
p es de 44 bytes, y aquí es de 60 bytes. Pero
Baggy Limites funciona para que en este caso no pase nada
malo , aunque el programador no debería haberlo hecho.
Ahora supongamos que lo siguiente que hacemos es asignar otro puntero, que será igual a
char * r = q + 16 . Ahora, esto realmente causará un error, porque el tamaño de desplazamiento será 60 + 16 = 76, que es 12 bytes más grande que las 4 ranuras (4x16 = 64 bytes) que
asignó el sistema de
límites de Baggy . Y este exceso es realmente más de la mitad de la ranura.

Si recuerda, en este caso, el sistema de
límites de Baggy responderá inmediatamente a un error crítico de sincronización, lo que hará que el programa se bloquee y, de hecho, lo detenga.
Así que imaginemos que solo tenemos dos líneas:
char * p = malloc (44)
char * q = p + 60Y no hay una tercera línea con el código. En cambio, haremos esto:
char * s = q + 8En este caso, el puntero tendrá un valor de 60 + 8 = 68 bits, que serán 4 bytes más que los límites asignados por los límites de
Baggy . De hecho, esto no causará un error crítico, aunque el valor va más allá de los límites. Lo que hicimos aquí es establecer un bit de orden superior para el puntero. Entonces, si alguien intenta desreferenciarlo posteriormente, esto conducirá a un error crítico en este punto.

Y lo último que haremos es asignar otro puntero:
char * t = s - 32De hecho, hicimos esto: devolvimos el puntero al borde. Entonces, si inicialmente
s fue más allá, ahora lo hemos devuelto al volumen originalmente asignado que creamos para el puntero. Por lo tanto, ahora
t no tendrá un bit de orden superior en su composición, y puede desreferenciarse fácilmente.
Audiencia: ¿cómo sabe el programa que
r tiene un exceso mayor que la mitad de la pila?
Profesor Mickens: tenga en cuenta que cuando creamos
r , obtuvimos un código de herramienta que funcionará en todas estas operaciones con punteros. Entonces podemos decir dónde se ubicará
q , y sabemos que está dentro de los
límites de Baggy . Por lo tanto, cuando realizamos esta operación
q + 16 , las herramientas de
límites de Baggy saben de dónde proviene este valor inicial. Y luego, si se
produce un desplazamiento de este tamaño original, los
límites holgados determinarán fácilmente que el desplazamiento es mayor que la mitad del tamaño de la ranura.
En principio, cuando realiza operaciones con punteros, debe observar si exceden el tamaño asignado o no. En algún momento, tiene un puntero ubicado dentro de los límites de los límites de
Baggy , y luego sucede algo que lo hace ir más allá de los límites. Entonces, justo cuando esto sucede, descubriremos que algún tipo de crochet "salió" de nuestro código.
Espero que esto sea comprensible. Fue una descripción muy breve de la tarea, pero espero que puedan entenderla fácilmente.
Entonces, tenemos un puntero que se ve así:
char * p = malloc (256) , luego agregamos el puntero
char * q = p + 256 , después de lo cual intentaremos desreferenciar este puntero.
Entonces, ¿qué pasará? Bueno, tenga en cuenta que 256 es una secuencia
2n , por lo que estará dentro de los
límites de Baggy . Por lo tanto, cuando agregamos otros 256 bits, esto significa que hacemos otro pase al final de los
límites de los
límites de Baggy . Como en el ejemplo anterior, esta línea es lo suficientemente buena, pero lleva al hecho de que se establecerá un bit de orden superior para
q . Por lo tanto, cuando tratamos de desreferenciarlo, todo explotará y tendrá que llamar a nuestro agente de seguros. ¿Eso está claro?

A partir de estos 2 ejemplos, puede comprender cómo funciona el sistema de
límites Baggy . Como mencioné en la última conferencia, realmente no necesita instrumentar cada operación de puntero si puede usar el análisis de código estático para descubrir que un conjunto específico de operaciones de puntero es seguro. Pospondré más discusiones sobre algunos análisis estáticos, pero basta con decir que no siempre es necesario realizar estas acciones matemáticas, ya lo hemos verificado antes.
Otra pregunta que se menciona en Piazza: cómo garantizar la compatibilidad de los
límites de
Baggy con bibliotecas anteriores sin herramientas. La idea es que cuando los
límites de Baggy inicializan las tablas de borde, establecen que todos los registros deben estar dentro de los 31 bits. Por lo tanto, cuando leemos la tabla de límites, cada registro en ella representa un valor de la forma
2n + 31 . Por lo tanto, al inicializar los límites iniciales de tamaño de 31 bits, suponemos que cada puntero tendrá el tamaño máximo posible de
2n + 31 . Déjame darte un ejemplo muy simple que aclarará esto.
Supongamos que tenemos un espacio de memoria que usamos para un montón. Este espacio de memoria está formado por dos componentes. En la parte superior, tenemos un montón que se ha asignado utilizando un código que no es de herramientas, y debajo hay un montón que se ha asignado con un código de herramienta. Entonces, ¿qué harán los
límites de Baggy ? Como recordará, este sistema tiene el concepto de una ranura, cuyo tamaño es de 16 bits. Por lo tanto, la tabla de límites constará de 2 secciones, iniciadas desde 31 bits.
Sin embargo, al ejecutar el código de la herramienta, en realidad usará el algoritmo de
límites de Baggy para establecer los valores apropiados para esta fila de la tabla.

Cuando un puntero proviene de la parte superior del espacio de memoria, siempre se establece en los límites máximos posibles de
2n + 31 . Esto significa que los
límites de Baggy nunca considerarán que una operación de puntero que ha "venido" de una biblioteca sin herramientas puede ir más allá de los límites.
La idea es que en el código de la herramienta, siempre realizaremos estas comparaciones para los punteros, pero si establecemos los límites de escritura del puntero para un código que no sea una herramienta de la forma
2n + 31 , entonces nunca tendremos un error de desreferencia. Es decir, tenemos una buena interacción entre las
entradas de código de
límites de Baggy y los registros no instrumentales de bibliotecas anteriores.
Esto significa que tenemos este sistema, lo cual es bueno, porque no bloquea el programa cuando se usan bibliotecas que no son herramientas, pero tiene un problema. El problema es que nunca podemos determinar los límites de los punteros generados por código que no es una herramienta. Debido a que nunca estableceremos un bit de orden superior cuando, por ejemplo, este puntero tenga demasiado o muy poco espacio. Por lo tanto, en realidad no podemos garantizar la seguridad de la memoria para las operaciones que se producen al usar código no instrumental. Tampoco puede determinar cuándo pasamos un puntero que ha ido más allá de los límites de tamaño del código instrumental al código no instrumental. En este caso, algo inimaginable puede suceder. Si tiene un puntero extraído del código de la herramienta, entonces tiene un bit de alto orden establecido en 1. Por lo tanto, parece que tiene dimensiones gigantescas.
Sabemos que si acabamos de colocar este código en el código de la herramienta, podemos borrar esta bandera en algunos puntos cuando vuelva a los bordes. Pero si solo pasamos esta gran dirección al código no instrumental, entonces puede hacer algo inimaginable. Incluso puede devolver este puntero a los límites, pero nunca tendremos la oportunidad de borrar este bit de orden superior. Entonces todavía podemos tener problemas incluso cuando usamos el circuito que se muestra aquí.
Público: si tenemos código de herramienta para asignar memoria, ¿utiliza la misma función
malloc que usa el código de atributo?
Profesor: Esta es una pregunta un poco delicada. Si consideramos el caso aquí, esto se observa estrictamente, ya que tenemos dos áreas de memoria, cada una de las cuales obedece las reglas establecidas para ello. Pero, en principio, dependerá del código que use el lenguaje de programación seleccionado. Imagine que en C ++, por ejemplo, puede asignar su propio calificador. Por lo tanto, depende de ciertos detalles del código.
Audiencia: ¿cómo puede verificar el calificador si el límite está establecido en 31 bits o no?
Profesor: en los niveles inferiores, los algoritmos de distribución funcionan de modo que cuando se llama a un sistema desconocido, el puntero se mueve hacia arriba. Entonces, si tiene varios asignadores, entonces todos tratan de asignar memoria, cada uno de ellos tiene su propia pieza de memoria, que se reservan para ellos, básicamente, correctamente. Por lo tanto, en la vida real puede estar más fragmentado que a un alto nivel.
Entonces, todo lo que examinamos anteriormente se relacionó con la operación de los
límites de
Baggy en sistemas de 32 bits. Considere lo que sucede cuando se usan sistemas de 64 bits. En tales sistemas, puede deshacerse de la tabla de límites, porque podemos almacenar información sobre los límites en el puntero.
Considere cómo se ve un puntero regular en los límites holgados. Consta de 3 partes. Se asignan 21 bits para la primera parte, cero, se asignan otros 5 bits para el tamaño, este es el tamaño principal del registro, y otros 38 son los bits de la dirección habitual.

La razón por la cual esto no limita masivamente el tamaño de la dirección del programa que está utilizando es que la mayoría de los bits de alto orden del sistema operativo y / o equipo ubicados en las primeras 2 partes del puntero no permiten que la aplicación se use por varias razones. Entonces, resultó que no estamos reduciendo en gran medida el número de aplicaciones utilizadas en el sistema. Así es como se ve un puntero regular.
¿Qué sucede cuando solo tenemos uno de estos punteros? Bueno, en un sistema de 32 bits, todo lo que podemos hacer es establecer un bit de alto orden y esperamos que esto nunca tenga más de la mitad del tamaño de la ranura. Pero ahora que tenemos todo este espacio de direcciones adicional, puede colocar el desplazamiento fuera de los bordes OOB (fuera del límite) directamente en este puntero. Entonces podemos hacer algo como lo que se muestra en la figura, dividiendo el puntero en 4 partes y redistribuyendo su tamaño.
Por lo tanto, podemos obtener 13 bits para los límites de desplazamiento, es decir, escribir qué tan lejos estará este puntero OOB de donde debería estar. Por otra parte, puede establecer el tamaño real del objeto indicado aquí como 5, y el resto de la parte cero, que ahora será 21-13 = 8 bits. Y luego sigue nuestra dirección parte de 38 bits. En este ejemplo, verá las ventajas de usar sistemas de 64 bits.

Tenga en cuenta que aquí tenemos el tamaño habitual para un puntero regular, en ambos casos este tamaño es de 64 bits y su descripción es elemental. Y esto es bueno, porque al usar punteros "gruesos", necesitaríamos muchas palabras para describirlos.
También noto que el código que no es herramienta se puede aplicar fácilmente aquí, porque funciona y usa el mismo tamaño que los punteros regulares. Podemos poner estas cosas en una
estructura , por ejemplo, y el tamaño de esta
estructura no cambiará. Esto es muy bueno cuando tenemos la oportunidad de trabajar en un mundo de 64 bits.
Público: ¿por qué en el segundo caso el desplazamiento se ubica delante del tamaño, y no como en el caso anterior, y qué sucederá si el tamaño del desplazamiento es grande?
Profesor: Creo que en algunos casos tenemos ciertos problemas limitantes en los que tendremos que trabajar. Por ejemplo, se producirá un problema si hay más bits. Pero básicamente, no creo que haya una razón por la que no puedas leer algunas de estas cosas. A menos que ciertas condiciones estrictas, en las que no pienso ahora, deberían haber estipulado el tamaño de la parte cero, de lo contrario podría haber problemas con el hardware.
Entonces, aún puede iniciar un desbordamiento de búfer en el sistema de
límites Baggy , ya que la aplicación de los enfoques anteriores no resuelve todos los problemas, ¿verdad? Otro problema que puede encontrar si tiene un código no instrumental, porque no podremos detectar ningún problema en el código no instrumental. También puede encontrar vulnerabilidades de memoria que surgen de un sistema dinámico de asignación de memoria. Si recuerdas, en una conferencia anterior vimos este extraño puntero para
liberar malloc , y los
límites holgados no pudieron evitar la ocurrencia de tales cosas.
También discutimos en la última conferencia el hecho de que los punteros de código no tienen límites asociados con ellos. Supongamos que tenemos una estructura en la que el búfer de memoria se encuentra en la parte inferior y el puntero en la parte superior, y el búfer se desborda. Suponemos que el desbordamiento del búfer todavía está dentro de los
límites de Baggy . Entonces debe redefinir este puntero de función. De lo contrario, si intentamos usarlo, puede enviarse a un código malicioso para atacar una parte controlada de la memoria. Y en este caso, los límites no nos ayudarán, porque no tenemos bordes asociados, asociados con estos punteros de función.
Entonces, ¿cuál es el precio de usar
límites holgados ? De hecho, solo tenemos 4 componentes de este precio.

El primero es el espacio. Porque si usa un puntero "grueso", entonces es obvio que debe agrandar los punteros. Si está utilizando el sistema de
límites holgados del que acabamos de hablar, debe guardar una tabla de borde. Y esta tabla tiene un tamaño de ranura que le permite controlar qué tan grande será esta tabla hasta que agote las posibilidades de la memoria asignada para ella.
Además, también aumentó la carga en la CPU, que se ve obligada a realizar todas estas operaciones instrumentales con punteros. Dado que para cada uno o casi todos los punteros es necesario verificar los límites utilizando los mismos modos de operación, lo que ralentizará la ejecución de su programa.
También hay un problema con las falsas alarmas. Ya hemos discutido lo que podría pasar si un programa genera punteros que van más allá de los límites, pero nunca intenta desreferenciarlos. Estrictamente hablando, esto no es un problema.
Los límites holgados marcarán estos indicadores "fuera de
límites " si van más allá de la mitad del tamaño de la ranura, al menos en la solución de 32 bits.
Lo que verá en la mayoría de las herramientas de seguridad es que las falsas alarmas reducen la probabilidad de que las personas usen estas herramientas. Porque en la práctica, todos esperamos que nos preocupemos por la seguridad, pero ¿qué es lo que realmente emociona a las personas? Quieren poder subir sus estúpidas fotos a Facebook, quieren acelerar el proceso de carga, etc. Por lo tanto, si realmente desea que sus herramientas de seguridad tengan demanda, deberían tener cero falsas alarmas. Intentar capturar todas las vulnerabilidades generalmente conduce a falsas alarmas, lo que molestará a los desarrolladores o usuarios.
También genera gastos improductivos que necesita soporte del compilador. Debido a que debe agregar todas las herramientas al sistema, omitiendo la verificación del puntero, y así sucesivamente.
Entonces, para usar el sistema de
límites Baggy tenemos que pagar un precio, que consiste en un uso excesivo de espacio, un aumento en la carga de la CPU, falsas alarmas y la necesidad de usar un compilador.
Esto
concluye la discusión de los
límites de
Baggy .
Ahora podemos pensar en otras dos estrategias de mitigación de desbordamiento de búfer. De hecho, son mucho más fáciles de explicar y comprender.
Uno de estos enfoques se llama
memoria no ejecutable . Su idea principal es que el hardware de intercambio indicará 3 bits de
R ,
W y
X (lectura, escritura y ejecución) para cada página que tenga en la memoria. , , . 2 , , , , .
, . , , , , - . , . «
W X » , , , , , . , , . . , , . ?
- , . , . , , .
, , , , . , , . .
, . –
just-in-time , .
-, JavaScript . JavaScript, , - - «» , - «» , x86 . , .
. , ,
just-in-time W ,
X . , , .
— . , , , .

, , . GDB, , . , . , . .
, . , , , , , .
: , , — . , , , , , , , - . , , .
, , , GDB , , , , . .
, , , , . - , - , . , . , , .
, ? , . , , . , , , , - , .
27:10
:
Curso MIT "Seguridad de sistemas informáticos". 3: « : », 2.
Gracias por quedarte con nosotros. ¿Te gustan nuestros artículos? ¿Quieres ver más materiales interesantes?
Apóyenos haciendo un pedido o recomendándolo a sus amigos, un
descuento del 30% para los usuarios de Habr en un análogo único de servidores de nivel de entrada que inventamos para usted: toda la verdad sobre VPS (KVM) E5-2650 v4 (6 núcleos) 10GB DDR4 240GB SSD 1Gbps de $ 20 o cómo dividir el servidor? (las opciones están disponibles con RAID1 y RAID10, hasta 24 núcleos y hasta 40GB DDR4).
Dell R730xd 2 veces más barato? ¡Solo tenemos
2 x Intel Dodeca-Core Xeon E5-2650v4 128GB DDR4 6x480GB SSD 1Gbps 100 TV desde $ 249 en los Países Bajos y los Estados Unidos! Lea sobre
Cómo construir un edificio de infraestructura. clase utilizando servidores Dell R730xd E5-2650 v4 que cuestan 9,000 euros por un centavo?