Hace unos días, en ESTE post nos quedábamos con que habíamos averiguado que tenemos 3139 bytes de espacio entre el inicio del buffer que podemos desbordar y la dirección de retorno de la función, pero nos faltaba ver que podemos sobreescribir en esta dirección.
Lo clásico sería haber dejado en este espacio un shellcode y buscar un trampolín (trampoline) en el código, que básicamente es una instrucción pop-ret, ret o jmp, o cualquier otra que lleve el puntero de ejecución hasta la pila. Sin embargo, desde que surgieron los sistemas NX (no-execution) la cosa es un poco más complicada, ya que no se permite ejecutar código que se encuentre en la pila. La alternativa que tenemos es ejecutar nuestro shellcode en ROP (Return Oriented Programming):
Para ejecutar una secuencia de acciones codificadas en ROP, necesitamos usar no solo la dirección de retorno de la función vulnerable, sino también las de las funciones "padre" de esta. Como sabéis, en los procesadores de Intel, al llamar a una función se almacenan en la pila una serie de valores, entre los que están los parámetros que se le pasan y la dirección de retorno a la función anterior. Al comenzar su ejecución, los parámetros de la función son recuperados de la pila y utilizados. Cuando esa función acaba, se desapilan todos estos valores y se devuelve la ejecución al mismo punto donde estaba antes de realizar la llamada. De esta forma, cuando tenemos una función que a su vez llama a otra, que a su vez llama a otra, lo que vamos a tener en la pila es una secuencia de parámetros y direcciones de retorno.
Sabiendo esto, podemos entender más fácilmente que lo que pretendemos con un ROP es cambiar el contenido de la pila de tal forma que "engañemos" al procesador, para que en lugar de volver la ejecución a la función llamante y continuar su ejecución, salte a una nueva función, para la cual ya habremos dejado sus parámetros preparados en la pila.
Programar un payload en ROP no es tarea sencilla, sobretodo si el sistema tiene ASLR (Address Space Layout Randomization), que básicamente lo que hace es reordenar la dirección de memoria en la que se cagan las librerías y, en algunos sistemas y si el binario ha sido compilado adecuadamente, hasta el propio código principal del binario. En este caso, el autor original del exploit utilizó funciones que se encontraban dentro del propio binario:
Como explica el propio autor en los comentarios de su exploit, utiliza la función cgi_input_unescape() para deshacer el URL Encode que se produce al transmitir la cadena a través de HTTP. Luego salta a un Pop-Ret, que es necesario para quitar la dirección de nuestro buffer de la pila y dejarla preparada para realizar la siguiente llamada a system(), que ejecutará la secuencia de comandos que le hayamos metido en el parámetro "host". El autor la llama "system_plt" porque es un símbolo que se encuentra dentro de la sección PLT (Procedure Linkage Table) del binario. Por último, realiza un salto a la función 0xdeafbabe, que es una dirección inventada, pero que nos da un poco igual, porque en este momento ya habremos conseguido la ejecución de código.
Pero claro, esto es para el binario compilado en Debian ¿estará todo en el mismo sitio en el binario con el que estamos trabajando ahora? La respuesta es que... ojalá! pero no vamos a tener tanta suerte, así que vamos a tener que buscar estas funciones por nosotros mismos. Lo más fácil de encontrar son las direcciones a system() o a exit(), que la vamos a utilizar para ser un poco más... limpios. Para ello únicamente tenemos que usar radare (o gdb, o IDA, ...) para volcar los "imports" del binario, y nos encontramos algo así:
$ rabin2 -i /usr/local/nagios/sbin/history.cgi
[Imports]
[...]
addr=0x08048bb0 off=0x00000bb0 ordinal=016 hint=000 bind=GLOBAL type=FUNC name=system
addr=0x08048e70 off=0x00000e70 ordinal=060 hint=000 bind=GLOBAL type=FUNC name=exit
[...]
60 imports
OK! Dos conseguidos, nos quedan tres más. Vamos a por el Pop-Ret, que tampoco debería ser muy complicado. Para encontrar este tipo de gadgets en el código hay un montón de herramientas diferentes, que no sabría deciros si una funciona mejor que las demás. Yo voy a usar las herramientas de Metasploit porque es con lo que estoy más familiarizado, pero podéis ver AQUÍ como se podría buscar lo mismo con otras herramientas.
$ cd /opt/msf4/
$ ./msfelfscan -p /usr/local/nagios/sbin/history.cgi
[history.cgi]
0x08048f03 pop ebx; pop ebp; ret
[...]
La herramienta de Metasploit me permite buscar un Pop-Pop-Ret, pero yo solo necesito un Pop-Ret, así que es tan sencillo como coger la dirección del segundo Pop, y así quedarnos con Pop-Ret, osea, la 0x08048f04. Tres conseguidos! Vamos a por el cuarto!
Nos queda la dirección del propio buffer donde se almacena el valor de host, y el de la función unescape(). Como esta segunda no aparecía en los imports ni hemos visto ninguna referencia a ella, vamos a ir a por el buffer que debería ser más fácil. Solo tenemos que utilizar una cadena reconocible (muchas 'A's, por ejemplo) y luego buscarlas en la memoria cuando se haya producido el fallo de segmentación. Podemos hacerlo, por ejemplo, con radare:
$ export QUERY_STRING="host=`python -c \"print 'A'*4000\"`"
$ r2 -d /usr/local/nagios/sbin/history.cgi
[0x00f72850]> dc
[...]
trace_pc: cannot get opcode size at 0x41414141
[R2] Breakpoint recoil at 0x41414141 = 0
r_debug_select: 19598 0
[0x41414140]> s 0x0
[0x00000000]> /x 4141414141414141
[ #]0x0a295800 < 0xffffffffffffffff hits = 1207
hits: 1207
0x08079b60 hit2_0 41414141414141414141414141414141
[...]
Genial! Parece que tenemos todo lo que necesitamos salvo la función unescape() pero, como decía, esta no aparece referenciada (o al menos yo no la he visto) en ninguna parte del binario, así que vamos a tener que hacer una búsqueda un poco más... a medida. Pero eso en el próximo post.
3 comentarios :
Muy buena info, gracias por compartir!
Ya esperando por la tercera parte :D
La verdad muy interesante los post-series que esta haciendo de la vulnerabilidad Sr. Selvi.
Publicar un comentario