Métodos anti-depuración en Linux: método basado en ptrace

Hace unos días leyendo un artículo me acordé sobre la API de windows IsDebuggerPresent localizada en Kernel32.dll para detectar si hay un depurador actuando sobre el programa se me ocurrió escribir una serie de artículos cortos sobre técnicas para detectar depuradores en Linux.

Es realmente frecuente encontrar en ejecutables técnicas para detectar si un depurador está activo o no, en función de si hay un depurador activo realizará una acción diferente a la acción realizada si no hubiese ningún depurador. Estas técnicas suelen ser usadas por aquellos programas que no se quiere que se le haga ingeniería inversa y de esta manera dificultar el estudio del ejecutable.

Unos cuantos ejemplo de ejecutables que usan este tipo de técnicas son los malware y los virus, pero también sistemas de empaquetado de ejecutables y sistemas de protección anticopia.

La primera técnica que vamos a ver se basa en el principio de que en Linux, un sistema que está siendo depurado por un proceso no puede ser depurado de nuevo por otro proceso. En linux la mayoría de depuradores usan la llamada al sistema ptrace para realizar su trabajo, de tal manera que si nosotros en nuestro programa llamamos a dicha función evitaremos que podamos ser depurados.

Si ejecutamos este programa desde la shell y luego probamos a ejecutarlo en gdb obtenemos

gdb

Vemos que si se ejecuta directamente sobre la shell nos dice que no hay depurador pero en cuanto lo ejecutamos en gdb nos dice ¡Depurador detectado!

Pero no solo eso, si lo ejecutamos con ltrace

ltrace

y con strace

strace

Vemos que obtenemos el mismo resultado ¡Depurador detectado!. En la última imagen se ve un poco mejor que es lo que realmente está pasando, vemos que cuando hace la llamada a ptrace nos devuelve -1

ptrace(PTRACE_TRACEME, 0, 0x1, 0)       = -1 EPERM (Operation not permitted)

Esto ocurre porque el programa está siendo traceado por strace y la llamada a ptrace de nuestro código no está permitida cuando ya hay otro ptrace en ejecución, el de strace.

Una de las formas mas sencillas para combatir esta técnica es modificar EAX a la salida de ptrace. EAX (o RAX en arquitecturas de 64 bits) contendrá el resultado devuelto por ptrace, que como hemos visto arriba, cuando hay un depurador activo es -1. Si modificamos y ponemos un valor 0 o mayor habremos conseguido saltarnos la técnica.

Para ello podemos cargar el programa en gdb, poner un breakpoint en main y ver cual es la instrucción inmediatamente posterior a ptrace y poner un breakpoint allí.

gdb1

si ponemos un breakpoint en la dirección 0x0804846c y continuamos la ejecución veremos que en eax devolverá -1 (como nos avisaba strace) si nosotros cambiamos este valor por 0 habremos conseguido saltarnos la protección

gdb2

Esta técnica es bastante sencilla pero requiere la modificación del registro EAX, otra técnica sería parchear el ejecutable y aquí tenemos bastantes opciones, podemos eliminar el call a ptrace y sustituirlo por nops, podríamos invertir el salto jns que hay a continuación de test, podríamos ponerle nops a ese salto, podríamos cambiar la instrucción test y cambiarla por alguna mas apropiada, etc.

Estas técnicas comentadas arriba son técnicas intrusivas porque requieren la modificación del binario o de la memoria del proceso en tiempo de ejecución, podemos realizar un enfoque mucho menos intrusivo si conseguimos que la llamada a ptrace devuelva automáticamente 0 (o cualquier número positivo).

Para ello nos programamos nuestra propia función ptrace

y la compilamos de la siguiente forma

gcc -m32 -shared -fPIC ptrace.c -o ptrace.so

