Un vistazo a la explotación de vulnerabilidades

Como me quedé con ganas de seguir tratando sobre este tema, permítanme contarles un poco de historia, teoría y práctica sobre las vulnerabilidaes. Todos hemos oído a estas alturas que  los fallos de seguridad pueden costar mucho, todos sabemos que debemos mantener nuestro software actualizado, todos sabemos que muchas actualizaciones se producen por errores de seguridad. Pero hoy les contaré un poco sobre cómo es que se encuentran y se explotan dichos errores 🙂 Pero antes de esto vamos a aclarar unos cuantos detalles para poder tener un mejor panorama.

Antes de empezar

Primero quiero decirles que nos vamos a centrar en la primer vulnerabilidad que aprendí a explotar, los conocidos Buffer Overflows, en esta vulnerabilidad aprovechamos una falta de verificación en la memoria para hacer cosas divertidas 🙂 Pero vamos a aclarar un poco más al respecto.

Esto no va a ser un escenario del mundo real

No puedo darme el lujo de enseñarles a romper cualquier programa que vean 🙂 primero porque es peligroso para sus computadoras, segundo porque eso me tomaría más de mi acostumbrada cuota de palabras.

Nos vamos de viaje a los 80s

Esto que les voy a mostrar lo puedo hacer en mi laptop, pero no quiere decir que se pueda realizar hoy por hoy de manera sencilla 🙂 muchos de estos conceptos ya han sido explotados tantas veces que han surgido nuevos métodos de protección y nuevos métodos para evadirlos 😛 pero eso nos regresa al mismo lugar, falta espacio para poder contar todo eso 🙂

Tal vez no funcione en tu procesador

Aunque voy a usar un ejemplo muy simple, quiero que desde el principio quede bastante claro que los pormenores de esto son tantos y tan variados que así como puede salirte igual que a mí, si deseas intentarlo, también puede que no se consiga el efecto deseado 🙂 Pero ya se imaginarán que eso no puedo explicarlo en este espacio, sobre todo porque con esta introducción ya me llevé más de 300 palabras, así que directo a lo nuestro.

Qué es un Buffer Overflow

Para responder esto primero tenemos que comprender la primera mitad de esta combinación.

Buffers

Como todo se trata de memoria en un equipo computacional, es lógico que debe existir algún tipo de contenedor de información. Cuando hablamos de inputs outputs, llegamos directamente al concepto de buffers. Para hacerlo corto, un buffer es un espacio de memoria de tamaño definido en el que vamos a almacenar una cantidad de información, simple 🙂

Los overflow ocurren, como su nombre lo indica, cuando un buffer se llena con más información de la que puede aguantar. Pero, ¿por qué es esto importante?

Stack

También conocido como pilas, son un tipo de datos abstracto en el que podemos apilar información, su principal característica es que cuentan con un ordenamiento LIFO (Last In First Out). Pensemos por un segundo en una pila de platos, nosotros los ponemos por encima uno a uno, y luego los sacamos uno a uno desde arriba, esto hace que el último plato que hayamos puesto (el que se encuentra hasta arriba) sea el primer plato que vamos a sacar, evidentemente si solo podemos sacar un plato a la vez y decidimos hacerlo en ese orden :P.

Ahora que ya conocen estos dos conceptos, tenemos que ordenarlos. Las pilas son importantes porque cada programa que ejecutamos tiene su propia pila de ejecución. Pero esta pila tiene una característica particularcrece hacia abajo. Lo único que deben saber de esto es que mientras un programa se ejecuta, cuando una función es llamada, la pila pasa de un número X de memoria a un número (X-n). Pero para poder seguir debemos comprender un concepto más.

Punteros

Este es un concepto que vuelve loco a muchos programadores cuando empiezan en el mundo de C, a decir verdad la gran potencia de la programación en C se debe en parte al uso de punteros. Para hacerlo simple, un puntero apunta a una dirección de memoria. Esto suena complejo, pero no lo es tanto, todos tenemos RAM en nuestras máquinas ¿cierto? Pues esta puede definirse como un arreglo consecutivo de bloques, normalmente dichas ubicaciones se expresan en números hexadecimales ( del 0 a 9 y luego de eso de A a F, como por ejemplo 0x0, 0x1, 0x6, 0xA, 0xF, 0x10). Aquí como nota curiosa, 0x10 NO es igual a 10 😛 si lo convertimos al orden decimal sería lo mismo que decir 15. Esto es algo que también confunde a más de uno al principio, pero vamos a lo nuestro.

