PrincipiantePEWindowsanalisis estaticoreverse engineeringformato binario

Formato PE de Windows: Estructura Completa del Ejecutable

Guia completa del formato Portable Executable (PE) de Windows. DOS header, PE signature, COFF header, Optional header, secciones, imports, exports y resources. Fundamentos para analisis de malware.

MalwareIntel Research··14 min lectura
Serie: Análisis de Binarios — Parte 1

El formato que domina el analisis de malware

Mas del 90% del malware en circulacion apunta a sistemas Windows. Y todo ejecutable de Windows, desde un simple .exe hasta un driver del kernel, sigue el formato PE (Portable Executable). Entender su estructura no es opcional para un analista de malware: es el primer paso obligatorio antes de abrir cualquier desensamblador.

El formato PE fue introducido con Windows NT en 1993 como evolucion del formato COFF (Common Object File Format) heredado de Unix. A pesar de tener mas de 30 anos, la estructura fundamental no ha cambiado. Lo que si ha cambiado es como los autores de malware abusan de cada campo para evadir detecciones, ocultar payloads y dificultar el analisis.

Este articulo cubre la estructura completa del PE, desde el primer byte hasta las tablas de imports y exports. Al terminar, sabras leer cualquier cabecera PE con herramientas como PEStudio, PE-bear o la libreria pefile de Python.

Estructura general del formato PE

Un archivo PE se organiza en capas, cada una construida sobre la anterior:

OffsetComponenteTamano tipicoFuncion
0x00DOS Header64 bytesCompatibilidad DOS, puntero a PE header
0x3C(e_lfanew)4 bytesOffset al PE Signature
VariableDOS Stub~100 bytesPrograma DOS "This program cannot be run in DOS mode"
e_lfanewPE Signature4 bytes"PE\0\0" (0x50450000)
+4COFF File Header20 bytesArquitectura, numero de secciones, timestamp
+24Optional Header96/112 bytesEntry point, image base, data directories
VariableSection Headers40 bytes cada unaDescripcion de cada seccion
VariableSection DataVariableCodigo, datos, recursos, imports

La lectura siempre empieza por el DOS Header, que apunta al PE Signature. Desde ahi se accede al COFF Header y al Optional Header. Los Section Headers describen donde esta cada bloque de datos en el archivo.

DOS Header: el legado de MS-DOS

Los primeros 64 bytes de todo archivo PE son el DOS Header, definido por la estructura IMAGE_DOS_HEADER. Dos campos son relevantes para el analisis de malware:

e_magic (offset 0x00, 2 bytes): El magic number "MZ" (0x4D5A), las iniciales de Mark Zbikowski, uno de los arquitectos de MS-DOS. Si un archivo no empieza con "MZ", no es un PE valido.

e_lfanew (offset 0x3C, 4 bytes): El offset absoluto dentro del archivo donde empieza el PE Signature. Este es el campo mas importante del DOS Header porque conecta la cabecera DOS con la cabecera PE moderna.

Ejemplo con pefile en Python:

import pefile

pe = pefile.PE("sample.exe")
print("e_magic:", hex(pe.DOS_HEADER.e_magic))
print("e_lfanew:", hex(pe.DOS_HEADER.e_lfanew))

Anomalias que delatan malware:

  • e_lfanew con valor inusual: Normalmente apunta a 0x80 o 0xF0. Valores muy grandes (por ejemplo 0x1000 o mas) pueden indicar que el autor inserto datos entre el DOS Stub y el PE Header, una tecnica para ocultar shellcode o confundir parsers.
  • DOS Stub modificado: El stub normalmente contiene el mensaje "This program cannot be run in DOS mode". Algunos packers lo eliminan o lo sustituyen por datos cifrados.
  • Campos DOS Header con valores no estandar: Los campos intermedios (e_cblp, e_cp, e_crlc, etc.) no se usan en Windows moderno. El malware a veces los usa como almacenamiento de flags o valores de configuracion.

DOS Stub: el programa fantasma

Entre el DOS Header y el PE Signature hay un pequeno programa de 16 bits que se ejecutaria si alguien intentara correr el archivo en MS-DOS puro. Su unica funcion es imprimir "This program cannot be run in DOS mode" y salir.

En la practica, ningun sistema moderno ejecuta el DOS Stub. Pero su presencia (o ausencia) puede ser un indicador:

  • Stub presente y estandar: Comportamiento normal, compilado con MSVC o similar.
  • Stub ausente o minimo: Compilado con MinGW, packers como UPX, o herramientas de generacion de shellcode.
  • Stub con contenido personalizado: Puede contener datos ocultos o incluso un loader secundario.

PE Signature: la firma de identidad

En el offset indicado por e_lfanew, encontramos 4 bytes que deben ser exactamente "PE\0\0" (0x50450000). Esta firma confirma que el archivo sigue el formato PE.

