Cuando examino la seguridad del software, uno de los puntos a verificar es trabajar con bibliotecas dinámicas. Los ataques como el DLL de secuestro ("dll spoofing" o "dll interception") son muy raros. Lo más probable es que esto se deba al hecho de que los desarrolladores de Windows agregan mecanismos de seguridad para evitar ataques, y los desarrolladores de software son más cuidadosos con la seguridad. Pero lo más interesante es la situación cuando el software objetivo es vulnerable.
Describiendo brevemente el ataque, la DLL de secuestro está creando una situación en la que algunos ejecutables intentan cargar el archivo dll, pero el atacante interviene en este proceso y, en lugar de la biblioteca esperada, se carga un archivo dll especialmente preparado con la carga del atacante. Como resultado, el código de la dll se ejecutará con los derechos de la aplicación iniciada, por lo tanto, las aplicaciones con derechos más altos generalmente se seleccionan como destino.
Para que la biblioteca se cargue correctamente, se deben cumplir varias condiciones: el tamaño de bits del archivo ejecutable y la biblioteca deben coincidir, y si la biblioteca se carga cuando se inicia la aplicación, el dll debe exportar todas las funciones que esta aplicación espera importar. A menudo, una importación no es suficiente; es muy deseable que la aplicación continúe su trabajo después de cargar el archivo DLL. Para esto, es necesario que las funciones de la biblioteca preparada funcionen igual que la original. La forma más fácil de hacerlo es simplemente pasando las llamadas de función de una biblioteca a otra. Estos son los dlls llamados proxy dlls.

