Análisis Forense de Archivos PST de Outlook
Descripción General
Los archivos PST (Tabla de Almacenamiento Personal) y OST (Tabla de Almacenamiento sin Conexión) de Microsoft Outlook son fuentes de evidencia críticas en las investigaciones forenses digitales. Los archivos PST almacenan mensajes de correo electrónico, eventos de calendario, contactos, tareas y notas en un formato binario propietario basado en el sistema de propiedades MAPI (Interfaz de Programación de Aplicaciones de Mensajería). El análisis forense de estos archivos permite la recuperación de correos electrónicos eliminados (de la carpeta Elementos Recuperables), la extracción de cabeceras de correo para rastrear rutas de mensajes, el análisis de archivos adjuntos para detectar malware o datos exfiltrados, y la reconstrucción de patrones de comunicación. Los archivos PST modernos usan formato Unicode con páginas de 4 KB y pueden crecer hasta 50 GB, mientras que el formato ANSI heredado está limitado a 2 GB.
Cuándo Usarlo
- Cuando se investigan incidentes de seguridad que requieran analizar archivos PST de Outlook para fines forenses de correo
- Cuando se construyen reglas de detección o consultas de caza de amenazas para este dominio
- Cuando los analistas de SOC necesitan procedimientos estructurados para este tipo de análisis
- Cuando se valida la cobertura de monitoreo de seguridad para técnicas de ataque relacionadas
Prerrequisitos
- libpff/pffexport (analizador PST de código abierto)
- Python 3.8+ con bibliotecas pypff o libratom
- MailXaminer, Forensic Email Collector o SysTools PST Forensics (comerciales)
- Microsoft Outlook (opcional, para acceso nativo a PST)
- Suficiente espacio en disco para el contenido extraído
Ubicaciones de Archivos PST
| Origen | Ruta |
|---|---|
| Outlook 2016+ (predeterminado) | %USERPROFILE%\Documents\Outlook Files*.pst |
| Outlook heredado | %LOCALAPPDATA%\Microsoft\Outlook*.pst |
| Caché OST | %LOCALAPPDATA%\Microsoft\Outlook*.ost |
| Archivo | %USERPROFILE%\Documents\Outlook Files\archive.pst |
Análisis con Herramientas de Código Abierto
libpff / pffexport
# Exportar todos los elementos de un archivo PST
pffexport -m all evidence.pst -t exported_pst
# Exportar solo mensajes de correo
pffexport -m items evidence.pst -t exported_emails
# Exportar elementos eliminados/recuperados
pffexport -m recovered evidence.pst -t recovered_items
# Obtener información del archivo PST
pffinfo evidence.pst
Análisis con Python
import pypff
import os
import json
import hashlib
import email
import sys
from datetime import datetime
from collections import defaultdict
class PSTForensicAnalyzer:
"""Análisis forense de archivos PST/OST de Outlook."""
def __init__(self, pst_path: str, output_dir: str):
self.pst_path = pst_path
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
self.pst = pypff.file()
self.pst.open(pst_path)
self.messages = []
self.attachments = []
self.stats = defaultdict(int)
def process_folder(self, folder, folder_path: str = ""):
"""Procesa carpetas PST de forma recursiva y extrae mensajes."""
folder_name = folder.name or "Root"
current_path = f"{folder_path}/{folder_name}" if folder_path else folder_name
for i in range(folder.number_of_sub_messages):
try:
message = folder.get_sub_message(i)
msg_data = self.extract_message(message, current_path)
if msg_data:
self.messages.append(msg_data)
self.stats["total_messages"] += 1
except Exception as e:
self.stats["parse_errors"] += 1
for i in range(folder.number_of_sub_folders):
try:
subfolder = folder.get_sub_folder(i)
self.process_folder(subfolder, current_path)
except Exception:
continue
def extract_message(self, message, folder_path: str) -> dict:
"""Extrae metadatos forenses de un solo mensaje de correo."""
msg_data = {
"folder": folder_path,
"subject": message.subject or "",
"sender": message.sender_name or "",
"sender_email": "",
"creation_time": str(message.creation_time) if message.creation_time else None,
"delivery_time": str(message.delivery_time) if message.delivery_time else None,
"modification_time": str(message.modification_time) if message.modification_time else None,
"has_attachments": message.number_of_attachments > 0,
"attachment_count": message.number_of_attachments,
"body_size": len(message.plain_text_body or b""),
"html_size": len(message.html_body or b""),
}
# Extraer cabeceras de transporte para análisis de enrutamiento
headers = message.transport_headers
if headers:
msg_data["headers_present"] = True
msg_data["headers_size"] = len(headers)
# Analizar cabeceras clave
parsed = email.message_from_string(headers)
msg_data["from_header"] = parsed.get("From", "")
msg_data["to_header"] = parsed.get("To", "")
msg_data["date_header"] = parsed.get("Date", "")
msg_data["message_id"] = parsed.get("Message-ID", "")
msg_data["x_originating_ip"] = parsed.get("X-Originating-IP", "")
msg_data["received_headers"] = parsed.get_all("Received", [])
# Procesar archivos adjuntos
for j in range(message.number_of_attachments):
try:
attachment = message.get_attachment(j)
att_data = {
"message_subject": msg_data["subject"],
"name": attachment.name or f"attachment_{j}",
"size": attachment.size,
"content_type": "",
}
self.attachments.append(att_data)
self.stats["total_attachments"] += 1
except Exception:
continue
return msg_data
def save_attachments(self, max_size_mb: int = 100):
"""Exporta archivos adjuntos al disco para su análisis."""
att_dir = os.path.join(self.output_dir, "attachments")
os.makedirs(att_dir, exist_ok=True)
root = self.pst.get_root_folder()
self._save_attachments_recursive(root, att_dir, max_size_mb)
def _save_attachments_recursive(self, folder, att_dir, max_size_mb):
for i in range(folder.number_of_sub_messages):
try:
message = folder.get_sub_message(i)
for j in range(message.number_of_attachments):
att = message.get_attachment(j)
if att.size and att.size < max_size_mb * 1024 * 1024:
name = att.name or f"unknown_{i}_{j}"
safe_name = "".join(c if c.isalnum() or c in ".-_" else "_" for c in name)
path = os.path.join(att_dir, safe_name)
try:
data = att.read_buffer(att.size)
with open(path, "wb") as f:
f.write(data)
except Exception:
continue
except Exception:
continue
for i in range(folder.number_of_sub_folders):
try:
self._save_attachments_recursive(folder.get_sub_folder(i), att_dir, max_size_mb)
except Exception:
continue
def generate_report(self) -> str:
"""Genera un informe completo de análisis forense de PST."""
root = self.pst.get_root_folder()
self.process_folder(root)
report = {
"analysis_timestamp": datetime.now().isoformat(),
"pst_file": self.pst_path,
"pst_size_bytes": os.path.getsize(self.pst_path),
"statistics": dict(self.stats),
"messages": self.messages[:500],
"attachments": self.attachments[:200],
}
report_path = os.path.join(self.output_dir, "pst_forensic_report.json")
with open(report_path, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[*] Mensajes totales: {self.stats['total_messages']}")
print(f"[*] Archivos adjuntos totales: {self.stats['total_attachments']}")
print(f"[*] Errores de análisis: {self.stats['parse_errors']}")
return report_path
def close(self):
self.pst.close()
def main():
if len(sys.argv) < 3:
print("Uso: python process.py <archivo_pst> <directorio_salida>")
sys.exit(1)
analyzer = PSTForensicAnalyzer(sys.argv[1], sys.argv[2])
analyzer.generate_report()
analyzer.close()
if __name__ == "__main__":
main()
Análisis de Cabeceras de Correo
Cabeceras clave para la investigación forense:
| Cabecera | Valor Forense |
|---|---|
| Received | Cadena de enrutamiento del mensaje (leer de abajo arriba) |
| X-Originating-IP | Dirección IP real del remitente |
| Message-ID | Identificador único para correlación |
| Date | Marca de tiempo de envío |
| Return-Path | Dirección de rebote (puede diferir de From) |
| DKIM-Signature | Firma de autenticación del dominio |
| Authentication-Results | Resultados de verificación SPF, DKIM, DMARC |
| X-Mailer | Cliente de correo utilizado |
Referencias
- MailXaminer PST Forensics: https://www.mailxaminer.com/blog/outlook-pst-file-forensics/
- Documentación de libpff: https://github.com/libyal/libpff
- Especificación del Formato de Archivo PST: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-pst/
- SANS Email Forensics: https://www.sans.org/blog/email-forensics/
Ejemplo de Salida
$ pffexport /evidence/jsmith_archive.pst -t /analysis/pst_output
pffexport 20231205 - Herramienta de Exportación PST/OST de libpff
=================================================
Entrada: /evidence/jsmith_archive.pst (2.3 GB)
Exportando contenido PST...
Carpetas: 45
Mensajes: 12,456
Archivos adjuntos: 3,234
Contactos: 567
Calendario: 234
Tareas: 89
Exportación completada en 3m 42s.
$ python3 pst_analyzer.py /analysis/pst_output /analysis/email_report
Informe de Análisis Forense PST
==============================
Fuente: jsmith_archive.pst ([email protected])
Rango de Fechas: 2023-06-01 a 2024-01-18
--- Estadísticas del Buzón ---
Correos Totales: 12,456
Enviados: 4,567
Recibidos: 7,889
Con Archivos Adjuntos: 3,234
Eliminados (recuperados): 234
--- Correos de Phishing / Sospechosos ---
Correo #8923
Fecha: 2024-01-15 14:30:22 UTC
De: "IT Support" <[email protected]>
Para: [email protected]
Asunto: Urgente: Restablecimiento de Contraseña Requerido
Cabeceras:
Return-Path: [email protected]
X-Originating-IP: 203.0.113.55
Received: de mail-relay.c0rporate-help.com (203.0.113.55)
SPF: FALLÓ (dominio c0rporate-help.com)
DKIM: NINGUNO
DMARC: FALLÓ
Archivos Adjuntos:
- Password_Reset_Form.xlsm (245 KB) SHA-256: 7a3b8c9d...e1f2a3b4
Vista Previa del Cuerpo: "Estimado Empleado, su contraseña caducará en 24 horas.
Abra el formulario adjunto para restablecer sus credenciales..."
--- Indicadores de Exfiltración de Datos ---
Correo #9102
Fecha: 2024-01-16 03:15:45 UTC
De: [email protected]
Para: [email protected]
Asunto: (sin asunto)
Archivos Adjuntos:
- archive_part1.7z (24.5 MB) - encriptado
- archive_part2.7z (24.5 MB) - encriptado
Correo #9103
Fecha: 2024-01-16 03:18:22 UTC
De: [email protected]
Para: [email protected]
Asunto: Re:
Archivos Adjuntos:
- archive_part3.7z (18.2 MB) - encriptado
--- Coincidencias de Palabras Clave ---
"confidencial": 45 correos
"contraseña": 23 correos
"transferencia": 12 correos
"renuncia": 3 correos
"eliminar evidencia": 1 correo (Correo #9200, 2024-01-17 22:30:00 UTC)
Resumen:
Correos de phishing detectados: 1 (vector de compromiso inicial)
Correos enviados sospechosos: 5 (a cuentas personales con archivos adjuntos)
Archivos adjuntos encriptados: 3 (67.2 MB en total - posible exfiltración)
Informe: /analysis/email_report/pst_forensic_report.json