Curso MIT "Seguridad de sistemas informáticos". Lección 3: Desbordamientos del búfer: exploits y protección, parte 2

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 3
Lección 2: "Control de ataques de hackers" Parte 1 / Parte 2 / Parte 3
Lección 3: “Desbordamientos del búfer: exploits y protección” Parte 1 / Parte 2 / Parte 3

Curiosamente, un atacante no puede saltar a una dirección específica, a pesar del hecho de que usamos principalmente direcciones codificadas. Lo que hace se llama un "ataque de montón", y si eres una mala persona, será muy divertido para ti. Con tal ataque, un hacker comienza a asignar dinámicamente toneladas de código shell y simplemente lo ingresa aleatoriamente en la memoria. Esto es especialmente efectivo si usa lenguajes dinámicamente de alto nivel como JavaScript. Por lo tanto, el lector de etiquetas está en un bucle estrecho y simplemente genera una gran cantidad de líneas de código de shell y luego llena un montón de ellas.

El atacante no puede determinar la ubicación exacta de las líneas, simplemente selecciona líneas de código de shell de 10 MB y realiza un salto arbitrario. Y si de alguna manera puede controlar uno de los punteros de ret , existe la posibilidad de que "aterrice" en el código de shell.



Puede usar un truco llamado NOP slide , NOP sled o NOP ramp , donde NOP son instrucciones de no operación , o comandos vacíos, inactivos. Esto significa que el flujo de ejecución del comando del procesador "se desliza" a su destino final deseado cuando el programa va a la dirección de memoria en cualquier lugar de la diapositiva.

Imagine que si tiene una línea de código de shell y va a un lugar aleatorio en esa línea, esto puede no funcionar, ya que no le permite desplegar el ataque de la manera correcta.

Pero tal vez lo que pones en el montón es básicamente solo una tonelada de NOP , y al final, tienes un código de shell. Esto es bastante inteligente, porque significa que ahora puedes llegar al lugar correcto donde estás saltando. Porque si saltas a uno de estos NOPs , simplemente sucede "boom, boom, boom, boom, boom, boom, boom", y luego entras en el código de shell.

Parece que a la gente se le ocurre esto, lo que probablemente veas en nuestro equipo. Inventan algo así, y ese es el problema. Entonces, esta es otra forma de sortear algunas cosas aleatorias simplemente haciendo robusta la aleatorización de sus códigos, si eso tiene sentido.

Entonces, hemos discutido algunos tipos de aleatoriedad que puede usar. Hay algunas ideas estúpidas que también surgieron en las personas. Entonces, ahora sabe que cuando desea realizar una llamada al sistema, por ejemplo, utilizando la función syscall libc , básicamente pasa cualquier número único que represente la llamada al sistema que desea realizar. Entonces, tal vez la función tenedor es 7, el sueño es 8, o algo así.

Esto significa que si un atacante puede averiguar la dirección de esta instrucción syscall y obtenerla de alguna manera, puede sustituir el número de llamada del sistema que desea usar directamente. Puede imaginar que cada vez que se ejecuta el programa, en realidad crea una asignación dinámica de números de syscall a syscalls válidas, para complicar la captura del atacante.



Incluso hay algunas sugerencias de vanguardia para cambiar el hardware para que el equipo contenga la clave de cifrado xor , que se utiliza para las funciones dinámicas xor . Imagine que cada vez que compila un programa, todos los códigos de instrucciones obtienen una determinada clave xor . Esta clave se almacena en el registro del equipo cuando descarga inicialmente el programa, y ​​después de eso, cada vez que ejecuta la instrucción, el equipo realiza automáticamente su operación antes de continuar con esta instrucción. Lo bueno de este enfoque es que ahora, incluso si un atacante puede generar código shell, no reconocerá esta clave. Por lo tanto, será muy difícil para él descubrir qué es exactamente lo que debe guardarse en la memoria.

Público: pero si puede obtener el código, también puede usar xor para volver a convertir el código en una instrucción.

Profesor: sí, ese es el problema canónico, cierto. Esto es algo similar a lo que sucede durante los ataques BROP , cuando parecemos aleatorizar la ubicación del código, pero el atacante puede "sentirlo" y descubrir qué está sucediendo. Uno puede imaginar que, por ejemplo, si un atacante conoce alguna subsecuencia de código que espera encontrar en un archivo binario, intentará usar la operación xor para este archivo para extraer la clave.

