Windows Forensics — Event Logs, Prefetch & Análisis Forense
Windows Forensics — Event Logs, Prefetch & Análisis Forense
Tipo: Referencia general de análisis forense en Windows
Aplicación: CTF, DFIR, Threat Hunting
Herramientas: evtx_dump, jq, python-prefetch, PECmd
Tags: #forensics #dfir #windows #eventlog #sysmon #prefetch #ctf
Índice
- El problema que Event Log resuelve
- Arquitectura del sistema de logs
- El formato binario EVTX
- Binary XML BXML
- Los tres componentes principales
- EventIDs de referencia
- El formato Prefetch PF
- evtx_dump — conversión a JSON
- Anatomía del JSON
- jq — referencia completa
- Correlación de artefactos
- Flujo de análisis en un CTF
El problema que Event Log resuelve
Windows necesita que todos sus componentes — kernel, drivers, servicios, aplicaciones — puedan dejar constancia de lo que hacen en un formato unificado, persistente y auditable.
Sin un sistema centralizado cada componente haría lo que quisiera:
Driver de red → C:\net.log (formato inventado)
Antivirus → C:\av\events.txt (formato distinto)
Servicio impresión → no escribe nada
Kernel → memoria RAM (se pierde al apagar)
La solución es el Windows Event Log Service — un único servicio que recibe mensajes de todos los componentes y los serializa en archivos .evtx con formato binario estructurado, CRC32 de integridad y canales separados por tipo de fuente.
Driver de red ─┐
SCM (servicios) ─┤
Sysmon driver ─┼──► Event Log Service ──► archivos .evtx en disco
LSASS (seguridad) ─┤
Aplicaciones ─┘
Ventajas del diseño:
- Formato único — una sola herramienta lee todos los logs
- Integridad — CRC32 detecta modificaciones post-escritura
- Persistencia — sobrevive reinicios
- Canales separados — cada fuente tiene su propio archivo
- Centralización — un SIEM puede recoger logs de toda la red
Arquitectura del sistema de logs
Canales (Channels)
Cada fuente de eventos escribe en su propio canal, que corresponde a un archivo .evtx en disco:
C:\Windows\System32\winevt\Logs\
├── System.evtx ← SCM, kernel, drivers
├── Security.evtx ← LSASS, auditoría
├── Application.evtx ← aplicaciones de usuario
├── Microsoft-Windows-Sysmon%4Operational.evtx ← Sysmon driver
├── Microsoft-Windows-PowerShell%4Operational.evtx ← PowerShell
├── Microsoft-Windows-TaskScheduler%4Operational.evtx ← Tareas programadas
└── Microsoft-Windows-TerminalServices-*.evtx ← RDP
Interface de escritura
Los componentes envían eventos usando:
ReportEvent() ← API antigua (Win XP)
EvtReportEvent() ← API moderna (Vista+)
EtwWrite() ← vía ETW (Event Tracing for Windows)
Interface de lectura
Las herramientas leen eventos usando:
EvtQuery() ← consulta con filtros XPath
EvtNext() ← itera resultados
EvtRender() ← serializa a XML/texto
evtx_dump usa estas APIs o parsea directamente el binario siguiendo la especificación del formato.
El formato binario EVTX
Un archivo .evtx tiene tres capas jerárquicas:
┌─────────────────────────────────────┐
│ FILE HEADER (4096 bytes) │ metadatos del archivo completo
├─────────────────────────────────────┤
│ CHUNK 0 (65,536 bytes) │
│ ┌───────────────────────────────┐ │
│ │ CHUNK HEADER │ │ metadatos del chunk
│ │ STRING TABLE │ │ strings reutilizables
│ │ TEMPLATE TABLE │ │ estructuras XML reutilizables
│ │ EVENT RECORD 1 │ │ evento en BXML
│ │ EVENT RECORD 2 │ │
│ │ ... │ │
│ └───────────────────────────────┘ │
├─────────────────────────────────────┤
│ CHUNK 1 (65,536 bytes) │
│ ... │
└─────────────────────────────────────┘
File Header (4096 bytes)
Offset Tamaño Campo Descripción
────────────────────────────────────────────────────────────────
0x0000 8 Magic "ElfFile\x00"
0x0008 8 Oldest chunk número del chunk más antiguo
0x0010 8 Current chunk chunk activo (donde se escribe)
0x0018 8 Next record ID próximo EventRecordID a usar
0x0020 4 Header size 128 (siempre)
0x0024 2 Minor version 1
0x0026 2 Major version 3
0x0028 2 Header chunk size 4096
0x002A 2 Number of chunks total de chunks en el archivo
0x002C 76 Reserved zeros
0x0078 4 File flags 0=normal, 1=dirty (escritura incompleta)
0x007C 4 CRC32 checksum del header
El Event Log Service usa Current chunk para saltar directamente al chunk activo sin escanear todo el archivo.
Chunk Header (65,536 bytes)
Offset Tamaño Campo Descripción
────────────────────────────────────────────────────────────────
0x0000 8 Magic "ElfChnk\x00"
0x0008 8 First record number EventRecordID del primer evento
0x0010 8 Last record number EventRecordID del último evento
0x0018 8 First file offset offset en el .evtx al primer evento
0x0020 8 Last file offset offset en el .evtx al último evento
0x0028 4 Header CRC32 cubre 0x00–0x77 excepto 0x28–0x2B
0x002C 76 Reserved
0x0078 4 Data CRC32 cubre 0x80–0xFFFF (el payload)
0x007C 4 Reserved
0x0080 ... Event Records eventos en BXML
...
0xFFFF fin del chunk
¿Por qué dos CRC32 separados?
Header CRC32 → protege los metadatos del chunk
Data CRC32 → protege el payload de eventos
Si el sistema se apaga durante una escritura:
Header CRC válido + Data CRC inválido
→ Solo el payload está corrupto
→ Los metadatos (qué record numbers hay) son confiables
Si Header CRC falla:
→ Chunk completamente descartado
→ Los chunks anteriores y posteriores siguen válidos
¿Por qué exactamente 65,536 bytes?
Es 2^16, potencia de 2 que se alinea con páginas de memoria del kernel (4KB). 65,536 / 4,096 = 16 páginas exactas. El kernel puede mapear el chunk directamente a páginas de memoria sin fragmentación, haciendo las operaciones de I/O eficientes.
Event Record Header
Cada evento dentro del chunk tiene su propio header:
Offset Tamaño Campo Descripción
────────────────────────────────────────────────────────────────
0x0000 4 Magic 0x00002A2A ("**\x00\x00")
0x0004 4 Size tamaño total del record en bytes
0x0008 8 EventRecordID contador secuencial — NUNCA se repite
0x0010 8 TimeCreated FILETIME (100ns desde 1601-01-01 UTC)
0x0018 ... BXML data el evento serializado
Size-4 4 Size (repetido) para navegar hacia atrás en el chunk
¿Qué es FILETIME?
Entero de 64 bits que cuenta intervalos de 100 nanosegundos desde el 1 de enero de 1601 UTC. Esta fecha es el inicio del calendario gregoriano proléptico.
Conversión FILETIME → datetime:
FILETIME = 133389163824654660
EPOCH_DELTA = 116444736000000000 (diferencia 1601↔1970 en 100ns)
unix_100ns = FILETIME - EPOCH_DELTA
unix_seconds = unix_100ns / 10_000_000
Resultado: 2023-09-07 11:53:02.465466 UTC
Resolución de 100 nanosegundos — mucho más preciso que los timestamps de texto. Crítico para correlación temporal en análisis forense.
EventRecordID — por qué es importante:
- Es un contador monotónico que el kernel incrementa secuencialmente
- No puede ser manipulado desde user-mode sin acceso directo al binario
- Sirve para ordenar eventos cuando los timestamps son idénticos o muy cercanos
- Si hay gaps en la secuencia (ej: 7313, 7324, 7330) significa que eventos intermedios fueron borrados o están en otro archivo
Binary XML (BXML)
Los eventos se guardan como Binary XML en vez de texto plano. Esto reduce el tamaño del archivo hasta un 75%.
El problema del texto plano
Si el EventID 7045 ocurre 10,000 veces, el string "Service Control Manager" aparecería 10,000 veces en el archivo. En BXML aparece una sola vez en la String Table y cada evento guarda solo un offset de 4 bytes.
Tokens BXML
Token Nombre Operandos
────────────────────────────────────────────────────────────────────────
0x00 EOF ninguno
0x01 Open Start Element uint16 dep_id + uint32 name_offset→StringTable
0x02 Close Start Element ninguno → emite ">"
0x03 Close Element ninguno → emite "</tag>"
0x04 Attribute uint32 name_offset → StringTable
0x05 Close Attribute ninguno
0x06 Template Instance uint32 template_offset + sustituciones
0x21 Value string (corto) uint16 length + bytes UTF-16LE
0x41 Value string (largo) uint32 length + bytes UTF-16LE
0x81 Value int8 1 byte con signo
0x82 Value uint8 1 byte sin signo
0x83 Value int16 2 bytes LE con signo
0x84 Value uint16 2 bytes LE sin signo
0x85 Value FILETIME 8 bytes uint64 LE
0x86 Value SYSTEMTIME 16 bytes
0x87 Value GUID 16 bytes
0x88 Value int32 4 bytes LE con signo
0x89 Value uint32 4 bytes LE sin signo
String Table
Diccionario de strings indexado por offset dentro del chunk:
Offset String
──────────────────────────────────────
0x0090 "Event"
0x009E "System"
0x00AC "Provider"
0x00BC "Name"
0x00C4 "Service Control Manager"
0x00E0 "EventID"
0x00F0 "TimeCreated"
0x0104 "Computer"
...
Cuando el parser encuentra token 0x01 + offset 0x00C4, lee "Service Control Manager" de la String Table.
Template Table
Guarda estructuras XML recurrentes. Si el EventID 7045 siempre tiene los mismos campos, esa estructura se guarda una vez:
Template para EventID 7045:
<Event>
<System>
<Provider Name="{VALOR_1}"/>
<EventID>{VALOR_2}</EventID>
<TimeCreated SystemTime="{VALOR_3}"/>
<Computer>{VALOR_4}</Computer>
</System>
<EventData>
<ServiceName>{VALOR_5}</ServiceName>
<ImagePath>{VALOR_6}</ImagePath>
</EventData>
</Event>
Cada evento solo guarda:
template_offset = 0x1234
VALOR_1 = "Service Control Manager"
VALOR_2 = 7045
VALOR_3 = 133389163824654660 (FILETIME)
VALOR_4 = "Forela-Wkstn002.forela.local"
VALOR_5 = "PSEXESVC"
VALOR_6 = "%SystemRoot%\PSEXESVC.exe"
Los tres componentes principales
SCM — Service Control Manager
¿Dónde vive?
DLL services.dll cargada dentro de services.exe. Arranca en la fase Session 0 del boot, antes de cualquier login de usuario, con privilegios de SYSTEM.
¿Qué es un servicio?
Un proceso especial que:
- Arranca automáticamente sin usuario logueado
- Corre en background sin interfaz gráfica
- Corre como
SYSTEM,LocalService,NetworkServiceo una cuenta específica - El SCM lo puede iniciar, detener, pausar y eliminar
¿Dónde guarda la configuración?
HKLM\SYSTEM\CurrentControlSet\Services\[NombreServicio]\
ImagePath = ruta del ejecutable
Start = 0=Boot | 1=System | 2=Auto | 3=Demand | 4=Disabled
Type = 1=KernelDriver | 2=FileSystemDriver | 16=Win32OwnProcess
ObjectName = cuenta bajo la que corre (LocalSystem, etc.)
Description = descripción legible
¿Cómo funciona CreateService() remotamente?
PsExec no puede crear un proceso directamente en una máquina remota — Windows no tiene esa API. Usa el SCM vía RPC sobre SMB:
Atacante Víctima (SCM)
│── SMB2 TREE_CONNECT \IPC$ ──────►│
│── SMB2 CREATE \svcctl ──────────►│ abre Named Pipe del SCM
│── RPC OpenSCManager() ──────────►│
│── RPC CreateService() ──────────►│ escribe en registro
│ │ genera EventID 7045
│── RPC StartService() ───────────►│ lanza el proceso
│ │ genera EventID 7036
│── Named Pipe stdin/stdout ───────│ comunicación establecida
\svcctl es el Named Pipe que expone la interfaz RPC del SCM. Visible en Wireshark como tráfico SMB2 hacia ese pipe.
EventIDs del SCM:
7045 Nuevo servicio instalado
ServiceName, ImagePath, ServiceType, StartType, AccountName
→ Instalación de cualquier servicio
7036 Cambio de estado del servicio
ServiceName, State (running | stopped)
→ Confirma que el servicio realmente arrancó o paró
7009 Timeout al iniciar servicio
→ El servicio no arrancó en el tiempo esperado
7034 Servicio terminó inesperadamente
→ Crash del servicio (posible error o kill forzado)
7040 StartType del servicio cambió
→ Alguien modificó la configuración de arranque
7001 Servicio depende de otro que falló
→ Problema de dependencias en la cadena de arranque
LSASS — Local Security Authority Subsystem Service
¿Dónde vive?
Proceso independiente lsass.exe. Arranca durante el boot con privilegios de SYSTEM. En Windows 10+ está protegido por PPL (Protected Process Light) — ni el administrador puede abrirlo con OpenProcess() directamente sin un driver firmado.
¿Qué hace exactamente?
1. AUTENTICACIÓN
Verifica credenciales contra SAM local o Domain Controller
Soporta múltiples protocolos: NTLM, Kerberos, Digest
2. TOKENS DE SEGURIDAD
Cuando un proceso arranca, LSASS le asigna un token con:
usuario, grupos, privilegios
Ese token acompaña cada operación del proceso
3. AUDIT LOG
Todo evento de seguridad relevante pasa por LSASS
antes de escribirse en Security.evtx
Authentication Packages (DLLs internas):
msv1_0.dll ← NTLM — protocolo challenge-response
kerberos.dll ← Kerberos — tickets cifrados con clave del dominio
wdigest.dll ← Digest — legacy, guarda credenciales en CLARO en memoria
tspkg.dll ← Terminal Services / RDP
cloudap.dll ← Azure AD / Microsoft accounts
¿Por qué Mimikatz extrae credenciales de LSASS?
Porque estos paquetes mantienen credenciales en memoria para el Single Sign-On. Mimikatz usa OpenProcess() + ReadProcessMemory() para leer esa memoria directamente. Por eso el EventID 10 de Sysmon con TargetImage=lsass.exe es una señal de alarma crítica.
El flujo de autenticación NTLM:
Cliente Servidor (LSASS)
│── NEGOTIATE ───────────────►│
│◄─ CHALLENGE (nonce) ────────│ número aleatorio
│── AUTHENTICATE ────────────►│ hash(contraseña + nonce)
│ │ LSASS verifica
│ │ → local: compara con SAM
│ │ → dominio: pregunta al DC (EventID 4776)
│◄─ Sesión establecida ────────│ genera EventID 4624
El flujo de autenticación Kerberos:
Cliente DC (KDC) Servidor
│── AS-REQ ───────►│
│◄─ TGT ───────────│ ticket cifrado con clave del DC
│── TGS-REQ ───────►│ "quiero acceder a servidor X"
│◄─ ST ────────────│ Service Ticket para el servidor
│── AP-REQ ──────────────────────────►│
│ │ LSASS verifica el ST
│ │ genera EventID 4624
│◄─ Sesión establecida ───────────────│
EventIDs de LSASS en Security.evtx:
4624 Logon exitoso
LogonType:
2 = Interactive (login físico en la máquina)
3 = Network (SMB, recursos compartidos, PsExec)
4 = Batch (tarea programada)
5 = Service (servicio arrancando con cuenta específica)
7 = Unlock (desbloqueo de pantalla)
10 = RemoteInteractive (RDP)
11 = CachedInteractive (credenciales cacheadas, sin red)
IpAddress: IP de origen
WorkstationName: hostname de origen
TargetUserName: usuario que se autenticó
AuthenticationPackageName: NTLM | Kerberos
4625 Logon fallido
SubStatus:
0xC000006D = nombre de usuario incorrecto
0xC000006A = contraseña incorrecta
0xC0000064 = usuario no existe
0xC0000234 = cuenta bloqueada
0xC000015B = logon type no permitido
→ Muchos 4625 seguidos = fuerza bruta o password spray
4634 Logoff
→ Correlaciona con 4624 para calcular duración de sesión
4648 Logon con credenciales explícitas
→ Ocurre cuando se usa runas, PsExec con -u/-p, o WMI
→ TargetUserName: usuario utilizado
→ TargetServerName: a qué servidor
4672 Privilegios especiales asignados
→ Ocurre junto con 4624 cuando el usuario es administrador
→ 4624 + 4672 = el atacante entró como admin
4688 Proceso creado (requiere auditoría activada)
→ Versión básica sin CommandLine completo
→ Sysmon EventID 1 es mucho más detallado
4697 Servicio instalado en el sistema
→ Similar a 7045 pero desde el canal Security
→ Incluye el AccountName que realizó la instalación
4698 Tarea programada creada
→ Técnica de persistencia común
4702 Tarea programada modificada
4720 Cuenta de usuario creada
→ Posible backdoor
4724 Intento de reset de contraseña
4732 Usuario añadido a grupo local
→ Si el grupo es Administrators: escalada de privilegios
4756 Usuario añadido a grupo universal
→ En dominios: escalada a grupos de dominio
4776 Validación de credenciales NTLM
→ En el DC cuando se autentica vía NTLM de dominio
5140 Recurso compartido accedido (share)
5142 Recurso compartido creado
5143 Recurso compartido modificado
5145 Acceso a objeto dentro de un share
Sysmon — System Monitor
¿Qué es?
Herramienta de Sysinternals (Microsoft) que instala un driver kernel (SysmonDrv.sys) para monitorizar actividad del sistema con mucho más detalle que el Event Log nativo. Se configura con un archivo XML de reglas que define qué capturar y qué filtrar.
¿Cómo funciona a nivel de kernel?
El driver registra kernel callbacks — hooks que el kernel llama automáticamente cuando ocurren eventos:
PsSetCreateProcessNotifyRoutineEx() → procesos nuevos / terminados
PsSetCreateThreadNotifyRoutine() → threads nuevos
CmRegisterCallback() → cambios en el registro
ObRegisterCallbacks() → acceso a objetos del kernel
FltRegisterFilter() (MiniFilter) → operaciones de archivo
Cuando el kernel crea un proceso, llama a todos los callbacks registrados antes de que el proceso empiece a ejecutar. Sysmon recibe la notificación, recoge los datos y los envía al Event Log Service.
¿Por qué Sysmon ve más que el Event Log nativo?
EventID nativo 4688 (proceso creado):
→ Solo disponible si "Audit Process Creation" está activado
→ CommandLine requiere configuración adicional
→ Sin hashes del ejecutable
→ Sin GUID del proceso padre
Sysmon EventID 1 (proceso creado):
→ Siempre disponible si Sysmon está instalado
→ CommandLine completo siempre
→ MD5, SHA256, IMPHASH del ejecutable
→ GUID único del proceso (para correlación)
→ Usuario y dominio
→ Resolución de 100ns
EventIDs de Sysmon:
1 ProcessCreate
Image: ruta completa del ejecutable
CommandLine: argumentos exactos
ParentImage: proceso padre
ParentCommandLine: argumentos del padre
Hashes: MD5 + SHA256 + IMPHASH
User: DOMINIO\usuario
UtcTime: timestamp 100ns precisión
ProcessGuid: GUID único del proceso
ParentProcessGuid: GUID del padre
2 FileCreationTimeChanged
→ Timestomping detectado (cambio de fecha de archivo)
3 NetworkConnect
Image: proceso que hizo la conexión
SourceIp: IP origen
SourcePort: puerto origen
DestinationIp: IP destino
DestinationPort: puerto destino
Protocol: tcp | udp
Initiated: true si el proceso inició la conexión
4 ServiceStateChanged
→ Estado del servicio Sysmon mismo
5 ProcessTerminated
Image: proceso que terminó
ProcessGuid: correlaciona con EventID 1
6 DriverLoaded
ImageLoaded: ruta del driver
Signed: si tiene firma válida
Signature: nombre del firmante
→ Drivers sin firma = muy sospechoso
7 ImageLoaded
Image: proceso que cargó la DLL
ImageLoaded: ruta de la DLL
Signed: si tiene firma válida
→ DLL sin firma en proceso del sistema = sospechoso
8 CreateRemoteThread
SourceImage: proceso que creó el thread
TargetImage: proceso donde se creó
StartAddress: dirección de inicio del thread
→ Técnica clásica de inyección de código
9 RawAccessRead
Image: proceso que leyó el disco directamente
Device: disco accedido
→ Herramientas de extracción de datos bypaseando el filesystem
10 ProcessAccess
SourceImage: proceso que abrió el handle
TargetImage: proceso accedido
GrantedAccess: permisos obtenidos (en hex)
→ SourceImage=mimikatz TargetImage=lsass = volcado de credenciales
→ 0x1010 = PROCESS_VM_READ — lectura de memoria
11 FileCreate
Image: proceso que creó el archivo
TargetFilename: ruta completa del archivo creado
CreationUtcTime: timestamp de creación
12 RegistryEvent (CreateKey / DeleteKey)
EventType: CreateKey | DeleteKey
TargetObject: ruta completa en el registro
13 RegistryEvent (SetValue)
EventType: SetValue
TargetObject: ruta completa
Details: nuevo valor
14 RegistryEvent (RenameKey)
15 FileCreateStreamHash
→ Detecta Alternate Data Streams (técnica de ocultamiento)
→ TargetFilename: archivo con el stream
16 ServiceConfigurationChange
→ Configuración de Sysmon modificada
17 PipeCreated
PipeName: nombre completo del Named Pipe
Image: proceso que creó el pipe
18 PipeConnected
PipeName: nombre del pipe
Image: proceso cliente que se conectó
22 DNSQuery
QueryName: dominio consultado
QueryResults: IPs resueltas
Image: proceso que hizo la consulta
→ C2 por DNS: proceso sospechoso consultando dominios aleatorios
23 FileDelete
TargetFilename: archivo borrado
Hashes: hash del archivo antes de borrarse
→ El atacante borró el binario pero este evento persiste
25 ProcessTampering
→ Técnicas de ocultamiento de procesos (process hollowing, etc.)
255 Error
→ Error interno de Sysmon
→ Puede indicar evasión activa o incompatibilidad
EventIDs de referencia rápida
Movimiento lateral
Artefacto EventID Descripción
──────────────────────────────────────────────────────────────
Security.evtx 4624 Logon exitoso tipo 3 (network) → origen del atacante
Security.evtx 4648 Logon con credenciales explícitas (runas, psexec -u)
Security.evtx 4672 Privilegios especiales → atacante entró como admin
Security.evtx 5140 Acceso a share → \IPC$, \C$, etc.
System.evtx 7045 Servicio instalado → payload del atacante
System.evtx 7036 Servicio arrancó → ejecución confirmada
Sysmon 1 Proceso creado → ejecución con CommandLine completo
Sysmon 3 Conexión de red → qué proceso se conectó a dónde
Sysmon 11 Archivo creado → payload dropeado en disco
Sysmon 17 Named Pipe creado → canal de comunicación
Sysmon 18 Named Pipe conectado → cliente conectó al pipe
Persistencia
Security.evtx 4698 Tarea programada creada
Security.evtx 4702 Tarea programada modificada
Security.evtx 4720 Cuenta de usuario creada
Security.evtx 4732 Usuario añadido a grupo local Administrators
System.evtx 7045 Servicio instalado con StartType=auto (persiste reboots)
Sysmon 12/13 Modificación de Run keys en registro
Sysmon 11 Archivo dropeado en carpetas de startup
Escalada de privilegios
Security.evtx 4672 Privilegios especiales asignados
Security.evtx 4688 Proceso creado (si auditoría activa)
Sysmon 1 Proceso con integridad elevada
Sysmon 10 Acceso a lsass.exe → volcado de credenciales
Sysmon 8 CreateRemoteThread → inyección de código
Exfiltración
Sysmon 3 Conexiones salientes a IPs externas inusuales
Sysmon 22 DNS queries a dominios sospechosos
Security.evtx 5140 Acceso a shares de red
Sysmon 11 Archivos comprimidos creados (rar, zip, 7z)
El formato Prefetch (.pf)
¿Qué es y por qué existe?
El Prefetch es un mecanismo de optimización de Windows. El sistema monitoriza los primeros 10 segundos de carga de cada proceso y guarda qué archivos y DLLs cargó, en qué orden y desde qué paths. La próxima vez que el proceso arranque, Windows puede precargar esos archivos en memoria antes de que el proceso los pida, reduciendo el tiempo de arranque.
¿Dónde se guarda?
C:\Windows\Prefetch\
├── CHROME.EXE-XXXXXXXX.pf
├── PSEXESVC.EXE-XXXXXXXX.pf
├── PSEXEC64.EXE-XXXXXXXX.pf
└── ...
El sufijo de 8 caracteres hexadecimales es un hash del path completo del ejecutable.
Cálculo del hash (sufijo)
# Windows XP / Vista — algoritmo rotate-right
def hash_xp(path: str) -> int:
h = 0
for ch in path.upper():
h = ((h >> 1) | (h << 31)) & 0xFFFFFFFF # rotate right 1 bit
h = (h + ord(ch)) & 0xFFFFFFFF
return h
# Windows Vista / 7 / 8 / 10 — algoritmo multiplicativo
def hash_vista(path: str) -> int:
h = 314159265 # seed constante
for ch in path.upper():
h = (37 * h + ord(ch)) & 0xFFFFFFFF
return h
# El hash se imprime como 8 dígitos hex uppercase
suffix = f"{hash_vista(path):08X}"
Implicación forense: Dos ejecuciones del mismo binario desde paths distintos generan hashes distintos y por tanto archivos .pf distintos. Si el atacante renombra psexec.exe a svchost.exe, el hash cambia aunque el contenido sea idéntico.
Compresión MAM (Windows 10+)
Windows 10 comprime los .pf con Xpress Huffman y añade un header de 8 bytes:
Offset Tamaño Campo
────────────────────────────────────────
0x00 3 Magic "MAM"
0x03 1 Tipo de compresión:
0x01 = XPRESS plain
0x02 = XPRESS LZ77
0x04 = XPRESS Huffman ← el más común en Win10
0x04 4 Uncompressed size (uint32 LE)
0x08 ... Payload comprimido
Para verificar si un .pf está comprimido:
xxd archivo.pf | head -1
# Si empieza con "4d 41 4d" (MAM) → comprimido
# Si empieza con "1e 00 00 00" (versión 30) → no comprimido
Descomprimir en Linux:
pip install ms-compress
python3 -c "
import ms_compress, struct, sys
data = open(sys.argv[1], 'rb').read()
size = struct.unpack_from('<I', data, 4)[0]
dec = ms_compress.xpress_huffman.decompress(data[8:], size)
open(sys.argv[1].replace('.pf','_dec.pf'), 'wb').write(dec)
" ARCHIVO.pf
Estructura interna del .pf (formato 30, Windows 10)
Sección Descripción
────────────────────────────────────────────────────────────────
FILE HEADER Versión, magic, nombre del exe, hash, tamaño
SECTION A File Metrics Array — tiempos de carga de cada archivo
SECTION B Volume Information — información del volumen
SECTION C String Data — rutas completas de todos los archivos
SECTION D Last Run Times + Execution Counter
FILE HEADER (84 bytes):
Offset Tamaño Campo Descripción
────────────────────────────────────────────────────────────────
0x00 4 Version 23=Win7 | 26=Win8 | 30=Win10
0x04 4 Signature "MAM\x00" o firma propia
0x0C 4 File Size tamaño total del .pf
0x10 60 Exe Name nombre del ejecutable en UTF-16LE (29 chars max)
0x4C 4 Hash sufijo del nombre de archivo
0x50 4 Flags reservado
SECTION D — Lo más importante para forense:
Offset Tamaño Campo
────────────────────────────────────────────────────────────────
+0x00 64 run_times[8] 8 × FILETIME — últimas 8 ejecuciones
orden descendente (más reciente = [0])
+0x40 4 run_count número total de ejecuciones registradas
Ejemplo de lectura:
import struct
from datetime import datetime, timezone, timedelta
def filetime_to_dt(ft):
if ft == 0: return None
EPOCH = 116444736000000000
us = (ft - EPOCH) // 10
return datetime(1970,1,1,tzinfo=timezone.utc) + timedelta(microseconds=us)
# Leer los 8 timestamps
for i in range(8):
ft = struct.unpack_from('<Q', section_d_data, i * 8)[0]
dt = filetime_to_dt(ft)
if dt:
print(f" Ejecución {i+1}: {dt.strftime('%Y-%m-%d %H:%M:%S.%f')} UTC")
run_count = struct.unpack_from('<I', section_d_data, 0x40)[0]
print(f" Total ejecuciones: {run_count}")
SECTION C — Rutas de archivos cargados:
Contiene strings null-terminated en UTF-16LE con las rutas completas de todos los archivos que el proceso cargó durante los primeros 10 segundos:
\DEVICE\HARDDISKVOLUME3\WINDOWS\SYSTEM32\NTDLL.DLL
\DEVICE\HARDDISKVOLUME3\WINDOWS\SYSTEM32\KERNEL32.DLL
\DEVICE\HARDDISKVOLUME3\WINDOWS\PSEXESVC.EXE
\DEVICE\HARDDISKVOLUME3\WINDOWS\PSEXEC-FORELA-WKSTN001-95F03CFE.KEY
Importancia forense: Aunque el atacante borre el binario, la ruta sigue en el .pf. Y el .pf registra la última ejecución aunque el binario ya no exista.
Herramientas para parsear .pf
PECmd.exe (Windows / Linux con .NET):
# Un archivo específico
dotnet PECmd.dll -f PSEXESVC.EXE-AD70946C.pf
# Todos los .pf de una carpeta
dotnet PECmd.dll -d /ruta/prefetch/ --csv /output/
# Salida relevante:
# Run count: 9
# Run time 1: 2023-09-07 12:10:03 ← más reciente
# Run time 5: 2023-09-07 12:06:54 ← 5ª última
# Files loaded: lista de rutas
python-prefetch (Linux):
pip install python-prefetch
python prefetch.py -f ARCHIVO.pf
Script Python directo (sin dependencias):
#!/usr/bin/env python3
import struct, sys
from datetime import datetime, timezone, timedelta
def filetime_to_dt(ft):
if ft == 0: return None
EPOCH = 116444736000000000
us = (ft - EPOCH) // 10
return datetime(1970,1,1,tzinfo=timezone.utc) + timedelta(microseconds=us)
def parse(path):
data = open(path, 'rb').read()
if data[:3] == b'MAM':
print("[!] Archivo comprimido MAM — descomprime primero con ms-compress")
return
version = struct.unpack_from('<I', data, 0)[0]
exe_name = data[0x10:0x4A].decode('utf-16-le', errors='ignore').rstrip('\x00')
pf_hash = struct.unpack_from('<I', data, 0x4C)[0]
print(f"Exe: {exe_name}")
print(f"Hash: {pf_hash:08X}")
print(f"Version: {version}")
# Offsets de secciones (formato 30)
sec_d_off = struct.unpack_from('<I', data, 0x6C)[0]
print("\n=== Ejecuciones ===")
run_count = struct.unpack_from('<I', data, sec_d_off + 0x10)[0]
print(f"Total: {run_count}")
for i in range(8):
ft = struct.unpack_from('<Q', data, sec_d_off + i*8)[0]
dt = filetime_to_dt(ft)
if dt:
print(f" [{i+1}] {dt.strftime('%Y-%m-%d %H:%M:%S.%f')} UTC")
# Section C — strings
sec_c_off = struct.unpack_from('<I', data, 0x64)[0]
sec_c_len = struct.unpack_from('<I', data, 0x68)[0]
raw = data[sec_c_off:sec_c_off+sec_c_len]
strings = raw.decode('utf-16-le', errors='ignore').split('\x00')
print("\n=== Archivos cargados ===")
for s in strings:
if s.strip():
print(f" {s}")
if __name__ == '__main__':
parse(sys.argv[1])
evtx_dump — conversión a JSON
¿Qué hace?
evtx_dump lee el binario .evtx y revierte el proceso de serialización:
archivo.evtx (binario)
↓
lee FILE HEADER → valida magic "ElfFile\x00"
↓
lee cada CHUNK → valida CRC32 header + CRC32 data
↓
lee cada EVENT RECORD → parsea BXML token por token
↓
resuelve String Table → reemplaza offsets por strings reales
↓
resuelve Template Table → reconstruye estructura XML
↓
convierte FILETIME → ISO 8601
↓
serializa a JSON → una línea por evento (JSONL)
Uso básico
# Convertir un .evtx a JSON (una línea por evento)
evtx_dump -o jsonl Sistema.evtx > system.json
evtx_dump -o jsonl Security.evtx > logs.json
evtx_dump -o jsonl 'Microsoft-Windows-Sysmon%4Operational.evtx' > sysmon.json
# El formato JSONL (JSON Lines) tiene un objeto por línea
# Esto permite procesarlo con jq línea a línea sin cargar todo en memoria
Por qué JSONL y no un array JSON
Un archivo .evtx puede tener millones de eventos. Si evtx_dump generara un array JSON [{...},{...},...] tendría que cargar todo en memoria para cerrarlo con ]. Con JSONL cada línea es un JSON completo e independiente — puedes procesarlo con jq sin límite de tamaño.
Anatomía del JSON
Estructura completa de un evento
{
"Event": {
"#attributes": {
"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"
},
"System": {
"Provider": {
"#attributes": {
"Name": "Service Control Manager",
"Guid": "{555908d1-a6d7-4695-8e1e-26931d2012f4}",
"EventSourceName": "Service Control Manager"
}
},
"EventID": {
"#attributes": {
"Qualifiers": 16384
},
"#text": 7045
},
"Version": 0,
"Level": 4,
"Task": 0,
"Opcode": 0,
"Keywords": "0x8080000000000000",
"TimeCreated": {
"#attributes": {
"SystemTime": "2023-09-07T11:53:02.465466Z"
}
},
"EventRecordID": 7313,
"Execution": {
"#attributes": {
"ProcessID": 760,
"ThreadID": 868
}
},
"Channel": "System",
"Computer": "Forela-Wkstn002.forela.local",
"Security": {
"#attributes": {
"UserID": "S-1-5-18"
}
}
},
"EventData": {
"ServiceName": "PSEXESVC",
"ImagePath": "%SystemRoot%\\PSEXESVC.exe",
"ServiceType": "user mode service",
"StartType": "demand start",
"AccountName": "LocalSystem"
}
}
}
Campo por campo
Event.#attributes.xmlns
Namespace XML. Siempre el mismo en todos los eventos. No tiene valor forense. Ignóralo.
Event.System.Provider
Quién generó el evento. Name es el nombre legible. Guid es el identificador único del provider — más confiable que el nombre porque no puede cambiar. EventSourceName es el nombre legacy para compatibilidad con APIs antiguas.
Event.System.EventID
El número de evento. La estructura tiene dos partes:
#attributes.Qualifiers— campo legacy de Windows XP, sin valor forense moderno#text— el número real del EventID, el que usas en todos los filtros
La razón por la que está dentro de #attributes y #text en vez de ser un número directo es que en XML original se escribe así: <EventID Qualifiers="16384">7045</EventID>. El atributo XML Qualifiers y el contenido del elemento 7045 se convierten en esa estructura JSON.
Event.System.Level
Severidad del evento:
1 = Critical
2 = Error
3 = Warning
4 = Information
5 = Verbose
Event.System.TimeCreated.#attributes.SystemTime
Timestamp en formato ISO 8601 UTC. Viene de convertir el FILETIME del Event Record Header. Tiene resolución de hasta microsegundos aunque muchos eventos solo tienen precisión de segundos.
Event.System.EventRecordID
Contador secuencial dentro del canal. Monotónico. Uso principal: ordenar eventos con timestamps idénticos o muy cercanos, y detectar gaps que indican eventos borrados.
Event.System.Execution.#attributes
ProcessID— PID del proceso que escribió el evento (no el proceso del que habla el evento)ThreadID— thread dentro de ese proceso
Para eventos del SCM, el ProcessID es el PID de services.exe. Para Sysmon, es el PID del proceso Sysmon. Útil para correlación avanzada.
Event.System.Computer
Hostname de la máquina que generó el evento. Crítico para entender si estás mirando logs de la víctima o del atacante.
Event.System.Security.#attributes.UserID
SID del usuario que generó el evento. S-1-5-18 es SYSTEM. SIDs con formato S-1-5-21-XXXX-XXXX-XXXX-YYYY son usuarios de dominio donde YYYY es el RID (identificador relativo).
Event.EventData
El contenido específico del evento. Completamente diferente según el EventID. Nunca asumas qué campos hay sin saber el EventID:
EventID 7045 → ServiceName, ImagePath, ServiceType, StartType, AccountName
EventID 4624 → LogonType, IpAddress, WorkstationName, TargetUserName
Sysmon 1 → Image, CommandLine, ParentImage, Hashes, User, UtcTime
Sysmon 11 → Image, TargetFilename, CreationUtcTime
Sysmon 17 → PipeName, Image
Por qué las rutas JSON son distintas
# EventID tiene atributos XML → necesita ["#text"]
.Event.System.EventID["#text"]
# TimeCreated tiene el timestamp como atributo XML → necesita ["#attributes"]
.Event.System.TimeCreated["#attributes"].SystemTime
# Execution también tiene atributos XML
.Event.System.Execution["#attributes"].ProcessID
# ServiceName es un elemento hijo simple → acceso directo
.Event.EventData.ServiceName
# Computer es un elemento hijo simple → acceso directo
.Event.System.Computer
La regla: si en el XML original es un atributo (<tag atributo="valor">), en el JSON está dentro de #attributes. Si es el contenido del elemento (<tag>valor</tag>), está en #text. Si es un elemento hijo (<parent><child>valor</child></parent>), es una clave directa en el objeto JSON.
jq — referencia completa
¿Qué es jq?
Procesador de JSON para línea de comandos. Opera en streams — lee JSON de stdin y aplica filtros transformando la entrada. Es el equivalente de grep + awk + sed pero para JSON.
Flags esenciales
-r # Raw output — imprime strings sin comillas
# Sin -r: "Forela-Wkstn002"
# Con -r: Forela-Wkstn002
-R # Raw input — lee línea a línea como strings crudas
# Necesario para JSONL donde cada línea es un JSON separado
# Sin -R, jq espera un único JSON válido en todo el input
-c # Compact output — una línea por objeto (sin indentación)
# Útil para pipear a otros comandos
-e # Exit status — retorna código de error si el output es null/false
# Útil en scripts
--arg nombre valor # Pasa una variable string a jq
# jq -r --arg ts "2023-09-07" 'select(.time == $ts)'
--argjson nombre json # Pasa una variable JSON a jq
Operadores fundamentales
fromjson?
Convierte cada línea string (que llegó con -R) a objeto JSON. El ? es el operador de supresión de errores — si la línea no es JSON válido (líneas vacías, fragmentos, separadores) la descarta silenciosamente en vez de abortar.
# Sin ?, falla en la primera línea no-JSON:
echo 'no es json' | jq -R 'fromjson'
# Error: Invalid numeric literal at EOF
# Con ?, la ignora y continúa:
echo 'no es json' | jq -R 'fromjson?'
# (sin output, sin error)
select(condición)
Filtra — pasa el objeto solo si la condición es true. Si es false o null, el objeto no aparece en el output.
# Solo eventos con EventID 7045
select(.Event.System.EventID["#text"] == 7045)
# Solo eventos de septiembre
select(.Event.System.TimeCreated["#attributes"].SystemTime | test("2023-09-07"))
# Solo eventos con ServiceName que contenga "PSEXE"
select(.Event.EventData.ServiceName | test("(?i)psexe"))
select(type == "object")
Filtra solo valores que sean objetos JSON {}. Necesario porque fromjson? a veces produce true, false, números o null desde líneas malformadas, y si intentas hacer .Event sobre un booleano, jq rompe con “Cannot index boolean with string”.
.ruta.al.campo
Navega el JSON. Cada . baja un nivel.
.Event # objeto Event
.Event.System # objeto System dentro de Event
.Event.System.Computer # string con el hostname
.Event.EventData.ServiceName # string con el nombre del servicio
["clave"]
Accede a una clave cuando el nombre tiene caracteres especiales como #.
.Event.System.EventID["#text"] # el número del EventID
.Event.System.EventID["#attributes"] # el objeto con Qualifiers
|
Pipe — pasa el output de la izquierda como input de la derecha. Igual que el pipe de bash pero dentro de jq.
.Event.System.EventID["#text"] | . == 7045 # ¿es igual a 7045?
.Event.EventData.ServiceName | test("PSEXE") # ¿contiene PSEXE?
.Event.EventData.ServiceName | ascii_upcase # convertir a mayúsculas
test("regex")
Aplica un regex POSIX al string. Devuelve true o false. Siempre va dentro de un select().
test("(?i)psexe") # case-insensitive, busca "psexe" en cualquier posición
test("^PSEXE") # debe empezar con PSEXE
test("\\.exe$") # debe terminar con .exe (escapa el punto)
test("2023-09-07") # contiene esa fecha
tostring
Convierte cualquier valor a string. null | tostring produce "null" en vez de fallar. Usar antes de test() cuando el campo puede no existir:
# Seguro — si Image es null, tostring lo convierte a "null"
# y test devuelve false sin error
select(.Event.EventData.Image | tostring | test("(?i)psexe"))
{ clave: expresión }
Construye un nuevo objeto JSON con solo los campos que necesitas:
{
tiempo: .Event.System.TimeCreated["#attributes"].SystemTime,
evento: .Event.System.EventID["#text"],
servicio: .Event.EventData.ServiceName,
host: .Event.System.Computer
}
[expresión]
Construye un array:
[.Event.EventData.ServiceName, .Event.EventData.ImagePath]
has("campo")
Verifica si un campo existe:
select(has("EventData"))
select(.Event.EventData | has("ServiceName"))
// valor_por_defecto
Operador alternativo — si la expresión es null o false, usa el valor por defecto:
.Event.EventData.IpAddress // "desconocido"
length
Longitud de un string, array u objeto:
.Event.EventData.ServiceName | length # número de caracteres
keys
Lista las claves de un objeto:
.Event.EventData | keys # ["AccountName", "ImagePath", "ServiceName", ...]
sort_by(expresión)
Ordena un array:
# Requiere recoger todos los eventos primero en un array
[...] | sort_by(.Event.System.EventRecordID)
group_by(expresión)
Agrupa elementos de un array:
[...] | group_by(.Event.System.EventID["#text"])
Patrones de uso en DFIR
Filtrar por EventID y extraer campos específicos:
jq -rR '
fromjson?
| select(type == "object")
| select(.Event.System.EventID["#text"] == 7045)
| {
time: .Event.System.TimeCreated["#attributes"].SystemTime,
record: .Event.System.EventRecordID,
host: .Event.System.Computer,
service: .Event.EventData.ServiceName,
path: .Event.EventData.ImagePath,
account: .Event.EventData.AccountName
}
' system.json
Filtrar por fecha y EventID:
jq -rR '
fromjson?
| select(type == "object")
| select(.Event.System.EventID["#text"] == 4624)
| select(.Event.System.TimeCreated["#attributes"].SystemTime | test("2023-09-07"))
| select(.Event.EventData.LogonType == 3)
| {
time: .Event.System.TimeCreated["#attributes"].SystemTime,
user: .Event.EventData.TargetUserName,
ip: .Event.EventData.IpAddress,
hostname: .Event.EventData.WorkstationName
}
' logs.json
Contar EventIDs únicos:
jq -rR 'fromjson? | select(type == "object") | .Event.System.EventID["#text"]' archivo.json \
| sort | uniq -c | sort -rn
Filtrar por regex en campo de texto:
jq -rR '
fromjson?
| select(type == "object")
| select(.Event.System.EventID["#text"] == 11)
| select(.Event.EventData.TargetFilename | tostring | test("(?i)\\.key$"))
| {
time: .Event.EventData.CreationUtcTime,
file: .Event.EventData.TargetFilename,
proc: .Event.EventData.Image
}
' sysmon.json
Extraer un campo como texto plano (para grep/sort):
# Extraer todos los timestamps de PSEXESVC
jq -rR '
fromjson?
| select(type == "object")
| select(.Event.System.EventID["#text"] == 7045)
| select(.Event.EventData.ServiceName == "PSEXESVC")
| .Event.System.TimeCreated["#attributes"].SystemTime
' system.json | sort
Combinar con grep para filtrado rápido:
# grep primero para reducir el dataset, luego jq para parsear
grep '"PSEXESVC"' system.json | jq -r '.Event.System.TimeCreated["#attributes"].SystemTime'
Named Pipes con patrón específico:
jq -rR '
fromjson?
| select(type == "object")
| select(.Event.System.EventID["#text"] == 17)
| select(.Event.EventData.PipeName | tostring | test("stderr$"))
| {
time: .Event.System.TimeCreated["#attributes"].SystemTime,
pipe: .Event.EventData.PipeName,
proc: .Event.EventData.Image
}
' sysmon.json
Correlación de artefactos
Los tres archivos y qué ve cada uno
system.json (SCM)
Ve: instalación y estado de servicios
No ve: quién lo instaló desde dónde, qué archivos creó
logs.json (LSASS)
Ve: autenticaciones, logons, accesos a objetos
No ve: qué hizo el usuario después de autenticarse
sysmon.json (SysmonDrv.sys)
Ve: procesos, archivos, pipes, red, registro — a nivel kernel
No ve: el contexto de negocio (por qué ocurrió)
Prefetch (.pf)
Ve: qué ejecutó, cuándo, cuántas veces, qué archivos cargó
No ve: qué hizo el proceso durante su ejecución
Tabla de correlación por pregunta forense
Pregunta Artefacto Campo
────────────────────────────────────────────────────────────────────────
¿Quién se conectó remotamente? logs.json 4624 IpAddress, WorkstationName
¿Qué instaló? system.json 7045 ServiceName, ImagePath
¿Cuándo ejecutó exactamente? sysmon.json 1 UtcTime
¿Qué archivos dejó? sysmon.json 11 TargetFilename
¿Cuántas veces ejecutó el .exe? Prefetch .pf run_count, run_times[8]
¿Qué DLLs cargó? Prefetch .pf Section C strings
¿Qué pipes abrió? sysmon.json 17/18 PipeName
¿A dónde se conectó en red? sysmon.json 3 DestinationIp, DestPort
¿Qué borró el atacante? sysmon.json 23 TargetFilename, Hashes
¿Se modificaron timestamps? sysmon.json 2 vs MFT $FN vs $SI
Detectar timestomping
El MFT de NTFS tiene dos sets de timestamps por archivo:
$STANDARD_INFORMATION— modificable desde user-mode conSetFileTime()$FILE_NAME— solo modificable por el kernel, sin API pública
Si $SI.mtime != $FN.mtime → los timestamps fueron manipulados.
Señales adicionales:
- Timestamp con segundos exactos (
.000000) en$SIpero no en$FN - Fecha en
$SIanterior a la fecha de compilación del ejecutable - Sysmon EventID 2 (FileCreationTimeChanged) con el archivo afectado
Flujo de análisis en un CTF
Metodología general
PASO 1 — Reconocimiento de artefactos
¿Qué archivos tengo? (.evtx, .pf, imagen de disco, memoria)
¿Qué canales están disponibles?
¿Qué rango temporal cubren?
PASO 2 — Conversión a formato analizable
evtx_dump → JSONL
Descomprimir .pf si es MAM
Extraer MFT si hay imagen de disco
PASO 3 — Reconocimiento del dataset
¿Qué EventIDs hay?
jq ... | sort | uniq -c | sort -rn
¿Qué rango temporal?
jq ... | .Event.System.TimeCreated... | sort | head -1
jq ... | .Event.System.TimeCreated... | sort | tail -1
PASO 4 — Identificar el evento inicial (patient zero)
Buscar el primer EventID anómalo por timestamp
Correlacionar con autenticaciones (4624 tipo 3)
PASO 5 — Construir la línea de tiempo
Recoger todos los eventos relevantes con timestamps
Ordenar por EventRecordID (más confiable que SystemTime)
Identificar la secuencia de acciones del atacante
PASO 6 — Responder las preguntas
Cada pregunta apunta a un campo específico de un EventID específico
No adivinar — encontrar el evento exacto con el valor exacto
Comandos de inicio rápido
# Ver qué EventIDs hay en un archivo
jq -rR 'fromjson? | select(type=="object") | .Event.System.EventID["#text"]' \
archivo.json | sort -n | uniq -c | sort -rn
# Ver rango temporal
jq -rR 'fromjson? | select(type=="object") | .Event.System.TimeCreated["#attributes"].SystemTime' \
archivo.json | sort | { head -1; tail -1; }
# Buscar cualquier campo que contenga una string
grep -i "psexec" archivo.json | jq '.'
# Ver todos los Computer hostnames únicos
jq -rR 'fromjson? | select(type=="object") | .Event.System.Computer' \
archivo.json | sort | uniq
# Ver EventData de un EventID específico (para conocer su estructura)
jq -rR 'fromjson? | select(type=="object") | select(.Event.System.EventID["#text"] == 4624) | .Event.EventData' \
logs.json | head -30
Documento generado durante el análisis del CTF Tracer — HackTheBox
Aplica a: Windows 10/11, Server 2016/2019/2022
Herramientas: evtx_dump, jq, python-prefetch, PECmd (Eric Zimmerman)