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.

Programar es Fácil

Hoy he encontrado una pequeña joya entre mis armarios.

Programar es fácil era una curso de introducción a la programación que estaba apoyado por IBM y que cuando lo acababas, tras superar un examen te daban un diploma.

El curso se vendió en 2001 en España en fascículos semanales si no recuerdo mal, y el primer tema a tratar era programación web, seguido de Delphi con Borland Delphi y C++ con Borland C++.

Y efectivamente en 2001 es donde empecé mi carrera como programador, tenía unos 11 años y gracias a estos fascículos conseguí dominar con bastante fluidez delphi, donde program´3 unas cuantas aplicaciones e incluso 2 juegos.

El curso no lo pude acabar porque mi paga semanal no me daba para comprarme los fascículos

Este era el anuncio del curso: https://www.youtube.com/watch?v=L656idB33Mo

Programar es Fácil

 

 

Transformar algoritmos recursivos a iterativos

Por lo general no suelo escribir sobre programación en general, pero este tema siempre me ha parecido bastante interesante.

Actualmente estoy programando un algoritmo de compresión de video/imagen sin perdida que se basa en árboles y predictores. La primera iteración del algoritmo fue hacerlo funcionar sin preocuparse demasiado en la optimización. Al ser un algoritmo basado en árboles la primera iteración del algoritmo usaba recursividad para recorrer los árboles.

Una vez que funciona el algoritmo me propuse eliminar la recursividad. El algoritmo debe funcionar de forma fluida en microcontroladores (poca memoria y poca potencia).

La recursividad en computación y para que se entienda facilmente es llamar a una función X dentro de la misma función X. Por lo general los métodos recursivos constan de un caso base y un caso general. El caso base es el que termina la recursividad y el caso general el que hace llamadas recursivas. Aquí tenemos una función que recorre un arbol en profundidad e imprime el contenido de las hojas.

Aquí el caso base es el tercer if, en dicho if no hay llamadas a la función, cuando sale de dicho if se termina la función y el programa continua ejecutandose en esta misma función, ya que posteriormente se habia hecho una llamada a esta misma función.

La recursividad funciona gracias a la pila o stack, esta estructura de datos es del tipo LIFO (Last In First Out) que significa que el último que entra en la pila es el primero que se coge cuando se vaya a sacar un elemento, podemos hacer un simil con un conjunto de plato apilados, cuando vayamos a dejar un plato lo dejaremos arriba del todo y cuando vayamos a necesitar un plato lo cogeremos de arriba. He hablado anteriormente sobre la pila en este blog, como por ejemplo en la categoría security: http://bitybyte.angelluispg.es/category/security/ donde hablo sobre vulnerabilidades de la pila y en esta entrada http://bitybyte.angelluispg.es/2016/07/10/emulacion-de-una-cpu/ donde emulo creo una CPU e implemento una pila.

Cuando hacemos llamadas a procedimientos desde lenguajes de alto nivel lo que se está haciendo a nivel de máquina es empujar los parametros de dicha función a la pila, incrementar el registro SP (Stack Pointer) y hacer un call a la función. Podemos eliminar la recursividad si implementamos una pila software y usamos dicha pila. Usar esta segunda aproximación es mas eficiente, ya que no tenemos que realizar el call a la función.

Los lenguajes de alto nivel suelen implementar una pila en su librería, no es el caso de C, en donde si queremos una pila tendremos que programarla nosotros mismos. Aquí dejo mi implementación de pila.

Básicamente lo que hacemos es usar nuestra pila en vez la del sistema. Es mejor ver el ejemplo en vez de explicarlo. Aquí se puede ver el mismo algoritmo de recorrido de árboles en profundidad pero esta vez de forma iterativa

 

Vemos como se han eliminado las llamadas a la función y ahora se hace uso de la pila software que se ha implementado.