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
DOS Stub
PE Header
Sección Header
Sección 1
Sección 2
. . .
Sección #
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,_
e_magic,_ ' Número mágico.
e_cblp,_ ' Bytes en la última página del archivo.
e_cp,_ ' Paginas en el archivo.
e_crlc,_ ' Relocalizaciones.
e_cparhdr,_ ' Tamaño del encabezado en el apartado.
e_minalloc,_ ' Tamaño extra mínimo del apartado.
e_maxalloc,_ ' Tamaño extra máximo del apartado.
e_s,_ ' Valor SS inicial (relativo).
e_sp,_ ' Valor inicial SP.
e_csum,_ ' Checksum.
e_ip,_ ' Valor inicial IP.
e_cs,_ ' Valor CS inicial (relativo).
e_lfarlc,_ ' Dirección en el archivo de la tabla de relocaciones.
e_ovno,_ ' Número de superposición.
e_res[4],_ ' Reservados.
e_oemid,_ ' Identificador OEM (para e_oeminfo).
e_oeminfo,_ ' Información OEM.
e_res2[10]:Word,_ ' Reservado.
e_lfanew:Entero ' Dirección en el archivo de la nueva cabecera.
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,_
Signature:Entero,_
FileHeader:IMAGE_FILE_HEADER,_
OptionalHeader:IMAGE_OPTIONAL_HEADER
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,_
Machine,_
NumberOfSections:Word,_
TimeDateStamp,_
PointerToSymbolTable,_
NumberOfSymbols:Entero,_
SizeOfOptionalHeader,_
Characteristics:Word
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,_
Magic:Word,_ ' 0
MajorLinkerVersion,_ ' 2
MinorLinkerVersion:Byte,_ ' 3
SizeOfCode,_ ' 4
SizeOfInitializedData,_ ' 8
SizeOfUnitializedData,_ ' 12
AddressOfEntryPoint,_ ' 16
BaseOfCode,_ ' 20
BaseOfData,_ ' 24
ImageBase,_ ' 28
SectionAlignment,_ ' 32
FileAlignment:Entero,_ ' 36
MajorOperatingSystemVersion,_ ' 40
MinorOperatingSystemVersion,_ ' 42
MajorImageVersion,_ ' 44
MinorImageVersion,_ ' 46
MajorSubsystemVersion,_ ' 48
MinorSubsystemVersion:Word,_ ' 50
W32VersionValue,_ ' 52
SizeOfImage,_ ' 56
SizeOfHeaders,_ ' 60
CheckSum:Entero,_ ' 64
SubSystem,_ ' 68
DllCharacteristics:Word,_ ' 70
SizeOfStackReserve,_ ' 72
SizeOfStackCommit,_ ' 76
SizeOfHeapReserve,_ ' 80
SizeOfHeapCommit,_ ' 84
LoaderFlags,_ ' 88
NumberOfRvaAndSizes:Entero,_ ' 92
DataDirectory[16]:IMAGE_DATA_DIRECTORY ' 96
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.
Indice- | Descripción |
0 | Funciones exportadas |
1 | Funciones importadas |
2 | Recursos (resources) |
3 | Información de excepciones |
4 | Información de seguridad |
5 | Tabla de reubicación base |
6 | Información de depuración |
7 | Datos de arquitectura especifica |
8 | Puntero global |
9 | Thread local storage |
10 | Configuración de carga |
11 | Importaciones vinculadas |
12 | Tabla de direcciones de importación |
13 | Delay load imports |
14 | COM runtime descriptor |
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,_
PhysicalAddress,_
VirtualSize:Entero
Estruct IMAGE_SECTION_HEADER,_
Name[8]:Byte,_
Misc:Misc,_
VirtualAddress,_
SizeOfRawData,_
PointerToRawData,_
PointerToRelocations,_
PointerToLinenumbers:Entero,_
NumberOfRelocations,_
NumberOfLinenumbers:Word,_
Characteristics:Entero
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:
- Abre el archivo dado y analiza las cabeceras DOS y PE.
- Trata de almacenar 'PEHeader.OptionalHeader.SizeOfImage' bytes en la posición 'PEHeader.OptionalHeader.ImageBase'.
- Analiza las secciones de cabecera y copia cada sección a sus respectivas secciones de memoria. La dirección de destino de cada sección, relativa a la base del bloque de memoria asignado, se almacena en el miembro 'VirtualAddress' de la estructura IMAGE_SECTION_HEADER.
- Si el bloque de memoria alojado difiere del miembro 'ImageBase', varias referencias en las secciones de código y/o datos deben ser ajustadas. Esto es llamado reubicación base (base relocation).
- Las importaciones necesarias para la DLL deben resolverse cargando las librerías correspondientes.
- Las regiones de memoria de cada sección deben ser protegidas dependiendo de las características de la sección. Muchas secciones son marcadas como descartables y por lo tanto se pueden liberar a partir de este punto. Estas secciones normalmente contienen información temporal que solo es requerida durante la importación. Como la información para la reubicación base.
- Nuestra librería esta cargada completamente, Se debe notificar sobre esto llamando al punto de entrada (Entry Point) usando la bandera (flag) DLL_PROCESS_ATTACH.
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
Mem = VirtualAlloc(PEHeader.OptionalHeader.ImageBase,PEHeader.OptionalHeader.SizeOfImage,MEM_RESERVE,PAGE_READWRITE)
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
Destino = VirtualAlloc(DirecciónBase + Sección.VirtualAddress,Sección.SizeOfRawData,MEM_COMMIT,PAGE_READWRITE)
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,_
VirtualAddress,_
SizeOfBlock:Entero
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,_
OriginalFirstThunk,_
TimeDateStamp,_
ForwarderChain,_
Name,_
FirstThunk:Entero
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
NameRef@ = BaseAddress + ImportDes.OriginalFirstThunk
SymbolRef@ = BaseAddress + ImportDes.FirstThunk
Mientras NameRef <> 0
Var @ThunkData:IMAGE_IMPORT_BY_NAME
ThunkData@ = CodeBase + NameRef
SymbolRef = GetProcAddress(Handle,CadDePtr(ThunkData.Name))
NameRef@ = NameRef@@+4
SymbolRef@ = SymbolRef@@+4
FinMientras
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
- PAGE_NOACCESS (&1)
- PAGE_WRITECOPY 8
- PAGE_READONLY (&2)
- PAGE_READWRITE (&4)
- PAGE_EXECUTE (&10)
- PAGE_EXECUTE_WRITECOPY (&80)
- PAGE_EXECUTE_READ (&20)
- PAGE_EXECUTE_READWRITE (&40)
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
Var Entry:EntryPoint
Entry@ = BaseAddress + PEHeader.OptionalHeader.AddressOfEntryPoint
Entry(BaseAddress,DLL_PROCESS_ATTACH,0)
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,_
Characteristics,_
TimeDateStamp:Entero,_
MajorVersion,_
MinorVersion:Word,_
Name,_
Base,_
NumberOfFunctions,_
NumberOfNames,_
AddressOfFunctions,_ ' RVA de Base.
AddressOfNames,_ ' RVA de Base.
AddressOfNameOrdinals:Entero ' RVA de Base.
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:
- Llamar al punto de entrada para informar que la vamos a liberar.
Código:
Entry(BaseAddress,DLL_PROCESS_DETACH,0)
- Liberar las librerías requeridas por la DLL que queríamos importar en realidad.
- Liberar la memoria alojada por VirtualAlloc.
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á.
El código de ejemplo puede ser visto en PasteBin.
¡Saludos!