IntermedioPERich Headertimestampsmetadatosatribucionanalisis estatico

Rich Header, Timestamps y Metadatos Ocultos en PE

Analisis del Rich Header, timestamps de compilacion, Debug Directory, rutas PDB y otros metadatos ocultos en archivos PE de Windows. Tecnicas de atribucion y deteccion de manipulacion.

MalwareIntel Research··11 min lectura
Serie: Análisis de Binarios — Parte 10

Metadatos que el autor no sabia que dejaba

Los binarios PE contienen metadatos que van mucho mas alla de las cabeceras documentadas. El Rich Header revela las herramientas exactas de compilacion. Los timestamps indican cuando y posiblemente donde fue compilado. Las rutas PDB exponen el entorno de desarrollo del autor. Incluso los recursos de version pueden contener informacion util.

Para el analista de malware, estos metadatos son una mina de oro para la atribucion: agrupar muestras del mismo autor, identificar campanas, y a veces apuntar a una region geografica o grupo de amenazas especifico.

Rich Header: la firma del toolchain

Que es y donde esta

El Rich Header es una estructura no documentada oficialmente que el linker de Microsoft Visual Studio inserta entre el DOS Stub y el PE Signature. No esta descrita en la especificacion PE, pero su formato ha sido reverse-engineered por la comunidad de seguridad.

Ubicacion en el archivo:

[DOS Header]        0x00 - 0x3F
[DOS Stub]          0x40 - variable
[Rich Header]       variable - e_lfanew   <-- Aqui
[PE Signature]      e_lfanew
[COFF Header]       e_lfanew + 4

Estructura del Rich Header

El Rich Header esta cifrado con XOR usando una clave de 32 bits. La estructura (una vez descifrada) es:

ComponenteDescripcion
"DanS" (0x536E6144)Magic de inicio (cifrado con XOR)
3 DWORDs de paddingCeros (cifrados)
Entradas comp_idPares (tool_id, version, count)
"Rich" (0x68636952)Magic de fin (en claro)
XOR keyClave de descifrado (4 bytes)

Cada entrada comp_id contiene:

  • tool_id (16 bits): Identificador de la herramienta (compilador C, C++, MASM, linker).
  • product_version (16 bits): Version del producto (build number de Visual Studio).
  • count (32 bits): Numero de archivos objeto compilados con esa herramienta.

Descifrado y parsing en Python

import struct

def parse_rich_header(filepath):
    with open(filepath, "rb") as f:
        data = f.read()

    # Buscar "Rich" marker
    rich_offset = data.find(b"Rich")
    if rich_offset == -1:
        print("No Rich Header found")
        return None

    # La XOR key esta inmediatamente despues de "Rich"
    xor_key = struct.unpack_from("<I", data, rich_offset + 4)[0]
    print("XOR key:", hex(xor_key))

    # Buscar inicio: descifrar hacia atras hasta encontrar "DanS"
    dans_value = 0x536E6144
    start = rich_offset - 4
    while start > 0:
        dword = struct.unpack_from("<I", data, start)[0] ^ xor_key
        if dword == dans_value:
            break
        start -= 4

    if start <= 0:
        print("DanS marker not found")
        return None

    # Parsear entradas (despues de DanS + 3 DWORDs padding)
    entries = []
    pos = start + 16  # DanS + 3 padding DWORDs

    print("\nRich Header Entries:")
    print("Tool ID    Version    Count    Description")
    print("-" * 60)

    while pos < rich_offset:
        val1 = struct.unpack_from("<I", data, pos)[0] ^ xor_key
        val2 = struct.unpack_from("<I", data, pos + 4)[0] ^ xor_key

        tool_id = val1 >> 16
        build = val1 & 0xFFFF
        count = val2

        desc = get_tool_description(tool_id)
        entry = dict(tool_id=tool_id, build=build, count=count, desc=desc)
        entries.append(entry)

        print(
            str(tool_id).ljust(10),
            str(build).ljust(10),
            str(count).ljust(8),
            desc
        )

        pos += 8

    return entries


