En la primera parte de esta entrada llegamos hasta el punto de sobrescribir registro EIP con el valor que nosotros queríamos. Ahora llega el momento de explotar la vulnerabilidad y ejecutar código.
Recordar que el enlace que me ha sido imprescindible para resolver el ejercicio ha sido la entrada del blog de Kroosec. En esta entrada espero, sobretodo, que quede clara la técnica para crear un ROP en múltiples fases y mostrar cuando nos puede venir bien.
Como en la primera entrada, nos plantearemos preguntas e iremos resolviéndolas hasta llegar a explotar la vulnerabilidad:
1. ¿Contra qué nos enfrentamos?
En este caso la pregunta tiene la solución en el propio enunciado del ejercicio ya que nos indican que tiene ASLR activado y tanto la pila como el heap no son ejecutables. Pero tenemos que ser conscientes que esto no es así en la vida real y tendremos que invertir tiempo en saber contra qué se enfrenta uno.
En esta ocasión hemos decidido jugar un poco y comprobar que efectivamente no nos deja ejecutar nada de la pila. Para ello lo que hacemos es rellenar el buffer con instrucciones NOP y saltaremos cuando sobrescribamos EIP al buffer.
Elegimos como dirección donde saltaremos 0xbfd29430. Si no reiniciamos el servidor esta dirección será válida (sino actuará ASLR, y no caeremos donde queremos). Sobre el código en python que hicimos en la primera entrada valdría con realizar las siguientes modificaciones:
...
nop = "\x90"
eip = pack("<L",0xbfd29430)
size_buf = 131092 # 0x00020014
size_buf_le = pack("<L",size_buf) # Hacemos el LE del entero
datos = 131088 * nop + eip
...
Si la pila es ejecutable cuando sobrescribamos EIP debería saltar en medio del buffer y empezar a ejecutar NOPs hasta que llegue a un punto donde nos de error por ejecutar una instrucción no controlada.
Vemos en la imagen anterior como cuando va a ejecutar el primer NOP es cuando nos da el “Segmentation fault”, por lo que hemos podido confirmar la protección. Otra opción para conocer las protecciones es usando la aplicación scanelf:
fusion@fusion:~$ scanelf -qe /opt/fusion/bin/ RWX --- --- /opt/fusion/bin/level00 RWX --- --- /opt/fusion/bin/level01 RWX --- --- /opt/fusion/bin/level10 RWX --- --- /opt/fusion/bin/level11 RWX --- --- /opt/fusion/bin/level12
Vemos como level02 no está en el listado. Así que ya sabemos cómo confirmar que realmente no es ejecutable la pila.
2. ¿Cómo saltamos la protección de que la pila no sea ejecutable y ASLR?
Para saltarnos la protección tendremos que colocar en la pila una cadena ROP (Return Oriented Programming). Cada elemento de la cadena apuntará a un trozo de código ejecutable que no está ni en la pila ni en el HEAP y que hará lo que nosotros queremos que haga. Estas instrucciones terminarán con una instrucción de retorno que nos permitirá continuar con nuestra cadena de gadgets ROP.
Lo primero que tenemos que tener claro, es que tenemos que construir un ROP que haga lo que queramos. Para eso tenemos que hacerlo con instrucciones situadas en zonas estáticas del código del propio binario, o de las librerías que utiliza los gadgets, ya que los elementos afectados por ASLR no nos servirán para construir el ROP.
Si probamos ROPGadget sobre el binario veremos como no nos genera un payload con la cadena de gadgets construidas. Otra opción es utilizar librerías que usa el binario, como las siguientes:
fusion@fusion:~$ ldd /opt/fusion/bin/level02 linux-gate.so.1 => (0xb784d000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb76c9000) /lib/ld-linux.so.2 (0xb784e000)
Si ejecutamos el comando anterior varias veces nos daremos cuenta que la librerías están también con ASLR porque la dirección base cambia, por lo que a priori no parece válido.
De acuerdo, ROPGadget no nos lo genera de manera automática, por lo que tendremos que hacerlo de manera manual. Los gadgets los vamos a sacar de los generados por la herramienta ropeme. Después de jugar un poco nos daremos cuenta de que el binario tiene pocos ROPs y que construir la cadena es difícil, por lo menos para mí. Así que llegado a este punto me puse a buscar y encontré esta entrada de @danigargu, que arrojaba un poco de luz y mostraba dos técnicas que podrían ayudarme: “GOT dereferencing” y “GOT overwriting”, descritas en el paper “Surgically returning to randomized lib(c)”. Parecía que resolvía el problema del ASLR de la libc y podría referenciar cualquier función. Llegado aquí, seguía teniendo el mismo problema; no encontraba los gadgets adecuados… ¡ays!.
De nuevo, me puse a revisar la entrada de Kroosec con mucho cariño y ví que hacía referencia al mismo problema que estaba teniendo y que se había apoyado en un paper de los creadores de la herramienta ropeme, “Payload already inside: data reuse for rop exploits”. Tras leerlo y entenderlo, y en resumidas cuentas, el paper explica una nueva técnica para crear una cadena de ROP en múltiples fases, donde la ventaja es que para la primera fase necesitas MUY pocos gadgets y que están en cualquier binario y después en una segunda fase ya no tienes limitaciones.
Lo que vamos a hacer a partir de ahora es explicar la técnica usando como ejemplo la solución de Kroosec; porque la solución va de lujo :-) y no tengo otra solución alternativa por el momento. Nos centraremos sobretodo en el juego con las pilas que hace la técnica y los elementos importantes para tener las pilas como queremos.
Lo primero de todo vamos a seleccionar los ingredientes necesarios para desarrollar la técnica:
1. Necesitamos un sitio donde vamos a construir nuestra pila:
Debe ser una zona que podamos escribir y que no esté afectada por ASLR. Como explica el paper secciones como bss y data son candidatos muy buenos. En nuestro caso:
readelf -S /opt/fusion/bin/level02 [25] .data PROGBITS 0804b420 002418 0000e0 00 WA 0 0 32
Elegimos como dirección donde alojar nuestra pila personalizada: 0x0804b420.
2. El siguiente ingrediente es seleccionar una función que esté en la PLT para hacer la copia a una dirección.
Lo que vamos a hacer es copiar byte a byte nuestra pila, mediante la técnica return-to-plt. La opción que vamos a utilizar es la función nread() para que cuando hagamos un segundo envío al socket este almacene la información en un buffer con una nueva ubicación:
fusion@fusion:~$ readelf -a /opt/fusion/bin/level02 | grep -i nread 52: 0804952d 115 FUNC LOCAL DEFAULT 13 nread
Kroosec selecciona nread(). Recordemos que la función nread necesitará tres argumentos y su prototipo de la función era:
ssize_t read(int fs, void *buf, size_t N);
Ahora vamos a confeccionar stage0. Stage0 lo que pretende es fijar EBP a 0x0804b420, ejecutar nread() y dar el control a los datos almacenados en nuestra pila (teóricamente el save eip).
STAGE-0
El punto donde la vulnerabilidad sobrescribe EIP es donde ponemos la dirección hacia una instrucción del tipo “pop ebp ; ret”. El siguiente elemento de la pila también en naranja es la dirección de la sección .bss. Por tanto al hacer el pop ebp y volver, lo que hemos hecho ha sido cumplir el primer objetivo, que es EBP = .BSS.
Después en color rojo tenemos la llamada a la función nread@plt que conocemos y sus argumentos. Y en verde tenemos la instrucción que se ejecutará cuando volvamos de la función nread(). Esta instrucción es un “leave ; ret” que equivale a “mov esp, ebp ; pop ebp ; ret”, por lo que devolverá el control a la “return address” y como tendremos en EBP nuestra sección .BSS, ya estaremos en una zona con menos protecciones para jugar. Para elegir la instrucción “leave ; ret”, usamos ropeme:
ROPeMe> search leave % Searching for ROP gadget: leave % with constraints: [] 0x8048b41L: leave ;; 0x8048b83L: leave ;; 0x8048b96L: leave ;;
Vamos a ver estos aspectos del stage0 en el gdb:
Lo primero, como veis después de volver de la función encrypt_file(), se ha ejecutado el pop ebp y ahora se va a ejecutar la instrucción ret. El valor que tiene EBP ya es el que queremos.
En la siguiente imagen estamos dentro de nread() como podéis ver:
Ahora en este mismo instante de la ejecución vemos el frame de la pila. Y vemos como los argumentos son los correctos y como el “save eip” (0x8048b41), se corresponde con la dirección de memoria que nosotros hemos proporcionado apuntando a “leave ; ret”:
Después de ejecutar el “leave ; ret” como ebp ya apunta a la sección .bss, la ejecución se traslada allí. Nuestra pila tendrá la siguiente información:
STAGE-1
La ejecución de “leave ; ret”, hará un epílogo de una función y como ebp está apuntando a nuestra sección lo que hará será “pop ebp”, lo que meterá el PADDING en EBP, y la instrucción RET ejecutará lo que hay en EBP + 4 que es la return address, en nuestro caso execve(). Después la dirección de exit@plt en verde será lo que ejecutará el programa al volver de execve() y acabará el programa de manera controlada. Con el código de Kroosec lo que hace es abrir el puerto 6667 y ejecutar un /bin/sh cuando nos conectamos.
Si lo probáis no os funcionará por defecto porque el netcat que viene en fusion no soporta la opción -e, para que os vaya tendréis que hacer:
$ sudo rm /etc/alternatives/nc && sudo ln -s /bin/nc.traditional /etc/alternatives/nc
Una maravilla la técnica y la solución de Kroosec. Nos hemos divertido un rato aprendiendo con este ejercicio :-).
Es más que probable que se pueda solucionar de otra manera, incluso con alguna de las opciones que yo he descartado porque no veía la forma. Si alguien lo ha resuelto de otra manera, le agradeceré que me lo diga para seguir aprendiendo.
Nota: he dejado una copia del código de Kroosec aquí. He intentado comentar el código para que sea más fácil de seguir y que esté alineado con la explicación de esta entrada.
magnífica explicación :)
Gracias! :p