
Un proyecto PoC para incrustar shellcode en la Tabla Hint/Name
HintInject es un insertador y cargador de shellcode que desarrollé mientras jugaba con las estructuras del archivo PE y la Tabla de Directorios de Importación. Toma un archivo de shellcode sin procesar y coloca el shellcode en fragmentos dentro de las entradas de la Tabla de Nombres/Pistas (Hint/Name Table) que son accesibles a través de la Tabla de Búsqueda de Importaciones (Import Lookup Table) de una entrada DLL falsa en el ejecutable cargador. Luego, el cargador fusiona estos fragmentos para ejecutar el shellcode. No sé si es una técnica novedosa o no, pero quería compartirla por diversión.
Antes de explicar cómo funciona HintInject, quiero mencionar brevemente el proceso de carga de DLL haciendo referencia a una publicación en Stack Overflow.
Para obtener la información sobre las DLL necesarias y las funciones/importaciones, primero se debe mirar la Tabla de Directorios de Importación (Import Directory Table). La Tabla de Directorios de Importación es una tabla de entradas, una entrada por cada DLL importada. Estas entradas contienen un puntero al nombre de la DLL importada, un puntero a la Tabla de Búsqueda de Importaciones (Import Lookup Table), un puntero a la Tabla de Direcciones de Importación (Import Address Table) y otros campos para información diversa.
En pocas palabras, la Tabla de Búsqueda de Importaciones conduce a la información sobre las importaciones, y la Tabla de Direcciones de Importación conduce a las direcciones de las importaciones. Sin embargo, cuando el ejecutable está en el disco, o justo antes del proceso de carga de DLL, la Tabla de Direcciones de Importación es idéntica a la de la Tabla de Búsqueda de Importaciones. El contenido de la Tabla de Direcciones de Importación se sobrescribe con la dirección de las importaciones durante el proceso de carga de DLL.
Sin entrar en demasiados detalles, podemos echar un vistazo al diagrama siguiente para ver la relación entre estas tres tablas.
Si profundizamos un poco más, la Tabla de Búsqueda de Importaciones no contiene directamente los nombres de las importaciones. Para las funciones que se importan por nombre, contiene RVA de las entradas de la Tabla de Nombres/Pistas (Hint/Name Table). Estas entradas almacenan los nombres de las funciones como cadenas ASCII terminadas en nulo. Por lo tanto, la definición de estructura de una entrada de la Tabla de Nombres/Pistas es la siguiente:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
El campo Hint aquí es en realidad un índice dentro de la tabla de punteros a nombres de exportación de la DLL. Se utiliza para acelerar la búsqueda de la posición de esa importación. Por lo tanto, podemos resumir la relación entre la Tabla de Búsqueda de Importaciones y la tabla de Nombres/Pistas con el diagrama siguiente.
Para concluir, para encontrar una DLL y sus importaciones, se deben seguir estos pasos:
Recientemente, mientras examinaba la técnica ImportDLLInjection compartida por x86matthew, vi cómo agregar una entrada falsa al Directorio de Importación de DLL cargadas en memoria. Posteriormente, quería desarrollar un pequeño proyecto que agregara una entrada falsa directamente a la tabla de importación de un binario en disco, tanto por diversión como para refrescar mis conocimientos. Mientras revisaba el proceso de carga de DLL para desarrollar este proyecto, el campo Hint en las entradas de la Tabla de Nombres/Pistas utilizadas en este proceso llamó mi atención.
Según el documento de MSDN, aprendí que este campo es utilizado por el Cargador de Windows para encontrar la dirección de esa importación, que se importa por nombre, directamente desde la tabla de nombres de exportación de la DLL en la que se encuentra. Sin embargo, en el mismo documento, se indicaba que si la función no se puede encontrar usando este campo, se buscará mediante una operación de búsqueda binaria en la tabla de nombres de exportación de la DLL. Basándome en esta frase, pensé que poner un valor incorrecto para este campo no interrumpiría el proceso de carga de DLL.
Como mencioné anteriormente, hay una entrada en la tabla de Nombres/Pistas correspondiente a cada función a importar. En otras palabras, tenemos 2 bytes para usar por cada importación. Combinando múltiples importaciones, se pueden obtener suficientes campos Hint para almacenar shellcodes maliciosos. Como resultado, se puede usar un binario cargador para incrustar el shellcode en las entradas de Nombres/Pistas de la entrada DLL falsa. Ese binario cargador puede acceder a estas entradas para fusionar el shellcode que se ejecutará durante el tiempo de ejecución.
HintInject se puede utilizar para crear dicho cargador que contiene el shellcode en su tabla de Nombres/Pistas. Primero crea una nueva sección llamada .rrdata y copia el directorio de importación actual en esta sección. Después, añade una nueva entrada falsa cuyas importaciones se utilizarán para contener el shellcode de entrada. Los bytes restantes de la sección se utilizan para almacenar la Tabla de Búsqueda de Importaciones, la Tabla de Direcciones de Importación, el nombre de la DLL y la tabla de Nombres/Pistas de la nueva entrada falsa. Como último paso, HintInject utiliza los campos Hint de las importaciones para colocar fragmentos del shellcode de entrada.
Ejemplo de importaciones de un binario cargador:
HintInject.exe con su shellcode en formato sin procesar y una ruta de salida para crear un cargador de shellcode: HintInject.exe <Archivo Shellcode> <Nombre de Salida>Loader.exe <PID>InjectShellcode en el archivo HintInjectLoader/Main.cpp.Probado en Windows 10 19044 con Visual Studio 2019:
dllNames en el archivo DllNamesForFakeImports.h.dllNames en el archivo DllNamesForFakeImports.h. Por defecto, HintInject elegirá funciones de las DLL según el orden del arreglo. En otras palabras, la herramienta primero selecciona funciones exportadas de user32.dll; si el tamaño del shellcode es mayor que 2 * el número de exportaciones de user32.dll, también comienza a usar las exportaciones de advapi32.dll y otras.static LPCSTR dllNames[] = {"user32.dll","advapi32.dll","gdi32.dll","wininet.dll","comctl32.dll","shell32.dll","wsock32.dll","oleaut32.dll","ws2_32.dll","urlmon.dll"};
Solo para pruebas de seguridad autorizadas. El uso indebido de esta herramienta contra sistemas sin permiso explícito es ilegal.