Debajo del corte habrá varias opciones para crear tales bibliotecas, tanto en forma de código como de utilidades.
Una pequeña revisión teórica.
Las bibliotecas se cargan con mayor frecuencia utilizando la función LoadLibrary, a la que se pasa el nombre de la biblioteca. Si en lugar del nombre pasa la ruta completa, la aplicación intentará cargar la biblioteca especificada. Por ejemplo, llamar a LoadLibrary ("C: \ Windows \ system32 \ version.dll") cargará el dll especificado. O, si la biblioteca no existe, no se cargará.
Un poco de tedioSi ya hay algún archivo DLL cargado en la aplicación, no se volverá a cargar. Dado que version.dll se carga al comienzo de casi cualquier archivo exe, de hecho, la llamada anterior en realidad no cargará nada. Pero aún consideramos el caso general, consideremos el ejemplo como una llamada a alguna biblioteca abstracta.
Es muy diferente si escribe LoadLibrary ("version.dll"). En una situación normal, el resultado será exactamente el mismo que en el caso anterior: se cargará C: \ Windows \ system32 \ version.dll, pero no tan simple.
Primero, se buscará una biblioteca, que irá en el siguiente
orden :
- Carpeta ejecutable
- Carpeta C: \ Windows \ System32
- Carpeta C: \ Windows \ Sistema
- Carpeta C: \ Windows
- La carpeta establecida como actual para la aplicación
- Carpetas de la variable de entorno PATH
Un poco más de tedioAl iniciar aplicaciones de 32 bits en un sistema de 64 bits, todas las llamadas a C: \ Windows \ system32 se reenviarán a C: \ Windows \ SysWOW64. Esto es solo por la precisión de la descripción, desde el punto de vista del atacante, la diferencia no es particularmente importante.
Cuando ejecuta el archivo exe, el sistema operativo carga todas las bibliotecas de la sección de importación de archivos. En un sentido general, podemos suponer que el sistema operativo obliga al archivo a llamar a LoadLibrary, pasando todos los nombres de biblioteca que están escritos en la sección de importación. Como en el 99.9% de los casos hay nombres y no rutas, cuando se inicia la aplicación, se buscarán en el sistema todas las bibliotecas cargadas.
De la lista de ubicaciones de búsqueda de dll, dos puntos son realmente importantes para nosotros: 1 y 6. Si colocamos version.dll en la misma carpeta desde donde se inicia el archivo, entonces, en lugar del sistema, se cargará el cargado. Esta situación casi nunca se encuentra, porque si existe la oportunidad de colocar una biblioteca, lo más probable es que sea posible reemplazar el archivo ejecutable. Pero aún así, tales situaciones son posibles. Por ejemplo, si el archivo ejecutable se encuentra en una carpeta de escritura y es un servicio con inicio automático, no se puede cambiar mientras el servicio se está ejecutando. O bien, el archivo iniciado se verifica externamente mediante una suma de comprobación antes de comenzar, luego reemplazar el archivo aún no es una opción. Pero poner la biblioteca al lado será bastante real.
Es posible que no pueda crear archivos junto a los archivos ejecutables, pero puede crear carpetas. En esta situación, el mecanismo de redireccionamiento WinSxS (también conocido como "DotLocal") puede funcionar.
Brevemente sobre DotLocalEl manifiesto del archivo puede contener una dependencia en la biblioteca de una versión específica. En este caso, al comienzo del archivo ejecutable (por ejemplo, que sea application.exe), el sistema operativo verificará la existencia de una carpeta llamada application.exe.local en la misma carpeta que el archivo mismo. Esta carpeta debe tener una subcarpeta con un nombre complejo como amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b dentro del cual ya hay una biblioteca comctl32.dll. El nombre de la biblioteca y la información para el nombre de la carpeta deben indicarse en el manifiesto, este es solo un ejemplo del primer proceso que se encontró. Si no hay carpetas o archivos, la biblioteca se tomará de C: \ Windows \ WinSxS. En el ejemplo, C: \ Windows \ WinSxS \ amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b \ comctl32.dll.
Pero esta es más la excepción que la regla. Pero las situaciones en las que la búsqueda de dll alcanza el sexto número en la lista son bastante reales. Si la aplicación intenta cargar un archivo dll que no está en el sistema o al lado del archivo, todas las búsquedas subirán a 6 puntos, que podrían ser carpetas con capacidad de escritura.
Por ejemplo, una instalación típica de Python ocurre con mayor frecuencia en la carpeta C: \ Python (o cerrar). El propio instalador de Python sugiere agregar sus carpetas a la variable del sistema PATH. Como resultado, tenemos un buen trampolín para comenzar un ataque: todos los usuarios pueden escribir en la carpeta y cualquier intento de cargar una biblioteca inexistente irá a la ruta de búsqueda desde PATH.
Ahora que la teoría se ha completado, considere la creación de la carga útil: las bibliotecas proxy mismas.
La primera opción Biblioteca proxy honesta
Comencemos con uno relativamente simple: crearemos una biblioteca proxy honesta. La honestidad en este caso implica que todas las funciones en el dll se registrarán explícitamente, y para cada función se escribirá una llamada a la función con el mismo nombre de la biblioteca original. Trabajar con una biblioteca de este tipo será completamente transparente para el código llamado: si llama a alguna función, recibirá la respuesta correcta, el resultado y todo lo que debería suceder lado a lado.
Aquí hay un enlace al ejemplo terminado (
github ) de la biblioteca version.dll.
Aspectos destacados del código:
- Todos los prototipos de funciones de la tabla de exportación de la biblioteca original se describen honestamente.
- La biblioteca original se carga y todas las llamadas a nuestras funciones se lanzan a ella.
Convenientemente , la aplicación continúa funcionando correctamente, sin experimentar ningún "efecto especial".
Es inconveniente que tuviera que escribir un montón de código uniforme para cada una de las funciones, además, verificando cuidadosamente la coincidencia de los prototipos.
La segunda opción. Simplifica la escritura de código
Cuando se trata de una biblioteca como version.dll, donde la tabla de importación es pequeña, solo hay 17 funciones y los prototipos son simples, entonces una biblioteca proxy honesta es una buena opción.

Pero si el proxy de la biblioteca, por ejemplo, bcrypt, entonces todo es más complicado. Aquí está su tabla de importación:

57 características! Y aquí hay un par de ejemplos de prototipos:


