Introducción Básica al Buffer Overflow
Ejemplo básico en C de Buffer Overflow con explicación a alto nivel
Dentro de las vulnerabilidades, una de las más escuchadas en el mundo de la ciberseguridad es, sin duda, el buffer overflow. Esta técnica lleva permitiendo a los expertos en ciberseguridad realizar denegaciones de servicio (DoS), cambiar el comportamiento esperado de los programas e incluso ejecutar comandos de forma remote (RCE).
Aunque a nivel técnico se algo muy complejo y exige tener una comprensión muy detallada del funcionamineto de la memoria durante la ejecución de un programa, a nivel conceptual es bastante sencillo de entender. Para poder explicarlo de manera sencilla haremos algunas simplificaciones que pueden no ser del todo exactas, pero que nos dan un nivel de compresión suficiente para entenderlo.
Ejemplo demostrativo
Para entenderlo, primero vamos a analizar un pequeño programa en C y su comportamiento con diferentes entradas de datos. Lo primero que vamos a hacer es crear un fichero secretKeeper.c
con el siguiente contenido:
#include <stdio.h> #include <string.h> int main(void) { int isAdmin = 0; char userPassword[16]; printf("Enter the password: "); scanf("%s", userPassword); if(strcmp("SecretPass$%123", userPassword)) { printf("Invalid password!!!\n"); } else { printf("Correct password!!!\n"); isAdmin = 1; } if(isAdmin) { printf("The user is logged as admin!!!\n"); printf("Here is the admin secret: I hate linux...\n"); } return 0; }
Aunque este programa no tenga demasiado sentido en el mundo real, nos sirve para demostrar de manera muy sencilla en qué consiste el buffer overflow. El programa hace lo siguiente:
- Importamos las dependencias necesarias para trabajar con inputs y strings
- Declaramaos la variable
isAdmin
con el valor0
, y creamos la variableuserPassword
reservando espacio en memoria para una cadena de texto de 16 caracteres - Le pedimos al usuario que introduzca la contraseña y la almacenamos en la variable
userPassword
- Comparamos la contraseña introducida por el usuario con la contraseña del programa
- Si es incorrecta mostramos un mensaje de contraseña errónea
- Si es correcta mostramos un mensaje de contraseña correcta y actualizamos la variable
isAdmin
a1
para indicar que la contraseña ha sido la correcta
- Comprobamos el valor de
isAdmin
y, si no es0
, mostramos el terrible secreto del administrador del sistema
Como podemos observar, el programa es bastante sencillo y, en apariencia, inocue e inofensivo, así que ahora vamos a compilarlo y probar su funcionamiento. Para ello es necesario tener instalado el compilador de C (gcc) y ejecutar el siguiente comando:
gcc secretKeeper.c -o secretKeeper -m32
Con este comando simplemente le indicamos que queremos que compile nuestro fichero secretKeeper.c
, que el resultado lo escriba en sercretKeeper
(-o
), y que lo compile con arquitectura de 32 bits (-m32
).
Ahora que ya tenemos todo listo vamos a empezar con las pruebas. La primera prueba que podemos hacer es pasar una contraseña incorrecta y comprobar que no nos desvela el terrible secreto:
┌─[parrot@parrot]─[~/Learning/buffer-overflow] └──╼ $./secretKeeper Enter the password: incorrectPass Invalid password!!!
Perfecto, obtenemos el resultado esperado. La contraseña no es correcta y no nos desvela el secreto.
Ahora podemos introducir la contraseña correcta para ver si nos desvela el secreto:
┌─[parrot@parrot]─[~/Learning/buffer-overflow] └──╼ $./secretKeeper Enter the password: SecretPass$%123 Correct password!!! The user is logged as admin!!! Here is the admin secret: I hate linux...
Y efectivamente, si la contraseña es correcta nos desvela el terrible e inadmisible secreto del administrador.
A partir de aquí es cuando las cosas se ponene extrañas. Ahora, vamos a poner una contraseña muy larga, en concreto un caracter más larga del espacio que hemos reservado para la constraseña introducida por el usuario. Si volvemos al programa vemos que habíamos reservado una longitud de 16, así que pondremos una contraseña de 17:
┌─[parrot@parrot]─[~/Learning/buffer-overflow] └──╼ $./secretKeeper Enter the password: qwerqwerqwerqwerA Invalid password!!! The user is logged as admin!!! Here is the admin secret: I hate linux...
¡¿Qué está pasando aquí?! Por un lado nos está indicando que la contraseña es incorrecta, pero por otro nos está desvelando el secreto del administrador. Esto sólo puede significar que de alguna manera el valor de isAdmin
ha sido modificado a pesar de no haber entrado por la condición.
Para asegurarnos de esto, vamos a poner un printf("isAdamin: %d\n", isAdmin);
para mostrar el valor de isAdmin
antes del if(isAdmin) {
que comprueba si tiene que mostrar o no el secreto del administrador. De esta manera podemos ver si nuestra teoría es correacta, así que hacemos el cambio, compilamos de nuevo y volvemos a ejecutar los 3 casos anteriores y comprobamos:
┌─[parrot@parrot]─[~/Learning/buffer-overflow] └──╼ $gcc secretKeeper.c -o secretKeeper -m32 ┌─[parrot@parrot]─[~/Learning/buffer-overflow] └──╼ $./secretKeeper Enter the password: incorrectPass Invalid password!!! isAdamin: 0 ┌─[parrot@parrot]─[~/Learning/buffer-overflow] └──╼ $./secretKeeper Enter the password: SecretPass$%123 Correct password!!! isAdamin: 1 The user is logged as admin!!! Here is the admin secret: I hate linux... ┌─[parrot@parrot]─[~/Learning/buffer-overflow] └──╼ $./secretKeeper Enter the password: qwerqwerqwerqwerA Invalid password!!! isAdamin: 65 The user is logged as admin!!! Here is the admin secret: I hate linux...
En los 2 primeros casos observamos el comportamiento esperado, si la contraseña es incorrecta el valor de isAdmin
es 0
y, si la contraseña es correcta el valor es 1
, mostrando en consecuencia el secreto. Sin embargo, en el tercero de los casos, el valor de isAdmin
no es 0
, es 65
y, en consecuencia, también se muestra la contraseña. ¿Qué está pasando aquí? Pues sí, como te puedes imaginar lo que está ocurriendo es un buffer overflow.
Y la pregunta qué se me ocurre a continuación es, ¿qué pasa si aún introduzco una contraseña más larga? Pues vamos a probar:
┌─[✗]─[parrot@parrot]─[~/Learning/buffer-overflow] └──╼ $./secretKeeper Enter the password: qwerqwerqwerqwer12345678 Invalid password!!! isAdamin: 875770417 The user is logged as admin!!! Here is the admin secret: I hate linux... Segmentation fault
¡Segmentation fault! En este caso la ejecución del programa ha detectado que ha habido un error de memoria, parece que hemos ido demasiado lejos, pero, ¿qué es lo que está pasando?
¿Qué es el buffer overflow?
Durante la ejecución de un programa, las variables que ese programa necesita se van almacenando en la memoria, en lo que llamamos buffer. Podemos imaginarnos ese buffer como un montón de cajitas en la que se almacena la información necesaria para la ejecución de la función actual. Por ejemplo, en nuestro caso hemos reservado 16 "cajitas" para la contraseña introducida por el usuario, y otra para el valor entero de isAdmin
:
Ahora si vemos el caso en el que hemos puesto una contrase incorrecta, en el que hemos introducido "incorrectPass", lo que tendríamos en el buffer sería lo siguiente:
Como podemos observar, en este caso la contraseña introducida por el usuario cabe en el espacio reservado para ello, por lo que el programa se comporta tal cual esperamos, pero, ¿qué pasa si introducimos una contraseña más larga? Si representamos lo que pasaría si usamos la contraseña "qwerqwerqwerqwerA":
Cómo podemos observar ha habido un desbordamiento en el que el contenido de la variable userPassword
ha desbordado el espacio reservado que tenía. Al ocurrir esto ha sobreescrito la variable isAdmin
. Además, si te fijas en lo que nos decía que había en ese caso en la variable, un 65, coincide con el valor ASCII de la letra "A". A esto es a lo que se llama buffer overflow.
¿Y por qué se produce este desbordamiento?
El motivo por el que se produce este desbordamiento de memoria es porque en C y C++ hay varios métodos que no comprueban que la longitud de lo que van a escribir es inferior o igual al espacio reservado en el buffer, sobrescribiendo otros registros del buffer. Algunos de estos métodos son:
- printf
- sprintf
- strcat
- strcpy
- gets
- ...
Si seguimos sobrescribiendo más registros, llegamos a los registros que indican por dónde tiene que continuar la ejecución del programa, permitiendo ganar control del mismo hasta el punto de llegar, en el peor de los casos, a ejecutar comandos de manera remota.
En futuros post profundizaremos en cómo funciona la memoria en más detalle, explicaremos los diferentes tipos de buffer overflow, y cómo nos podemos aprovechar de esto para ganar el control del programa y hacer que ejecute instrucciones que nosotros queramos. También exploraremos las diferentes protecciones existentes y cómo podemos burlarlas.