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 3 James Mickens: De la conferencia anterior, aprendimos todo sobre los ataques de desbordamiento del búfer, y hoy continuaremos discutiendo algunos métodos para lanzar estos ataques. La idea básica detrás de un ataque de desbordamiento de búfer es la siguiente.

En primer lugar, noto que estos ataques afectan varias circunstancias diferentes. La primera circunstancia que utilizan es que el software del sistema a menudo se escribe en C.
Por software de sistema, me refiero a bases de datos, compiladores, servidores de red y similares. Puede recordar algo como su shell de comandos favorito. Todo este "software" generalmente está escrito en C. ¿Por qué en C? Porque, en primer lugar, es más rápido y, en segundo lugar, C se considera un ensamblador de alto nivel que mejor se adapta a las necesidades de una variedad de plataformas de hardware. Por lo tanto, todos los sistemas críticos están escritos en este lenguaje de programación de bajo nivel. El problema con el software escrito en C es que realmente usa direcciones de memoria sin procesar y no tiene herramientas ni módulos de software para verificarlas. En algunos casos, esto puede llevar a consecuencias desastrosas.
¿Por qué no hay verificación de índice de matriz en C, es decir, no hay verificación de borde? Una razón es que el hardware no. Y las personas que escriben en C generalmente desean la velocidad de ejecución del programa más rápida posible. Otra razón es que en C, como veremos más adelante, en realidad es muy difícil definir la semántica de qué es un puntero y en qué medida debe actuar. Por lo tanto, en algunos casos sería muy difícil automatizar procesos de software en C.
Analicemos algunas tecnologías que realmente están tratando de crear algún tipo de administración automática de memoria. Pero, como veremos, ninguno de estos métodos es completamente "a prueba de balas".
Además, los ataques de desbordamiento de búfer utilizan el conocimiento x86 de la arquitectura, por ejemplo, en qué dirección está creciendo la pila. ¿Qué es una convención de llamada para funciones? Cuando accede a la función C, ¿cómo se ve la pila? Y cuando selecciona un objeto en el montón, ¿cómo se ven estas estructuras principales seleccionadas?
Veamos un ejemplo simple. Esto es muy similar a lo que viste en la última conferencia. Entonces, aquí tenemos una solicitud de lectura estándar, y luego obtenemos un búfer, aquí, luego viene el
inti canónico, seguido por el infame comando
gets . Y a continuación tenemos otras cosas necesarias.

Entonces, como discutimos en la conferencia la semana pasada, esto es problemático, ¿verdad? Porque esto
consigue la operación no comprueba los límites del búfer. Si el usuario llena el búfer con datos y usamos esta función insegura aquí, entonces podemos desbordar el búfer. Podemos reescribir todo el contenido de la pila. Déjame recordarte cómo se ve.
En la parte inferior está la matriz "i". Un búfer se encuentra arriba, tiene la primera dirección en la parte inferior y la última en la parte superior. En cualquier caso, encima del búfer tenemos el valor guardado del indicador de brecha: el valor guardado EBP. Arriba está la dirección de retorno de la función, y aún más arriba hay algunas cosas del marco anterior.
Y no olvide que aquí en la parte inferior, a la izquierda de "i", tenemos un puntero de pila ESP que va allí, y un nuevo puntero de quiebre viene en la sección guardada de EBP. La dirección de retorno incluye ESP, y el resto del cuadro anterior incluye un punto de interrupción.