De esta forma le decimos que va a ser una librería compartida (-shared) y que el código va a ser independiente de la posición (-fPIC), esto lo que hace es que en el binario en vez de guardar direcciones de memoria va a almacenar offset de esta manera esta librería podrá ser cargada en tiempo de ejecución en cualquier posición dentro del espacio virtual del proceso (gracias a esto funciona ASLR, aquí explico como funciona ASLR y como romper su seguridad: http://bitybyte.angelluispg.es/2016/11/13/bypassing-aslr/)

Ahora podríamos decirle a Linux que utilice nuestro ptrace.so en vez de la librería del sistema para ello tendríamos que establecer la siguiente variable de entorno

export LD_PRELOAD=./ptrace.so

En mi caso no me interesa exportarla en la shell de linux así que la voy a exportar dentro de gdb con

set environment LD_PRELOAD ./ptrace.so

Si ahora ejecutamos el programa que implementa la técnica antidepuración vemos

gdb3

Efectivamente hemos conseguido evitar la la técnica antidepuración de una forma más limpia.

Repositorio para arquitecturas armhf con python3.6 y opencv3.2.0

Ahora que estoy en trabajando en un grupo de investigación de la Universidad de Murcia he creado un repositorio debian y con algunos paquetes como libopencv (3.2.0) y python (3.6) para usar en arquitecturas armhf, como por ejemplo raspberry pi.

Para poder usar este repositorio primero es necesario descargarse mi firma pública:

wget -qO - http://angelluispg.es/debianrepos.angelluispg.key  | sudo apt-key add -

a continuación debemos añadir en /etc/apt/sources.list la siguiente línea

deb http://debianrepos.angelluispg.es/debian jessie main

Ahora procedemos a actualizar mediante

apt-get update

Y ya podemos instalar los paquetes de mi repositorios que básicamente son libopencv (en la versión 3.2.0) y python3.6 ambos para arquitecturas armhf (como raspberry pi).

Además he modificado las fuentes de python3.6 para que haga referencia a la Universidad de Murcia.

rasp

Por cierto, después de instalar python hay que instalar easy_install mediante el siguiente comand:

wget https://bootstrap.pypa.io/ez_setup.py -O -| sudo python3.6

Si queremos instalar cualquier módulo de python podremos hacerlo mediante

easy_install [modulo]

ByPassing DEP y ASLR

Para compilar el programa name.c para este artículo es necesario desactivar DEP:

gcc -m32 -fno-stack-protector name.c -o name

Esta vez no hay que desactivar ASLR, si estuviese desactivado se puede activar de la siguiente forma:

echo 2 > /proc/sys/kernel/randomize_va_space

En los dos capítulos previos estudiamos las protecciones DEP y ASLR y rompimos su seguridad por separado. Lógicamente este artículo va a ir sobre como conseguir atacar satisfactoriamente un programa protegido por estas dos protecciones.

Lo primero que hay que tener en cuenta es que ahora ya no podemos ejecutar desde la zona de datos (porque DEP protege contra tal acto) y además no tenemos una zona fija donde almacenar datos (puesto que ASLR aleatoriza el espacio de direcciones de un proceso).

Hay varias soluciones para afrontar este problema. Aquí voy a explicar una bastante sencilla basada en técnicas usadas en artículos anteriores, por lo que este artículo no será muy largo.

Para empezar hemos hecho una pequeña reforma a nuestro programa vulnerable:

Vemos que los principales cambios es añadir la función exit(0) en vez de return 0 y el uso de la función system() en vez de printf.

En este ejemplo en concreto queda raro y forzado, pero no es raro encontrar estas funciones en programas.

¿Por que hemos hecho estos cambios? Básicamente porque vamos a emplear una técnica llamada Return to PLT que es similar a Return to Libc. Si recordamos bien, Return to Libc la utilizamos cuando quisimos saltarnos DEP y básicamente consistia en proporcionar en EIP la dirección de una función de Libc (en nuestro caso fue de system).

Ahora no podemos usar Return to Libc porque no podemos proporcionar una dirección exacta de la función system debido a la protección ASLR. Pero aquí viene en nuestro ayuda la PLT.

PLT significa Procedure Linkage Table y es usada junto a GOT (Global Offset Table) para que el loader del Sistema Operativo sepa linkar nuestro programa con las librerías compartidas. Básicamente nuestro programa tiene una PLT donde almacena las llamadas a procedimientos de librerias compartidas. La primera vez que nuestro programa llama a una de estas funciones, la tabla se rellena con la dirección real de dicha función. Por tanto, la primera vez que llamamos a printf, nuestro programa consulta PLT ve que no tiene la dirección de printf y la resuelve en tiempo de ejecución, la próxima vez que se llame a printf su direccion real ya estará resuelta y no hará falta resolverla de nuevo. PLT y GOT se escapan del ámbito de estos artículos pero se puede encontrar mucha información sobre ellos buscando sobre Linkers y Loaders.

Y no, la tabla PLT no se ve afectada por la aleatorizacion de ASLR.

Conclusión, tenemos una tabla con las direcciones a funciones que nuestro programa usa, y estas direcciones son fijas ¡incluso con ASLR activado!. La desventaja es que la llamada a dicha función se debe encontrar dentro de nuestro programa (de ahí que hayamos tenido que usarla de una forma un tanto forzada).

Vale, tenemos un programa vulnerable con una función system y que podemos llamar gracias a PLT, pero a system hay que pasarle un argumento y seguimos teniendo ASLR activo por lo que nuestro argumento tendrá una dirección variable de memoria.

¿Os acordais del artículo anterior? En él introdujimos nuestro exploit en una variable de entorno con una gran cantidad de NOP's para que una vez que caiga en el primer NOP se vaya desplazando hasta llegar a nuestro shellcode real. Pues esta vez vamos a emplear algo parecido, solo que en vez de introducir en nuestra variable de entorno un shellcode, vamos a introducir la orden “/bin/sh” que nos va a proporcionar una shell pero, como en el artículo anterior, esto nos arrojaria muchos fallos, hasta el punto de tener que usar un ataque de fuerza bruta de miles de intentos, así que vamos a meter también un colchon de NOPs para que una vez que la dirección proporcionada como argumento de system caiga en nuestra variable de entorno, esta se vaya desplazando hasta encontrar el comando /bin/sh.

Bueno, basta de teoría, pasemos a la práctica. Primero tenemos que hacernos con las direcciones de memoria de system@plt y exit@plt, para ello usamos gdb y desensamblamos la funcion main

0

En la imagen podemos ver como mi binario tiene la función system@plt en la dirección 0x8048370 y la función exit@plt en la dirección 0x8038390. Vamos a exportar ahora una variable de entorno que contenga una gran cantidad de comandos inútiles (o incluso inválidos, en mi caso usaré estos ultimos porque mi shell no me permite introducir espacios) y un comando “/bin/sh”

export xploit=`python -c "print 'echo_a;'*18000+'/bin/sh'"`

Una vez más hay que tener en cuenta que la variable de entorno debe ser bastante grande pero hay que tener cuidado porque algunas shells imponen límites de tamaño para las variables de entorno, en mi caso la variable es de 6*18000+7 bytes, lo que hace un total de 9007 bytes. Vamos a ejecutar ahora nuestro ya conocido programa getenv que nos devuelve la dirección de una variable de entorno con respecto al programa vulnerable.

1

Lo ejecutamos unas cuantas veces y vemos que efectivamente la dirección de memoria varía. Ahora elegimos una, la que sea, por ejemplo, en mi caso voy a elegir la última 0xfff3733e. Y si recordamos el artículo sobre Return to Libc sabremos que tendremos que pasar los argumentos de la siguiente forma

AAAA...AAAA + DIRECCIÓN_SYSTEM + DIRECCIÓN_EXIT + DIRECCIÓN_PARÁMETRO_SYSTEM

para que la pila quede así

DIRECCIÓN_PARÁMETRO_SYSTEM
DIRECCIÓN_EXIT
DIRECCIÓN_SYSTEM
AAAA...AAAA

Pero esto, es un ataque de fuerza bruta, por tanto deberemos crear una shellscript como hicimos en el artículo anterior para ir probando hasta que obtengamos una shell. El script puede ser el siguiente

damos permisos de ejecución al script mediante

chmod +x xploit.sh

y ejecutamos mediante

./xploit.sh

2

¡Tachan! ¡Volvimos a obtener una shell con DEP y ASLR activado!

Conclusiones

Aquí acaba esta serie de artículos, nos hemos dejado en el camino muchas técnicas y algunas protecciones pero hemos estado viendo lo básico sobre stacks overflows y como romper dos protecciones usadas en entornos reales.

En el camino nos hemos dejado una protección implementada por el compilador basada en cookies o canaries, estas técnicas se basan en poner un número en una dirección de memoria entre nuestro buffer y el valor de EIP, al regresar de la función, si este valor no coincide con el almacenado en un lugar seguro por el programa entonces se detendrá la ejecución. Además algunas de estas ténicas del compilador son capaces de reorganizar nuestras variables para situar a los buffers en las primeras zonas de memoria después del valor de EIP, dejando variables primitivas y punteros debajo de los buffers.

En GCC, el estandar es Stack Smash Protector o ProPolice que es capaz de usar 2 canaries distintos, o bien uno generado aleatoriamente y que es comprobado por el programa siempre que se va a recuperar el valor de EIP, o bien usar un canary prefijado: 0x000aff0d. Este canary está muy bien pensando y rompe nuestros ataques completamente, 0x00 es el byte null por lo que acabaria con los ataques basados en string, 0x0a es el salto de línea \n y acabaria con los desbordamientos mediante las familias gets, 0xff es el indicador de fin de archivo y 0x0d es el carater \r. Esta protección en algunas distribuciones Linux viene desactivada por defecto, en mi ubuntu viene activada y en cada programa vulnerable he tenido que desactivarla. En efecto, esta protección se corresponde a la opción -fno-stack-protector, si quitamos esa opción cuando compilamos, ninguno de los ataques empleados aquí tendría efecto.

3

Si realizamos el ataque de este artículo sobre el mismo programa pero quitando la opción -fno-stack-protector, veremos que el programa detecta que se ha desbordado el buffer y termina.

También se nos ha quedado en el tintero los ataques de heap overflow y los de las cadenas de formato.

Es posible que en un futuro escriba algo sobre ello.