def get_tool_description(tool_id):
    """Mapeo conocido de tool IDs."""
    tools = dict()
    tools[1] = "Import (old)"
    tools[2] = "Linker"
    tools[3] = "Compiler (CVTOMF)"
    tools[4] = "Compiler (C)"
    tools[5] = "Compiler (C++)"
    tools[6] = "Compiler (MASM)"
    tools[7] = "Resource Compiler"
    tools[10] = "Linker"
    tools[14] = "Compiler (MASM)"
    tools[40] = "Linker (newer)"
    tools[93] = "Compiler (C) VS2008"
    tools[154] = "Compiler (C++) VS2012"
    tools[170] = "Compiler (C) VS2013"
    tools[199] = "Compiler (C) VS2015"
    tools[258] = "Compiler (C) VS2019"
    tools[259] = "Compiler (C++) VS2019"
    tools[260] = "Linker VS2019"
    tools[261] = "MASM VS2019"

    return tools.get(tool_id, "Unknown (" + str(tool_id) + ")")

Rich Header para atribucion

Agrupacion de muestras: Binarios compilados en el mismo entorno de desarrollo producen Rich Headers identicos o muy similares. El hash del Rich Header (RichHash) se usa para agrupar muestras:

import hashlib

def rich_hash(filepath):
    with open(filepath, "rb") as f:
        data = f.read()

    rich_offset = data.find(b"Rich")
    if rich_offset == -1:
        return None

    xor_key = struct.unpack_from("<I", data, rich_offset + 4)[0]

    # Buscar DanS
    dans = 0x536E6144
    start = rich_offset - 4
    while start > 0:
        dword = struct.unpack_from("<I", data, start)[0] ^ xor_key
        if dword == dans:
            break
        start -= 4

    # Hash del contenido descifrado
    decrypted = bytearray()
    for i in range(start, rich_offset):
        decrypted.append(data[i] ^ (xor_key >> (8 * (i % 4)) & 0xFF))

    return hashlib.md5(bytes(decrypted)).hexdigest()

Caso celebre: Kaspersky uso el Rich Header para analizar Olympic Destroyer (2018). El malware tenia un Rich Header que parecia pertenecer a Lazarus Group, pero era falso (copiado de otra muestra de Lazarus para crear una false flag). Esto demostro que el Rich Header puede falsificarse, pero la falsificacion misma es un indicador.

Ausencia de Rich Header

Si un PE no tiene Rich Header:

  • No fue compilado con MSVC (MinGW, Go, Delphi, Rust no generan Rich Header).
  • Fue compilado con MSVC pero el Rich Header fue eliminado intencionalmente.
  • El packer o protector lo removio durante el empaquetado.

Timestamps de compilacion

COFF File Header TimeDateStamp

El campo TimeDateStamp del COFF Header (offset e_lfanew + 8) contiene la fecha de compilacion como epoch Unix.

import pefile
import datetime

pe = pefile.PE("sample.exe")
ts = pe.FILE_HEADER.TimeDateStamp

try:
    dt = datetime.datetime.utcfromtimestamp(ts)
    print("Timestamp:", hex(ts))
    print("Fecha:", dt.strftime("%Y-%m-%d %H:%M:%S UTC"))

    # Verificar anomalias
    now = datetime.datetime.utcnow()
    if dt > now:
        print("[!] Fecha en el futuro: timestamp manipulado")
    elif dt.year < 2000:
        print("[!] Fecha anterior a 2000: probablemente manipulado")
    elif ts == 0:
        print("[!] Timestamp = 0: eliminado intencionalmente")

    # Analisis de horario laboral
    hour = dt.hour
    if 7 <= hour <= 18:
        print("Horario laboral UTC (podria ser Europa)")
    elif 0 <= hour <= 9:
        print("Horario laboral UTC+8 (podria ser Asia Oriental)")

except (ValueError, OSError):
    print("[!] Timestamp invalido:", hex(ts))

Timestamps adicionales en el PE

El TimeDateStamp del COFF Header no es el unico. Hay timestamps en:

UbicacionDescripcion
COFF HeaderCompilacion principal
Export DirectoryTimestamp de la tabla de exports
Import DescriptorsTimestamp de binding (si bound)
Resource DirectoryTimestamp de recursos
Debug DirectoryTimestamp del PDB
IMAGE_LOAD_CONFIGTimestamp de configuracion de seguridad

Consistencia de timestamps: En un PE legitimo, todos los timestamps deberian ser similares (del mismo momento de compilacion). Si el COFF timestamp es de 2024 pero el Debug timestamp es de 2019, algo fue manipulado.

timestamps = []

# COFF header
timestamps.append(("COFF", pe.FILE_HEADER.TimeDateStamp))

# Debug directory
if hasattr(pe, "DIRECTORY_ENTRY_DEBUG"):
    for dbg in pe.DIRECTORY_ENTRY_DEBUG:
        timestamps.append(("Debug", dbg.struct.TimeDateStamp))

