lunes, 6 de agosto de 2012

Minicurso de exploiting (parte 3): Introducción a los procesos

La pila de un proceso:

En la entrada anterior vimos cómo es un proceso mientras se esta ejecutando, vimos que tenía varias regiones de memoria cada una con un propósito concreto. En esta entrada vamos a estudiar una de esas regiones en profundidad, en concreto la pila/stack (a veces la llamo de una forma y a veces de otra).

Recordemos cómo es esta región "vista desde fuera":

$ ps | egrep bash
 2312 pts/0    00:00:00 bash
$ cat /proc/2312/maps | egrep stack
bfa6c000-bfa8d000 rw-p 00000000 00:00 0          [stack]

Como podemos ver la región de memoria donde está la pila de este proceso (bash) comienza en bfa6c000 y termina en bfa8d000, pero esto no tiene que ser así siempre, nos referimos a que estas direcciones (en concreto la primera) puede cambiar ya que la pila es una parte del proceso que crece y decrece según el proceso se ejecuta, es decir que es dinámica.

También podemos ver los permisos de esta región rw-p, lo que significa que es de lectura (r), escritura (w) y privada (p). Para que no nos quedemos en la duda comentaré que lo de privada significa que esa zona pertenece sólo a ese proceso y no a todas sus copias, y que por lo tanto si una copia de este proceso va a modificar una página de memoria asociada a una dirección dentro de esta región, se debe hacer una copia de esa página (se marca debido al sistema de copy-on-write que usa Linux). Tengo que resaltar que los permisos de ejecución están desactivados. Esto es una medida de seguridad, la podemos llamar Exec-Shield, bit NX, DEP, W^X o de otras formas si nos movemos en otros sistemas. En Linux se lo suele conocer como Exec-Shield, en x86 (si el procesador lo soporta directamente) es bit NX y en Windows DEP (Data Execution Prevention), en BSD o al menos OpenBSD es W^X. Sea como sea el resultado es el mismo, impedir la ejecución de instrucciones que se encuentran en regiones de datos. Tiempo ha, esto no era así, la pila estaba marcada con permisos de ejecución. Ya veremos en otras entradas como esto fue una cagada bien grande. Por suerte y para hacer ciertas prácticas y pruebas, vamos a poderlo desactivar (back to 90s). También dedicaré algún día una pequeña entrada a explicar un poco más como funciona esta tecnología (más histórico que otra cosa).

Esto es lo que podemos comentar de lo que es "el entorno de la pila", pero ¿qué hay en la pila? y sobre todo ¿cómo está ordenado lo que quiera que haya?. Estas son las preguntas que vamos a responder a continuación y que deberemos aprender bien para poder desarrollar exploits con éxito. Un último detalle, lo que a continuación explicaré es cómo se hacen las cosas normalmente, pero no es un estándar (no que yo sepa) y podría hacerse de otras formas (aunque no sería cómodo) así que esto se podrá aplicar al 99% de los programas, pero no nos creamos que esto es impepinable y que no podríamos encontrarnos cosas raras.

Empecemos contestando qué es lo que hay en la pila. Un proceso al final acaba siendo la ejecución de un conjunto de funciones. Estas funciones necesitan cierto espacio de memoria donde hacer sus cosas (no, las funciones no hacen caca), un trozo de memoria donde existir mientras se ejecutan y que puedan liberar fácilmente cuando terminen. Pues lo que hay en la pila básicamente son estos espacios donde una función se ejecuta. Existe un espacio por cada ejecución de una función (si una función se ejecuta más de una vez, en la pila se habrá creado un espacio por cada ejecución), estos espacios se dan cuando una función va a ser ejecutada y se libera al finalizar la misma, fácil ¿no?. A estos espacios se les suele llamar de muchas formas, yo lo suelo llamar "stack frame" (aunque no sé si es correcto, no sé de donde lo he sacado), en la wikipedia tenemos esta entrada donde los llaman "call stack" por si queremos profundizar.
Evolución de la pila durante la ejecución de un proceso

En la figura anterior podemos apreciar que la pila crece hacia direcciones más pequeñas. Esto al principio puede hacer que nos perdamos, pero con un poco de práctica acabaremos interiorizándolo muy rápidamente. El por qué esto es así se debe a una decisión de diseño del repertorio de instrucciones de x86, las instrucciones orientadas a añadir cosas a la pila (push y variantes) lo que hacen es decrementar el puntero de pila y las que quitan (pop y variantes) lo que hacen es incrementarlo. Aquí ya estamos añadiendo nuevos conceptos, el puntero de pila. Por ahora simplemente diremos que, como es lógico, existen un par de registros/valores/números que indican el comiezo y del final del stack frame de la función ejecutándose en cada momento (se llaman ESP y EBP). Seguro que a muchos ya les suena esto o incluso saben perfectamente de lo que hablo, pero aún así le dedicaré una entrada a analizar un poco en detalle ciertas características de la arquitectura x86 necesario para moverse con más facilidad en estos temas (si lo que quieres es hacer exploiting en ARM, SPARC u otra arquitectura vas a tener que analizar por tu cuenta dicha arquitectura xD).

