Archivo

Artículos etiquetados y‘stack frame’

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.