# Export directory
if hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
    timestamps.append(("Export", pe.DIRECTORY_ENTRY_EXPORT.struct.TimeDateStamp))

print("Consistency check:")
for name, ts in timestamps:
    if ts > 0:
        dt = datetime.datetime.utcfromtimestamp(ts)
        print("  " + name + ": " + dt.strftime("%Y-%m-%d %H:%M:%S"))

Timestamps y atribucion de APTs

APTPatron de timestamps
APT28 (Fancy Bear)Frecuentemente falsificados a fechas antiguas
APT29 (Cozy Bear)Generalmente validos, compilacion en horario laboral Moscu
Lazarus (DPRK)Horario laboral Pyongyang (UTC+9)
APT41 (China)Horario laboral Beijing (UTC+8)
Equation GroupTimestamps consistentes con zona horaria EST/EDT

Debug Directory y rutas PDB

Que es la Debug Directory

La Debug Directory (Data Directory indice 6) apunta a informacion de debug que el compilador incluye para facilitar el debugging. La mas relevante es la referencia al archivo PDB (Program Database):

if hasattr(pe, "DIRECTORY_ENTRY_DEBUG"):
    for entry in pe.DIRECTORY_ENTRY_DEBUG:
        if entry.struct.Type == 2:  # IMAGE_DEBUG_TYPE_CODEVIEW
            # Parsear la estructura CodeView
            debug_data = pe.get_data(
                entry.struct.PointerToRawData,
                entry.struct.SizeOfData
            )
            if debug_data[:4] == b"RSDS":
                # RSDS format (modern)
                guid = debug_data[4:20]
                age = struct.unpack_from("<I", debug_data, 20)[0]
                pdb_path = debug_data[24:].split(b"\x00")[0].decode("utf-8", errors="replace")
                print("PDB Path:", pdb_path)
                print("GUID:", guid.hex())
                print("Age:", age)

Informacion que revela la ruta PDB

La ruta PDB es una de las fugas de informacion mas reveladoras:

Elemento de la rutaInformacion
C:\Users\username\Nombre de usuario del desarrollador
D:\Projects\malware_v2\Nombre del proyecto
C:\Usuarios\Sistema operativo en espanol
/home/user/Compilacion en Linux (cross-compile)
C:\Users\admin\Desktop\Proyecto en escritorio (desarrollador casual)
C:\BuildAgent\Compilacion automatizada (CI/CD)

Ejemplos reales de rutas PDB en malware:

# Lazarus Group
C:\Users\user\Desktop\round5\payload\Release\payload.pdb

# APT28
D:\Projects\xAgent\x64\Release\xAgent.pdb

# Malware generico
C:\Users\Administrador\source\repos\RAT\Release\RAT.pdb

Ausencia de Debug Directory

Si un PE no tiene Debug Directory:

  • Compilado en modo Release sin informacion de debug.
  • El autor la elimino intencionalmente (strip).
  • El packer/protector la removio.

La ausencia es comun en malware pero no es sospechosa por si sola, ya que muchos binarios legitimos se distribuyen sin PDB.

Recursos de version (VERSION_INFO)

La tabla de version (recurso RT_VERSION) contiene metadatos del producto:

if hasattr(pe, "VS_VERSIONINFO"):
    for info in pe.FileInfo:
        for entry in info:
            if hasattr(entry, "StringTable"):
                for st in entry.StringTable:
                    for key, value in st.entries.items():
                        print(key.decode() + ": " + value.decode())

Campos relevantes:

CampoQue revela
CompanyNameEmpresa (a veces falsa, imitando Microsoft)
FileDescriptionDescripcion del archivo
OriginalFilenameNombre original (diferente al actual = renombrado)
InternalNameNombre interno del proyecto
ProductNameNombre del producto
LegalCopyrightCopyright (puede revelar idioma)
PrivateBuildInformacion de build privado

Indicadores sospechosos:

  • CompanyName dice "Microsoft Corporation" pero no tiene firma de Microsoft.
  • OriginalFilename no coincide con el nombre actual del archivo.
  • Version 1.0.0.0 en un binario que dice ser software maduro.
  • Copyright en idioma no ingles combinado con CompanyName anglofona.

Manifest XML

El manifest es un recurso RT_MANIFEST que describe las dependencias y permisos del ejecutable:

<!-- Manifest tipico de malware que quiere elevacion -->
<assembly>
  <trustInfo>
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel
          level="requireAdministrator"
          uiAccess="false"/>
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