Registros

Los procesadores trabajan con una serie de registros, los cuales funcionan para transmitir ubicacioines desde la memoria física al procesador, para arquitecturas que usan 64-bits, la cantidad de registros es grande y difícil de describir aquí, pero para hacernos a la idea, los registros son como punteros, indican entre otras cosas, un espacio en memoria (ubicación).

Ahora la práctica

Sé que ha sido mucha información para procesar hasta ahora, pero en realidad son temas algo complejos que intento explicar de manera muy simple, vamos a ver un pequeño programa que usa buffers y lo vamos a romper para entender esto de los overflows, evidentemente este no es un programa real, y vamos a «evadir» muchas de las contramedidas que se usan hoy en día, solo para mostrar cómo se hacían las cosas antes 🙂 y porque algunos de estos principios son necesarios para poder aprender cosas más complejas 😉

GDB

Un gran programa que es sin duda uno de los más usados por programadores en  C. Entre sus múltiples virtudes tenemos el hecho de que nos permite ver todo esto que hemos estado conversando hasta ahora, registros, la pila, buffers, etc 🙂 Vamos a ver el programa que vamos a usar para nuestro ejemplo.

retinput.c

Diseño propio. Christopher Díaz Riveros

Este es un programa bastante simple, vamos a usar la librería stdio.h para poder obtener información y mostrarla en un terminal. Podemos ver una función llamada return_input la cual genera un buffer llamado array, que tiene una longitud de 30 bytes (el tipo de dato char es de 1 byte de largo).

La función gets(array); solicita información por consola y la función printf() devuelve el contenido de array y lo muestra en pantalla.

Todo programa escrito en C comienza por la función main(), esta solo se va a encargar de llamar a return_input, ahora vamos a compilar el programa.

Diseño propio. Christopher Díaz Riveros

Vamos a desprender un poco de lo que acabo de hacer. La opción -ggdb le indica a gcc que tiene que compilar el programa con información para que gdb sea capaz de realizar un debug adecuado. -fno-stack-protector es una opción que evidentemente no deberíamos estar usando, pero que vamos a usar porque en caso contrario nos sería posible genererar el buffer overflow en el stack. Al final he probado el resultado. ./a.out solo ejecuta lo que acabo de compilar, me pide información y la devuele. Funcionando 🙂

Advertencias

Otra nota aquí. ¿Pueden ver las advertencias? claramente es algo a tener en cuenta cuando trabajamos con código o compilamos, esta es un poco obvia y son pocos los programas que hoy en día tienen la función gets() en el código. Una ventaja de Gentoo es que al compilar cada programa, puedo ver lo que puede estar mal, un programa «ideal» no debería tenerlas, pero les sorprendería cuántos programas grandes tienen estas advertencias porque simplemente son MUY grandes y es difícil mantener el rastro de las funciones peligrosas cuando son muchas advertencias al mismo tiempo. Ahora si sigamos

Depurando el programa

Diseño propio. Christopher Díaz Riveros

Ahora, esta parte puede ser un poco confusa, pero como ya he escrito bastante, no puedo darme el lujo de explicarlo todo, así que perdón si ven que voy muy rápido 🙂

Desarmando el código

Vamos a empezar viendo nuestro programa compilado en lenguaje máquina.

Diseño propio. Christopher Díaz Riveros

Este es el código de nuestra función main en Assembly, esto es lo que entiende nuestro procesador, la línea de la izquierda es la dirección física en memoria, el <+n> se conoce como offset, básicamente la distancia desde el principio de la función (main) hasta esa instrucción (conocido como opcode). Luego vemos el tipo de instrucción (push/mov/callq…) y uno o más registros. Resumido podemos decir que es la indicación seguida de la fuente/origen y el destino. <return_input> hace referencia a nuestra segunda función, vamos a darle un vistazo.

Return_input

Diseño propio. Christopher Díaz Riveros