Esencialmente, discutimos todo tipo de ataques de aleatorización que quería contarles hoy. Antes de pasar a la programación, vale la pena discutir cuáles de estos métodos de protección se utilizan en la práctica. Resulta que tanto GCC como Visual Studio incluyen el enfoque de canarios de pila de forma predeterminada . Esta es una comunidad muy popular y muy famosa. Si observa Linux y Windows, también aprovechan cosas como la memoria no ejecutable y la aleatorización del espacio de direcciones. Es cierto que el sistema de límites holgados no es tan popular entre ellos, probablemente debido al costo de la memoria, el procesador, las falsas alarmas, etc., de los que ya hemos hablado. Básicamente, hemos examinado cómo van a evitar las cosas el problema de desbordamiento del búfer.

Ahora hablaremos sobre ROP , programación orientada hacia atrás. Hoy ya te dije lo que representa en términos de aleatorizar el espacio de direcciones y evitar que los datos se ejecuten: leer, escribir y ejecutar. Estas son en realidad cosas muy poderosas. Porque la aleatorización evita la posibilidad de que un atacante entienda dónde están nuestras direcciones codificadas. Y la capacidad de evitar la ejecución de datos garantiza que incluso si coloca el código de shell en la pila, un atacante no puede simplemente saltar y ejecutarlo.

Todo esto parece bastante progresivo, pero los hackers están constantemente desarrollando métodos de ataque contra tales soluciones de defensa progresivas.

Entonces, ¿cuál es la esencia de la programación orientada hacia atrás?

¿Qué pasa si, en lugar de simplemente crear un nuevo código durante un ataque, un atacante podría combinar los fragmentos de código existentes y luego combinarlos de manera anormal? Después de todo, sabemos que el programa contiene toneladas de dicho código.



Entonces, afortunadamente, o desafortunadamente, todo depende de qué lado estés. Si puede encontrar algunos fragmentos de código interesantes y combinarlos, puede obtener algo como el lenguaje Turing , donde el atacante puede hacer esencialmente lo que quiera.

Veamos un ejemplo muy simple que le parecerá familiar al principio, pero luego se convertirá rápidamente en algo loco.

Digamos que tenemos el siguiente programa. Entonces, tengamos algún tipo de función y, lo cual es conveniente para el atacante, aquí está esta agradable función de ejecución de shell . Entonces, esto es solo una llamada al sistema, ejecutará el comando bin / bash y finalizará. A continuación, tenemos un proceso de desbordamiento de búfer canónico o, lo siento, una función que anunciará la creación de un búfer y luego usará una de estas funciones inseguras para llenar el búfer con bytes.



Entonces, sabemos que aquí el desbordamiento del búfer ocurre sin problemas. Pero lo interesante es que tenemos esta función de ejecución de shell , pero es difícil llegar a ella de manera basada en desbordamientos de búfer. ¿Cómo puede un atacante invocar este comando de ejecución de shell ?

En primer lugar, el atacante puede desmontar el programa, iniciar GDB y averiguar la dirección de esta cosa en el archivo ejecutable. Probablemente esté familiarizado con estos métodos de trabajo de laboratorio. Luego, durante un desbordamiento del búfer, un atacante puede tomar esta dirección, ponerla en el desbordamiento del búfer generado y verificar que la función regrese al shell de ejecución .

Para que quede claro, lo dibujaré. Entonces, tiene una pila que se ve así: en la parte inferior hay un búfer desbordado, arriba hay un indicador de espacio guardado, arriba está la dirección de retorno de prosess_msg . Abajo a la izquierda tenemos un nuevo puntero de pila que inicia la función, encima de él un nuevo puntero de ruptura, luego el puntero de pila que se usará, y aún más alto es el puntero de ruptura del cuadro anterior. Todo parece bastante familiar.



Como dije, durante el ataque, GDB se usó para averiguar cuál es la dirección del shell de ejecución . Por lo tanto, cuando el búfer se desborda, simplemente podemos poner la dirección del shell de ejecución aquí a la derecha. Esta es en realidad una extensión bastante simple de lo que ya sabemos cómo hacer. Esencialmente, esto significa que si tenemos un comando que inicia el shell, y si podemos desmontar el archivo binario para averiguar dónde está esta dirección, simplemente podemos ponerlo en esta matriz de desbordamiento ubicada en la parte inferior de la pila. Es bastante simple

Entonces, este fue un ejemplo extremadamente frívolo, porque el programador, por alguna loca razón, puso esta función aquí, presentando así al atacante un verdadero regalo.
Ahora suponga que en lugar de llamar a esto run_shell , lo llamaremos run_boring , y luego simplemente ejecuta el comando / bin / ls . Sin embargo, no perdimos nada, porque tendremos la cadena char * bash_path en la parte superior , que nos indicará la ruta a este bin / bash .