Un manifest que solicita requireAdministrator significa que el binario pide elevacion UAC al ejecutarse. Esto es sospechoso en binarios que no deberian necesitar privilegios de administrador.

Checksum del PE

El campo CheckSum del Optional Header contiene un checksum del archivo. No todos los compiladores lo rellenan:

import pefile

pe = pefile.PE("sample.exe")
declared = pe.OPTIONAL_HEADER.CheckSum
calculated = pe.generate_checksum()

print("Declarado:", hex(declared))
print("Calculado:", hex(calculated))

if declared == 0:
    print("CheckSum no establecido (comun en binarios no firmados)")
elif declared != calculated:
    print("[!] CheckSum invalido: binario modificado post-compilacion")
else:
    print("CheckSum valido")

Un checksum invalido indica que el binario fue modificado despues de la compilacion (por un packer, un patch manual, o la eliminacion de la firma digital).

Script de extraccion de metadatos completo

import pefile
import struct
import datetime
import hashlib
import sys

def extract_all_metadata(filepath):
    pe = pefile.PE(filepath)

    print("=== PE Metadata Extraction Report ===")
    print("File:", filepath)
    print()

    # Timestamp principal
    ts = pe.FILE_HEADER.TimeDateStamp
    print("--- Timestamps ---")
    if ts > 0:
        try:
            dt = datetime.datetime.utcfromtimestamp(ts)
            print("COFF:", dt.strftime("%Y-%m-%d %H:%M:%S UTC"))
        except (ValueError, OSError):
            print("COFF: Invalid (" + hex(ts) + ")")
    else:
        print("COFF: Not set")

    # Debug/PDB
    print("\n--- Debug Info ---")
    if hasattr(pe, "DIRECTORY_ENTRY_DEBUG"):
        for entry in pe.DIRECTORY_ENTRY_DEBUG:
            debug_ts = entry.struct.TimeDateStamp
            if debug_ts > 0:
                try:
                    ddt = datetime.datetime.utcfromtimestamp(debug_ts)
                    print("Debug timestamp:", ddt.strftime("%Y-%m-%d %H:%M:%S UTC"))
                except (ValueError, OSError):
                    pass

            if entry.struct.Type == 2:
                data = pe.get_data(
                    entry.struct.PointerToRawData,
                    entry.struct.SizeOfData
                )
                if data[:4] == b"RSDS":
                    pdb = data[24:].split(b"\x00")[0].decode("utf-8", errors="replace")
                    print("PDB Path:", pdb)
    else:
        print("No debug directory")

    # Version info
    print("\n--- Version Info ---")
    if hasattr(pe, "FileInfo"):
        for info in pe.FileInfo:
            for entry in info:
                if hasattr(entry, "StringTable"):
                    for st in entry.StringTable:
                        for key, value in st.entries.items():
                            print(key.decode() + ": " + value.decode())
    else:
        print("No version info")

    # Rich Header
    print("\n--- Rich Header ---")
    raw = open(filepath, "rb").read()
    rich_pos = raw.find(b"Rich")
    if rich_pos > 0:
        print("Rich Header presente en offset", hex(rich_pos))
        xor_key = struct.unpack_from("<I", raw, rich_pos + 4)[0]
        print("XOR key:", hex(xor_key))
    else:
        print("No Rich Header (no MSVC)")

    # Checksum
    print("\n--- Checksum ---")
    declared = pe.OPTIONAL_HEADER.CheckSum
    calculated = pe.generate_checksum()
    if declared == 0:
        print("Not set")
    elif declared == calculated:
        print("Valid:", hex(declared))
    else:
        print("MISMATCH: declared=" + hex(declared) + " calculated=" + hex(calculated))

    # ImpHash
    print("\n--- Hashes ---")
    print("ImpHash:", pe.get_imphash())

if __name__ == "__main__":
    extract_all_metadata(sys.argv[1])

Conclusion

Los metadatos ocultos en un PE (Rich Header, timestamps, rutas PDB, version info, checksum) proporcionan informacion que el autor del malware a menudo no sabe que esta dejando. Aunque ninguno de estos indicadores es definitivo por si solo (todos pueden falsificarse), su combinacion permite agrupar muestras, identificar campanas y, a veces, apuntar a una atribucion geografica o temporal del actor de amenazas.

El analista experimentado verifica la consistencia entre todos los timestamps, busca fugas de informacion en las rutas PDB, y usa el Rich Hash para agrupar muestras del mismo toolchain de compilacion.

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.