Encontrar un binario válido para realizar DLL sideloading no suele ser algo difícil por lo general. Lo más complicado es todo lo que viene alrededor: revisar imports, comprobar rutas, descartar KnownDLLs, reconstruir exports, ejecutar el binario y repetir el proceso hasta dar con una combinación estable.
Con la herramienta SideFinder que hemos publicado desde RedTeamer.es, la idea es poder simplificar este proceso. El funcionamiento es sencillo: cargar un PE, reunir en una sola vista la información que normalmente buscaríamos con varias herramientas y convertir una lista de DLL en candidatos que podamos utilizar.
En este artículo vamos a recorrer ese proceso con dos ejemplos: Analizar y descubrir Sideloading en Notepad++ y encontrar candidatos en los más de 150 binarios que tiene Sysinternals Suite de forma rápida.
Qué estamos buscando realmente
DLL sideloading consiste en conseguir que una aplicación legítima cargue una DLL controlada por nosotros. El código termina ejecutándose dentro del proceso de esa aplicación, que puede estar firmada por Microsoft o por un tercero de confianza.
La técnica suele apoyarse en alguno de estos escenarios:
- La aplicación busca una DLL en un orden que permite colocar nuestra copia antes que la legítima.
- El binario referencia una DLL que no está presente en el sistema o en el paquete de la aplicación.
- Sustituimos una DLL propia de la aplicación y preservamos sus funciones mediante una DLL proxy.
Esta técnica es especialmente útil en acceso inicial, movimiento lateral o persistencia. Pero que una DLL aparezca en la tabla de imports no significa que tengamos un sideload funcional. Entre medias hay bastantes preguntas: ¿se carga de verdad?, ¿desde qué ruta?, ¿es una KnownDLL?, ¿qué exports necesita el programa?, ¿la aplicación sigue funcionando después o lo hace de forma inestable?
Con esta herramienta prodremos resolver todas esas dudas y generar nuestros sideloads de forma fácil y rápida.
Primer análisis: Notepad++
Podemos cargar el ejecutable arrastrándolo a SideFinder o mediante el botón Load Binary. Tras el análisis inicial, la interfaz se divide en tres zonas principales.
Target Info
Resume el contexto del binario: arquitectura, ruta, firma digital y protecciones relevantes. No tiene mucho más misterio.
Import Table
Aquí encontramos las dependencias declaradas por el PE:
- Imports contiene las dependencias convencionales que el loader resuelve durante la carga del proceso.
- Delay Imports recoge las librerías cuya resolución se retrasa hasta que alguna parte del programa las necesita.
- Bound Imports muestra, cuando existe, información de dependencias enlazadas previamente contra versiones concretas.
Los delay imports son especialmente interesantes porque una DLL puede no aparecer durante los primeros segundos de ejecución. Quizá solo se cargue al abrir un diálogo, comprobar actualizaciones o utilizar una función concreta.
Sideload Candidates
Esta tabla contiene la lista de DLL encontradas ordenadas mediante un algorimo de puntuación propio.
SideFinder ordena las DLL mediante un scoring contextual. Dependiendo de los indicadores recibidos se puntúa según la viabilidad de la imagen para ser susceptible a DLL Sideloading. Cuanta mayor puntuación, más posibilidades de resultar en la carga de código arbitrario de forma satisfactoria.
La información que utiliza para realizar los cálculos y que además se muestra de manera visual para el operador es la siguiente:
- Hijackable: indica si podemos aprovechar el orden de búsqueda de Windows para que el ejecutable cargue una DLL colocada por nosotros. Es habitual encontrar aquí librerías del sistema como
VERSION.dlloWININET.dll: la copia legítima se encuentra normalmente enC:\Windows\System32, pero si la aplicación busca primero en su propio directorio, una DLL con el mismo nombre situada junto al ejecutable puede resolverse antes. - App DLL: señala que la DLL pertenece a la propia aplicación o que ya se encuentra en su mismo directorio. Suplantar una librería específica del programa puede resultar menos evidente que introducir una DLL con nombre de componente de Windows, cuya firma, ruta o hash no coincidirían con los esperados. La contrapartida es que estas DLL suelen contener funcionalidad necesaria para la aplicación, por lo que probablemente tendremos que conservarla mediante un proxy para evitar errores o cierres inesperados.
- Delay: muestra que la DLL no se resuelve al arrancar el proceso junto al resto de la IAT, sino más adelante, cuando la aplicación utiliza alguna de sus funciones. Puede ser una carga asociada a acciones como abrir un diálogo, cargar un plugin o buscar actualizaciones. Esto permite retrasar la ejecución y evita alterar el inicio del programa, pero también obliga a identificar y reproducir el flujo exacto que provoca la carga.
- Missing: informa de que el binario referencia una DLL que SideFinder no ha podido localizar siguiendo el orden de búsqueda evaluado. Es un candidato especialmente interesante porque colocarla en la ruta esperada podría permitir ejecutar código sin reemplazar ningún archivo existente. Aun así, una referencia ausente no significa que la aplicación llegue a solicitarla durante su uso real; el análisis dinámico nos ayuda a confirmar si finalmente intenta cargarla.
- Documented: indica que el nombre o el caso de hijacking aparece previamente documentado en HijackLibs. Además de confirmar que esa DLL ya se ha estudiado con estos fines, nos permite consultar otros binarios, rutas y condiciones con los que se ha observado. Lo utilizamos como contexto y punto de partida, no como garantía de que vaya a funcionar en nuestra versión concreta.
- Exports: muestra el número total de funciones exportadas por la DLL. Es una referencia rápida de la complejidad que puede tener el artefacto: cuantos más exports exponga y utilice la aplicación, más llamadas tendremos que reproducir o reenviar y mayor será el riesgo de provocar inestabilidad. Un número bajo suele hacer más sencilla la primera validación, aunque lo verdaderamente importante es qué funciones consume el binario y cuándo las invoca.
Comprobar un candidato
En nuestro ejemplo, el análisis estático de Notepad++ devuelve 19 DLL. Seis aparecen con prioridad alta por combinar una ruta potencialmente secuestrable con documentación previa; otras tres mantienen interés aunque no estén documentadas. Las KnownDLLs quedan descartadas con puntuación cero.
Ahora que tenemos una lista de posibles DLL a las que realizar Sideloading. El siguiente paso es comprobar la funcionalidad mediante una prueba controlada con el botón Test Sideload. SideFinder genera una DLL con los exports necesarios y una carga inocua basada en MessageBox, la compila y ejecuta la aplicación automáticamente en cuestión de segundos. Si al crearse el proceso vemos un popup, significa que el candidato es válido.
Si aparece el mensaje, ya tenemos evidencia de ejecución. Aun así, todavía debemos observar la estabilidad del programa. Una aplicación que muestra el popup y se cierra inmediatamente no es lo más deseable en cuanto a OPSEC ya que puede hacer sospechar al usuario.
En esta prueba funcionaron candidatos como SensApi.dll, VERSION.dll, WININET.dll, UxTheme.dll y dwmapi.dll. Por otro lado dbghelp.dll también permitió ejecutar código, pero ocasionó ventanas de error durante la carga de plugins. Ese matiz es importante: el sideload existe, aunque para utilizarlo sin romper la aplicación necesitaríamos preservar la DLL original mediante proxy.
Lo que el análisis estático no ve
Una tabla de imports es una fotografía parcial. Las aplicaciones también pueden resolver dependencias durante la ejecución, construir sus nombres dinámicamente o cargar módulos únicamente después de una acción del usuario.
Con el botón Run Dynamic Analysis, SideFinder ejecuta el binario durante una ventana de observación y registra las DLL cargadas. En nuestro ejemplo aparecen 69 módulos y ninguna dependencia ausente. Por lo que tras el análisis dinámico la superficie de carga aumenta considerablemente.
Al cruzar esa observación con el análisis anterior aparecen nuevos candidatos que no estaban presentes en la vista estática inicial.
Ahora es posible probar nuevos candidatos basados en módulos de la aplicación, que también son funcionales. Una vez en este punto donde conocemos opciones potencialmente buenas para realizar la técnica, podríamos directamente generar nuestra DLL proxy que ejecute shellcode. Pero esto lo veremos más adelante.
Cuando hay demasiados binarios
Analizar un ejecutable a mano es razonable. Hacerlo con los más de cien binarios como en el caso de Sysinternals Suite ya no lo es.
Scan Directory automatiza el análisis de todos los binarios con los que se encuentre a partir de un directorio definido y recorre subdirectorios hasta tres niveles por defecto. En pocos segundos obtienes un resumen para encontrar candidatos.
La vista permite detectar rápidamente qué binarios concentran candidatos confirmados o potenciales. En vez de abrir 150 ejecutables uno por uno, podemos saltar directamente a los que presentan una superficie interesante y continuar allí el análisis habitual.
Para la demostración seleccionamos Bginfo64.exe y probamos VERSION.dll. La carga se confirma de nuevo mediante un MessageBox.
Enriquecer el análisis con HijackLibs
HijackLibs mantiene una base de datos de DLL utilizadas para realizar hijacking mantenida por la comunidad. SideFinder puede descargarla y utilizarla para marcar hallazgos ya documentados.
La etiqueta Documented aporta información sobre aquellas DLL que ya han sido documentadas previamente como útiles para realizar hijacking en algunos binarios. Con doble click sobre la etiqueta "YES" podemos consultar qué binarios y rutas están asociados a esa DLL y utilizarlo como punto de partida para la validación.
Además, una vez obtenida la base de datos, es posible buscar en todo el equipo aplicaciones que coincidan con rutas documentadas. De esa forma podríamos tratar de replicar entornos objetivo y encontrar rutas donde desplegar persistencias o aprovechar para movimiento lateral.
Generar los artefactos funcionales
Una vez hemos encontrado y probado que una DLL es válida para nuestro Sideloading. El siguiente paso es construir una DLL que ejecute nuestra rutina sin impedir que la aplicación siga haciendo aquello para lo que fue diseñada.
Para ello, podemos usar SideFinder para generarla rápidamente. Con el candidato seleccionado, pulsamos Generate Proxy DLL.
La aplicación nos dará diferentes opciones para generar el artefacto que definiremos a continuación:
Proxy DLL o sideload directo
La elección depende de cuánto necesite la aplicación a la biblioteca legítima:
- Una Sideload DLL reproduce los exports que espera el ejecutable mediante funciones mínimas o vacías. Es útil para comprobar rápidamente la ejecución, pero la aplicación puede perder funcionalidad o terminar fallando cuando intente utilizar una de esas funciones.
- Una Proxy DLL reenvía las llamadas a la DLL original, que se conserva con otro nombre. Así podemos ejecutar nuestra rutina y mantener el comportamiento esperado del programa.
En el caso de VERSION.dll con Bginfo64.exe elegimos Proxy DLL. De esta forma buscamos mantener la estabilidad y funcionalidad original.
Al generar el proxy debemos indicar el nombre con el que se guardará la biblioteca legítima. Si usamos notVERSION.dll, la DLL generada ocupará el nombre VERSION.dll y reenviará hacia notVERSION.dll los exports originales.
De dónde obtener los exports
SideFinder permite construir la lista desde la IAT del ejecutable o desde la EAT de la DLL original.
- La IAT refleja las funciones que importa el binario analizado. Puede ser suficiente para una prueba concreta, pero no garantiza que otros módulos o plugins no utilicen exports adicionales.
- La EAT describe todo lo que exporta realmente la DLL original. Cuando tenemos acceso a ella, suele ser la opción más segura para generar un proxy completo.
En este caso mantenemos EAT como fuente. SideFinder recoge nombres y ordinales y genera los stubs o directivas necesarios para reenviar las llamadas. Aun así, que los exports coincidan no elimina la necesidad de probar: una DLL puede depender de su inicialización, de una versión específica o de estado compartido con otros módulos.
Elegir cuándo ejecutar nuestra rutina
La opción más directa es DllMain: la rutina se inicializa cuando la DLL se adjunta al proceso. SideFinder desplaza el trabajo a un contexto separado para evitar concentrar lógica pesada dentro del loader lock.
También podemos asociar la ejecución a un export específico. Esto resulta útil cuando queremos que la rutina se active al alcanzar una función concreta de la aplicación y no inmediatamente al arrancar. La contrapartida es evidente: si ese export no llega a invocarse, la carga tampoco se ejecutará.
Por lo general, es más seguro apostar por DllMain. Una vez que el artefacto es estable, tiene sentido estudiar el flujo del programa y valorar un disparador más tardío.
Seleccionar la carga
El generador ofrece tres puntos de partida:
- MessageBox sirve para validar de forma visible e inocua que el artefacto se carga.
- Shellcode Runner prepara la lectura y ejecución en memoria de un fragmento de shellcode proporcionado.
- Custom entrega la estructura del proxy lista para incorporar nuestra propia lógica.
Para pasar de la prueba con popup a un artefacto útil elegimos Shellcode Runner y seleccionamos el archivo binario que contiene nuestra carga maliciosa.
SideFinder mantiene ese contenido fuera de la DLL para evitar aumentar el tamaño del ejecutable o poseer secciones o recursos con alta entropía que disparen indicadores. Podemos de esta forma generar un archivo .dat, .ini o .txt, o utilizar cualquier otro tipo de archivo como una imagen PNG. En este último modo el shellcode se escribe al final de los bytes de la imagen. Posteriormente la DLL localiza el contenido, lo recupera y lo descifra durante la ejecución.
Para esta prueba utilizamos un archivo test.dat y un fragmento de shellcode sencillo que inicia el proceso calc.exe.
Método de ejecución
SideFinder incluye distintos métodos para transferir la ejecución dentro del mismo proceso. La intención es aprovechar el contexto que ya hemos obtenido mediante sideloading, sin añadir innecesariamente una segunda inyección sobre otro proceso.
- Callbacks: utilizan una API que termina invocando una función proporcionada por el programa. Su sencillez y restricciones dependen del callback seleccionado.
- Thread Pool: programa el trabajo sobre el pool de hilos del proceso. Evita crear explícitamente un hilo dedicado, aunque introduce dependencia sobre la inicialización y el ciclo de vida del pool.
- Fibers: transforma el flujo del hilo en un contexto cooperativo y cambia a un fiber que ejecuta la rutina. Ofrece control sobre la transferencia, pero exige cuidar especialmente el retorno y el estado del hilo.
Para las pruebas realizadas en entornos con soluciones de seguridad avanzada, la ejecución mediante Callbacks ha proporcionado buenos resultados.
Compilar y revisar la salida
Cuando pulsamos Generate, SideFinder intenta localizar una cadena de compilación compatible. Puede utilizar Visual Studio Build Tools mediante cl.exe o una instalación de MinGW-w64 con g++.exe. Si encuentra el compilador, construye automáticamente el proyecto y abre su directorio; si no, conserva el código fuente y los archivos de compilación para que podamos hacerlo manualmente.
Dentro del directorio output podemos encontrar los binarios compilados listos para copiar y pegar.
En nuestro ejemplo, la carpeta output contiene tres artefactos:
VERSION.dll: la proxy DLL generada, con el nombre que BgInfo intentará cargar.notVERSION.dll: la biblioteca legítima renombrada, que conserva la funcionalidad original.test.dat: el fichero donde se almacena el shellcode proporcionado de manera cifrada.
Los tres archivos se colocan junto a Bginfo64.exe. Al iniciar el programa, Windows resuelve VERSION.dll desde ese directorio, el proxy reenvía las funciones hacia notVERSION.dll y nuestra rutina recupera el contenido de test.dat.
Como resultado podemos observar que el programa se carga correctamente manteniendo funcionalidad y estabilidad mientras que además aparece una calculadora, resultado de la ejecución de las instrucciones contenidas en el shellcode.
Para consultar todas las opciones y resolver errores de compilación o carga, queda disponible la guía completa de SideFinder.