Título: [Guía] Cargar DLL de memoria Publicado por: Yuki en 6 Agosto 2017, 05:10 am Información
Las funciones predeterminadas de la API de Windows para cargar bibliotecas externas en un programa (LoadLibrary, LoadLibraryEx) sólo funcionan con archivos del sistema. Por lo tanto, es imposible cargar un DLL de memoria. Pero a veces, necesitas exactamente esta funcionalidad (por ejemplo, si no quieres distribuir muchos archivos o quieres hacer el des-ensamblado más difícil). Soluciones comunes para estos problemas son escribir el DLL en un archivo temporal primero e importarlo desde ahí. Cuando el programa termina, el archivo temporal se elimina. En este tutorial, describiré primero cómo se estructuran los archivos DLL y se presentará código que se puede utilizar para cargar una DLL completamente desde la memoria, sin tocar el disco. Ejecutables de Windows - Formato PE La mayoría de los binarios de Windows que pueden contener código ejecutable (.exe, .dll, .sys) comparten un formato de archivo común que consta de las siguientes partes: Código: DOS Header Todas las estructuras que se dan a continuación se pueden encontrar en el archivo Estructuras.cml Cabecera DOS (DOS Header) - STUB El encabezado DOS sólo se utiliza para la compatibilidad con versiones anteriores. Precede al stub de DOS que normalmente sólo muestra un mensaje de error acerca de que el programa no se puede ejecutar desde el modo DOS. Microsoft define el encabezado DOS de la siguiente manera: Código: Estruct IMAGE_DOS_HEADER,_ Cabecera PE (PE Header) La cabecera PE contiene información acerca de las diferentes secciones dentro del ejecutable que son usados para almacenar códigos y datos o también para definir importaciones de otras librerías o exportaciones que esta proporciona. Esta se define como la siguiente estructura: Código: Estruct IMAGE_NT_HEADERS,_ El miembro 'FileHeader' describe el formato físico del archivo. Por ejemplo: contenidos, información sobre símbolos, etc. Código: Estruct IMAGE_FILE_HEADER,_ El 'OptionalHeader' contiene información sobre el formato lógico de la librería, incluyendo la versión requerida del SO, los requisitos de memoria y los puntos de entrada. Código: Estruct IMAGE_OPTIONAL_HEADER,_ El 'DataDirectory' contiene 16 (IMAGE_NUMBEROF_DIRECTORY_ENTRIES) entradas que definen los componentes lógicos de la librería: Citar Nota: Las descripciones en rojo no pudieron ser traducidas.
Para importar una DLL nosotros solo necesitamos las entradas que describen las importaciones y la tabla de reubicaciones. Para proporcionar acceso a las funciones exportadas, se requiere la entrada de las exportaciones. Cabecera de secciones (Section Header) La cabecera de secciones es almacenada después de la estructura 'OptionalHeader' en la cabecera PE. Si usted usa C. Microsoft le provee la macro 'IMAGE_FIRST_SECTION' para obtener la dirección de inicio basado en la cabecera PE. Actualmente, la cabecera de secciones (Section Header) es una lista de información acerca de cada sección en el archivo. Código: Unión Misc,_ Una sección puede contener código, datos, información de reubicaciones, recursos, definiciones de importación/exportación, etc. Cargando la librería Para emular la carga PE, nosotros primero debemos entender cuales son los pasos necesarios para cargar un archivo en la memoria y preparar las estructuras para que puedan ser llamadas por otros programas. Al invocar a la API LoadLibrary, básicamente, Windows realiza estas tareas:
En los siguientes párrafos, cada paso es descrito. Almacenando memoria Toda la memoria requerida por la libreria debe ser reservada/alojada usando VirtualAlloc, como Windows provee funciones para proteger estos bloques de memoria. Esto es requerido para restringir el acceso a la memoria, como bloquear el acceso a escritura del código o datos constantes. La estructura 'OptionalHeader' define el tamaño del bloque de memoria requerido por la librería. Si es posible, esta debe ser almacenado en la dirección especificada por 'ImageBase'. Citar Nota: En el siguiente mini-ejemplo se asume que usted hizo apuntar 'PEHeader' a la librería en memoria. Código: Var Mem:Entero Si la memoria reservada difiere de la dirección dada en 'ImageBase', se debe realizar la reubicación de base como se describe mas adelante. Copiar secciones Una vez que la memoria ha sido reservada, el contenido del archivo debe ser copiado al sistema. Las secciones de cabecera (Section Headers) deben ser evaluadas para determinar la posición en el archivo y el área de destino en la memoria. Antes de copiar los datos, el bloque de memoria debe estar comprometido. Citar Nota: Se aloja nueva memoria con la bandera 'MEM_COMMIT'. Código: Var Destino:Entero Secciones sin datos en el archivo (como secciones de datos para ser usadas como variables) tienen un 'SizeOfRawData' de 0. Entonces puedes usar la 'SizeOfInitializedData' o 'SizeOfUninitializedData' de 'OptionalHeader'. Cada uno debe ser elegido dependiendo de las banderas de bits 'IMAGE_SCN_CNT_INITIALIZED_DATA' e 'IMAGE_SCN_CNT_UNINITIALIZED_DATA' que se pueden establecer en las características de la sección (miembro 'Characteristics'). Reubicación base Todas las direcciones de memoria en las secciones de código/datos de una libreria son almacenadas relativamente a la dirección definida por 'ImageBase' de 'OptionalHeader'. Si la librería no puede ser importada de esta dirección de memoria, las referencias deben ser re-ajustadas (reubicadas). El formato de archivo ayuda a esto almacenando información sobre todas estas referencias en la tabla de reubicación de base, la cual puede ser encontrada en el directorio 5 de la 'DataDirectory' en la 'OptionalHeader'. Esta tabla consta de una serie de esta estructura: Código: Estruct IMAGE_BASE_RELOCATION,_ Contiene (SizeOfBlock - IMAGE_SIZEOF_BASE_RELOCATION) / 2 entradas de 16 bits cada una. Los 4 bits superiores definen el tipo de reubicación, los 12 bits inferiores definen el desplazamiento en relación con el VirtualAddress. Los únicos tipos que pueden ser usados en librerías son: IMAGE_REL_BASED_ABSOLUTE (0) Ninguna operación de reubicación. IMAGE_REL_BASED_HIGHLOW (3) Agregue el delta entre ImageBase y el bloque de memoria asignado a los 32 bits encontrados en el offset. Resolviendo importaciones La entrada de directorio 1 del 'DataDirectory' en 'OptionalHeader' especifica una lista de bibliotecas para importar símbolos. Cada entrada en esta lista se define de la siguiente manera: Código: Estruct IMAGE_IMPORT_DESCRIPTOR,_ El miembro 'Name' describe el offset a una cadena terminada en nulo con el nombre de la librería (ej: KERNEL32.DLL). El miembro 'OriginalFirstThunk' apunta a una lista de referencias a los nombres de las funciones a importar de la librería externa. 'FirstThunk' apunta a una lista de direcciones que se llenó con punteros a los símbolos importados. Cuando nosotros resolvemos las importaciones, nosotros recorremos ambas listas en paralelo, importando la función definida en la primera lista y almacenando el puntero en los símbolos de la segunda lista. Código: Var @NameRef,@SymbolRef:Entero Protegiendo la memoria Cada sección especifica los indicadores de permiso en su entrada de características. Estas banderas pueden ser una o una combinación de IMAGE_SCN_MEM_EXECUTE (&20000000) Esta sección contiene datos que pueden ser ejecutados. IMAGE_SCN_MEM_READ (&40000000) Esta sección contiene datos que solo pueden ser leídos. IMAGE_SCN_MEM_WRITE (&80000000) Esta sección contiene datos que pueden ser escritos. Estos indicadores deben asignarse a las banderas de protección
Ahora, la función VirtualProtect puede ser usada para limitar el acceso a la memoria. Si el programa trata de escribir algo en un camino no autorizado, una excepción es generada por Windows. Además de los indicadores de sección anteriores, se puede agregar los siguientes: IMAGE_SCN_MEM_DISCARDABLE (&02000000) Los datos en esta sección pueden ser liberados después de importar. Usualmente esta es especificada por los datos de reubicación. IMAGE_SCN_MEM_NOT_CACHED (&04000000) Los datos de esta sección no deben ser almacenados en caché por Windows. Agregue el indicador de bits PAGE_NOCACHE a los indicadores de protección anteriores. Notificar librería La ultima cosa que hacemos es llamar el punto de entrada de la DLL (definido por 'AddressOfEntryPoint') y por lo tanto notificar a la biblioteca acerca de estar conectado a un proceso. La función de punto de entrada es definida como: Código: Prototipo EntryPoint(hInstance,dwReason,Reserved):Entero Entonces el código que nosotros debemos ejecutar es: Código: Prototipo EntryPoint(hInstance,dwReason,Reserved):Entero Después podemos utilizar las funciones exportadas como con cualquier biblioteca normal. Funciones exportadas Si usted quiere obtener acceso a las funciones que la librería exporta, necesita buscar el punto de entrada del simbolo, por ejemplo. El nombre de la función a llamar. El directorio de entrada 0 de la 'DataDirectory' en la 'OptionalHeader' contiene información acerca de las funciones exportadas. Esta es definida como la siguiente estructura: Código: Estruct IMAGE_EXPORT_DIRECTORY,_ Lo primero que debemos hacer es referenciar el nombre de la función al número ordinal del símbolo exportado. Por lo tanto, sólo sondear las matrices definidas por 'AddressOfNames' y 'AddressOfNameOrdinals' en paralelo hasta que encuentre el nombre necesario. Ahora puede utilizar el número ordinal para leer la dirección evaluando el n-ésimo elemento de la matriz 'AddressOfFunctions'. Liberando la libreria Para liberar la librería cargada de manera personalizada, realice los siguientes pasos:
Código: Entry(BaseAddress,DLL_PROCESS_DETACH,0)
Citar Este manual fue escrito por Joachim Bauch, traducido por Yuki para Underc0de y traído a ustedes por amor a la información libre. Código fuente de ejemplo (escrito en Cramel) El código fuente de la librería se puede encontrar acá (https://pastebin.com/PWNyNQkh). El código de ejemplo puede ser visto en PasteBin (https://pastebin.com/C9tY1sFB). ¡Saludos! |