En We Live Security hemos estado publicando una serie de ejercicios acerca de exploiting de buffer overflows. Los sistemas actuales incluyen diversas medidas para evitar el uso de este tipo de técnicas, por lo que me parece que es un buen momento para empezar a hablar de estos mecanismos de protección y cómo suelen ser evadidos.

Vamos a empezar por el bit de no ejecución, o bit NX. Esta medida de seguridad garantiza que ciertas áreas de memoria (como el stack y el heap) no sean ejecutables, y otras, como la sección del código, no puedan ser escritas. Dado que, hasta el momento, hemos aprovechado la explotación de buffer overflows para ejecutar código arbitrario contenido en el stack, el bit NX ha tenido que estar apagado. Tomemos la siguiente aplicación vulnerable:

Código vulnerable return-to-libc

La vulnerabilidad puede ser explotada mediante un buffer overflow que produzca un salto a una porción de código arbitrario, como hemos venido explicando anteriormente. Para poder tener éxito en la ejecución del payload ingresado en dicho buffer, era necesario compilar la aplicación de la siguiente manera:

Compilando con stack ejecutable

Le estamos indicando al compilador que haga el stack ejecutable y que no tome medidas de protección si se corrompe el stack. Al no desactivar el bit NX, se obtiene un stack no ejecutable:

Compilando sin stack ejecutable

Dado que ahora no será posible que ejecutemos nuestro propio código desde el buffer que se almacena en el stack, la solución consiste en ejecutar código de librería de C, solicitándole que abra una shell por nosotros. En otras palabras, vamos a producir un buffer overflow que altere el valor almacenado en el stack de la dirección de retorno de main, para ejecutar código de libc que abra una root shell en el sistema. Para lograrlo, primero buscaremos conocer la cantidad de caracteres necesarios para sobrescribir EIP:

04- Buffer overflow a medias

Vemos que con 270 caracteres basura (recordar que el buffer tiene tamaño de 256), se ha pisado la dirección de retorno a medias. Luego, con 272 caracteres se pisa completamente y los últimos 4 serán la dirección a la que se quiere lograr el salto.

La técnica a utilizar es conocida como return-to-libc y, en este caso, consiste en hacer una llamada a system() de libc, con la cadena “sh” como argumento. Esto es equivalente a ejecutar bash -c “sh”, lo que produce que se abra una shell. Debemos, por lo tanto, averiguar en qué dirección de memoria se encuentra la rutina system():

Dirección de system en libc

La dirección de system() es 0xb7e61060: ese valor será el que pisará la dirección de retorno original de main. Pero ese no es el único valor que debemos modificar. Para que la llamada sea exitosa, es necesario ingresar los argumentos requeridos por system en el stack. A saber: la dirección de retorno posterior a la ejecución de system, y una cadena con el comando a ejecutar por system.

En consecuencia, construiremos la shellcode de la siguiente forma:

  • Bytes de relleno: 268 bytes para producir el overflow
  • Dirección de system: 0xb7e61060
  • Dirección de retorno de system: utilizaremos la dirección de exit(), ya que no nos interesa mantener la aplicación vulnerable en ejecución luego de abrir la shell
  • Puntero al comando a ejecutar: en nuestro caso, un puntero a “sh” en memoria

Queda entonces la tarea de insertar la cadena “sh” (terminada en byte nulo), o buscarla en memoria, si tenemos suerte y ya se encuentra allí.

Secciones del ejecutable vulnerable

Si observamos nuevamente el código fuente de la aplicación vulnerable, veremos que se utiliza la función fflush(). Por lo tanto, debe existir el símbolo “fflush” en el ejecutable, que contiene la subcadena “sh”, algo de lo que podemos tomar ventaja. Por ello, podemos observar las secciones de memoria del ejecutable y buscar la cadena “sh\x00” (terminada en null) en esas secciones cargadas en memoria:

Dirección de cadena sh en memoria

La cadena ”sh” está en 0x804827d. Construimos el buffer de entrada así:

268 bytes basura + 0xb7e61060 (system) + 0xb7e54be0 (exit) + 0x804827d (“sh”)

Root shell por return-to-libc

Primero puede probarse dentro del debugger y luego puede que sea necesario ajustar alguno de los punteros antes de que funcione fuera de gdb. Sin embargo, en este caso funciona sin necesidad de ajustes, y se logra abrir una shell de root.

Ahora bien, ¿qué pasa si el ejecutable no contiene “sh” en memoria? Pues hay dos opciones: una es agregar la cadena a una variable de entorno, previo a la explotación; la otra es agregar la cadena al final de nuestro shellcode y hacer que el puntero dirija hacia allí. Para que esto funcione, obligadamente debemos observar las direcciones del stack, para que el último puntero apunte 4 bytes delante de su propia dirección. Podría modificarse así, por ejemplo:

perl -e ‘print “A”x268 . “\x60\x10\xe6\xb7\xe0\x4b\xe5\xb7\xa8\xb2\xff\xbf” . “sh\x00”

¿Y qué pasa con las otras medidas de seguridad? En todos los ejercicios que hemos visto ASLR estaba desactivado, pero ¿qué pasa si se activa ASLR? ¿Cómo garantizamos la correcta ejecución del payload? En las próximas semanas repasaremos los restantes mecanismos de seguridad modernos y las técnicas utilizadas para evadirlos.