Entonces, lo más interesante de esto es que un atacante que quiere ejecutar ls puede "analizar" el programa y encontrar la ubicación de run_boring , y esto no es nada divertido. Pero, de hecho, tenemos una línea en la memoria que apunta a la ruta del shell, además, sabemos algo más interesante. Esto es que incluso si el programa no llama al sistema con el argumento / bin / ls , todavía hace algún tipo de llamada.

Entonces, sabemos que el sistema debe estar conectado de alguna manera con este programa - sistema ("/ bin / ls") . Por lo tanto, podemos usar estas dos operaciones nulas para asociar realmente el sistema con este argumento char * bash_path . Lo primero que hacemos es ir a GDB y descubrir dónde se encuentra este sistema ("/ bin / ls") en la imagen del proceso binario. Entonces, simplemente vaya a GDB , simplemente escriba print_system y obtenga información sobre su desplazamiento. Esto es bastante simple, y puede hacer lo mismo para bash_path . Es decir, simplemente usa GDB para averiguar dónde vive esta cosa.

Una vez que lo haya hecho, debe hacer otra cosa. Porque ahora realmente necesitamos descubrir de alguna manera cómo invocar el sistema usando el argumento que elegimos. Y la forma en que hacemos esto consiste esencialmente en falsificar el marco de llamada para el sistema. Si recuerda, un marco es lo que usan tanto el compilador como el hardware para implementar la llamada de pila.

Queremos organizar en la pila algo como lo que describí en esta figura. De hecho, vamos a falsificar un sistema que debería haber estado en la pila, pero justo antes de que realmente ejecute su código.

Entonces, aquí tenemos el argumento del sistema, esta es la línea que queremos ejecutar. En la parte inferior tenemos una línea donde el sistema debería regresar cuando se complete la línea con el argumento. El sistema espera que la pila se vea de esa manera justo antes de que comience la ejecución.



Solíamos suponer que no hay argumentos cuando pasa la función, pero ahora se ve un poco diferente. Solo necesitamos asegurarnos de que el argumento esté en el código de desbordamiento que estamos creando. Solo tenemos que asegurarnos de que este marco de llamada falso esté en esta matriz. Por lo tanto, nuestro trabajo será el siguiente. Recuerde que el desbordamiento de la pila va de abajo hacia arriba.



Primero, vamos a poner la dirección del sistema aquí. Y encima colocaremos alguna dirección de devolución de basura . Este es el lugar donde el sistema volverá una vez que finalice. Esta dirección será un conjunto aleatorio de bytes. Encima pondremos la dirección bash_path . ¿Qué sucede cuando el búfer se desborda ahora?

Después de que prosess_msg llegue a la línea de meta, dirá: "OK, ¡este es el lugar al que debería regresar"! El código del sistema continúa ejecutándose, se mueve más alto y ve el marco de llamada falso que creamos. Para el sistema, no ocurrirá nada sorprendente, dirá: "sí, aquí está, el argumento que quiero ejecutar es bin / bash ", lo ejecuta y está listo: ¡el atacante ha capturado el shell!

¿Qué hemos hecho ahora? Aprovechamos el conocimiento de la convención de llamadas , la convención de llamadas , como una plataforma para crear marcos de pila falsos o nombres de marcos falsos, diría yo. Usando estos marcos de llamada falsos, podemos realizar cualquier función a la que se haga referencia y que ya esté definida por la aplicación.

La siguiente pregunta que debemos hacernos es: ¿qué sucede si el programa no tiene esta línea char * bash_path ? Observo que esta línea casi siempre está presente en el programa. Sin embargo, supongamos que vivimos en un mundo invertido, y todavía no está allí. Entonces, ¿qué podríamos hacer para poner esta línea en un programa?

Lo primero que puede hacer para esto es especificar la dirección correcta para bash_path , colocándola más arriba, aquí en este compartimento de nuestra pila, insertando allí tres elementos, cada uno de los cuales tiene un tamaño de 4 bytes:

/ 0
/ pat
/ bin



Pero en cualquier caso, nuestro puntero viene aquí y ... ¡boom! - La cosa está hecha. De esa manera, ahora puede invocar argumentos simplemente colocándolos en su código de shell. Aterrador, ¿no es así? Y todo esto se construye antes de un ataque BROP completo. Pero antes de señalar un ataque BROP completo, debe comprender cómo simplemente encadena las cosas que ya están dentro del código. Cuando tengo esta dirección de retorno volcada aquí, solo queremos acceder al shell. Pero si eres un atacante, entonces podrías dirigir esta dirección de retorno, o dirección de retorno, a algo que realmente podría usarse. Y si hiciste esto, entonces podrías encadenar varias funciones en una fila, varios signos de una función en una fila. Esta es de hecho una opción muy poderosa.

