Jugando con técnicas anti-debugging (III)

En esta nueva entrega vamos a ver una técnica para detectar un debugger sin recurrir a librerías del sistema como ocurría en las entregas anteriores (ver la anterior y la primera). Se basa en un concepto muy simple para detectar el “step-by-step” (paso a paso) cuando se está analizando la aplicación dentro de un debugger.

Como ya sabemos, la mayoría de los debuggers permiten avanzar de forma diferente en la ejecución del programa. Por ejemplo, en OllyDbg:

  • Run (F9) → Ejecuta el programa hasta llegar al final o hasta encontrar una interrupción.
  • Step into (F7) → Ejecuta una sentencia del programa cada vez (“step-by-step”).
  • Step over (F8)→ Ejecuta el programa sin entrar a analizar las funciones (calls)
  • etc. (Ver menú “Debug”)

Cuando analizamos una aplicación, lo normal es que en alguna sección que consideremos importante o crítica nos detengamos a analizarla con detalle. En estos casos, la ejecución “step-by-step” nos facilita el trabajo. Ahora supongamos el siguiente código de alto nivel:

t1=time();
//sección critica
for (i=0;i<100;i++)
{
   código;
}
t2=time();
t3=t2-t1;

Primero se calcula el tiempo actual (t1), posteriormente se ejecuta una sección de código de la aplicación, se vuelve a calcular el tiempo actual (t2) y por último, se obtiene la diferencia de ambos tiempos (t3). En una ejecución normal de la aplicación, el tiempo total empleado en ejecutar el código intermedio (sección crítica) entre las funciones de tiempo es X. Si se ejecutara en un debugger y avanzáramos paso a paso a través del código intermedio, el tiempo total sería mucho mayor que X.

De esta forma, podemos estimar el tiempo medio de ejecución de esa sección de código en condiciones normales; si el tiempo empleado es mucho mayor, puede que la aplicación esté siendo debuggeada y, entonces, procedemos a realizar las acciones pertinentes para dificultar el análisis.

A continuación se presenta el código del crackme en lenguaje C:

/*****************************************************************
* File:		crackme03_GetTickCount.c
* Description:	Es un crackme que implementa la protección 
* 		anti-debugging basada en la función GetTickCount()
* Date: 	06/07/2015
* Author: 	reverc0de
* Twitter:	@reverc0de
* Site:		www.reverc0de.com
* Github:	https://github.com/reverc0de/saw-anti-debugging	
*****************************************************************/
#include 
#include 

int main(void)
{
unsigned long t1,t2,t3;
	int i,x;
	t1 = GetTickCount(); //Tiempo en milisegundos
	//Código intermedio
	for (i=0;i<10000000;i++)
	{
		x = i; //Código de relleno
	}
	t2 = GetTickCount(); //Tiempo en milisegundos
	t3 = t2-t1; //Tiempo empleado
	printf("t1: \t%ld\nt2: \t%ld\nt2-t1: \t%ld",t1,t2,t3);
	//Si el tiempo empleado es mayor a 100 ms, entonces está siendo debuggeado
	if (t3 > 100) 
	{
		MessageBox(0, "Debugger detectado!!","Crackme03 GetTickCount",MB_OK);
		exit(0);
	}	
	MessageBox(0, "Debugger NO detectado!!","Crackme03 GetTickCount",MB_OK);
	return 0;
}

El crackme utiliza la función “GetTickCount()()” de la API de Windows para el cálculo de los tiempos. Esta función devuelve el número de milisegundos desde que se arrancó el sistema (uptime).

Se ha estimado que el tiempo necesario para consumir el bucle no lleva más de 100 ms, por lo que si el tiempo final es superior, el mensaje con “Debugger detectado!!” será mostrado.

Al igual que en las entradas anteriores, podéis descargar el crackme ya compilado y los sources en la carpeta “crackme03_GetTickCount” del repositorio:

Una vez descargado el binario “crackme03_GetTickCount.exe” lo ejecutamos, obteniendo los siguientes resultados:

jf0

El tiempo total de ejecución ha sido de 46ms y, como está por debajo de los 100 ms estimados como máximo, el mensaje mostrado es el correcto, ya que no se está debuggeando.

Ahora vamos a cargar el crackme en OllyDbg. Si ejecutamos la aplicación con F9, los resultados son similares a si se ejecuta fuera del debugger. Efectivamente, el crackme no ha detectado el debugger, pero esta técnica se basa en la detección del “step-by-step” del debugger. Para mostrarlo, vamos a establecer los siguientes breakpoints:

Address         Descripción
0040153B       GetTickCount, t1
0040155A       Salto bucle for
00401561       GetTickCount, t2
00401594       Comparación tiempos
00401598       Salto condicional

jf1

jf2

Ejecutamos la aplicación con F9 y se detendrá en el primer breakpoint donde obtiene el tiempo actual (t1). A continuación de esta función de tiempo se encuentra la implementación del bucle “for”. Se puede ver como se incrementa el contador en 1 hasta llegar al valor de 9999999 (0x98967f).

Si a partir de aquí avanzamos paso a paso con F7, lo que hacemos es interrumpir la ejecución normal del programa y el tiempo que perdamos en recorrer las sentencias del bucle se irá incrementando hasta superar con creces el tiempo de 100 ms.

Después se encuentra de nuevo la función “GetTickCount()” (t2). Acto seguido calcula la diferencia de tiempos (t3) y los muestra con la función “printf()”.

Ahora, en la dirección 00401594 donde teníamos otro breakpoint, se puede ver cómo realiza la operación de comparación de la diferencia de tiempos (t3) y 100 ms (0x64).

El resultado de esta operación actualiza el valor del flag “Z” poniéndolo a 0. “CMP” establece “Z=0” si los operandos son distintos, y “Z=1” si son iguales.

El mensaje mostrado viene definido por el salto condicional que se encuentra en la siguiente sentencia (“JBE”). “JBE” salta a la dirección indicada (en este caso, hacia el mensaje de debugger no detectado) si los flags “Carry (C)” o “Zero (Z)” están activados. En nuestro caso ambos flags están a 0, por lo tanto, el salto no se realiza y se muestra el mensaje de debugger detectado.

jf3

Al igual que en los crackmes anteriores, podemos modificar el tipo de salto para evadir esta protección. No vamos a entrar en más detalles ya que el proceso se ha visto en las entregas anteriores.

Consideraciones a tener en cuenta:

  • Si no se realiza un análisis “step-by-step” de la sección crítica de código, o si no se realiza una interrupción por un breakpoint dentro de la sección crítica, entonces no se detectará el debugger.
  • La sección crítica puede ser cualquier código de relleno, o bien, código necesario de la aplicación propenso a ser analizado detenidamente.
  • Es conveniente complementar esta técnica con alguna más, así mejoramos la probabilidad de detectar el análisis.
  • El tiempo estimado para ejecutar la sección crítica debe ser lo suficientemente amplio para que se ejecute en diferentes ordenadores de forma normal.
  • Hemos usado la función “GetTickCount()” de la API de Windows, pero podría haberse implementado con otras funciones propias del lenguaje C, por ejemplo, mediante la librería “time.h”.
  • GetTickCount()” tiene ciertas limitaciones, por ejemplo, se limita a un uptime del sistema de máximo 49,7 días. Además, no es recomendable su uso para calcular tiempos demasiado precisos, su precisión se encuentra entre 10-16 ms, pero suficientes para nuestra prueba de concepto.