Si la firma no coincide, el loader de Windows rechaza el archivo. Algunas herramientas de analisis buscan esta firma en offsets no estandar para detectar PE embebidos dentro de otros archivos (un PDF con un PE dentro, por ejemplo).

COFF File Header: la identidad del binario

Inmediatamente despues del PE Signature, los siguientes 20 bytes forman el COFF File Header (IMAGE_FILE_HEADER). Contiene informacion fundamental sobre el binario:

CampoTamanoDescripcion
Machine2 bytesArquitectura: 0x14C (x86), 0x8664 (x64), 0xAA64 (ARM64)
NumberOfSections2 bytesCuantas secciones tiene el PE
TimeDateStamp4 bytesFecha de compilacion (epoch Unix)
PointerToSymbolTable4 bytesNormalmente 0 en ejecutables
NumberOfSymbols4 bytesNormalmente 0 en ejecutables
SizeOfOptionalHeader2 bytesTamano del Optional Header que sigue
Characteristics2 bytesFlags: ejecutable, DLL, stripped, etc.

Machine: identificar la arquitectura

El campo Machine indica para que procesador fue compilado el binario:

ValorArquitecturaContexto malware
0x014CIntel 386 (x86)Mayoria del malware legacy
0x8664AMD64 (x64)Malware moderno, drivers
0x01C4ARM Little-EndianMalware IoT portado
0xAA64ARM64Malware para Windows on ARM

TimeDateStamp: cuando fue compilado

Este campo almacena la fecha y hora de compilacion como un valor epoch Unix (segundos desde el 1 de enero de 1970). Es uno de los metadatos mas utiles y mas manipulados:

import datetime

timestamp = pe.FILE_HEADER.TimeDateStamp
compile_date = datetime.datetime.utcfromtimestamp(timestamp)
print("Compilado:", compile_date)

Indicadores de manipulacion:

  • Fecha en el futuro: el autor modifico el timestamp.
  • Fecha anterior a 2000: probablemente manipulada o corrupta.
  • Fecha exacta a medianoche: timestamp puesto a un valor round.
  • APT28 y APT29 son conocidos por setear timestamps falsos para dificultar la atribucion.

Characteristics: flags del binario

Las flags mas relevantes para analisis:

FlagValorSignificado
IMAGE_FILE_EXECUTABLE_IMAGE0x0002Es un ejecutable
IMAGE_FILE_LARGE_ADDRESS_AWARE0x0020Puede usar mas de 2GB de memoria
IMAGE_FILE_DLL0x2000Es una DLL
IMAGE_FILE_SYSTEM0x1000Es un driver de sistema

Un .exe con la flag DLL activa o un .dll sin ella es sospechoso.

Optional Header: el corazon del PE

A pesar de su nombre, el Optional Header no es opcional en ejecutables (solo lo es en archivos objeto .obj). Es la parte mas rica en informacion del PE y la mas relevante para el analisis.

Campos criticos del Optional Header

Magic (2 bytes): Distingue entre PE32 (0x10B) y PE32+ (0x20B).

AddressOfEntryPoint (4 bytes): La RVA (Relative Virtual Address) donde el loader empieza a ejecutar codigo. En malware empaquetado, este punto apunta al stub del packer, no al codigo original.

print("Entry Point:", hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint))

ImageBase (4/8 bytes): La direccion preferida donde el PE quiere cargarse en memoria. Valores tipicos:

  • 0x00400000 para .exe
  • 0x10000000 para .dll
  • 0x00010000 para drivers

SectionAlignment: Alineacion de secciones en memoria (tipicamente 0x1000, una pagina).

FileAlignment: Alineacion de secciones en disco (tipicamente 0x200, un sector).

SizeOfImage: Tamano total del PE cuando esta cargado en memoria.

SizeOfHeaders: Tamano combinado de todas las cabeceras hasta el inicio de la primera seccion.

Subsystem: Indica el tipo de ejecutable:

ValorSubsistemaEjemplo
1NativeDrivers .sys
2Windows GUIAplicaciones graficas
3Windows ConsoleHerramientas CLI

DllCharacteristics: Flags de seguridad habilitadas:

FlagValorProteccion
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE0x0040ASLR habilitado
IMAGE_DLLCHARACTERISTICS_NX_COMPAT0x0100DEP habilitado
IMAGE_DLLCHARACTERISTICS_NO_SEH0x0400No usa SEH
IMAGE_DLLCHARACTERISTICS_GUARD_CF0x4000Control Flow Guard

Indicador clave: Si ASLR y DEP estan deshabilitados en un binario moderno, es sospechoso. El malware a veces desactiva estas protecciones para facilitar la explotacion de memoria.