Permítame recordarle que la forma en que la pila se desborda es que los datos se acumulan hacia arriba, en la dirección de esta flecha a la derecha. Cuando se inicia la operación de obtención, comenzamos a escribir bytes en el búfer, al final, comenzará a sobrescribir todo lo que se encuentra en sentido ascendente. Básicamente, todo te debería resultar familiar.
¿Qué hace un atacante para aprovechar esto? Básicamente, ingresa una larga secuencia de datos. Por lo tanto, la idea clave es que dicha técnica se puede utilizar para atacar.
Y si el atacante captura la dirección de retorno, puede determinar dónde saltará la función después del desbordamiento. Es decir, lo único que puede hacer un hacker es interceptar la dirección del remitente y saltar a donde quiera. La mayoría de los atacantes ejecutan código con privilegios para controlar el proceso de intercepción.
Entonces, si este proceso fue de alta prioridad, por ejemplo, se ejecutó como root o administrador, no importa lo que llamemos el superusuario de su sistema operativo favorito, ahora este programa, que está controlado por un atacante, puede hacer lo que quiera usando los privilegios de esta prioridad. Por lo tanto, un hacker puede leer archivos o enviar spam si daña un servidor de correo. Incluso puede vencer a los cortafuegos, porque la idea de un cortafuegos es que hay máquinas "buenas" detrás y "malas" fuera de él. Por lo general, las computadoras dentro del firewall "confían" entre sí, y si logras hackear al menos una computadora dentro de una red protegida por un firewall, será genial. Porque ahora simplemente puede omitir las muchas comprobaciones que estas computadoras suelen hacer con respecto a las máquinas "extrañas", porque lo considerarán una persona de confianza.
Hay algo en lo que tendrías que pensar y que yo pensaba como estudiante:
“Genial, nos mostraron cómo desbordar el búfer, pero ¿por qué el sistema operativo no puede detener esto? ¿No actúa como alguien como los Guardianes de la Galaxia, que protege el bien de las cosas malas que suceden a su alrededor? "
Es importante tener en cuenta que el sistema operativo no lo supervisa constantemente. Y el hardware observa, extrae instrucciones y las descifra y hace muchas cosas similares. Pero en una primera aproximación, ¿qué hace el sistema operativo? Básicamente, configura tablas de páginas que permiten que la aplicación funcione, y si le pide al sistema operativo, por ejemplo, que envíe un paquete de red, o si desea hacer algún tipo de solicitud de IPC o cosas similares, recurrirá al sistema operativo para obtener ayuda. Pero el sistema operativo no sigue todas las instrucciones que ejecuta su aplicación. En otras palabras, cuando este búfer está lleno, el sistema operativo no supervisa en absoluto cómo se usa la memoria de esta pila. Todo este espacio de direcciones le pertenece a usted, como iniciador del proceso, y esto no se aplica al sistema operativo. Puede hacer lo que quiera con esto, y el sistema operativo no puede ayudarlo con los problemas.
Más adelante discutiremos algunas de las cosas que el sistema operativo puede hacer con respecto al hardware para defenderse de este tipo de ataque. Permíteme recordarte nuevamente: de hecho, solo el hardware monitorea lo que estás haciendo y reacciona a él. Por lo tanto, puede aprovechar algunos tipos especiales de protección, lo discutiremos más a fondo.
Así es como se ve un desbordamiento de búfer. ¿Cómo vamos a arreglar todas estas cosas?
Una forma de evitar desbordamientos del búfer es simplemente evitar errores en el código C. Este es un enfoque constructivo, porque si su programa no tiene errores, el atacante no puede usarlos. Sin embargo, esto es más fácil decirlo que hacerlo. Hay algunas cosas muy simples que los programadores pueden hacer para proporcionar una "higiene" de seguridad. Por ejemplo, características como gets, que ahora sabemos, pueden llamarse "go-tos" o "capturar el sistema operativo", lo cual es una violación de seguridad.
Entonces, cuando compila su código usando un compilador moderno como GCC o Visual Studio, indicarán las desventajas de tales funciones. Dirán: "Oye, estás usando algo peligroso, mejor considera usar la función fgets u otras funciones que realmente puedan hacer un seguimiento del cumplimiento de las fronteras". Esta es una de las cosas simples que los programadores pueden hacer.
Pero tenga en cuenta que muchas aplicaciones en realidad manipulan buffers sin recurrir a todas estas funciones. Esto es muy común en los servidores de red que definen sus propios procedimientos de análisis y luego se aseguran de que los datos se recuperen del búfer de la manera que deseen. Por lo tanto, simplemente limitándose a la selección de las funciones de comando correctas, no será posible resolver completamente el problema.
Otra circunstancia que hace que este enfoque del problema sea más difícil es que no siempre es obvio que el problema es causado por un error en un programa escrito en C. Si alguna vez ha trabajado en un programa a gran escala escrito en C, usted sabe , como sucede con los identificadores de función que tienen 18 estrellas sobre el puntero void *. Creo que solo Zeus sabe lo que esto puede significar, ¿verdad? Con lenguajes como C, incluso un programador puede tener dificultades para comprender si se produjo un error o no.
En general, uno de los temas principales de nuestras conferencias será que el lenguaje C es un producto del diablo. Y lo usamos solo porque siempre queremos ser más rápidos que los demás, ¿verdad? Pero a medida que el hardware se vuelve cada vez más rápido, usamos lenguajes más avanzados para escribir código de sistema voluminoso. Sin embargo, no siempre tiene sentido escribir su código C, incluso si cree que será más rápido. Discutiremos este tema más tarde.