Bueno, una vez sabemos qué hay en la pila pasemos a ver cómo se ordena cada uno de esos stack frames. El diseño e implementación de los stack frame es una decisión tomada a la hora de implementar un compilador. Es el compilador el que genera el código de los programas y que por lo tanto pondrá el código necesario para que antes de una llamada a función se cree su stack frame, y también pondrá el código necesario para liberar el stack frame cuando la ejecución de la función termine.

Cada implementación de un compilador para un lenguaje concreto podría organizar los stack frames como le diera la gana, pero por hacer las cosas bien, facilitar el desarrollo de debuggers y mil motivos más, los stack frames suelen ser iguales... y con esto me refiero a iguales a los que produce un compilador de C. Veamos entonces qué contiene un stack frame y en qué orden lo contiene:
  • Parámetros para la función.
  • La dirección a la que retornar cuando la función termine su ejecución.
  • El comienzo del stack frame de la función anterior.
  • Espacio variables locales a la función.
No es mucho, ¿verdad?. Esto es todo lo que hay dentro de un stack frame, aunque claro, dependiendo del número y tipo de parámetros y número y tipo de variables locales, leer un stack frame puede ser laborioso. Una aclaración, los parámetros de la función es lo primero que se pushea a la pila (es decir, que estará en direcciones más altas de memoria, más alejado de la cima de la pila) y las variables locales es lo último que se aparece (es decir, que estará en direcciones más bajas de memoria, más cerca de la cima de la pila). La siguiente figura puede que aclare un poco esto.
Apariencia de un stack frame en la pila

Para ver un stack frame en la realidad vamos con un ejemplo real.

int func(int foo, int bar) {
   int local;

   return 0;
}

int main(int argc, char *argv[]) {
   func(1, 2);
   return 0;
}


Este será el programa que compilaremos, tiene una función (func) con dos parámetros (foo y bar) y una variable local (local). Compilamos normalmente.

$ gcc -o stack_frame stack_frame.c

Y ahora vamos a meternos en sus entrañas. Usaremos el gdb, este programa es grande y complejo, no usaremos muchos comandos pero los suficientes como para perderse. No voy a explicar como usar gdb (demasiado grande y demasiado tiempo me llevaría), pero sobran manuales en internet sobre como manejarse. Además los ejemplos serán (relativamente) fáciles de seguir.

$ gdb -q stack_frame
Leyendo símbolos desde /path/stack_frame...(no se encontraron símbolos de depuración)hecho.
(gdb)disas main
Dump of assembler code for function main:
   0x080483a1 <+0>:    push   %ebp
   0x080483a2 <+1>:    mov    %esp,%ebp
   0x080483a4 <+3>:    sub    $0x8,%esp
   0x080483a7 <+6>:    movl   $0x2,0x4(%esp)
   0x080483af <+14>:    movl   $0x1,(%esp)
   0x080483b6 <+21>:    call   0x8048394 <func>
   0x080483bb <+26>:    mov    $0x0,%eax
   0x080483c0 <+31>:    leave
   0x080483c1 <+32>:    ret  
End of assembler dump.


Aquí tenemos el desamblado de main(), he marcado en negrita la instrucción que llama a la función func(). Sin embargo no es aquí donde empieza a construirse el stack frame de func() sino antes, concretamente en la instrucción main+3 (el sub). Aquí gcc usa truquillos de optimización, en vez de pushear cada uno de los parámetros de func() lo que hace es, primero reservar directamente ese espacio restándole 8 directamente al puntero de pila y luego poner los dos parámetros en la pila (¿por qué es eso más eficiente que dos instrucciones push? A los ingenieros de Intel y de GCC, no a mi), en cualquier caso el resultado es el mismo. Con eso ya tendríamos la parte de los parámetros, luego viene la dirección a la que debe retornar func(), la instrucción call se encarga de eso, además de redirigir el flujo de ejecución a func() también pushea a la pila la dirección de la siguiente instrucción (0x08483bb o main+26 que es más fácil :). Con eso ya tenemos construido parámetros y dirección de retorno. Falta guardar el comienzo del stack frame anterior (es decir, el de main()) y también espacio para las variables locales. Veamos el desamblado de func().

(gdb) disas func
Dump of assembler code for function func:
   0x08048394 <+0>:    push   %ebp
   0x08048395 <+1>:    mov    %esp,%ebp
   0x08048397 <+3>:    sub    $0x10,%esp

   0x0804839a <+6>:    mov    $0x0,%eax
   0x0804839f <+11>:    leave
   0x080483a0 <+12>:    ret  
End of assembler dump.


