INTRODUCCIÓN A LOS RECURSOS DEL ARCHIVO PE
¡Saludos, queridos lectores!
En este artículo, te contaremos sobre una parte importante e interesante de los archivos PE: PE IMAGE RESOURCES (datos vinculados al programa después de su compilación). Profundizaremos en las estructuras internas del árbol de recursos y escribiremos nuestro propio analizador de recursos nativo sin utilizar WinAPI.
Los recursos en archivos PE (Portable Executable) son datos integrados que forman una parte integral de la aplicación. Incluyen una variedad de elementos como imágenes, iconos, cadenas de texto, cuadros de diálogo, fuentes. Estos recursos están integrados directamente en el archivo ejecutable, asegurando su disponibilidad para la aplicación durante su ejecución. Es importante señalar que recursos como imágenes, textos y sonidos a menudo se agregan al programa después de que ha sido compilado. Este enfoque tiene varias ventajas significativas, haciendo el proceso de desarrollo más flexible y eficiente.
Imagina que formas parte de un equipo de desarrollo trabajando en la creación de una nueva aplicación. Tu equipo consta de programadores, diseñadores y gestores de contenido. Cada uno de ustedes contribuye a crear algo único y útil.
Al comienzo del proyecto, los programadores se centran en escribir y probar el código. Crean el marco de la aplicación, asegurándose de que todas las funciones funcionen correctamente. Al mismo tiempo, los diseñadores y gestores de contenido trabajan en la creación de recursos - imágenes, sonidos, textos para la interfaz. Este trabajo paralelo permite que el equipo avance rápidamente y de manera eficiente.
Cuando la parte principal de la programación está completada, es hora de integrar los recursos en la aplicación. Esto se realiza utilizando herramientas especiales que permiten añadir recursos a una aplicación ya compilada sin la necesidad de recompilar. Esto es muy conveniente, especialmente si necesitas hacer cambios o actualizar recursos - no hay necesidad de recompilar todo el proyecto.
Uno de los aspectos clave de este proceso es la localización. Gracias a la separación de recursos del código principal, localizar la aplicación se vuelve mucho más sencillo. Los textos de la interfaz y los mensajes de error pueden traducirse y reemplazarse fácilmente sin interferir con el código principal. Esto permite que la aplicación se adapte a diferentes mercados lingüísticos, haciéndola accesible y comprensible para una amplia gama de usuarios en todo el mundo.
EJEMPLOS DE USO DE RECURSOS
- Iconos de Aplicación: Iconos mostrados en el Explorador de Windows o en la barra de tareas que proporcionan una representación visual de la aplicación.
- Cuadros de Diálogo: Definiciones de los cuadros de diálogo que la aplicación muestra para la interacción del usuario, como ventanas de ajustes o avisos.
- Menús: Estructuras de menú utilizadas en la interfaz de usuario que proporcionan navegación y funcionalidad.
- Cadenas: Cadenas localizadas utilizadas para mostrar texto en la aplicación, incluyendo mensajes de error, tooltips y otras interfaces de usuario.
- Sonidos: Archivos de audio que pueden ser reproducidos por la aplicación en ciertas situaciones, como notificaciones sonoras.
- Cursosres: Cursores gráficos utilizados para interactuar con la interfaz de usuario, como flechas, punteros o cursores animados.
- Información de la Versión (Info de Versión): Contiene información sobre la versión de la aplicación, derechos de autor, nombre del producto y otros datos relacionados con la versión.
- Manifiesto: Un archivo XML que contiene información sobre la configuración de la aplicación, incluyendo requisitos de versión de Windows y configuraciones de seguridad.
- RC_DATA: Datos arbitrarios definidos por el desarrollador, que pueden incluir datos binarios, archivos de configuración u otros recursos específicos de la aplicación.
¿CÓMO VER Y EDITAR RECURSOS EN UN ARCHIVO EJECUTABLE PORTÁTIL?
¿Podemos ver y editar recursos en una aplicación compilada? ¡Absolutamente! Todo lo que necesitas es la herramienta adecuada. Hay herramientas que ofrecen una amplia gama de capacidades para trabajar con recursos en archivos ejecutables compilados, incluyendo visualización, edición, adición y eliminación de recursos.
Aquí tienes una lista de editores de recursos que se pueden usar para ver o editar recursos en archivos ejecutables ya compilados:
- Resource Hacker: Es un editor de recursos para aplicaciones de Windows de 32-bit y 64-bit. Te permite visualizar y editar recursos en archivos ejecutables (.exe, .dll, .scr, etc.) y bibliotecas de recursos compiladas (.res, .mui). Resource Hacker
- ResEdit: Un editor de recursos gratuito para programas Win32. Adecuado para trabajar con diálogos, iconos, información de versión y otros tipos de recursos. ResEdit
- Resource Tuner: Un editor de recursos que te permite abrir archivos ejecutables con problemas y editar datos ocultos que otros editores simplemente no ven. Resource Tuner
- Resource Builder: Un potente editor de recursos con todas las funciones para Windows. Te permite crear, editar y compilar archivos de recursos (.RC, .RES y otros), así como editar recursos en archivos ejecutables compilados. Resource Builder
- Visual Studio: Ofrece un editor de recursos que te permite añadir, eliminar y modificar recursos. Visual Studio
- Resource Tuner Console: Una poderosa herramienta de línea de comandos para editar recursos, ideal para usar en archivos de lote (.bat). Resource Tuner Console
- Qt Centre: Permite editar archivos de recursos de archivos ejecutables compilados utilizando Qt. Qt Centre
Echemos un vistazo más de cerca a la primera herramienta de la lista: Resource Hacker.
Esta herramienta no solo te permite ver y extraer recursos de un archivo ejecutable, ¡sino también editarlos!
Estos recursos, que pueden incluir íconos, menús, cuadros de diálogo y otros tipos de datos, se encuentran típicamente en una sección específica del archivo PE conocida como .rsrc (sección de recursos). Sin embargo, es importante señalar que esta no es una regla estricta y pueden ocurrir excepciones.
Un aspecto esencial para navegar y acceder a estos recursos dentro de un archivo PE es el IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES]. Esta entrada de directorio es parte de la cabecera opcional del archivo PE, específicamente dentro del arreglo de directorios de datos. Sirve como un puntero o referencia a los recursos en la imagen. El IMAGE_DIRECTORY_ENTRY_RESOURCES proporciona información sobre la ubicación (como la dirección virtual relativa) y el tamaño de los datos de recursos.
ESTRUCTURA DE RECURSOS EN ARCHIVOS EJECUTABLES PORTABLES
Visión General
Echemos un vistazo detallado a las estructuras utilizadas en la sección de recursos de un archivo PE (Portable Executable). La sección de recursos en los archivos PE de Windows tiene una estructura jerárquica de árbol de tres niveles única. Este árbol se utiliza para organizar y acceder a recursos como íconos, cursores, cadenas, diálogos y otros. Así es como está estructurado:
Nivel 1: Tipos de Recursos
En el nivel superior del árbol se encuentran los tipos de recursos. Cada tipo de recurso puede identificarse ya sea por un identificador numérico (ID) o por un nombre de cadena.
Nivel 2: Nombres de Recursos
En el segundo nivel, cada tipo de recurso tiene sus propios nombres o identificadores. Esto te permite tener múltiples recursos del mismo tipo, como múltiples íconos o filas.
Nivel 3: Idiomas de Recursos
En el tercer nivel, cada recurso tiene variantes para diferentes localizaciones de idiomas. Esto permite que el mismo recurso, como un diálogo, sea localizado en diferentes idiomas.
Estructuras de Datos
Las siguientes estructuras de datos se utilizan para representar esta jerarquía:
- IMAGE_RESOURCE_DIRECTORY: Esta estructura representa un encabezado para cada nivel del árbol y contiene información general sobre las entradas en ese nivel.
- IMAGE_RESOURCE_DIRECTORY_ENTRY: Estos son elementos que pueden ser subdirectorios (apuntando a otro IMAGE_RESOURCE_DIRECTORY) o las hojas finales del árbol, que apuntan a los datos del recurso real.
- IMAGE_RESOURCE_DATA_ENTRY: Esta estructura apunta a los datos del recurso en sí y contiene su tamaño y desplazamiento.
La visualización del árbol de recursos podría verse de la siguiente manera:
Root (IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES].VirtualAddress)
|
+-- Type (RT_ICON, RT_STRING, ...)
|
+-- Name (ID or String)
|
+-- Language (Locale ID)
|
+-- Data (Actual resource data)
IMAGE_RESOURCE_DIRECTORY
|
|-- IMAGE_RESOURCE_DIRECTORY_ENTRY (Resource Types)
| |-- IMAGE_RESOURCE_DIRECTORY (Resource names)
| | |-- IMAGE_RESOURCE_DIRECTORY_ENTRY (Names)
| | | |-- IMAGE_RESOURCE_DIRECTORY (Languages)
| | | | |-- IMAGE_RESOURCE_DIRECTORY_ENTRY (Languages)
| | | | | |-- IMAGE_RESOURCE_DATA_ENTRY (Resource data)
Cada nodo en este árbol representa un IMAGE_RESOURCE_DIRECTORY, y las hojas son IMAGE_RESOURCE_DATA_ENTRIES, que apuntan directamente a los datos del recurso. Cuando se analizan los recursos manualmente, un desarrollador debe recorrer este árbol, comenzando desde la raíz, y navegar secuencialmente todos los niveles para encontrar los datos necesarios.
IMAGE_RESOURCE_DIRECTORY
Esta estructura funciona como un encabezado para cada nivel del árbol de recursos y contiene información sobre las entradas en ese nivel.
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
- Characteristics: Generalmente no usadas y establecidas en 0.
- TimeDateStamp: La marca de tiempo de la creación del recurso.
- MajorVersion y MinorVersion: La versión del directorio de recursos.
- NumberOfNamedEntries: El número de entradas de recursos con nombres.
- NumberOfIdEntries: El número de entradas de recursos con identificadores numéricos.
IMAGE_RESOURCE_DIRECTORY_ENTRY
Elementos que pueden ser subdirectorios o las hojas finales del árbol.
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
- Name: Si NameIsString está establecido en 1, este campo contiene un desplazamiento que apunta a una cadena UNICODE que representa el nombre del recurso. Si NameIsString está establecido en 0, se utiliza el campo Id para identificar el recurso mediante un identificador numérico.
- OffsetToData: Si DataIsDirectory está establecido en 1, este campo contiene un desplazamiento que apunta a otro IMAGE_RESOURCE_DIRECTORY (es decir, un subdirectorio). Si DataIsDirectory está establecido en 0, este desplazamiento apunta a un IMAGE_RESOURCE_DATA_ENTRY.
IMAGE_RESOURCE_DATA_ENTRY
Esta estructura apunta a los datos reales del recurso.
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData;
DWORD Size;
DWORD CodePage;
DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
- OffsetToData: El desplazamiento desde el inicio de la sección de recursos hasta los datos del recurso.
- Size: El tamaño de los datos del recurso en bytes.
- CodePage: La página de códigos utilizada para codificar los datos del recurso.
- Reserved: Reservado; normalmente establecido en 0.
¡IMPORTANTE! Los desplazamientos en IMAGE_RESOURCE_DIRECTORY y IMAGE_RESOURCE_DIRECTORY_ENTRY se calculan desde el inicio de los recursos (IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES].VirtualAddress), y solo los desplazamientos en IMAGE_RESOURCE_DATA_ENTRY se calculan desde el inicio de la imagen base!
¡ESCRIBAMOS UN ANALIZADOR DE RECURSOS NATIVO!
Es hora de escribir nuestro propio analizador de recursos nativos sin usar WinAPI. El análisis manual de recursos, en lugar de usar funciones de la API de Windows como EnumResourceTypes o EnumResourceNames, tiene varias ventajas, especialmente en el contexto del análisis de seguridad y el escaneo antivirus:
- Seguridad: Funciones de API como EnumResourceTypes y EnumResourceNames requieren que el archivo ejecutable se cargue en el espacio de dirección del proceso, lo que podría llevar a la ejecución de código malicioso si el archivo contiene virus o troyanos. El análisis manual de recursos evita este riesgo.
- Independencia de la Plataforma: El análisis manual de recursos no depende de la versión del sistema operativo ni de su WinAPI, lo que lo convierte en una solución más universal.
- Análisis Heurístico: El análisis manual permite la aplicación de heurísticas complejas y algoritmos de detección que pueden ser necesarios para identificar amenazas nuevas o desconocidas.
- Rendimiento: El análisis puede ser optimizado para un mejor rendimiento en comparación con el uso de WinAPI, especialmente al escanear un gran número de archivos.
- Control: Con el análisis manual, el analista tiene control total sobre el proceso y puede ajustarlo para necesidades específicas de análisis, mientras que las funciones de API proporcionan un control limitado y pueden no revelar todos los aspectos de los recursos.
- Protección: El malware puede utilizar varios métodos para evadir la detección, incluyendo la manipulación de recursos de manera que no sean detectados por las APIs estándar. El análisis manual permite la detección de dichas manipulaciones.
- Acceso Completo: Las funciones de API pueden no proporcionar acceso a todos los recursos, especialmente si están corruptos o alterados deliberadamente. El análisis manual permite el análisis de todos los datos sin las limitaciones impuestas por la API.
- Manejo de Errores: Cuando se utilizan funciones de API, el manejo de errores puede ser limitado, mientras que el análisis manual permite respuestas más flexibles a situaciones no estándar y anomalías en la estructura del archivo.
struct ResourceInfo
{
DWORD Size; // Size of the resource data
PBYTE data; // Offset of the resource data from the beginning of the file
union {
WORD TypeID; // Resource type ID or
PIMAGE_RESOURCE_DIR_STRING_U Type; // resource type
};
union {
WORD NameID; // Resource name ID or
PIMAGE_RESOURCE_DIR_STRING_U Name; // resource name
};
WORD Language; // Language of the resource
};
std::optional> getAllResources(BYTE* pBase, uint64_t fileSize)
{
IMAGE_RESOURCE_DIRECTORY* pTypesDirectory = nullptr;
std::vector resources;
try
{
//********************************************************
// parse PE header
//********************************************************
IMAGE_DOS_HEADER* pDosHeader = reinterpret_cast(pBase);
IMAGE_NT_HEADERS* pNtHeaders = reinterpret_cast(pBase + pDosHeader->e_lfanew);
// Verify that the PE signature is valid, indicating a valid PE file.
if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
return std::nullopt;
// Depending on the machine type (32-bit or 64-bit), obtain the resource directory data.
IMAGE_DATA_DIRECTORY resourceDirectory;
switch (pNtHeaders->FileHeader.Machine)
{
case IMAGE_FILE_MACHINE_I386:
resourceDirectory = reinterpret_cast(pNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
break;
case IMAGE_FILE_MACHINE_AMD64:
resourceDirectory = reinterpret_cast(pNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
break;
default:
return std::nullopt;
};
// If the resource directory is empty, exit as there are no resources.
if (resourceDirectory.Size == 0)
return std::nullopt;
// Convert the RVA of the resources to a RAW offset
uint64_t resourceBase = ntpe::RvaToOffset(pBase, resourceDirectory.VirtualAddress);
IMAGE_RESOURCE_DIRECTORY* pResourceDir = reinterpret_cast(pBase + resourceBase);
//********************************************************
// Start parsing the resource directory
//********************************************************
// Iterate through type entries in the resource directory.
// parse types
pTypesDirectory = pResourceDir;
IMAGE_RESOURCE_DIRECTORY_ENTRY* pTypeEntries = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pTypesDirectory + 1);
for (uint64_t ti = 0; ti < pTypesDirectory->NumberOfNamedEntries + pTypesDirectory->NumberOfIdEntries; ti++)
{
// parse names
IMAGE_RESOURCE_DIRECTORY_ENTRY* pTypeEntry = &pTypeEntries[ti];
IMAGE_RESOURCE_DIRECTORY* pNamesDirectory = (IMAGE_RESOURCE_DIRECTORY*)(pBase + (pTypeEntry->OffsetToDirectory & 0x7FFFFFFF) + resourceBase);
for (uint64_t ni = 0; ni < pNamesDirectory->NumberOfNamedEntries + pNamesDirectory->NumberOfIdEntries; ni++)
{
// parse langs
IMAGE_RESOURCE_DIRECTORY_ENTRY* pNamesEntries = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pNamesDirectory + 1);
IMAGE_RESOURCE_DIRECTORY_ENTRY* pNameEntry = &pNamesEntries[ni];
IMAGE_RESOURCE_DIRECTORY* pLangsDirectory = (IMAGE_RESOURCE_DIRECTORY*)(pBase + (pNameEntry->OffsetToDirectory & 0x7FFFFFFF) + resourceBase);
for (uint64_t li = 0; li < pLangsDirectory->NumberOfNamedEntries + pLangsDirectory->NumberOfIdEntries; li++)
{
// parse data
IMAGE_RESOURCE_DIRECTORY_ENTRY* pLangsEntries = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pLangsDirectory + 1);
IMAGE_RESOURCE_DIRECTORY_ENTRY* pLangEntry = &pLangsEntries[li];
IMAGE_RESOURCE_DATA_ENTRY* pDataEntry = (IMAGE_RESOURCE_DATA_ENTRY*)(pBase + resourceBase + pLangEntry->OffsetToData);
// Save the resource information in a structured format.
ResourceInfo entry = {};
entry.Language = pLangsEntries->Id;
entry.Size = pDataEntry->Size;
entry.Type = (PIMAGE_RESOURCE_DIR_STRING_U)(pTypeEntry->NameIsString) ? (PIMAGE_RESOURCE_DIR_STRING_U)(pBase + pTypeEntry->NameOffset + resourceBase) : (PIMAGE_RESOURCE_DIR_STRING_U)(pTypeEntry->Id);
entry.Name = (PIMAGE_RESOURCE_DIR_STRING_U)(pNameEntry->NameIsString) ? (PIMAGE_RESOURCE_DIR_STRING_U)(pBase + pNameEntry->NameOffset + resourceBase) : (PIMAGE_RESOURCE_DIR_STRING_U)(pNameEntry->Id);
entry.data = ntpe::RvaToRaw(pBase, pDataEntry->OffsetToData);
resources.push_back(entry);
}
}
}
return resources;
}
catch (std::exception&)
{
return std::nullopt;
};
}
Puedes encontrar el código del proyecto completo en nuestro github:
https://github.com/SToFU-Systems/DSAVE
El analizador de recursos devuelve un vector con estructuras que contienen punteros a tipos de recursos, sus nombres y el identificador de idioma de cada recurso. En cada estructura, hay un puntero a los datos del recurso. Cada estructura permanece válida mientras el archivo ejecutable que leemos y analizamos esté en memoria. Esto es muy conveniente para escribir tu propio antivirus y escanear archivos. Después de que se libera el archivo, los punteros se vuelven inválidos.
LISTA DE HERRAMIENTAS UTILIZADAS
- Herramientas PE: https://github.com/petoolse/petools Esta es una herramienta de código abierto para manipular los campos de encabezado PE. Soporta archivos x86 y x64.
- Resource Hacker: https://www.angusj.com/resourcehacker. Este es un editor de recursos para aplicaciones de Windows de 32 bits y 64 bits. Te permite ver y editar recursos en archivos ejecutables (.exe, .dll, .scr, etc.) y bibliotecas de recursos compiladas (.res, .mui).
CONCLUSIÓN
¡Y eso es todo, amigos!
Hemos explorado los recursos de los archivos PORTABLE_EXECUTABLE del sistema operativo Windows y escribimos nuestro propio analizador de recursos nativo, ¡simple pero bastante efectivo!
Agradecemos su apoyo y esperamos con interés su continuo compromiso en nuestra comunidad!
Cualquier pregunta a los autores del artículo puede enviarse al correo electrónico: articles@stofu.io
¡Gracias!