Saltar al contenido

El formato PE de Windows para analistas: cabeceras, secciones, imports y firmas

    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).

    CampoQué indicaPor qué le importa al analista
    AddressOfEntryPointRVA de la primera instrucción que se ejecutaSi apunta fuera de .text (p. ej. a .data o a la última sección), sospecha de empaquetado
    ImageBaseDirección de carga preferidaJunto a la RVA da la dirección virtual: VA = ImageBase + RVA
    SectionAlignment / FileAlignmentAlineación en memoria y en discoExplican por qué una sección ocupa distinto en disco que en RAM
    SubsystemGUI (2), consola (3), driver nativo (1)Un binario «de consola» que crea ventanas, o al revés, llama la atención
    DllCharacteristicsMitigaciones activadasASLR (DYNAMIC_BASE), DEP (NX_COMPAT), CFG; su ausencia es habitual en código a medida
    SizeOfImageTamaño total ya mapeado en memoriaUn 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ípicaContenidoPermisosNota de análisis
    .textCódigo ejecutableR-XEl grueso de la lógica; entropía moderada en código normal
    .rdataDatos de solo lectura, tablas de import/export, stringsR–Aquí suelen estar la IAT y las cadenas literales
    .dataVariables globales inicializadasRW-VirtualSize mayor que SizeOfRawData por las variables a cero (.bss embebido)
    .rsrcRecursos: iconos, versión, manifiesto, diálogosR–Un ejecutable dentro de los recursos es un patrón de dropper
    .relocTabla de relocations para cargar fuera de ImageBaseR–Su ausencia con ASLR activo obliga al cargador a usar la base preferida
    Nombres raros (UPX0, .themida, sin nombre)Secciones creadas por un packerRWX frecuenteSecció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 importadasCapacidad que sugieren
    VirtualAlloc, VirtualProtect, WriteProcessMemory, CreateRemoteThreadInyección de código en otro proceso
    InternetOpen, WinHttpConnect, socket, send / recvComunicación de red / posible canal C2
    RegSetValueEx, CreateService, schtasks (vía CreateProcess)Persistencia
    CryptEncrypt, BCryptGenRandom, CryptAcquireContextCifrado (ransomware, protección de cadenas, C2)
    IsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcessAnti-análisis / detección de depurador
    GetProcAddress + LoadLibrary casi en solitarioResolució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ódigoInterpretación habitual
    4,5 – 6,8Código compilado normal
    6,8 – 7,2Zona gris: recursos mixtos, algo de datos comprimidos, o compilador agresivo
    7,2 – 8,0Compresió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

    HerramientaTipoPara qué
    PE-bearGUI, Windows/LinuxVisor de cabeceras y secciones muy claro, comparador de dos PE
    CFF ExplorerGUI, WindowsClásico; editor de cabeceras, visor de recursos, calculadora de RVA
    PEStudioGUI, WindowsMarca indicadores sospechosos y cruza imports con listas de APIs de riesgo
    Detect It Easy (DiE)GUI/CLIIdentifica compilador, linker y packer; gráfica de entropía por zonas
    pefileLibrería PythonAutomatizar el parseo en scripts y pipelines de triaje
    LIEFLibrería multi-lenguajeParseo y también reconstrucción de PE/ELF/Mach-O
    dumpbin / llvm-readobjCLIVolcado 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)

    Resumen de privacidad

    Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.