Qué lee Windows antes de ejecutar un PE
DOS Header, NT Headers, Data Directories y secciones muestran dónde está el código en disco y memoria y qué conviene revisar al analizar una muestra sospechosa.
Introducción
Un ejecutable de Windows no es solo una secuencia de instrucciones. Sus PE Headers indican cómo se mapea el archivo en memoria, dónde comienza la ejecución y qué otras estructuras necesita. También ofrecen un primer mapa de una muestra sospechosa antes de ejecutar una sola instrucción.
PE significa Portable Executable. El formato se usa en ejecutables y DLL, con variantes PE32 y PE32+. Aquí se recorren los headers de una imagen y se usa un cálculo de direcciones para relacionar los bytes del archivo con lo que aparece en un depurador.
Dónde empiezan los NT Headers
Una imagen PE comienza con el MS-DOS Header. Sus dos primeros bytes son MZ (0x4D5A). En el offset 0x3C del archivo está e_lfanew, un campo que indica el offset de la firma PE. En esa posición deben aparecer los cuatro bytes PE\0\0.
Offset del archivo
0x0000 MS-DOS Header (MZ)
0x003C e_lfanew -> offset de la firma PE
... DOS stub / padding
e_lfanew PE\0\0 | COFF File Header | Optional Header | Section TableEl DOS stub existe por compatibilidad. Su conocido mensaje sobre DOS no es el código que Windows empieza a ejecutar. Antes de seguir e_lfanew, conviene comprobar que apunta dentro del archivo. Un parser que confía en offsets de una muestra no confiable puede intentar leer fuera de sus límites.
La posición del NT Header no es fija. Si e_lfanew vale 0x80, la firma PE empieza en el offset 0x80; si vale 0xF0, empieza en 0xF0. El valor se lee como entero de 32 bits en little endian. En un editor hexadecimal, por ejemplo, los bytes 80 00 00 00 en 0x3C representan 0x80.
Después de la firma hay 20 bytes de COFF File Header. A continuación se encuentra el Optional Header, cuyo tamaño declara el propio COFF Header. La Section Table comienza al terminar ese bloque. Por eso no conviene calcular su posición suponiendo un tamaño fijo del Optional Header: PE32 y PE32+ tienen disposiciones distintas y el campo SizeOfOptionalHeader es el que permite avanzar correctamente.
COFF Header y Optional Header
Después de la firma PE viene el COFF File Header. Machine identifica la arquitectura, NumberOfSections indica cuántas entradas tiene la tabla de secciones y SizeOfOptionalHeader permite ubicar el comienzo de esa tabla. Characteristics contiene flags que describen la imagen, incluido si es ejecutable o DLL.
En una imagen, el Optional Header es obligatorio pese a su nombre. Su campo Magic distingue PE32 (0x10B) de PE32+ (0x20B). Esta diferencia cambia el tamaño de algunos campos, entre ellos ImageBase, pero no convierte todos los campos en valores de 64 bits.
Machine y Magic responden preguntas diferentes. Machine identifica la arquitectura de destino, mientras que Magic identifica la variante del Optional Header. Leer únicamente la extensión .exe o .dll no aporta esa información. Tampoco hay que confundir TimeDateStamp con una fecha verificada de compilación: es un valor almacenado en el archivo y puede alterarse.
Al revisar una muestra, estos campos suelen ser los primeros en mirar:
| Campo | Qué indica |
|---|---|
AddressOfEntryPoint | RVA donde empieza la ejecución tras cargar la imagen. |
ImageBase | Dirección base preferida para la imagen mapeada. |
SectionAlignment / FileAlignment | Alineación de las secciones en memoria y en disco. |
SizeOfImage | Espacio reservado para la imagen en memoria, incluidas las secciones alineadas. |
SizeOfHeaders | Tamaño de los headers tal como están dispuestos en el archivo. |
Subsystem | Entorno previsto, por ejemplo consola o interfaz gráfica. |
Un entry point en una sección inusual justifica mirar esa sección, pero no permite sacar una conclusión por sí solo. Un packer suele comenzar en un pequeño stub de desempaquetado; algunos protectores legítimos y procesos de compilación poco habituales producen estructuras parecidas.
AddressOfEntryPoint no guarda una dirección virtual absoluta. Si vale 0x1234 y la imagen se carga en 0x140000000, el punto de entrada queda en 0x140001234. La base real puede diferir de ImageBase, entre otras razones por ASLR. Una RVA permanece referida al comienzo de la imagen y por eso sigue siendo útil para comparar el archivo con una sesión de depuración.
Data Directories
Al final del Optional Header están los Data Directories. Cada entrada da una dirección y un tamaño para estructuras como imports, exports, recursos, relocations o TLS. La mayoría de esas direcciones son RVA. La tabla de certificados es una excepción: usa un offset de archivo porque los certificados no se mapean en la imagen de la forma habitual.
El Import Directory permite ver qué DLL y funciones declara la imagen. Es una vista incompleta del comportamiento: el código puede resolver APIs durante la ejecución o cargar otros módulos. Una tabla de imports pequeña tampoco demuestra, por sí sola, que el archivo esté empaquetado.
Al interpretar los directorios hay que respetar NumberOfRvaAndSizes y SizeOfOptionalHeader. Un directorio no tiene por qué comenzar al inicio de una sección ni estar en una sección con un nombre determinado.
Imports: del nombre de la DLL a la IAT
La entrada Import Directory apunta a una tabla de descriptores. Cada descriptor corresponde a una DLL y proporciona, entre otras cosas, una referencia al nombre de la biblioteca y a las tablas de funciones importadas. La Import Lookup Table describe las funciones por nombre u ordinal. La Import Address Table, o IAT, es la tabla utilizada para que el código acceda a las direcciones resueltas. Antes de la resolución puede contener información similar a la lookup table; durante la carga se preparan las direcciones de las funciones.
Esto explica por qué buscar una cadena como CreateFileW en el archivo puede ser útil, pero no definitivo. El nombre puede aparecer como import, como texto sin uso o no aparecer aunque la función se resuelva durante la ejecución. Para entender una llamada concreta interesa seguir la referencia desde el código hasta la IAT y observar a qué función apunta una vez cargado el proceso.
Relocations y TLS
Si la imagen no se carga en su ImageBase preferida, ciertas referencias absolutas necesitan ajustarse. El directorio de base relocations indica qué ubicaciones deben recibir la diferencia entre la base prevista y la real. No se modifica cada RVA del archivo: se corrigen valores concretos que dependen de la dirección de carga.
El directorio TLS merece una inspección aparte. Puede contener callbacks que se ejecutan durante la inicialización del proceso o de un hilo, antes de llegar al entry point habitual. Al depurar una muestra, empezar directamente en AddressOfEntryPoint puede omitir ese código. La presencia de callbacks tampoco implica comportamiento malicioso; TLS es una capacidad normal del formato.
Secciones: del archivo a la memoria
Cada entrada de la Section Table describe dos vistas de una región. PointerToRawData y SizeOfRawData ubican los bytes inicializados en el archivo. VirtualAddress y VirtualSize describen su posición y tamaño en la imagen mapeada. Characteristics incluye permisos de lectura, escritura y ejecución.
Supongamos una sección con VirtualAddress = 0x1000 y PointerToRawData = 0x400. Un entry point con RVA 0x1234 está a 0x234 bytes del inicio de esa sección. Si esos bytes existen en los datos de la sección dentro del archivo, su offset es:
offset = RVA - VirtualAddress + PointerToRawData
= 0x1234 - 0x1000 + 0x400
= 0x634Si la imagen se carga en 0x140000000, esa misma RVA corresponde a la dirección virtual 0x140001234. La RVA es relativa a la base de la imagen cargada; el offset es una posición dentro del archivo. Confundirlos lleva a examinar bytes equivocados.
La conversión solo funciona para datos presentes en el rango raw de esa sección. Si VirtualSize supera SizeOfRawData, el espacio restante se rellena con ceros al mapear la imagen. Los headers también tienen su propio mapeo. Por eso, un parser general necesita comprobar límites y no aplicar la fórmula a cualquier RVA.
También puede ocurrir lo contrario: SizeOfRawData puede ser mayor que VirtualSize por el padding que introduce FileAlignment. Ese padding no convierte automáticamente todos los bytes finales en contenido significativo. Para ubicar una RVA hay que identificar primero la sección que la contiene y después verificar que el desplazamiento calculado corresponde a bytes presentes en el archivo.
En un PE típico, el contenido de .text es código, .rdata contiene datos de solo lectura, .data datos modificables y .rsrc recursos. Son nombres habituales, no tipos exigidos por el loader. Una sección con nombre aleatorio puede contener código válido, y una llamada .text puede no parecerse a una sección de código convencional.
Un recorrido con números completos
Supongamos un PE32+ de prueba con e_lfanew = 0x80, dos secciones y los siguientes datos. Los valores son inventados para seguir las operaciones; no describen una muestra real.
| Estructura o campo | Valor |
|---|---|
| Firma PE | Offset de archivo 0x80 |
AddressOfEntryPoint | RVA 0x1234 |
ImageBase | 0x140000000 |
.text | RVA 0x1000, raw 0x400, tamaños virtual/raw 0x600 / 0x600 |
.rdata | RVA 0x2000, raw 0xA00, tamaños virtual/raw 0x400 / 0x400 |
| Import Directory | RVA 0x2100, tamaño 0x80 |
La firma se busca en 0x80, no justo después del DOS Header. El entry point cae en .text porque 0x1234 está entre 0x1000 y 0x1600. Su offset es 0x634, como en el cálculo anterior. Si la imagen se carga en su base preferida, su dirección virtual es 0x140001234.
El Import Directory cae en .rdata: 0x2100 - 0x2000 = 0x100 bytes desde el inicio de la sección. En el archivo comienza en 0xA00 + 0x100 = 0xB00. Su tamaño declarado, 0x80, lleva el intervalo hasta 0xB80, todavía dentro de los 0x400 bytes raw de .rdata. Esa comprobación del intervalo completo importa: que la dirección inicial sea válida no garantiza que toda la estructura lo sea.
Si el proceso se carga en 0x150000000, el entry point pasa a 0x150001234, mientras que los offsets 0x634 y 0xB00 del archivo no cambian. La diferencia entre bases es 0x10000000; las relocations se ocupan de los valores absolutos que requieran ese ajuste. La tabla de imports, en cambio, sigue localizándose mediante su RVA 0x2100 dentro de la imagen.
Qué revisar en una muestra sospechosa
Los headers ayudan a formular preguntas concretas. ¿El entry point cae en una sección con pocos datos en disco? ¿Hay una sección escribible y ejecutable? ¿SizeOfRawData es mucho menor que VirtualSize? ¿Los imports son escasos frente a lo observado al ejecutar la muestra? ¿Algún directorio apunta fuera del archivo o se solapa con otra estructura?
Son señales para orientar el análisis, no reglas que identifiquen malware. Un campo aislado rara vez demuestra intención. Primero se compara la estructura con los bytes a los que apunta; después se comprueba la hipótesis en un desensamblador o depurador. Una muestra empaquetada, por ejemplo, puede empezar en un stub que reconstruye código e imports antes de saltar a su entry point original.
Un recorrido concreto ayuda a ordenar esas comprobaciones. Supongamos que un archivo declara tres secciones: .text, .rdata y UPX1. El entry point cae en UPX1, que es ejecutable, mientras que la tabla de imports contiene pocas funciones. Esa combinación sugiere examinar primero el código de UPX1, pero todavía no demuestra qué hace el archivo ni siquiera que sea malicioso.
El siguiente paso es calcular el offset del entry point y mirar sus bytes en el archivo. Después se comprueba si el código escribe en otra región de memoria, cambia permisos o transfiere el control a una dirección distinta. Si aparece un segundo punto de entrada tras desempaquetar, conviene reconstruir allí la vista de imports y comparar la imagen en memoria con el archivo original. El nombre UPX1 por sí solo no sustituye ese recorrido: las secciones pueden renombrarse.
Hay otros dos límites útiles. Un TimeDateStamp extraño puede ser una pista sobre la construcción del archivo, pero no una cronología fiable. Y una firma o certificado presente en la Certificate Table requiere validación criptográfica y evaluación de confianza; la mera presencia del directorio no autentica la muestra.
La práctica útil es distinguir siempre tres coordenadas: offset para los bytes en disco, RVA para la posición dentro de la imagen y dirección virtual para el proceso cargado. Con esa separación, el resto de la estructura PE se vuelve más fácil de inspeccionar.
- 01 Microsoft - PE Format learn.microsoft.com/en-us/windows/win32/debug/pe-format ↗