Data Directories: punteros a estructuras clave

El Optional Header termina con un array de 16 Data Directories. Cada entrada es un par (RVA, Size) que apunta a una estructura importante dentro del PE:

IndiceNombreContenido
0Export TableFunciones exportadas (DLLs)
1Import TableDLLs y funciones importadas
2Resource TableIconos, strings, manifests, datos embebidos
3Exception TableTabla de excepciones (x64)
4Certificate TableFirma digital Authenticode
5Base Relocation TableRelocaciones para ASLR
6Debug DirectoryInformacion de debug, rutas PDB
9TLS TableThread Local Storage callbacks
12IATImport Address Table
14CLR Runtime HeaderMetadata .NET

Para el analisis de malware, los directorios mas importantes son Import Table (que APIs usa), Resource Table (datos embebidos), Debug Directory (pistas sobre el entorno de compilacion) y TLS Table (codigo que se ejecuta antes del entry point).

Section Headers: el mapa del contenido

Despues del Optional Header vienen los Section Headers, uno por cada seccion del PE. Cada header tiene 40 bytes con esta informacion:

CampoTamanoDescripcion
Name8 bytesNombre de la seccion (.text, .data, etc.)
VirtualSize4 bytesTamano en memoria
VirtualAddress4 bytesRVA donde se carga
SizeOfRawData4 bytesTamano en disco
PointerToRawData4 bytesOffset en el archivo
Characteristics4 bytesPermisos: read, write, execute

Secciones estandar

SeccionContenidoPermisos normales
.textCodigo ejecutableRead + Execute
.dataVariables globales inicializadasRead + Write
.rdataDatos de solo lectura, importsRead
.bssVariables no inicializadasRead + Write
.rsrcRecursos (iconos, strings)Read
.relocTabla de relocacionesRead

Las secciones se cubren en profundidad en el siguiente articulo de la serie.

Import Table: que funcionalidad necesita el binario

La Import Table es probablemente la estructura mas reveladora para el analisis de malware. Lista todas las DLLs que el binario necesita y las funciones especificas que importa de cada una.

Por ejemplo, un binario que importa CreateRemoteThread de kernel32.dll y VirtualAllocEx esta practicamente declarando que va a inyectar codigo en otro proceso.

La tabla se organiza en dos niveles:

  1. Import Directory Table: Un array de estructuras IMAGE_IMPORT_DESCRIPTOR, una por cada DLL importada.
  2. Import Lookup Table (ILT) / Import Address Table (IAT): Arrays paralelos que listan las funciones importadas de cada DLL.
for entry in pe.DIRECTORY_ENTRY_IMPORT:
    print(entry.dll.decode())
    for imp in entry.imports:
        if imp.name:
            print("  ", imp.name.decode())

La Import Table se analiza en detalle en el articulo 3 de esta serie.

Export Table: funciones que ofrece el binario

La Export Table es el reverso de la Import Table: lista las funciones que este binario expone para que otros modulos las llamen. Es comun en DLLs y rara en ejecutables .exe.

En el contexto de malware, la Export Table revela:

  • DLLs maliciosas: Que funciones exporta una DLL sideloaded.
  • Nombres sospechosos: Exports como ServiceMain, DllRegisterServer o nombres genericos que imitan DLLs del sistema.
  • Exports por ordinal: Funciones exportadas solo por numero, sin nombre, para dificultar el analisis.
if hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
    for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        print(exp.ordinal, exp.name)

Resource Table: datos embebidos

La Resource Table contiene datos no ejecutables embebidos en el PE: iconos, cursores, dialogos, manifests, tablas de strings y, en el caso de malware, a menudo payloads cifrados o configuraciones.

La estructura es un arbol de tres niveles:

  1. Tipo de recurso (icono, string, version, RT_RCDATA para datos arbitrarios)
  2. ID o nombre del recurso
  3. Idioma del recurso

Los recursos RT_RCDATA son especialmente interesantes en malware porque permiten embeber datos arbitrarios: shellcode cifrado, configuraciones de C2, o incluso PEs completos que se desempaquetan en runtime.

for resource_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
    print("Tipo:", resource_type.id)
    for entry in resource_type.directory.entries:
        data = pe.get_data(
            entry.directory.entries[0].data.struct.OffsetToData,
            entry.directory.entries[0].data.struct.Size
        )
        print("  Tamano:", len(data), "bytes")

Herramientas esenciales para analisis PE

PEStudio (Windows, gratuito)

La herramienta de referencia para un primer vistazo rapido. Abre el PE y muestra inmediatamente:

  • Indicadores de anomalias (entropia alta, imports sospechosos)
  • Strings con ponderacion de relevancia
  • Informacion de compilador/packer detectado
  • Verificacion de firmas digitales
  • Comparacion con VirusTotal