Porque si simplemente establecemos la dirección de retorno para el salto, entonces el programa generalmente se bloquea, lo que tal vez no queremos. Por lo tanto, vale la pena vincular algunas de estas cosas para hacer cosas más interesantes con el programa.

Supongamos que nuestro objetivo es llamar al sistema un número arbitrario de veces. No solo queremos hacer esto una vez, lo haremos un número arbitrario de veces. Entonces, ¿cómo se puede hacer esto?

Para hacer esto, usamos dos piezas de información que ya sabemos cómo obtener. Sabemos cómo obtener la dirección del sistema; solo tiene que buscar en GDB y encontrarlo allí. También sabemos cómo encontrar la dirección de esta línea, bin / bash . Ahora, para iniciar este ataque usando múltiples llamadas al sistema, necesitamos usar gadgets. Esto nos acerca a lo que sucede en BROP .

Entonces, lo que necesitamos ahora es encontrar la dirección de estas dos operaciones de código: pop% eax y ret . El primero elimina la parte superior de la pila y lo coloca en el registro eax , y el segundo lo coloca en el puntero de instrucción eip . Esto es lo que llamamos el gadget. Parece un pequeño conjunto de instrucciones de ensamblaje que un atacante puede usar para construir ataques más ambiciosos.



Estos gadgets son herramientas estándar que los hackers usan para encontrar cosas como archivos binarios. También es fácil encontrar uno de estos gadgets, suponiendo que tenga una copia del binario, y no nos molestó la aleatorización. Estas cosas son muy fáciles de encontrar, así como también es muy fácil encontrar la dirección del sistema, etc.

Entonces, si tenemos uno de estos gadgets, ¿por qué podemos usarlo? ¡Por supuesto, hacer el mal! Para hacer esto, puede hacer lo siguiente.

Supongamos que cambiamos nuestra pila para que se vea así, el exploit, como antes, se dirige de abajo hacia arriba. Lo primero que hacemos es colocar la dirección del sistema aquí, y encima colocamos la dirección del gadget pop / ret . Aún más alto, colocamos la dirección de bash_path , y luego repetimos todo: desde arriba colocamos nuevamente la dirección del sistema, la dirección del gadget pop / ret y la dirección de bash_path .



¿Qué pasará aquí ahora? Será un poco complicado, por lo que las notas de esta conferencia están disponibles en Internet, y por ahora puedes escuchar lo que está sucediendo aquí, pero cuando entendí esto por primera vez, ¡fue como entender que Santa Claus no existía!

Comenzaremos desde el lugar donde se encuentra la entrada de entrada , de regreso al sistema donde la declaración ret eliminará el elemento de la pila usando el comando pop , por lo que ahora la parte superior del puntero de la pila está aquí. Entonces, eliminamos el elemento usando pop , luego devolvemos el procedimiento ret , que transfiere el control a la dirección de retorno seleccionada de la pila, y esta dirección de retorno se coloca allí con el comando de llamada . Entonces, nuevamente hacemos una llamada al sistema, y ​​este proceso puede repetirse una y otra vez.



Está claro que podemos relacionar esta secuencia para realizar un número arbitrario de cosas. Esencialmente, el núcleo obtiene lo que se llama programación orientada hacia atrás. Tenga en cuenta que no realizamos nada en esta pila. Hicimos lo que nos permitió evitar la ejecución de datos sin destruir nada. Acabamos de hacer un salto inesperado para hacer lo que queremos. En realidad es muy, muy, muy inteligente.

Y lo interesante es que a un alto nivel hemos identificado este nuevo modelo para la informática. , , , . , , . , - . , , . , . . , . , , stack canaries.

, «» , . , , «» ret address saved %ebp , - , «». , ret , , «», , - . stack canaries .

, «». , . , «»?

, , , .
, , , «» , «» «».

, , , «» , , .
, - , «» , . , ? ?
, fork . , fork . , , , fork , , , , «» . , stack canaries .

«»? . , , , «». «» . .



, , – , «». , , 0. , «», . , :

«, «»! , 0. «»! 1 – «», 2 – . , 2- . , , «».



, , , .

«», , , . , , «».

57:10

:

Curso MIT "Seguridad de sistemas informáticos". Lección 3: Desbordamientos del búfer: exploits y protección, Parte 3


La versión completa del curso está disponible aquí .

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?

Source: https://habr.com/ru/post/es418093/


All Articles