Digamos que nada es imposible, pero hacer un proxy honesto para tal biblioteca no es muy agradable.
Puede simplificar el código si hace un poco de trampa con las funciones. Declararemos todas las funciones en la biblioteca como __declspec (desnudo), y en el cuerpo usaremos código ensamblador que simplemente haga jmp en la función de la biblioteca original. Esto nos permitirá no usar prototipos largos, sino poner anuncios simples en todas partes sin parámetros de visualización:
vacío foo ()
Cuando la aplicación llama a nuestra función, la biblioteca proxy no realizará ninguna manipulación con el registro y la pila, permitiendo que la función original haga todo el trabajo como debería.
Un ejemplo (
github ) de la biblioteca version.dll con este enfoque.
Destacados:
- La biblioteca original se carga y todas las llamadas a nuestras funciones se lanzan a ella. Los cuerpos de funciones y la carga están envueltos en macros.
Funcionamiento
conveniente y correcto de la aplicación y el hecho de que incluso una gran cantidad de funciones se describen fácilmente, gracias a las macros.
Es inconveniente ese rake bastante inesperado en x64. Visual Studio (en algún lugar desde 2012, si no recuerdo mal) prohíbe el uso de insertos desnudos y asm en código de 64 bits. Al escribir un proxy desde cero, es necesario que cada función verifique que se describe en el archivo def, que el original está cargado y que se describe el cuerpo de la función.
La tercera opción. Tiramos el cuerpo en general
Usar desnudo sugiere una opción más. Puede crear una tabla de importación, que para todas las funciones se referirá a una línea de código real:
vacío nop () {}
Dicha biblioteca será cargada por la aplicación, pero no funcionará. Al llamar a cualquiera de las funciones, lo más probable es que la pila se rompa o se produzca algún otro problema. Pero esto no siempre es malo: si, por ejemplo, el objetivo de una inyección dll es simplemente ejecutar el código con los derechos necesarios, entonces es suficiente ejecutar la carga desde la biblioteca proxy DllMain y finalizar la aplicación de forma silenciosa de inmediato. En este caso, no se realizará una llamada real a las funciones, y no habrá bloqueos por error.
Un ejemplo en un
github , nuevamente para version.dll.
Aspectos destacados del código:
- Todas las funciones del archivo def se refieren a una función nop.
Convenientemente, dicha biblioteca proxy se escribe solo por un par de minutos.
Es inconveniente que la aplicación llamada deje de funcionar.
La cuarta opción. Tome utilidades ya hechas
Escribir un archivo DLL es bueno, pero no siempre es conveniente y no es muy rápido, por lo que debe considerar las opciones automatizadas.
Puede seguir el camino de los virus antiguos: tome la biblioteca cuyos servidores proxy queremos crear, cree una sección de código ejecutable, escriba la carga útil allí y cambie el punto de entrada a esta sección. No es la forma más fácil, porque accidentalmente puede romper algo, debe escribir en ensamblador, recuerde el dispositivo del archivo PE. Este no es nuestro camino.
Para operar el secuestro de dll agregaremos otro secuestro de dll.

Esto es relativamente fácil de hacer. Copiamos la biblioteca cuyo proxy queremos hacer y agregamos algunos dll con una función arbitraria a la tabla de importación de esta copia. Ahora la descarga irá a lo largo de la cadena: al comienzo del archivo ejecutable se cargará el dll proxy, que cargará la biblioteca especificada.
“Oye, reemplazaste cargar una biblioteca con otra. Cual es el punto? De todos modos, será necesario codificar dll! ". Todo es correcto, pero todavía hay un sentido. Ahora habrá menos requisitos para una biblioteca con una carga útil. Puede especificar cualquier nombre, lo principal es exportar solo una función, que puede tener cualquier prototipo. Ingrese el nombre principal de la biblioteca y la función en la tabla de importación.
Una biblioteca con una carga útil puede ser una para todas las ocasiones.
Puede modificar la tabla de importación con muchos editores de PE, por ejemplo, CFF explorer o pe-bear. Para mí, escribí una pequeña utilidad en C # que corrige una tabla sin gestos innecesarios. Fuentes en
github , binar en la sección
Release .
Conclusión
En el artículo, intenté revelar los métodos básicos para crear proxy dll, que usé yo mismo. Solo queda decir cómo defenderse.
No hay muchas recomendaciones universales:
- No almacene archivos ejecutables, especialmente aquellos ejecutados con permisos elevados, en carpetas que los usuarios puedan escribir.
- Es mejor encontrar primero y verificar la existencia de la biblioteca antes de hacer LoadLibrary.
- Mire los métodos de protección existentes disponibles en el sistema operativo. Por ejemplo, en Windows 10, puede establecer el indicador PreferSystem32 para que la búsqueda de dll no comience con la carpeta del archivo ejecutable, sino con system32.
Gracias por su atención, me complacerá escuchar preguntas, sugerencias, sugerencias y comentarios.
UPD: Siguiendo el consejo de los comentaristas, le recuerdo que debe elegir una biblioteca cuidadosa y cuidadosamente. Si la biblioteca está incluida en la lista de KnownDlls o el nombre es similar a MinWin (ApiSetSchema, api-ms-win-core-console-l1-1-0.dll, eso es todo), lo más probable es que no sea posible interceptarlo debido a las características de procesamiento tales dlls en el sistema operativo.