Esta es un poco más compleja, pero solo quiero que revisen un par de cosas, existe un label llamado <gets@plt> y un último opcode llamado retq que indica el final de la función. Vamos a poner un par de breakpoints, uno en la función gets y otro en el retq.

Diseño propio. Christopher Díaz Riveros

Run

Ahora vamos a correr el programa para ver cómo empieza a comenzar la acción.

Diseño propio. Christopher Díaz Riveros

Podemos ver que aparece una pequeña flecha que indica el opcode donde nos encontramos, quiero que tengan en cuenta la dirección 0x000055555555469b, esta es la dirección que se encuentra tras la llamada a return_input en la función main , esto es importante puesto que ahí es donde debería regresar el programa cuando acabe de recibir el input, vamos a entrar en la función. Ahora vamos a revisar la memoria antes de entrar a la función gets.

Diseño propio. Christopher Díaz Riveros

Les he vuelto a poner la función main arriba, y he resaltado el código al que me refería, como pueden ver, debido al endianess se ha separado en dos segmentos, quiero que tengan en cuenta la dirección 0x7fffffffdbf0 (la primera de la izquierda tras el commando x/20x $rsp) puesto que esta es la ubicación que tenemos que usar para revisar el resultado de gets, sigamos:

Rompiendo el programa

Diseño propio. Christopher Díaz Riveros

He resaltado esos 0x44444444porque son la representación de nuestras Ds 🙂 ahora hemos empezado a agregar input al programa, y como pueden apreciar, estamos a solo dos líneas de nuestra dirección deseada, vamos a llenarlo hasta estar justo antes de las direcciones que resaltamos en el paso anterior.

Cambiando la ruta de retorno

Ahora que hemos logrado entrar en esta sección del código donde indica el retorno de la función, vamos a ver qué sucede si cambiamos la direacción 🙂 en lugar de ir a la ubicación del opcode que sigue al que teníamos hace un momento, ¿qué les parece si regreamos a return_input? Pero para esto, es necesario escribir en binario la direacción que deseamos, vamos a hacerlo con la función printf de bash 🙂

Diseño propio. Christopher Díaz Riveros

Ahora hemos recibido dos veces la información 😀 seguramente el programa no estaba hecho para eso, pero hemos conseguido romper el código y hacer que repita algo que no se suponía que haga.

Reflexiones

Este simple cambio puede considerarse un exploit muy básico 🙂 ha logrado romper el programa y hacer algo que nosotros deseamos que haga.

Este es solo el primer paso en una casi infinita lista de cosas por ver y agregar, existen formas de agregar más cosas que simplemente repetir una orden, pero esta vez he escrito mucho y todo lo relacionado a shellcoding es un tema para escribir más que artículos, libros completos diría yo. Disculpen si no he podido ahondar un poco más en temas que me hubieran gustado, pero seguro ya habrá oportunidad 🙂 Saludos y gracias por llegar hasta aquí.


14 comentarios, deja el tuyo

Deja tu comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*

