El formato PE de Windows para analistas:
cabeceras, secciones, imports y firmas
Malware y Reversing | unfantasmaenelsistema.com
Casi todo lo que se ejecuta en Windows —un .exe, una .dll, un driver .sys, un control ActiveX .ocx— comparte el mismo envoltorio: el formato PE (Portable Executable). Es la estructura que el cargador del sistema lee para saber cuánta memoria reservar, dónde empieza el código, qué librerías hacen falta y con qué permisos mapear cada trozo. Para quien analiza malware o hace ingeniería inversa, ese envoltorio es la primera fuente de información: antes de abrir un desensamblador ya se puede decir mucho de un binario solo leyendo sus cabeceras.
Este artículo recorre el PE de fuera hacia dentro con la mirada del analista: qué campos importan, qué cuenta cada sección, qué revelan la tabla de importaciones y los recursos, cómo la entropía delata un empaquetador y qué garantiza (y qué no) una firma Authenticode. Todos los ejemplos son de lectura: inspeccionar un fichero, nunca modificarlo ni ejecutarlo fuera de un laboratorio aislado.
Qué aprenderás en este artículo
El recorrido de cabeceras: DOS header y stub, firma PE, File Header y Optional Header · Cómo se traduce una RVA a un offset de fichero y por qué VirtualSize y SizeOfRawData difieren · Qué cuenta la tabla de secciones y qué esperar de .text, .rdata, .data, .rsrc y .reloc · Cómo leer la tabla de importaciones y exportaciones y qué pistas da sobre el comportamiento · Entropía por sección, recursos y overlay como indicadores de empaquetado · Qué valida una firma Authenticode y los catálogos de firma del sistema
1. Qué es un PE y por qué se mira primero
El PE es una evolución del viejo formato COFF de Unix que Microsoft adoptó con Windows NT. Describe un mapa: el fichero en disco está dividido en regiones que el cargador copia a memoria en direcciones concretas, con permisos concretos (leer, escribir, ejecutar). Ese mapa lo forman una serie de cabeceras encadenadas al principio del fichero, seguidas de la tabla de secciones y del contenido de cada sección.
Para el analista, las cabeceras responden gratis a preguntas caras: ¿es de 32 o 64 bits?, ¿es una GUI, una consola o un driver?, ¿cuándo se compiló?, ¿qué APIs llama?, ¿tiene recursos incrustados?, ¿está firmado?, ¿parece empaquetado? Ninguna de esas respuestas exige ejecutar nada.
2. El recorrido de cabeceras
El fichero empieza con el DOS header, 64 bytes que arrancan con la firma MZ (las iniciales de Mark Zbikowski). Su único campo relevante hoy es e_lfanew, un puntero al offset donde empiezan las cabeceras PE reales. Entre medias está el DOS stub, un miniprograma de 16 bits cuya única función es imprimir «This program cannot be run in DOS mode» si alguien lo lanza en DOS. Que ese texto esté cambiado o que el stub sea anómalo ya es una señal.
En e_lfanew aparece la firma PE\0\0 y, pegado a ella, el File Header (o COFF header): número de secciones, marca de tiempo de compilación (TimeDateStamp), tamaño del Optional Header y las Characteristics (si es DLL, si es ejecutable, etc.). El TimeDateStamp es orientativo: es trivial de falsificar y muchos compiladores modernos lo ponen a un valor reproducible.
2.1 El Optional Header: el que más dice
Pese al nombre, en un ejecutable el Optional Header es obligatorio. Contiene la parametrización de carga y, al final, los Data Directories: punteros a estructuras clave (importaciones, exportaciones, recursos, relocations, certificado de firma, TLS…). Su primer campo, la Magic, distingue PE32 (0x10B, 32 bits) de PE32+ (0x20B, 64 bits).
| Campo | Qué indica | Por qué le importa al analista |
|---|---|---|
| AddressOfEntryPoint | RVA de la primera instrucción que se ejecuta | Si apunta fuera de .text (p. ej. a .data o a la última sección), sospecha de empaquetado |
| ImageBase | Dirección de carga preferida | Junto a la RVA da la dirección virtual: VA = ImageBase + RVA |
| SectionAlignment / FileAlignment | Alineación en memoria y en disco | Explican por qué una sección ocupa distinto en disco que en RAM |
| Subsystem | GUI (2), consola (3), driver nativo (1) | Un binario «de consola» que crea ventanas, o al revés, llama la atención |
| DllCharacteristics | Mitigaciones activadas | ASLR (DYNAMIC_BASE), DEP (NX_COMPAT), CFG; su ausencia es habitual en código a medida |
| SizeOfImage | Tamaño total ya mapeado en memoria | Un descuadre grande respecto al fichero apunta a secciones que se expanden al cargar |
3. La tabla de secciones
Justo después del Optional Header viene un array de entradas, una por sección. Cada entrada da el nombre (hasta 8 caracteres), la VirtualAddress (RVA donde se mapea), el VirtualSize (cuánto ocupa ya en memoria), el PointerToRawData y el SizeOfRawData (offset y tamaño en el fichero) y las Characteristics con los permisos de la página.
La distinción RVA frente a offset de fichero es la que más cuesta al principio. Un dato vive en una RVA cuando el binario está cargado, pero en el fichero está en otro sitio porque las alineaciones difieren. Para convertir, se localiza en qué sección cae la RVA y se aplica: offset = RVA – VirtualAddress + PointerToRawData. Las herramientas lo hacen solas, pero conviene entender la aritmética.
| Sección típica | Contenido | Permisos | Nota de análisis |
|---|---|---|---|
| .text | Código ejecutable | R-X | El grueso de la lógica; entropía moderada en código normal |
| .rdata | Datos de solo lectura, tablas de import/export, strings | R– | Aquí suelen estar la IAT y las cadenas literales |
| .data | Variables globales inicializadas | RW- | VirtualSize mayor que SizeOfRawData por las variables a cero (.bss embebido) |
| .rsrc | Recursos: iconos, versión, manifiesto, diálogos | R– | Un ejecutable dentro de los recursos es un patrón de dropper |
| .reloc | Tabla de relocations para cargar fuera de ImageBase | R– | Su ausencia con ASLR activo obliga al cargador a usar la base preferida |
| Nombres raros (UPX0, .themida, sin nombre) | Secciones creadas por un packer | RWX frecuente | Sección con permiso de escritura Y ejecución = bandera roja |
Señales estructurales que se revisan de un vistazo
Una sección con permisos de escritura y ejecución a la vez (WRITE + EXECUTE) · El entry point fuera de la primera sección de código · SizeOfRawData igual a cero en una sección que sí ocupa en memoria (se rellena en tiempo de ejecución) · Número de secciones inusual, nombres duplicados o con bytes no imprimibles · Solapamiento de secciones o huecos grandes entre PointerToRawData consecutivos
4. Importaciones y exportaciones: qué llama y qué ofrece
El Import Directory lista, por cada DLL de la que depende el binario, las funciones que necesita. En disco hay dos arrays paralelos: la ILT (Import Lookup Table, los nombres u ordinales que se piden) y la IAT (Import Address Table, que el cargador rellena en tiempo de carga con las direcciones reales). Cada función se referencia por hint/name (un string con el nombre) o por ordinal (solo un número, más opaco y típico de algunas DLL del sistema).
La tabla de importaciones es la radiografía funcional más rápida de un binario. No prueba nada por sí sola —un editor de texto también abre ficheros y toca el registro— pero un conjunto de APIs concreto orienta la hipótesis:
| APIs importadas | Capacidad que sugieren |
|---|---|
| VirtualAlloc, VirtualProtect, WriteProcessMemory, CreateRemoteThread | Inyección de código en otro proceso |
| InternetOpen, WinHttpConnect, socket, send / recv | Comunicación de red / posible canal C2 |
| RegSetValueEx, CreateService, schtasks (vía CreateProcess) | Persistencia |
| CryptEncrypt, BCryptGenRandom, CryptAcquireContext | Cifrado (ransomware, protección de cadenas, C2) |
| IsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcess | Anti-análisis / detección de depurador |
| GetProcAddress + LoadLibrary casi en solitario | Resolución dinámica: la tabla de imports estática está vacía a propósito |
El Export Directory es el espejo: las funciones que una DLL pone a disposición de otros módulos, por nombre y por ordinal. En malware se ve en payloads en forma de DLL con un export llamado, por ejemplo, «DllRegisterServer» o un nombre único que el lanzador invoca con rundll32.
# Lectura de la tabla de importaciones con pefile (Python). Solo inspecciona.
import pefile
pe = pefile.PE('muestra.bin', fast_load=True)
pe.parse_data_directories(directories=[
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT']])
for dll in pe.DIRECTORY_ENTRY_IMPORT:
print(dll.dll.decode(errors='replace'))
for imp in dll.imports:
nombre = imp.name.decode() if imp.name else f'ordinal {imp.ordinal}'
print(f' {nombre}')5. Recursos y overlay
La sección .rsrc es un árbol jerárquico (tipo -> nombre/ID -> idioma) que guarda iconos, cursores, el bloque VERSIONINFO (nombre de empresa, versión, copyright… a menudo copiados de un binario legítimo para camuflar), el manifiesto XML y los diálogos. Extraer y revisar los recursos es rutina: un recurso de tipo «RT_RCDATA» o «RT_BITMAP» anormalmente grande y con entropía alta suele ser un segundo binario cifrado que el dropper descifra y ejecuta.
El overlay es todo lo que hay en el fichero después de la última sección declarada. El cargador de Windows lo ignora por completo, así que es un escondite habitual: datos de configuración, un archivo comprimido, una segunda etapa, o simplemente el instalador que se autoextrae. Comparar SizeOfRawData + PointerToRawData de la última sección con el tamaño real del fichero revela cuánto overlay hay.
6. Entropía: la pista de empaquetado
La entropía de Shannon mide, en una escala de 0 a 8 bits por byte, cómo de impredecible es una secuencia de bytes. El código y los datos normales rondan 5,5-6,5. El contenido comprimido o cifrado se acerca a 8, porque ya no tiene patrones que explotar. Calcular la entropía por sección es la forma más rápida de sospechar de un packer: si .text tiene 7,9 y casi no hay imports, ese binario está empaquetado y lo que se ve en el desensamblador es el stub de descompresión, no el código real.
| Entropía de una sección de código | Interpretación habitual |
|---|---|
| 4,5 – 6,8 | Código compilado normal |
| 6,8 – 7,2 | Zona gris: recursos mixtos, algo de datos comprimidos, o compilador agresivo |
| 7,2 – 8,0 | Compresión o cifrado: empaquetador, protector, o payload incrustado |
# Entropia por seccion con pefile. Lectura, no modifica el fichero.
import pefile
pe = pefile.PE('muestra.bin')
for s in pe.sections:
nombre = s.Name.rstrip(b'\x00').decode(errors='replace')
print(f'{nombre:8s} vsize={s.Misc_VirtualSize:#8x} '
f'rawsize={s.SizeOfRawData:#8x} entropia={s.get_entropy():.2f}')La entropía alta no es una acusación
Muchos instaladores legítimos (NSIS, Inno Setup), binarios .NET con recursos y ejecutables firmados con secciones comprimidas dan entropías altas. Es un indicador para priorizar el análisis, no un veredicto: se combina con imports escasos, entry point desplazado y nombres de sección atípicos.
7. Firmas: Authenticode y catálogos
Un PE puede ir firmado digitalmente con Authenticode. La firma es una estructura PKCS#7 que se guarda en el propio fichero (apuntada por el Data Directory de seguridad, la Attribute Certificate Table) y que contiene un hash del ejecutable —calculado excluyendo unas zonas concretas: la propia tabla de certificados, el campo Checksum y ese Data Directory— más la cadena de certificados que avala al firmante.
Verificar una firma responde a dos preguntas: ¿el fichero está intacto desde que se firmó? y ¿quién lo firmó, según una CA de confianza? Lo que no garantiza es que el software sea benigno: hay malware firmado con certificados robados o comprados por empresas fantasma, y el propio Windows acepta binarios cuya firma vive en un catálogo del sistema (ficheros .cat en la carpeta CatRoot de System32) en lugar de ir incrustada. Comprobaciones útiles: si la cadena valida, si el certificado estaba vigente en la fecha de firma (hay un sello de tiempo), y si el «Subject» del certificado encaja con lo que el binario dice ser en su VERSIONINFO.
8. Herramientas para inspeccionar un PE
| Herramienta | Tipo | Para qué |
|---|---|---|
| PE-bear | GUI, Windows/Linux | Visor de cabeceras y secciones muy claro, comparador de dos PE |
| CFF Explorer | GUI, Windows | Clásico; editor de cabeceras, visor de recursos, calculadora de RVA |
| PEStudio | GUI, Windows | Marca indicadores sospechosos y cruza imports con listas de APIs de riesgo |
| Detect It Easy (DiE) | GUI/CLI | Identifica compilador, linker y packer; gráfica de entropía por zonas |
| pefile | Librería Python | Automatizar el parseo en scripts y pipelines de triaje |
| LIEF | Librería multi-lenguaje | Parseo y también reconstrucción de PE/ELF/Mach-O |
| dumpbin / llvm-readobj | CLI | Volcado de cabeceras, imports y exports desde la toolchain del compilador |
Un triaje estático de PE en cinco minutos: hash y tipo de fichero; compilador y packer con DiE; cabeceras y tabla de secciones en PE-bear mirando permisos y entry point; entropía por sección; tabla de importaciones filtrando por APIs de red, cripto e inyección; recursos y overlay; y estado de la firma. Con eso ya se decide si la muestra pasa a desensamblador, a sandbox o a desempaquetado.
Marco legal y de seguridad
Inspecciona solo binarios propios, de retos públicos (crackmes, CTF) o muestras de malware dentro de un laboratorio aislado y sin salida a la red; no las ejecutes en tu equipo habitual · La ingeniería inversa de software de terceros está acotada en la UE a fines concretos (interoperabilidad, corrección de errores, seguridad) y con matices según el país y la licencia del producto · Usar el análisis de binarios para atacar sistemas ajenos, o distribuir cracks y parches de evasión de licencias, es ilícito: en España aplican los artículos 197 bis, 197 ter y 264 del Código Penal, además de la normativa de propiedad intelectual · Si al analizar una muestra encuentras datos de una víctima real o indicios de un incidente en curso, documenta y deriva a INCIBE-CERT o a las fuerzas y cuerpos de seguridad; no lo difundas por tu cuenta
Referencias
Microsoft — «PE Format» en la documentación oficial de Win32 (la especificación de referencia) · «Practical Malware Analysis» (Sikorski y Honig, No Starch Press) — capítulos de análisis estático · corkami/pics y los PoC de Ange Albertini — casos límite y PE malformados que siguen cargando · Documentación de pefile y de LIEF — API y ejemplos de parseo · Detect It Easy (github.com/horsicq/DIE-engine) — firmas de packers y compiladores
Conclusión
El formato PE es el contrato entre un ejecutable y el cargador de Windows, y ese contrato está escrito en texto plano al principio del fichero. Antes de una sola instrucción desensamblada, las cabeceras dicen la arquitectura, el subsistema, las mitigaciones, la fecha aproximada de compilación, qué APIs se piden, qué hay en los recursos, cuánto overlay cuelga del final y si la firma valida.
La rutina que rinde es siempre la misma: recorrer cabeceras y tabla de secciones mirando permisos y entry point, medir la entropía para descartar empaquetado, leer la tabla de importaciones como un índice de capacidades y comprobar recursos, overlay y firma. Cuando ese retrato estático deja preguntas abiertas —y con una muestra empaquetada las deja casi siempre— es cuando toca bajar al desensamblador o a la sandbox, ya con una hipótesis en la mano.
unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