Por lo tanto, el primer enfoque para resolver el problema es evitar errores en el código del programa C, y el segundo es crear herramientas que ayuden a los programadores a encontrar dichos errores. Un ejemplo de dicha herramienta es el análisis de código estático. Más adelante hablaremos en detalle, y ahora diré que el análisis estático es una forma de analizar el código fuente de su programa incluso antes de que comience, y ayuda a detectar posibles problemas.
Imagine que tiene dicha función, llamémosla
void foo (int, * p) , contiene datos enteros y un puntero. Digamos que declara un valor de compensación de entero
int off . Esta función declara otro puntero y le agrega un desplazamiento:
int * z = p + off . Incluso ahora, al escribir una función, el análisis de código estático puede decirnos que esta variable de desplazamiento no se inicializa.

Por lo tanto, al analizar el programa, es posible responder a la pregunta de si nuestra función funcionará correctamente. Y en este ejemplo, es muy simple ver la respuesta "no, no lo hará" porque no hay inicialización compensada. El análisis estático es un software, y cuando use el compilador popular para construir su código, le dirá: "Hola, amigo, esto no se ha inicializado". ¿Estás seguro de que quieres hacer exactamente eso? Este es uno de los ejemplos más simples del uso del análisis estático.
Otro ejemplo considera el caso cuando tenemos una rama de una función, es decir, su ejecución bajo una determinada condición.

Entonces, si el desplazamiento es mayor que 8,
si (desactivado> 8) , esto lleva a una llamada a alguna
barra de función
(desactivado) . Entonces, esta condición nos dice cuál es el valor de compensación. Incluso ignorando el hecho de que el desplazamiento no se inicializa, al analizar esta rama de la función, todavía descubrimos que puede ser mayor que 8. Por lo tanto, cuando comenzamos a realizar un análisis estático de la barra, descubrimos que el desplazamiento solo puede tomar ciertos valores. Observo una vez más que esta es una introducción muy superficial al análisis estático, luego consideraremos esta herramienta con más detalle. Pero este ejemplo muestra cómo puede detectar algunos tipos de errores incluso sin ejecutar código.
Entonces, una cosa más en la que podría pensar es que hace lo mismo que el análisis estático. Esto es software difuso. Su idea es que tome todas las funciones en su código de programa y luego ingrese valores aleatorios en ellas. Por lo tanto, todas las opciones para los valores y formatos de su código se superponen. Es decir, Fuzzing es una herramienta para buscar vulnerabilidades automáticamente al enviar datos no válidos o datos en el formato incorrecto a la entrada del programa. Por ejemplo, ingresa los valores 2, 4, 8 y 15 en las pruebas unitarias y recibe un mensaje de que el número 15 es probablemente incorrecto, ya que todos los números son pares, pero es impar.
De hecho, debe observar cuántas ramas del programa en su conjunto afectan su código de prueba, porque estos son generalmente los lugares donde se ocultan los "errores". Los programadores no piensan en tales "rincones y grietas" y, como resultado, pasan algunas pruebas unitarias, puede decir la mayoría de estas pruebas. Sin embargo, no examinan todos los "rincones y grietas" del programa, y aquí es donde el análisis estático puede ayudar. Nuevamente, usando cosas como el concepto de restricción. Por ejemplo, en nuestra sección del programa, esta es una condición para bifurcar una función que define un desplazamiento de más de ocho. Por lo tanto, podemos descubrir que este desplazamiento es estático. Y si utilizamos la generación automática de Fuzzing de datos de entrada basada en esta restricción, entonces podemos asegurarnos de que uno de los valores de entrada para el desplazamiento sea menor que 8, uno sea 8 y uno sea mayor que 8. ¿Está claro?

