Introducción:
En este artículo realizaremos una prueba de concepto (PoC), donde crearemos un código vulnerable a desbordamiento de pila y realizaremos un bypass a la contramedida stack canary estando activas otras contramedidas cómo el ASLR y NX. Pero antes y como siempre un poquito de historia.
Historia:
¿ Canary? Pero si esto me recuerda a una jaula con un canario. Pues efectivamente, esta contramedida hace una clara referencia a la utilización de canarios en las minas de carbón. Los canarios se encontraban en cabeza de la galería y cómo se veían afectados por gases tóxicos antes que los mineros, proporcionaban un sistema de alerta biológico.
Vale a ver si me queda claro, ¿si es canary porque también es cookie? A primera vista nada tienen que ver, pero si tenemos en cuenta que un canario muerto indica una galería rota y la imagen de una galleta rota evoca también algo que se encuentra dañado, la similitud es obvia. En ambos casos el valor se encuentra dañado.
funcionamiento:
¿ Y cómo funciona esta contramedida?
El stack canary es otra de las contramedidas usada en los sistemas Linux. También es conocida como stack cookie, stack protector, stack guard o SSP.
Esta contramedida tiene un valor de 4-bytes conocidos como Canaries, canarios o Canary words y son colocados sobre la pila cuando se ingresa una función. Cuando la función acaba su tarea, y se define el marco de la pila, el valor de esta contramedida se compara con el valor previamente enviado. Si el resultante es diferente, el programa termina su ejecución por la llamada a la función _stack_chk_fail.
En la imagen se ilustra cómo funciona la contramedida.
Un valor canario no debe confundirse con un valor centinela ( flag value, trip value, rogue value, signal value, o dummy data). Este es un valor especial en el contexto de un algoritmo, cuya presencia se utiliza como una condición de terminación normalmente en un bucle o algoritmo recursivo.
¿ Cuántos tipos de canaries hay en uso? Hay 5 tipos de canarios en uso: terminador, aleatorio, XOR aleatorio, carácter nulo y personalizado. Vamos a ver brevemente en que consisten cada uno de ellos:
- Terminador: se trata de fijar a una combinación de cadena terminaciones cómo 0x00, 0xff, 0x0a, 0x0d
- Nulo: este canary tiene fijado un valor de 0x00000000. Supuestamente, será imposible entregar ceros a la pila porque es un terminador nulo para cadenas.
- XOR aleatorio: el operador lógico realiza una comparativa con los datos de control almacenados.
- canario aleatorio: son canarios generados aleatoriamente y se rigen a partir de la recolección de entropía.
- Por supuesto, los canarios pueden ser implementados de forma personalizada, pero no suelen resultar tan efectivos. Por ejemplo, en la implementación de un canario personalizado es posible codificar el valor de una cookie en una variable. Este valor puede ser fácilmente obtenido durante el proceso de ingeniería reversa sobre el binario.
¿ Y cómo se cuándo un canario cumple su función? El aviso es mostrado en la terminal mediante los siguientes mensajes:
- ‘ Stack smashing detected’ Este mensaje, se muestra en un intento claro desbordamiento de búfer
- Llamada a la función __stack_chk_fail en el GDB, como ya hemos explicado anteriormente finaliza automáticamente el proceso.
Como ya sabemos no existe una sola manera de realizar un bypass a cualquiera de las contramedidas. Sin embargo, vamos a explicar la forma más común de realizarlo.
Bypaseando la contramedida:
Antes de comenzar con el bypass deberíamos hacernos las siguientes preguntas:
- ¿Cuál es el valor del canario?
- ¿El valor del canario es bien diferente cada vez que ejecuto el binario?
- ¿Qué tipo de canario es?
- ¿Qué funciones son vulnerables en el binario?
- ¿Cuáles son los caracteres permitidos?
- ¿Es posible explotar la vulnerabilidad antes que la función retorne?
- ¿El exploit es remoto o local? Y si es así, ¿puedo utilizar la fuerza bruta?
Una vez obtengamos las respuestas a esas preguntas nos podremos hacer una idea de cómo actuar para poder realizar con éxito nuestro bypass; o por lo menos intentarlo…
La técnica más común es la fuerza bruta. Consiste en hacer correr el exploit en bucle donde pensamos que la parte de la cookie será aleatoria. Otras técnicas comunes son la fuga de información como es el caso de ret2write o exploits en formato cadena.
Para esta PoC, veremos la fuga de información y el formato en cadena.
En esta prueba de concepto usaremos un Ubuntu 18 64 bits, Un binario vulnerable de 32 bits y la herramienta en Python Pwntools.
Creando el binario vulnerable:
- compilación:
Procedemos a compilar el binario con el siguiente comando:
La instrucción -m32 crea un ejecutable de 32 bits, -no-pie indica que no genere un ejecutable independiente de posición vinculado dinámicamente. La contramedida stack_canary es añadida por defecto, así como las contramedidas ASLR y NX si éstas se encuentran presentes en el sistema.
- Ejecutando el binario:
Lo primero que haremos será ejecutar el binario recién creado en nuestra consola de comandos para comprobar si efectivamente existe alguna contramedida activa. Como vemos, efectivamente hace una doble lectura del comando introducido (en este caso una cadena con el carácter ‘A’) para luego imprimir el aviso del canario y dar por terminada la ejecución.
En este punto hay que añadir que, en sistemas operativos más actuales, efectivamente imprime el aviso, pero no genera el archivo core. Esto, a la postre, causa problemas de ejecución del exploit final. En la imagen inferior se puede apreciar lo que acabamos de explicar:
- Observando el comportamiento:
Es el momento de hacer uso de nuestro GDB. Lo primero que haremos, sabiendo que la función vulnerable se llama vuln, será desensamblar dicha función y observar su comportamiento. Podemos ver como después de la función printf, el valor ebp-0xc es añadido al acumulador. Seguidamente, el operador lógico XOR realiza la comparativa del valor gs:0x14 con el valor añadido al acumulador. gs es un segmento del registro cuyo valor no puede ser obtenido directamente desde GDB. Entonces no podemos saber aun el valor de la cookie.
Después de la instrucción XOR vemos que hay una instrucción je, la cual realiza un salto a la instrucción __stack_chk_fail. je se llevará a cabo si la comparativa que realiza XOR entre ambos valores son iguales. Si el resultado de los valores no son iguales, se llamará a la funcion __stack_chk_fail, el cual dará por finalizada la ejecución. Je es la pieza clave qué hará posible el bypass.
- Determinando el tipo de canario:
Vamos a colocar un punto de ruptura en la instrucción nop y procedemos a ejecutar el binario en nuestro GDB:
Comprobamos la localización del ebp-0xc. Por supuesto el canario está presente y es un terminador en carácter nulo.
- Obteniendo los offset:
Continuamos usando el mismo punto de ruptura para ver en qué punto se sobrescribe el canario. Para ello debemos saber cuáles son los offset. A tal efecto crearemos un patrón el cual copiaremos tras hacer correr nuevamente el binario.
Tras la ejecución, Podemos examinar la localización del cookie en ebp-0xc. Vemos que comienza a sobrescribirse después de 100 bytes.
Es el momento de saber en qué punto se produce el bypass a la llamada de la función __stack_chk_fail y cuál es su offset. Para ello miraremos en la función vuln cual es la instrucción inmediatamente posterior a dicha función.
- Cambiando el terminador a valor final “a”:
Ahora que sabemos que cambiando el canario se sobrescribe después de la escritura por parte del usuario, vamos a generar una cadena de caracteres de 100 bytes para posteriormente volviendo hacer uso del punto de ruptura de la función nop, volver a relanzar la aplicación.
Si nos fijamos un poco, después de la segunda ejecución del string aparece un símbolo ¿eso qué quiere decir?
Cuando usamos una función de lectura usando stdin, una vez que presionamos enter, se introduce una nueva línea con valor terminado en (0x0a) en el buffer. Sabiendo esto, ahora vamos a volver a realizar una nueva lectura de $ebp-0xc hasta encontrar el valor mencionado.
- Cambiando el valor a nulo:
Ahora que tenemos claro donde se encuentra el canario vamos a volver a darle un valor nulo. Para ello setearemos el valor directamente desde el GDB, solicitaremos que nos imprima el registro para comprobar que el valor ha cambiado y pulsaremos la tecla C para que el programa continúe con su ejecución. Podemos ver como el programa finaliza baypaseando la función __stack_chk_fail. Queda probado que el canario ahora es vulnerable a la fuga de información.
- Sobrescribiendo el canario desde printf:
Es el momento de volver a relanzar la aplicación. Desensamblamos nuevamente la función vuln() y marcaremos como punto de ruptura la llamada a la función printf.
Hacemos correr el programa. Vemos que esta vez no realiza la copia de la cadena, simplemente la ejecuta. ¿ A qué se debe esto? Una vez que el puntero es colocado sobre la cadena, printf comienza a poner bytes dentro del registro stdout hasta encontrarse con un terminador nulo. Solicitamos que nos muestre información del registro ESP y podemos comprobar cómo efectivamente, la ejecución se detiene al encontrarse con el terminador nulo 0x00000200.
- Visualizar el valor de stdout :
¿Pero qué es lo que se estaba ejecutando en el valor 0xffffcf78? Para responder a esa pregunta pedimos que nos muestre las cuatro últimas palabras del registro ESP. Vemos que se está ejecutando el carácter ‘A’. También se puede observar, si miramos en registros, el acumulador(EAX), el contador (ECX) y el marco superior de la pila (EBX), además de la pila propiamente dicha en las dos primeras instrucciones.
En este caso hemos sobrescrito un terminador nulo donde antes se encontraba el canario con los caracteres 0x0a. Es el momento de solicitar que nos imprima las últimas 40 palabras del valor 0xffffcf78 y poder determinar en qué valor se encuentra el carácter nulo. Como podemos observar en la imagen inferior, se encuentra justo después de los caracteres basura del final de Cadena.
Es el momento de intentar fugar al canario sin causar un desbordamiento de pila y así poder hacer uso de una función legítima para poder bypasear con un canario válido y tomar el control del retorno de una dirección para después colocarlo en el puntero a instrucción o controlador del programa EIP; lo que nos dará un valor aleatorio, pero completamente legítimo.
- Creando el exploit:
Como la función encargada de crear una Shell se encuentra presente pero no definida en la función principal, no nos bastará con colocar el valor de dicha función en el puntero del registro EIP. Por lo tanto, debemos crear un exploit que habilite dinámicamente lectura de producción de aplicaciones y convertir la fuga de información o fuga de canario en un segundo aporte.
Para ello volvemos a lanzar el GDB y tras solicitar la información de las funciones copia el valor que tiene la función getshell:
Antes de ejecutar nuestro exploit debemos cambiar en propietario del binario y los permisos a root:
Llegados a este punto ya no haremos más uso del GDB. Ha llegado el momento de escribir nuestro exploit.
Ejecutamos y observamos su funcionamiento:
Hasta aquí esta prueba de concepto. Espero que la disfrutéis.
Happy Hack!