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

  1. El problema que Event Log resuelve
  2. Arquitectura del sistema de logs
  3. El formato binario EVTX
  4. Binary XML BXML
  5. Los tres componentes principales
  6. EventIDs de referencia
  7. El formato Prefetch PF
  8. evtx_dump — conversión a JSON
  9. Anatomía del JSON
  10. jq — referencia completa
  11. Correlación de artefactos
  12. 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, NetworkService o 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 con SetFileTime()
  • $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 $SI pero no en $FN
  • Fecha en $SI anterior 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)