Esta es la idea principal detrás del concepto de crear herramientas para ayudar a los programadores a encontrar errores. Incluso el análisis de código parcial puede ser muy útil cuando se trabaja con el lenguaje C. Muchas de las herramientas que veremos que sirven para evitar desbordamientos del búfer o verificar la inicialización de las variables no pueden detectar todos los problemas del código del programa. Pero pueden ser de utilidad práctica para mejorar la seguridad de estos programas. La desventaja de estas herramientas es que no están completas. El progreso prospectivo no es un progreso completo. Por lo tanto, debe explorar activamente el problema de la protección contra exploits tanto en programas escritos en C como en otros programas. Examinamos 2 enfoques para resolver el problema de protección de desbordamiento del búfer, pero hay otros enfoques.
Entonces, el tercer enfoque es el uso de un lenguaje seguro para la memoria, o un lenguaje que garantice la seguridad de la memoria. Estos lenguajes incluyen Python, Java, c #. No quiero poner a Perl a la par con ellos, porque lo usan las "malas personas". De esta manera, puede usar un lenguaje seguro para la memoria, y parece que esto es lo más obvio que puede hacer. Les acabo de explicar que básicamente C es un codificador de ensamblaje de alto nivel, pero proporciona punteros sin formato y otras cosas no deseadas, entonces, ¿por qué no usar uno de estos lenguajes de programación de alto nivel?
Hay varias razones para esto. En primer lugar, en estos idiomas hay muchos elementos del código heredados de C. Todo está bien si inicia un nuevo proyecto y utiliza uno de los lenguajes de alto nivel que garantizan la seguridad de la memoria. Pero, ¿qué pasaría si se le diera un archivo binario grande o una distribución de fuente grande que se escribió en C y se mantuvo durante 10-15 años, este fue un proyecto generacional, quiero decir que incluso nuestros hijos continuarán trabajando en él? ? En este caso, no podrá decir: "¡Solo lo reescribo todo en C # y cambio el mundo!".
Y el problema no es solo en el lenguaje C, hay sistemas que debes temer aún más, ya que usan códigos Fortran y COBOL, cosas de la Guerra Civil. ¿Por qué está pasando esto? Porque, como ingenieros, queremos pensar que podemos construir todo nosotros mismos, y será increíble, será como yo quiera, y llamaré a mis variables como quiera.
Pero en el mundo real esto no sucede. Aparece en el trabajo, y tiene este sistema que ya existe, y mira la base del código y piensa por qué no hace lo que se necesita. Y luego te dicen: "Escucha, haremos todo lo que quieras, pero solo en la segunda versión del programa, y ahora tienes que hacer lo que tenemos para trabajar, porque de lo contrario los clientes recuperarán su dinero".
Entonces, ¿cómo lidiamos con el gran problema del uso forzado del código heredado? Como saben, una de las ventajas de los sistemas con una definición errónea de límites es que funcionan perfectamente con este código obsoleto. Esta es una de las razones por las que no puede deshacerse del problema de desbordamiento del búfer simplemente cambiando a idiomas que proporcionan un uso seguro de la memoria.

¿Qué pasa si necesitamos acceso de bajo nivel al hardware? Por ejemplo, para actualizar controladores y otras cosas.
Por lo tanto, surge otro problema si necesita acceso de bajo nivel a los equipos, lo que sucede al escribir controladores para algunos dispositivos. En este caso, realmente necesita las ventajas que ofrece C, por ejemplo, la capacidad de mirar registros y elementos de función similares.
Además, la necesidad de usar C surge cuando le preocupa el rendimiento del sistema. , , , . , , . , , memory-safe . , JIT. , Java, Java Script. , , «». , . , «» x86.
, , -. , , JVM, - Java. , - . , - JVM , . , , . . , , .
, , 86. JIT- , . JIT- , .
, JavaScript , , «» 32- , . JIT-, «» . , , JIT-, , , .

«» , asm.js – JavaScript, , , . , , , JavaScript , . JavaScript, JavaScript, C ++.
, -, IO. . , , , , . «» , .
. , - . , C C++, , . Python, , , . . .
, , , .
, . , . , . , «» , . , C C++, .
, ? , , ? ?
. , – . , - , . , . , -, , . IP , , . , - . , , . , , «» .
, , , , , , . , - , . , , , . , , , , , , .

, . stack canaries, « », , . « » , , , . , , .
. , « », . , , «».
28:30
:
Curso MIT "Seguridad de sistemas informáticos". 2: « », 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 EE. UU. 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?