PE-bear (Windows/Linux, open source)

Editor visual que permite navegar toda la estructura PE de forma grafica. Ideal para explorar headers, secciones y tablas de imports/exports con una interfaz intuitiva.

pefile (Python, open source)

Libreria Python para parsear PEs programaticamente. Indispensable para automatizar analisis:

import pefile

pe = pefile.PE("sample.exe")

# Informacion basica
print("Machine:", hex(pe.FILE_HEADER.Machine))
print("Secciones:", pe.FILE_HEADER.NumberOfSections)
print("Entry Point:", hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint))

# Imports
for entry in pe.DIRECTORY_ENTRY_IMPORT:
    print(entry.dll.decode())

# Entropia por seccion
for section in pe.sections:
    print(
        section.Name.decode().rstrip("\x00"),
        "Entropia:", round(section.get_entropy(), 2)
    )

CFF Explorer (Windows, gratuito)

Editor avanzado de PE que permite modificar cualquier campo de las cabeceras. Util para:

  • Corregir cabeceras danadas por packers
  • Modificar el entry point para analisis
  • Explorar el Resource Directory en detalle

Detect It Easy (DIE)

Detecta el compilador, linker y packer utilizado analizando patrones en el PE. Soporta firmas para cientos de compilers y packers.

Anomalias PE que delatan malware

Tras entender la estructura normal, estas son las anomalias mas comunes que senalan actividad maliciosa:

Entry Point fuera de .text: El punto de entrada normalmente esta en la seccion .text. Si apunta a otra seccion (especialmente una con nombre no estandar), probablemente esta empaquetado.

Secciones con permisos Read+Write+Execute: Las secciones legitimas rara vez necesitan los tres permisos simultaneamente. RWX indica codigo auto-modificable o desempaquetado en runtime.

Tamano en disco vs tamano en memoria: Si VirtualSize es mucho mayor que SizeOfRawData, la seccion se expande en memoria (tipico de packers que descomprimen codigo).

Numero de secciones inusual: Los binarios compilados con MSVC tipicamente tienen 4 a 7 secciones. Un PE con 1 o 2 secciones suele estar empaquetado. Mas de 10 secciones puede indicar un packer personalizado.

Imports minimas: Un ejecutable que solo importa LoadLibrary y GetProcAddress esta resolviendo todas sus funciones en runtime para ocultar su funcionalidad.

Timestamp cero o fecha futura: Manipulacion deliberada para dificultar la atribucion temporal.

Checksum invalido: El checksum del Optional Header no coincide con el contenido real del archivo. Habitual en binarios modificados post-compilacion.

De la teoria a la practica: primer analisis

Para consolidar estos conceptos, analiza cualquier ejecutable legitimo de tu sistema con pefile:

import pefile
import datetime

pe = pefile.PE("C:/Windows/System32/notepad.exe")

# DOS Header
print("=== DOS Header ===")
print("Magic:", hex(pe.DOS_HEADER.e_magic))
print("e_lfanew:", hex(pe.DOS_HEADER.e_lfanew))

# COFF Header
print("\n=== COFF Header ===")
print("Machine:", hex(pe.FILE_HEADER.Machine))
print("Secciones:", pe.FILE_HEADER.NumberOfSections)
ts = pe.FILE_HEADER.TimeDateStamp
print("Compilado:", datetime.datetime.utcfromtimestamp(ts))

# Optional Header
print("\n=== Optional Header ===")
print("Magic:", hex(pe.OPTIONAL_HEADER.Magic))
print("Entry Point:", hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint))
print("Image Base:", hex(pe.OPTIONAL_HEADER.ImageBase))
print("Subsystem:", pe.OPTIONAL_HEADER.Subsystem)

# Secciones
print("\n=== Secciones ===")
for s in pe.sections:
    name = s.Name.decode().rstrip("\x00")
    print(
        name,
        "VA:", hex(s.VirtualAddress),
        "VS:", hex(s.Misc_VirtualSize),
        "Raw:", hex(s.SizeOfRawData),
        "Entropy:", round(s.get_entropy(), 2)
    )

Compara los resultados con un binario sospechoso y las diferencias saltaran a la vista: entry points anomalos, secciones con entropia alta, imports minimalistas.

Conclusion

El formato PE es la base sobre la que se construye todo el analisis estatico de malware en Windows. Conocer cada campo de sus cabeceras permite identificar anomalias sin ejecutar el binario, detectar packers por las caracteristicas de sus secciones, y anticipar el comportamiento del malware por sus tablas de imports.

En los siguientes articulos de esta serie profundizaremos en las secciones PE, la Import Address Table y las tecnicas que el malware usa para manipular cada una de estas estructuras.

Preguntas frecuentes

Artículos relacionados

Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.