
ExportHider: Genera la Tabla de Exportaciones en tiempo de ejecución para ocultar las funciones exportadas del archivo DLL.
ExportHider genera una plantilla de DLL en C++ que contiene un stub de código que te permite ocultar Funciones Exportadas del Directorio de Exportación de la DLL en el sistema de archivos. Después de colocar las definiciones de funciones y compilar el archivo, no verás las funciones de exportación ocultas a través de visores de archivos PE como CFF Explorer. Sin embargo, dado que el stub de código en la plantilla recrea el Directorio de Exportación durante el tiempo de ejecución, las llamadas legítimas a GetProcAddress se ejecutarán con éxito. Este método solo funciona para la carga dinámica de DLL o casos de cargadores de DLL personalizados.
Normalmente, cuando quieres definir una Función Exportada en los archivos DLL (en C o C++), simplemente colocas la palabra clave __declspec(dllexport) antes del nombre de la función, o creas un archivo .def. Después de la compilación, el compilador crea una tabla específica llamada Directorio de Exportación para almacenar la información relacionada con las Funciones Exportadas. La estructura del Directorio de Exportación se puede ver a continuación:
Cuando un proceso quiere usar una función de un archivo DLL, el Cargador de Windows simplemente analiza esta estructura e importa las funciones solicitadas usando los arreglos AddressOfFunctions, AddressOfNames, AddressOfNameOrdinals.
La rutina específica para importar una función se explica en detalle en la publicación del blog de ferreirasc, pero en resumen, para una función importada por nombre, el Cargador itera el arreglo AddressOfNames (los valores de este arreglo son solo valores RVA) y busca el nombre dado. Una vez que el cargador encuentra una coincidencia en la posición 'i', se referirá al índice i-ésimo del arreglo AddressOfNameOrdinals y obtendrá el ordinal asociado con esta función. Teniendo el ordinal, el cargador se referirá a AddressOfFunctions en la posición del valor ordinal para finalmente obtener el RVA asociado con la función importada.
El punto crucial aquí es que todas estas operaciones de búsqueda y acceso por parte del Cargador de Windows, cuando se llama a LoadLibrary, se realizan después de que la DLL se asigna al espacio de direcciones del proceso. Durante la asignación de la DLL, todo el archivo DLL, incluidos sus encabezados PE, se escribe en la memoria, y el Cargador analiza los encabezados en la memoria para llegar al Directorio de Exportación. Esto significa que si la DLL misma es capaz de sobrescribir sus encabezados PE en la memoria para cambiar la Dirección del Directorio de Exportación (simplemente el índice 0 del DataDirectory) después de adjuntarse al proceso, el cargador puede buscar funciones para importar en un Directorio de Exportación arbitrario en lugar del Directorio de Exportación agregado por el compilador.
__ _ _ _
/__\_ ___ __ ___ _ __| |_ /\ /(_) __| | ___ _ __
/_\ \ \/ / '_ \ / _ \| '__| __|/ /_/ / |/ _` |/ _ \ '__|
//__ > <| |_) | (_) | | | |_/ __ /| | (_| | __/ |
\__/ /_/\_\ .__/ \___/|_| \__\/ /_/ |_|\__,_|\___|_|
|_|
by @R0h1rr1m
Usage of C:\Users\Public\DLLDemo\ExportHider.exe:
-h | --help Show the help message.
-i | --input <Input Path> Input path for the list of function names to be hidden. (Mandatory)
-o | --output <Output Path> Output path for the DLL template. (Mandatory)
-n | --name <DLL Name> Name of the DLL for the Export Directory. (Mandatory)
-c | --count <Number of Other Functions> Number of other exported functions that won't be hidden.
Con respecto al parámetro -i | --input <Input Path>, debes especificar una ruta para el archivo de entrada que almacena los nombres de las funciones a ocultar línea por línea. El contenido de un archivo de entrada de ejemplo sería:
TestFunction1
TestFunction2
TestFunction3
Con respecto al parámetro -c | --count, si no quieres ocultar todas tus funciones exportadas (es decir, hay algunas funciones exportadas con __declspec(dllexport) o archivo .def, y aparecen en el Directorio de Exportación del archivo DLL en el sistema de archivos a través de visores de archivos PE), simplemente especifica cuántas hay usando este parámetro porque la herramienta necesita esta información al realizar cálculos de memoria.
Si quieres jugar con la técnica, hay dos puntos interesantes que encontré durante el desarrollo del proyecto. Es posible que necesites conocerlos antes de modificar el proyecto:
GetProcAddress con un nombre. Debido a eso, los nombres de todas las funciones exportadas, incluidas las ocultas, deben estar ordenados en el arreglo AddressOfNames. De lo contrario, la función GetProcAddress devuelve NULL. Por lo tanto, utilicé el algoritmo de Ordenamiento Burbuja para ordenar los miembros de este arreglo.AddressOfNames, el arreglo AddressOfFunctions, el arreglo DataDirectory y algunos otros campos requieren valores en Direcciones Virtuales Relativas (RVAs). Además de eso, almacenan estos valores RVA en campos del tamaño de DWORD. Cuando asignas una región de memoria para un nuevo directorio de exportación arbitrario utilizando Funciones de Asignación de Memoria Dinámica como VirtualAlloc o HeapAlloc, las direcciones dadas estarán lejos de la región mapeada de la DLL, y los valores RVA no caben en los campos de tamaño DWORD, y te encuentras con un desbordamiento de enteros. Por eso utilicé variables globales con el tipo arreglo de bytes en la plantilla de DLL para los requisitos de memoria.Mi primer objetivo al crear este proyecto era generar una DLL que tuviera exportaciones faltantes (o una falta completa de una tabla de exportación) pero que aún así se cargara con éxito por el proceso recién creado. Pensé que ese comportamiento podría traer un nuevo campo de juego para las cargas de DLL sideloading. Sin embargo, no pude encontrar ninguna función o manera de que el stub de reparación del directorio de exportación se ejecute antes de que el Cargador de Windows verifique las funciones exportadas de la DLL.

Más técnicamente, noté el siguiente flujo y llamada de función para cada DLL en las secciones de código relacionadas con el Cargador de Windows en NTDLL (recopilado de mis notas antiguas; puede haber algunos errores porque no soy un ingeniero inverso experto):
1. LdrpMapDll - This is where the DLL is mapped to the Process Address Space. All DLLs that satisfy the DLL name condition
are directly put into the memory, no precheck control for the filesystem version.
2. LdrpSnapModule - This is where the Windows Loader starts to resolve imports. For each import descriptor, it parses the PE
structure, checks the export table, binary searching for the imported function, calculating RVA of that,
and writes its address to the corresponding caller's process' Import Address Table entry during this function.
3. LdrpDoPostSnapWork - If step 2 succeeds for each imported function, memory protections, TLS initialization, CFG enablement
are done in this function.
4. LdrpInitializeNode - If step 3 succeeds, there are module linking functions in this step.
5. LdrpCallTlsInitializers - This is where the TLS callbacks are called before the DllMain function.
6. LdrpCallInitRoutine - This is where the DLLMain itself is called for the first time for the imported DLL. In the original
solution, this function is too late to fix the export table.
Cuando ejecutas un ejecutable que importa una DLL, si el cargador de Windows no puede encontrar el nombre de la función requerida en la tabla de exportación de la DLL, detiene la ejecución y no ejecuta las funciones que se ejecutan después de LdrpSnapModule.
Para el caso de DLL sideloading, no podemos modificar el proceso llamante; por lo tanto, la única oportunidad de arreglar la Tabla de Exportación dinámicamente es encontrando una oportunidad de ejecución de código entre las funciones LdrpMapDll y LdrpSnapModule porque el Cargador se detiene inmediatamente durante las comprobaciones de la función LdrpSnapModule. He probado callbacks TLS, cargas de segunda DLL, exportaciones reenviadas y algunas otras soluciones, pero ninguna me ayudó a encontrar tal lugar, así que desafortunadamente, este método no funciona directamente para DLL sideloading o DLLs importadas estáticamente. Si descubres una solución o una solución viable para este problema, estaré muy feliz de explorarlo más a fondo — ya sea discutiendo la idea, pensando juntos en el enfoque, o implementándolo. Cualquier contribución en esta dirección será muy apreciada.
Una posible forma de utilizar este método para casos de DLL Sideloading es dividir todo el trabajo en dos DLLs, a saber, la DLL Proxy y la DLL Payload. La DLL Proxy toma el nombre que el EXE espera, y tiene exportaciones visibles que satisfacen la verificación de importación estática del cargador, mientras que la DLL Payload contiene la funcionalidad oculta real, tiene exportaciones faltantes y reconstruye su tabla de exportación en DllMain. No creo que esta sea una buena solución para este problema, así que no la implementé.
Solo para pruebas de seguridad autorizadas. El mal uso de esta herramienta contra sistemas sin permiso explícito es ilegal.