He marcado en negrita las tres instrucciones que cumplen ese cometido. La primera pushea el EBP (Extended Base Pointer) que es el registro que contiene la referencia al stack frame de main() (a partir de ahora a los EBP que se van guardando en cada stack frame los llamaremos Saved EBP), ya tenemos parámetros, dirección de retorno y Saved EBP (de main). Vemos como ahora hay una instrucción mov, lo que hace es cambiar EBP y hacerlo apuntar al mismo sitio que ESP (la cima de la pila) es decir, el EBP de un stack frame apunta al Saved EBP. Esto es lioso pero con el tiempo y la práctica también se interioriza al igual que el crecimiento de la pila. Y finalmente está la instrucción sub que reserva espacio para variables locales. Aquí nos puede saltar la duda de por qué si func() sólo tiene una variable local entera, que en x86 de 32 bits son 4 bytes, se reservan 16 bytes. Buena pregunta, no lo sé pero es así xDD. A veces pienso que puede ser para dejar un margen entre las variables locales (memoria que supuestamente se va a usar con frecuencia) y el Saved EBP y la dirección de retorno (valores que no se deberían tocar) y si el programador ha cometido algún error de saltarse rangos pero no demasiado bestia (1 byte, 2 bytes... 4 bytes, márgenes pequeños), pues con este margen se evita que el programa pete. Pero no lo sé seguro.

Bueno, ahora vamos a algo un poco más hardcore, vamos a ver este stack frame.

(gdb) br *func+6
Punto de interrupción 1 at 0x804839a

(gdb) r
Starting program: /path/stack_frame

Breakpoint 1, 0x0804839a in func ()

(gdb) print /x $esp
$1 = 0xbffff3d8
(gdb) print /x $ebp
$2 = 0xbffff3e8
(gdb) x/20xw $esp
0xbffff3d8:    0x080483e0    0xbffff3f8    0x0015e985    0x0011eb60
0xbffff3e8:    0xbffff3f8    0x080483bb    0x00000001    0x00000002
0xbffff3f8:    0xbffff478    0x00145ce7    0x00000001    0xbffff4a4
0xbffff408:    0xbffff4ac    0xb7fff848    0xbffff554    0xffffffff
0xbffff418:    0x0012cff4    0x08048215    0x00000001    0xbffff460


Si al ver esto te ha entrado miedo en vez de curiosidad no te recomiendo seguir con el curso xD, ya que esto se va a repetir continuamente, ésta es la esencia del reversing y el exploiting jejeje.

Vayamos poquito a poco, simplemente he parado el programa justo al terminar de construir el stack frame de func() y he volcado un poco de la memoria a partir del puntero de pila (ESP). Veamos este volcado con colores.

0xbffff3d8:    0x080483e0    0xbffff3f8    0x0015e985    0x0011eb60
0xbffff3e8:    0xbffff3f8    0x080483bb    0x00000001    0x00000002
0xbffff3f8:    0xbffff478    0x00145ce7    0x00000001    0xbffff4a4
0xbffff408:    0xbffff4ac    0xb7fff848    0xbffff554    0xffffffff
0xbffff418:    0x0012cff4    0x08048215    0x00000001    0xbffff460


He usado los mismos colores que en la figura anterior dónde veíamos como estaba organizado un stack frame. En amarillo tenemos los 16 bytes que se reservan para variables locales, en azul el Saved EBP, en verde la dirección de retorno y en rojo los dos parámetros. Podemos confirmarlo, antes mostré los contenidos de EBP y ESP (marcados en negrita) y vemos como efectivamente EBP apunta a 0xbffff3e8 y ESP a 0xbffff3d8. 0x080483bb efectivamente es la siguiente instrucción al call que llama a func() en main().

(gdb) disas main
Dump of assembler code for function main:
   0x080483a1 <+0>:    push   %ebp
   0x080483a2 <+1>:    mov    %esp,%ebp
   0x080483a4 <+3>:    sub    $0x8,%esp
   0x080483a7 <+6>:    movl   $0x2,0x4(%esp)
   0x080483af <+14>:    movl   $0x1,(%esp)
   0x080483b6 <+21>:    call   0x8048394 <func>
   0x080483bb <+26>:    mov    $0x0,%eax
   0x080483c0 <+31>:    leave 
   0x080483c1 <+32>:    ret   
End of assembler dump.

Y como era de esperar, los parámetros son 1 y 2 ya que así lo escribimos en el código fuente.

int main(int argc, char *argv[]) {
    func(1, 2);
    return 0;
}

Bueno, pues ya hemos visto qué es lo que hay en la pila y cómo está ordenado. Debo advertir una cosa, hemos visto una función de las más comúnes, las que crean los programadores, pero los stack frames no siempre se crean así. Por poner un ejemplo, si nos ponemos a analizar el comienzo de la función main() de diferentes programas (alguno nuestro, bash, sleep, etc...) muchas veces encontraremos una instrucción tal que "and    $0xfffffff0,%esp", que no hemos visto aquí. ¿Y esto por qué? bueno, los motivos pueden llegar a ser muy oscuros y yo no me los sé, además esta instrucción no aparece siempre y no sé exactamente bajo que condiciones aparece... pero me la juego a que lo que pretende hacer es que a partir de main() la pila esté alineada a direcciones de 16 bytes. Con esto lo que quiero mostrar es que no te creas todo lo que digo ni te lo tomes al pie de la letra, investiga cada caso.

Bueno, esta entrada es bastante larga pero es que lo que aquí se describe (los stack frames) debe quedar meridianamente claro.

Saludos.

No hay comentarios:

Publicar un comentario