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.
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:
| Componente | Descripcion |
|---|---|
| "DanS" (0x536E6144) | Magic de inicio (cifrado con XOR) |
| 3 DWORDs de padding | Ceros (cifrados) |
| Entradas comp_id | Pares (tool_id, version, count) |
| "Rich" (0x68636952) | Magic de fin (en claro) |
| XOR key | Clave 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:
| Ubicacion | Descripcion |
|---|---|
| COFF Header | Compilacion principal |
| Export Directory | Timestamp de la tabla de exports |
| Import Descriptors | Timestamp de binding (si bound) |
| Resource Directory | Timestamp de recursos |
| Debug Directory | Timestamp del PDB |
| IMAGE_LOAD_CONFIG | Timestamp 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
| APT | Patron 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 Group | Timestamps 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 ruta | Informacion |
|---|---|
| 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:
| Campo | Que revela |
|---|---|
| CompanyName | Empresa (a veces falsa, imitando Microsoft) |
| FileDescription | Descripcion del archivo |
| OriginalFilename | Nombre original (diferente al actual = renombrado) |
| InternalName | Nombre interno del proyecto |
| ProductName | Nombre del producto |
| LegalCopyright | Copyright (puede revelar idioma) |
| PrivateBuild | Informacion 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
Libros recomendados
Artículos relacionados
Formato PE de Windows: Estructura Completa del Ejecutable
Secciones PE: .text, .data, .rsrc, .reloc y Anomalias
Analisis Estatico Basico: Strings, Hashes y Metadatos
Import Address Table: APIs Sospechosas y Resolucion Dinamica
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.