*

  1. Responsable de los datos: Miguel Ángel Gatón
  2. Finalidad de los datos: Controlar el SPAM, gestión de comentarios.
  3. Legitimación: Tu consentimiento
  4. Comunicación de los datos: No se comunicarán los datos a terceros salvo por obligación legal.
  5. Almacenamiento de los datos: Base de datos alojada en Occentus Networks (UE)
  6. Derechos: En cualquier momento puedes limitar, recuperar y borrar tu información.

  1.   2p2 dijo

    Sé más directo. Escribe menos y céntrate en lo importante

    1.    ChrisADR dijo

      Hola, gracias por el comentario.

      A decir verdad he recortado una buena parte de ideas, pero aún así me parecía que deje lo mínimo como para que alguien que no tenga conocimientos de programación se pueda dar una idea.

      Saludos

      1.    Anónimo dijo

        El problema es que quién no tiene conocimientos de programación no se va a enterar de nada porque es demasiado complejo para empezar, pero quien sabe programar agradece que se sea más directo.

        Supongo que no se puede llegar a todo el mundo, hay que elegir, y en este caso has pecado de querer abarcar mucho.

        Por cierto, te lo digo como crítica constructiva, me encantan estos temas y me gustaría que siguieras escribiendo artículos, enhorabuena!

    2.    Anónimo dijo

      Opino lo mismo.

      1.    ChrisADR dijo

        Muchas gracias a ambos!! ciertamente es difícil entender cómo llegar al público objetivo cuando la verdad es que el número de personas con un nivel avanzado de programación que leen estos artículos es poco (al menos eso se puede inferir en base a los comentarios)

        Ciertamente he pecado de querer simplificar algo que requiere una amplia base de conocimientos para poder entenderse. Espero que entiendan que como recién estoy empezando en esto de los blogs todavía no he descubierto el punto exacto donde mis lectores conocen y entienden lo que digo. Eso lo haría mucho más fácil a decir verdad 🙂

        Intentaré ser más breve cuando amerite sin despersonalizar el formato, puesto que desligar la forma de escribir del contenido es un poco más complicado de lo que uno podría imaginar, yo al menos los tengo bastante ligados, pero supongo que en última instancia podré agregar líneas en lugar de cortar contenido.

        Saludos

  2.   Mario dijo

    Donde se podria saber más del tema? Algún libro recomendado?

    1.    ChrisADR dijo

      El ejemplo lo saqué de The Shellcoder’s Handbook de Chris Anley, John Heasman, Felix Linder y Gerardo Richarte, pero para poder hacer la traducción a 64 bits tuve que aprender sobre mi arquitectura, el manual de developer de intel, tomos 2 y 3 son una fuente bastante confiable para eso. También es bueno leer la documentación de GDB, que viene con el comando ‘info gdb’, Para aprender Assembly y C existen muchos libros muy buenos, excepto que los de Assembly son un poco antiguos por lo que hay una brecha que llenar con otro tipo de documentación.

      El shellcode mismo ya no es tan efectivo en estos días por varios motivos, pero no deja de ser interesante para aprender nuevas técnicas.

      Espero que ayude un poco 🙂 Saludos

  3.   Franz dijo

    Buen artículo, el viejo blog desdelinux ha vuelto a renacer =)
    Cuando dices que el Shell remoto no es tan efectivo, te refieres a la contramedidas diseñadas para mitigar ataques, le llaman seguridad ofensiva.
    Saludos y sigue asi

    1.    ChrisADR dijo

      Muchas gracias Franz 🙂 muy amables palabras, en realidad me refería a que el Shellcoding hoy por hoy es mucho más complejo que esto que vemos aquí. Tenemos el ASLR ( generador de ubicaciones de memoria random) el protector de stack, las diversas medidas y contramedidas que limitan la cantidad de opcodes que se pueden inyectar en un programa, y solo es el principio.

      Saludos,

  4.   Software Libre dijo

    Hola, harás otra parte expandiendo el tema? Es interesante

    1.    ChrisADR dijo

      Hola, ciertamente es bastante interesante el tema, pero el nivel de complejidad que tomaríamos se tornaría muy elevado, probablemente implicando una gran cantidad de posts para explicar los diversos pre-requisitos para entender lo otro. Probablemente escriba al respecto, pero no van a ser los siguientes posts, quiero escribir unos cuantos temas antes de continuar con este.

      Saludos, y graicas por compartir

  5.   cactus dijo

    Muy bueno che! Estas aportando grandes posts! Una pregunta, estoy comenzando esto de Seguridad IT leyendo un libro que se llama «Assuring security by pen testing». Es recomendado este libro? Como me sugeris que empiece a indagar sobre estos temas?

    1.    ChrisADR dijo

      Hola cactus, pues es un universo entero esto de las vulnerabilidades, y demás, a decir verdad depende mucho de lo que te llame la atención, y las necesidades que tengas, un gerente de TI no requiere saber lo mismo que un pen-tester, o un investigador de vulnerabilidades, o un analista forense, un equipo de recuperación frente a desastres tiene otro juego de skills muy distinto. Evidentemente cada uno de ellos requiere un nivel distinto a nivel técnico de conocimientos, yo te recomiendo que empieces descubriendo exactamente qué te gusta, y empieces a devorar libros, artículos, y demás, y lo más importante práctica todo lo que leas, así esté desactualizado, eso va a marcar la diferencia al final.
      Saludos,

  6.   Eitzen dijo

    Hola.
    Muchísimas gracias por explicar este tema, además de comentar que para información extra tenemos «The Shellcoder’s Handbook». Ya tengo una lectura pendiente 😉