Un Sysmon bien configurado dice qué proceso hizo qué en un host concreto. La red dice algo distinto: con quién habló ese host, cuánto duró la conversación y qué aspecto tenía, incluso cuando el otro extremo no tiene ningún agente instalado (un servidor Linux sin EDR, una impresora, el propio firewall). Esa visibilidad, la que no depende de un agente vivo en la máquina atacada, es la que cubren Suricata y Zeek: dos programas que miran el mismo cable con preguntas distintas.
Este módulo no enseña a leer un pcap byte a byte ni a reconstruir a mano una sesión TCP fragmentada; eso es trabajo de forense de red y vive en el curso de DFIR. Aquí interesa la pregunta de quien trabaja en un SOC: qué regla escribir, qué log consultar cuando salta una alerta, y qué se puede (y qué no) afirmar sobre un flujo cifrado que nadie va a descifrar.
Qué aprenderás
- Por qué un SOC serio necesita firma y metadatos a la vez, y qué pregunta distinta responde cada motor.
- La anatomía completa de una regla de Suricata: acción, cabecera con dirección de flujo, y las opciones entre paréntesis.
- Las familias de keywords que de verdad se usan a diario: contenido (
content,nocase,depth,offset,distance,within), protocolo (http.uri,http.user_agent,dns.query,tls.sni,ja3.hash) y flujo (flow,flowbits,threshold). - A depurar una regla que no dispara con el analizador del motor, y a medir su coste real con el perfilado de reglas.
- Qué tipos de evento trae la salida EVE JSON, qué campos tiene cada uno y cómo se ingiere en un SIEM.
- Cómo se gestiona un ruleset público con
suricata-updatey qué exige su licencia antes de reutilizarlo. - Los logs de Zeek que se consultan de verdad en un SOC (conn, dns, ssl, x509, http, files, notice) y sus campos determinantes.
- A correlacionar los logs de Zeek entre sí, y con las alertas de Suricata, por el identificador de conexión.
- Qué detecciones de red funcionan de verdad, y qué no se puede concluir jamás desde tráfico cifrado.
Dos preguntas distintas sobre el mismo cable
Suricata es un motor de reglas: recorre cada paquete (o cada buffer reconstruido de una sesión) buscando un patrón definido de antemano, y cuando lo encuentra, genera una alerta. Responde a «¿esto coincide con algo que ya sé que es malo?». Es, ante todo, un motor de firmas (compatible con la sintaxis de reglas de Snort), con capacidad añadida de IPS en línea.
Zeek no busca nada malo por defecto. Observa cada conexión y cada transacción de protocolo que reconoce, y escribe un registro estructurado de lo que vio: quién habló con quién, qué certificado se presentó, qué dominio se resolvió, qué fichero se transfirió. El propio proyecto lo resume así: Zeek «captures high-fidelity transaction logs, file contents, and customizable data outputs«, pensados para revisión manual o para alimentar un SIEM. Responde a una pregunta distinta: «¿qué pasó exactamente en esta conexión?». Tiene también un mecanismo de alertas propio (el framework de notificaciones, más abajo), pero su valor central está en el registro, no en el veredicto.
Un SOC que solo tiene Suricata detecta rápido lo conocido y se queda ciego ante lo nuevo: sin firma que lo cubra, un canal de mando y control inédito pasa sin dejar ni una alerta. Un SOC que solo tiene Zeek no pierde nada de lo observado, pero tampoco avisa de nada por sí solo; alguien tiene que ir a buscar, y para buscar hace falta saber qué buscar. Juntos cubren el hueco del otro: Suricata avisa en el momento de lo ya conocido, Zeek deja el historial completo para cuando aparece una hipótesis nueva o hay que reconstruir qué pasó antes de que existiera la regla que hoy sí lo detectaría.
Anatomía de una regla de Suricata
Toda regla de Suricata tiene tres partes: una acción, una cabecera y un bloque de opciones entre paréntesis. Este es el ejemplo de la propia introducción a las reglas (versión 8.0.5, la rama estable al escribir esto: docs.suricata.io/en/latest documenta la rama de desarrollo 9.0.0-dev, así que todo lo citado aquí va contra la documentación fijada a la versión estable):
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"HTTP GET Request Containing Rule in URI"; flow:established,to_server; http.method; content:"GET"; http.uri; content:"rule"; fast_pattern; classtype:bad-unknown; sid:123; rev:1;)
alert es la acción. La cabecera es todo lo anterior al paréntesis: protocolo (http), IP y puerto de origen ($HOME_NET any), operador de dirección (->) e IP y puerto de destino ($EXTERNAL_NET any). Lo que sigue, entre paréntesis y separado por punto y coma, son las opciones.
Acción y dirección del flujo
Las acciones válidas son alert (genera una alerta), pass (deja de inspeccionar ese paquete), drop (lo descarta y alerta, solo con efecto en modo IPS en línea) y las variantes de reject: reject/rejectsrc envían un RST o ICMP de inalcanzable al origen, rejectdst al destino, rejectboth a los dos lados. En modo IPS, cualquier variante de reject también activa el descarte del paquete.
$HOME_NET y $EXTERNAL_NET son variables que se definen una vez en suricata.yaml y se reutilizan en todas las reglas, para no repetir tu rango de IPs internas en cada firma. El operador de dirección importa: -> exige que el tráfico vaya exactamente en ese sentido para que la regla matchee; <> permite cualquiera de los dos sentidos. Una regla escrita con -> cuando el tráfico fluye al revés (por confundir quién es cliente y quién servidor, por ejemplo) no da ningún error: simplemente no dispara nunca.
El protocolo de la cabecera merece un matiz que se pasa por alto a menudo: con http, tls, dns u otro protocolo de aplicación en vez de tcp o udp, Suricata no filtra por el puerto de la cabecera, filtra por el resultado de su propia detección de protocolo. Una regla alert tls ... dispara sobre TLS en el puerto 4444 igual que en el 443, siempre que el motor reconozca esa sesión como TLS. Vuelvo sobre esto en la detección de puertos no estándar.
Las familias de keywords
El grueso de una regla vive en las opciones, agrupadas en tres familias que conviene distinguir porque cada una resuelve un problema distinto.
| Familia | Qué resuelve | Keywords principales |
|---|---|---|
| Contenido | Busca una secuencia de bytes en un buffer, con reglas de posición relativa o absoluta | content, nocase, depth, offset, distance, within, fast_pattern |
| Protocolo (sticky buffers) | Redirige el content siguiente a un campo ya parseado del protocolo, en vez de al payload crudo | http.uri, http.user_agent, http.method, dns.query, tls.sni, ja3.hash, ja3.string, ja4.hash |
| Flujo | Condiciona la regla al estado de la conexión, a una marca puesta por otra regla, o a la frecuencia del disparo | flow, flowbits, threshold |
De la familia de contenido, content es la base: busca una cadena literal, con notación hexadecimal entre barras verticales para bytes no imprimibles (content:"|61 0D 62 63|"). nocase la hace insensible a mayúsculas. depth y offset son absolutos, cuentan desde el principio del buffer; distance y within son relativos, cuentan desde el final del match anterior. Así lo ejemplifica la documentación de payload keywords, con dos contenidos encadenados:
content:"first"; content:"second"; distance:0; within:3;
Esto exige que «second» aparezca justo tras «first» (distancia cero) y dentro de los 3 bytes siguientes. fast_pattern, de la misma familia, no cambia qué matchea la regla: le dice al motor qué content usar para el filtrado previo, el que descarta tráfico irrelevante antes de evaluar la regla entera. Por defecto Suricata elige el content más largo y menos repetitivo, pero a veces conviene forzarlo a mano, según explica la documentación de prefilter: si una regla tiene dos content y uno de ellos, como «User-Agent:», aparece en casi todo el tráfico HTTP, conviene marcar como fast_pattern el otro, más raro.
La familia de protocolo son buffers «sticky»: una vez declaras http.uri, todo content posterior se evalúa contra ese campo ya extraído, no contra el paquete crudo, hasta que cambies de buffer o acabe la regla. http.uri trabaja sobre la URI ya normalizada (Suricata colapsa, por ejemplo, las dobles barras // antes de comparar), y http.user_agent sobre la cabecera User-Agent. Ejemplo real de la documentación de keywords HTTP:
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"HTTP URI Example"; flow:established,to_server; http.uri; content:"/index.html"; bsize:11; classtype:bad-unknown; sid:3; rev:1;)
dns.query hace lo mismo con el nombre consultado en una petición DNS (no en la respuesta): el buffer trae el dominio con los puntos ya reconstruidos, sin byte nulo final. Ejemplo de la documentación de keywords DNS:
alert dns any any -> any any (msg:"Test dns.query option"; dns.query; content:"google"; nocase; sid:1;)
tls.sni matchea el Server Name Indication que el cliente envía en claro durante el saludo TLS, según la documentación de TLS: tls.sni; content:"oisf.net"; nocase; isdataat:!1,relative;. Y ja3.hash/ja3.string (más ja4.hash para el sucesor de JA3), descritos en la documentación de JA3/JA4, matchean la huella del cliente TLS, calculada a partir del orden y los valores de las extensiones del ClientHello; hace falta activarlos antes (app-layer.protocols.tls.ja3-fingerprints: yes en suricata.yaml), o la regla carga sin error pero nunca tiene contra qué comparar:
alert tls any any -> any any (msg:"match JA3 hash"; ja3.hash; content:"e7eca2baf4458d095b7f45da28c16c34"; sid:100001;)
(El sid 100001 de este ejemplo es el que trae la documentación; en tus propias reglas usa el rango reservado a uso local que se explica más abajo, no sids bajos como este.)
La familia de flujo condiciona cuándo cuenta un match. flow:established,to_server exige conexión ya establecida y paquete yendo del cliente al servidor; la documentación de flow keywords añade también to_client/from_server, not_established y stateless. flowbits deja que una regla marque algo en el flujo para que otra, más adelante en ese mismo flujo, lo consulte; sus acciones son set, isset, isnotset, toggle, unset y noalert (marca la condición sin alertar). Ejemplo real:
alert http any any -> any any (msg:"User1 or User2 logged in"; content:"login"; flowbits:isset,user1|user2; sid:1;)
threshold limita cuántas alertas produce una regla en una ventana de tiempo: el tipo limit genera como máximo N alertas por periodo, threshold exige un mínimo de N coincidencias antes de alertar, y both combina las dos condiciones. Se rastrea por track (por IP origen, destino, ambas, por regla o por flujo). Ejemplo de la documentación de thresholding:
alert http $HOME_NET any -> any any (msg:"IE 6 Detection"; flow:established,to_server; http.user_agent; content:"MSIE 6.0"; threshold: type limit, track by_src, seconds 180, count 1; sid:2010706; rev:10;)
msg, sid, rev y classtype
msg es el texto legible que describe la alerta, siempre entre comillas. classtype asigna la regla a una categoría predefinida en classification.config, cada una con nombre corto, descripción larga y prioridad; ejemplos reales tomados directamente del fichero de clasificación del repositorio de Suricata:
| classtype | Descripción larga | Prioridad |
|---|---|---|
| trojan-activity | A Network Trojan was detected | 1 |
| command-and-control | Malware Command and Control Activity Detected | 1 |
| web-application-attack | Web Application Attack | 1 |
| policy-violation | Potential Corporate Privacy Violation | 1 |
| bad-unknown | Potentially Bad Traffic | 2 |
| attempted-recon | Attempted Information Leak | 2 |
| network-scan | Detection of a Network Scan | 3 |
sid es el identificador único de la firma, un entero mayor que cero; rev es su número de revisión, que sube cada vez que se modifica la regla sin cambiar el sid. La convención de orden (classtype antes que sid, y sid antes que rev, como últimos dos keywords) la fija la propia documentación de metadatos. Para no chocar con ningún ruleset público, el registro comunitario sidallocation.org reserva el rango 1000000-1999999 para uso local, el que deberías usar en tus reglas propias. reference, por último, apunta a documentación externa sobre el problema que la regla cubre, con formato reference:tipo,valor, y puede repetirse.
Depurar una regla que no dispara, y medir su coste
Antes de sospechar del motor, valida la sintaxis: suricata -c suricata.yaml -T -v carga la configuración y las reglas, informa de cualquier error de parseo, y sale sin capturar tráfico. Una regla con un error de sintaxis simplemente no se carga, y sin -v ese aviso puede pasar desapercibido entre el resto del log de arranque.
Si la regla carga bien pero no dispara, las causas más frecuentes ya salieron arriba: el operador de dirección no coincide con el sentido real del flujo, $HOME_NET/$EXTERNAL_NET no cubren las IPs reales de la prueba, el protocolo de aplicación de la cabecera no llegó a detectarse (una sesión TLS en un puerto insólito, por ejemplo), o un sticky buffer como ja3.hash nunca se rellenó porque la opción de configuración sigue desactivada.
Para ver cómo interpretó el motor una regla concreta (qué buffer eligió como fast pattern, si necesita reensamblado de stream, si hay algún aviso de rendimiento) se usa el análisis de motor: suricata -c suricata.yaml -S local.rules -l /tmp/analysis --engine-analysis. Según la documentación de tipos de regla, este modo escribe un informe (rules_analysis.txt, en el directorio que fijes con -l) con el sid, el protocolo, el número de opciones de contenido y de PCRE, y el buffer elegido para fast_pattern.
Para probar una regla propia contra una captura ya grabada, sin tocar la configuración de producción: suricata -c suricata.yaml -r captura.pcap -S local.rules -k none -l /tmp/salida. -r lee el pcap en modo offline, -S carga en exclusiva ese fichero de reglas, y -k none desactiva la verificación de checksums, necesaria porque muchas capturas guardadas ya no traen checksums válidos fuera de la tarjeta de red original. El resultado se revisa en fast.log o eve.json, dentro del directorio de -l.
Para recargar reglas sin reiniciar Suricata hay dos vías, según la documentación de recarga de reglas: la señal kill -USR2 $(pidof suricata), o el socket de control con suricatasc -c reload-rules (bloqueante) o suricatasc -c ruleset-reload-nonblocking.
El coste de una regla no se intuye, se mide. El perfilado se activa en la sección profiling de suricata.yaml, tal como aparece en la plantilla de configuración del propio repositorio:
rules:
enabled: yes
filename: rule_perf.log
append: yes
#sort: avgticks
limit: 10
json: yes
El fichero resultante, rule_perf.log, reporta por regla los ticks totales de CPU invertidos, cuántas veces se evaluó (checks) y matcheó, el promedio de ticks por evaluación, el máximo de una sola evaluación, y el porcentaje que esa regla sola pesa sobre el coste total de inspección: la única forma honesta de saber si una regla nueva, sobre todo con expresiones regulares o un content mal elegido como fast_pattern, pesa más de lo razonable. En paralelo, stats.log (que se escribe cada 8 segundos por defecto) da la salud general del motor: capture.kernel_drops cuenta paquetes descartados a nivel de captura, antes de llegar a las reglas, y tcp.reassembly_gap señala huecos en el reensamblado de stream. Si esos contadores suben, ninguna regla va a ver el tráfico que se perdió antes de llegar a ella.
La salida EVE JSON
EVE (Extensible Event Format) es la salida estructurada de Suricata: un fichero de líneas JSON, una por evento, con un campo event_type que dice de qué tipo es. Según la documentación del formato, junto a alert incluye, entre otros, dns, http, tls, ssh, flow, fileinfo, anomaly y quic. Todos comparten un núcleo de campos: timestamp, flow_id (agrupa los eventos de una misma conexión), src_ip, src_port, dest_ip, dest_port, proto y app_proto.
Un detalle que conviene interiorizar: Suricata escribe eventos de protocolo para toda transacción que reconoce, dispare o no una regla. Visibilidad de protocolo y detección por firma son capas independientes: con cero reglas cargadas se sigue viendo en EVE cada consulta DNS y cada SNI de cada sesión TLS del sensor.
El evento de alerta trae, dentro de un objeto alert, el sid como signature_id, la revisión (rev), el msg como signature, la category (descripción larga del classtype) y la severity numérica. Ejemplo real de la propia documentación:
"alert": {
"action": "allowed",
"gid": 1,
"signature_id": 2024056,
"rev": 4,
"signature": "ET MALWARE Win32/CryptFile2 Ransomware",
"category": "Malware Command and Control Activity Detected",
"severity": 1,
"metadata": {
"affected_product": ["Windows_XP_Vista_7_8_10_Server_32_64_Bit"],
"attack_target": ["Client_Endpoint"]
}
}
Los eventos dns distinguen petición de respuesta con el campo type; una petición trae la lista queries con rrname y rrtype, una respuesta trae rcode y la lista answers. Ejemplo, también de la documentación oficial:
"dns": {
"version": 3,
"type": "request",
"id": 16000,
"queries": [
{ "rrname": "twitter.com", "rrtype": "A" }
]
}
Y el evento tls trae el certificado y el saludo ya interpretados:
"tls": {
"subject": "C=US, ST=California, O=Google Inc, CN=*.google.com",
"issuerdn": "C=US, O=Google Inc, CN=Google Internet Authority G2",
"sni": "calendar.google.com",
"version": "TLS 1.2",
"notbefore": "2017-01-04T10:48:43",
"notafter": "2017-03-29T10:18:00"
}
Para ingerir EVE en el SIEM que ya monta este curso, Wazuh lo lee como un fichero JSON de líneas: la propia guía de integración de un IDS de red indica añadir este bloque al agente que corre Suricata:
<ossec_config>
<localfile>
<log_format>json</log_format>
<location>/var/log/suricata/eve.json</location>
</localfile>
</ossec_config>
Wazuh decodifica el JSON campo a campo sin decoder propio, y las alertas quedan consultables filtrando por rule.groups:suricata. Si tu SIEM es Elastic, hay una integración equivalente del propio fabricante, con soporte confirmado para Suricata 5.x, 6.x y 7.x a fecha de esta sesión (comprueba la matriz de compatibilidad si tu sensor va ya por la rama 8.x).
Conjuntos de reglas: gestión y licencia
suricata-update es la herramienta oficial (empaquetada junto a Suricata desde la versión 4.1) para descargar y mantener rulesets. Ejecutado sin argumentos descarga el ruleset Emerging Threats Open en /var/lib/suricata/rules/. suricata-update list-sources lista otras fuentes disponibles en el índice comunitario, y suricata-update enable-source oisf/trafficid (seguido de otra ejecución de suricata-update) añade una fuente concreta al conjunto activo.
Emerging Threats Open (mantenido hoy por Proofpoint) es el ruleset de firmas gratuito más usado con Suricata. Su fichero de licencia, distribuido junto al ruleset, reparte las reglas en tres bloques según el rango de sid: las de sid 1 a 3464 y 100000000 a 100000908 van bajo GPLv2; el grueso (sid 2000000 a 2799999) va bajo licencia BSD, que permite uso y redistribución comercial si se conserva el aviso de copyright; y las reglas de pago de ET Pro (sid 2800000 en adelante) tienen su propia licencia comercial, distinta y más restrictiva. No reproduzco aquí el texto de ninguna licencia: se explican y se enlazan, y conviene leerlas en la fuente antes de redistribuir nada.
Existen rulesets abiertos más pequeños y de licencia más simple: el de SSL Blacklist, de abuse.ch, se publica bajo CC0 (dominio público, sin restricción, ni siquiera de atribución), según indica su propia página de condiciones. El mismo proveedor distribuye también un ruleset de URLhaus para Suricata (verifica su licencia antes de redistribuirlo, menos visible que la de SSL Blacklist). Cada ruleset que sumes tiene un coste de mantenimiento real: hay que revisar sus actualizaciones, vigilar que no introduzca reglas ruidosas para tu tráfico, y decidir con qué frecuencia corre suricata-update, seguido de una recarga en caliente para no perder tráfico durante el reinicio.
Zeek: qué genera y por qué compensa
Zeek no analiza paquete a paquete para un humano: interpreta cada protocolo que reconoce y lo convierte en filas de una tabla, con un writer que por defecto produce texto tabulado y que puede escribir JSON en su lugar. El propio proyecto cifra su superficie por defecto en «70+ Log files» y más de 3.000 eventos de script para quien quiera extender el análisis. La captura completa de paquetes, en cambio, se vuelve cara de guardar y consultar según crece el ancho de banda; lo resume así Corelight (empresa que vende appliances basados en Zeek, léase con esa procedencia en mente): «long term, full packet analysis is impractical for many SOCs due to the huge increase in network throughput and transmissions speeds, and limited budgets and capacity for storage». Zeek ocupa el punto intermedio: no guarda cada byte, pero sí cada hecho relevante de cada transacción, con un volumen que crece con el número de conexiones y no con el ancho de banda bruto.
La rama estable de la documentación (docs.zeek.org, a fecha de esta sesión) corresponde a la serie 8.2.x; en paralelo, el proyecto mantiene una rama de soporte extendido, Zeek 8.0.x, que seguirá recibiendo soporte durante todo el ciclo de la próxima versión mayor según el anuncio de desarrollo de la 8.2. Como referencia de despliegue real, Security Onion, ya usada en el laboratorio de este curso, empaqueta en su versión 3.1.0 (mayo de 2026) Suricata 8.0.5 y Zeek 8.0.8 juntos, el emparejamiento del que habla este módulo.
Los logs que de verdad se consultan
Zeek escribe decenas de logs, pero el trabajo diario de un SOC vuelve, una y otra vez, sobre siete. Campos verificados contra la referencia de logs oficial.
| Log | Qué responde | Campos determinantes |
|---|---|---|
| conn.log | Quién habló con quién, cuánto duró y cómo terminó | uid, id (orig_h/orig_p/resp_h/resp_p), proto, service, duration, orig_bytes, resp_bytes, conn_state, history |
| dns.log | Qué se resolvió y con qué respuesta | uid, query, qtype_name, rcode_name, answers, TTLs, rejected |
| ssl.log | Qué se negoció en el saludo TLS y con qué nombre de servidor | uid, version, cipher, curve, server_name, resumed, established, cert_chain_fps |
| x509.log | Qué certificado se presentó, por quién y con qué validez | certificate.subject, certificate.issuer, certificate.not_valid_before, certificate.not_valid_after, certificate.key_alg, san.dns |
| http.log | Qué se pidió por HTTP y qué contestó el servidor | uid, method, host, uri, referrer, user_agent, status_code, resp_mime_types |
| files.log | Qué ficheros cruzaron la red y con qué hash | fuid, conn_uids, source, filename, mime_type, md5, sha1, sha256 |
| notice.log | Qué situación marcó explícitamente un script de Zeek como digna de revisión | note, msg, sub, src, dst, p, actions |
conn_state, en concreto, resume en dos o tres letras cómo terminó cada conexión TCP, y merece memorizarse porque aparece en casi cualquier pivote desde conn.log. Estos son los valores más habituales, tomados de la referencia del tipo Conn::Info (que documenta también otras variantes menos frecuentes de cierre, como RSTOS0 o SH):
| Valor | Significado |
|---|---|
| S0 | Intento de conexión visto, sin respuesta |
| S1 | Conexión establecida, no terminada |
| SF | Establecimiento y cierre normales |
| REJ | Intento de conexión rechazado |
| RSTO | Conexión establecida, el originador la abortó con RST |
| RSTR | El respondedor envió el RST |
| OTH | No se vio el SYN, solo tráfico a mitad de conexión |
notice.log merece un matiz frente a los otros seis: no describe una transacción, describe un juicio. Un script de Zeek (de fábrica o de un paquete instalado) decide de forma explícita que una situación, un certificado autofirmado, un intento de fuerza bruta detectado por su propio contador, es «digna de inspección», y ese juicio se escribe en note. Es, según la propia documentación, «the closest Zeek is coming to IDS alerts»: el punto donde deja de ser solo un generador de metadatos y se comporta como motor de detección, con reglas en Zeek script en vez de en sintaxis de Suricata.
El framework de Intel convierte un feed externo (dominios, IPs, hashes) en algo que Zeek compara automáticamente contra lo que observa; con frameworks/intel/do_notice cargado, una coincidencia contra un indicador Intel::DOMAIN visto en dns.log o ssl.log se escribe en intel.log y puede escalar a notice.log.
Correlación por identificador de conexión
Todos los logs de la tabla anterior comparten un campo, uid, que identifica de forma única cada conexión dentro de esa misma instancia de Zeek. Un mismo uid aparece en la fila de conn.log que resume la conexión, en la de ssl.log con su saludo TLS, en la de x509.log con el certificado intercambiado, y en la de files.log si por ahí se transfirió algún fichero. Es la propiedad que convierte siete tablas independientes en una sola historia: pivotar de «esta conexión tenía un certificado raro» a «este fue el fichero descargado justo después» es un filtro por uid, no una reconstrucción manual de timestamps.
Ese identificador es propio de Zeek y no significa nada para Suricata. Para correlacionar una alerta de Suricata con el registro de esa misma conexión en Zeek existe un identificador neutral entre herramientas: el Community ID, un hash calculado a partir de las cinco tuplas de la conexión (IPs, puertos, protocolo) que da el mismo resultado sin importar en qué sentido se calculó ni qué herramienta lo generó. En Suricata se activa en eve-log:
community-id: true
community-id-seed: 0
La propia documentación de EVE lo describe como pensado para «dar a los registros un id de flujo predecible que se pueda usar para casar registros con la salida de otras herramientas como Bro» (el nombre anterior de Zeek). En Zeek, desde la versión 6, el soporte va incluido en el motor; basta con cargar el script que lo añade a conn.log: @load policy/protocols/conn/community-id-logging.zeek. Con la misma semilla en ambas herramientas, el community_id de un evento de alerta en EVE y el de la fila correspondiente en conn.log coinciden byte a byte: el pivote que sustituye a casar una alerta con el registro de Zeek por IP, puerto y ventana de tiempo, un método que falla en cuanto hay NAT o varias conexiones casi simultáneas entre los mismos extremos.
Detecciones de red que sí funcionan
Mando y control con comunicación periódica
Un implante que llama a casa cada cierto intervalo deja un patrón que ningún usuario humano reproduce con la misma regularidad: conexiones repetidas al mismo destino, separadas por intervalos parecidos entre sí. La forma correcta de medir esto no es fijar un umbral de «cada X segundos es sospechoso» (cualquier atacante mínimamente cuidadoso añade jitter para esquivarlo); es medir la regularidad misma. RITA, la herramienta abierta de Active Countermeasures pensada para esto sobre conn.log de Zeek, calcula, para cada par de hosts que se repiten, la diferencia de tiempo entre conexiones consecutivas y aplica dos medidas estadísticas: la asimetría de esa distribución de intervalos (coeficiente de Bowley) y su dispersión alrededor de la mediana (desviación absoluta mediana, MADM), y hace lo mismo con el tamaño de los datos transferidos. Como explica la propia documentación del proyecto, «the score is a metric calculated by taking into account the interval skew, dispersion, and duration as well as the data size skew, dispersion, and mode», y cuanto más cerca de 1 queda ese resultado combinado, más regular es la comunicación. No hay ningún umbral universal que copiar de un manual: cada red tiene su propio ruido de fondo de tareas programadas y comprobaciones de salud, y ese ruido hay que caracterizarlo antes de decidir qué grado de regularidad es, en tu entorno concreto, anómalo.
Esta técnica cubre parte de lo que MITRE ATT&CK cataloga bajo la táctica de mando y control (por ejemplo, T1071.004, comunicación de C2 sobre el protocolo de aplicación DNS), aunque el patrón de periodicidad en sí es independiente del protocolo que se use para transportarlo.
Resolución de dominios recién registrados
Un dominio recién creado no tiene reputación previa: mientras las listas de reputación no lo conocen, es invisible para cualquier control que dependa de ellas. Unit 42 (Palo Alto Networks), sobre su propio sistema de detección de dominios recién observados en tráfico DNS pasivo, publicó una cifra que conviene leer como el resultado de su pipeline concreto, no como una ley general: de los dominios sospechosos que su detector aísla cada día, un 37,11 % terminó confirmado como malicioso 30 días después, y de media empezaron a llevar tráfico malicioso 5,57 días tras su registro (con casos aislados de más de un año de latencia). El método es propio de ese vendor (dominios «recién observados» en tráfico no es exactamente lo mismo que «recién registrados» según WHOIS), pero el mecanismo que describe, una ventana temprana con reputación en blanco, justifica el caso de uso con la fuente que tengas: cruzar la fecha de creación o primera aparición del dominio contra el query de dns.log o el server_name de ssl.log, usando el framework de Intel de Zeek como mecanismo de comparación si mantienes tu propia lista.
Exfiltración por DNS
El protocolo DNS no está pensado para transportar datos arbitrarios, pero admite suficiente libertad en una etiqueta como para que herramientas de túnel lo usen igualmente. Ninguna cifra de «longitud sospechosa» o «entropía sospechosa» es universal (varía con el idioma, con el software que genera las consultas, con si hay CDN de por medio), así que lo que se mide, en vez de un umbral fijo, es la desviación frente a la línea base de tu propia red: la longitud de las etiquetas de query en dns.log (un túnel de datos se acerca al límite del protocolo de forma sistemática, una consulta habitual casi nunca), su entropía de caracteres (datos codificados en base32 o base64 dentro de un subdominio se ven, a simple vista, como ruido frente a un nombre legible), y la proporción de tipos de registro poco habituales (qtype_name con muchos TXT o NULL hacia el mismo dominio, frente a la mezcla de A/AAAA del tráfico normal). Un volumen alto y sostenido hacia subdominios distintos de una misma raíz, sobre todo si ese dominio no aparece en el tráfico HTTP o TLS de la red, suele acompañar a los tres indicadores anteriores. La técnica corresponde en ATT&CK a exfiltración por un protocolo alternativo al canal de C2 (T1048, variante T1048.003 cuando el canal no va cifrado, el caso habitual de DNS).
Certificados y huellas de cliente anómalas
x509.log permite detectar certificados autofirmados, con periodos de validez llamativamente cortos, o con un subject que no coincide con el server_name de ssl.log para la misma conexión (correlados, otra vez, por uid). En el cliente, ja3.hash en Suricata (o su equivalente en Zeek con el paquete de terceros de Salesforce, no incluido en el núcleo) identifica la librería TLS y su configuración, no el software exacto ni la intención: programas distintos que comparten librería y parámetros de negociación producen el mismo JA3. Es una huella de «cómo negocia TLS este proceso», útil para detectar herramientas de post-explotación con librerías poco comunes en tu entorno, nunca una identidad inequívoca.
Tráfico saliente a puertos no estándar
Una sesión TLS por el puerto 8443, un HTTP por el 4444: nada de esto es ilegal a nivel de protocolo, y por eso una regla que dependa solo del número de puerto se salta cualquier servicio que lo cambie a propósito. Aquí vuelve a valer la detección de protocolo por contenido de la anatomía de la regla: una firma escrita contra el protocolo de aplicación (alert tls ..., alert http ...) dispara sin importar el puerto. El campo service de conn.log en Zeek hace lo mismo del lado de los metadatos: si el protocolo detectado por contenido no coincide con lo que ese puerto suele llevar, o directamente no se reconoce ninguno sobre un puerto alto, es un candidato razonable a revisar. Esto cubre parte de T1571 (puerto no estándar) y, cuando el destino cambia de dominio constantemente, se solapa con T1568 (resolución dinámica, variante de algoritmos de generación de dominios, T1568.002).
Qué no se puede concluir desde la red
TLS 1.3 cambió las reglas del juego para quien observa de forma pasiva: el certificado del servidor viaja cifrado dentro del propio saludo, así que un sensor que no intercepta el tráfico (y en este curso no se intercepta nada) no llega a verlo. La propia documentación de Zeek lo dice sin rodeos: «there is no mention of certificates in the ssl.log. TLS 1.3 hides these from passive observation systems«. Por eso x509.log, en la práctica, solo se rellena con sesiones que aún negocian TLS 1.2. Y si el servidor usa ESNI o ECH (cifrado del propio nombre de servidor), ni el server_name de ssl.log queda disponible, según confirma la misma documentación.
Esto no es un defecto de Zeek ni de Suricata, es una propiedad del protocolo que ambos observan honestamente: cuando el contenido está cifrado de verdad, ninguna herramienta que se limite a mirar el cable (sin descifrado activo, que este curso no cubre porque cambia el modelo de confianza de la red entera) puede reconstruirlo. Lo que sigue disponible son los metadatos de la conexión: tamaños, tiempos, cadencia, JA3/JA4, y lo poco que el protocolo deja sin cifrar. Y ninguno de esos datos, por sí solo, dice qué proceso en qué host originó la conexión ni con qué intención: eso exige telemetría de endpoint, del tipo que cubren los módulos anteriores de este curso.
Por eso una alerta de red se redacta con el nivel de certeza que de verdad tiene: no «esto es un beacon de C2», sino «esta pareja de hosts muestra una cadencia de conexión regular, sin jitter perceptible, hacia un destino sin reputación previa». La conclusión definitiva, si llega, la pone la investigación completa, no la alerta de red sola.
La frontera con DFIR
Cuando una alerta de red se confirma (el beacon periódico resulta ser un implante real, el certificado autofirmado pertenece a un servidor de C2 activo), el trabajo deja de ser de detección y pasa a ser de respuesta. Adquirir memoria o disco del host implicado, reconstruir la cadena de custodia y seguir el proceso PICERL son tareas del curso de DFIR; aquí termina la ingeniería de detección de este caso, con el traspaso documentado y el uid/community_id implicados como punto de partida.
Laboratorio: comparar Suricata y Zeek sobre el mismo pcap
Necesitas Suricata y Zeek instalados (si ya montaste Security Onion en el laboratorio de este curso, los tienes de fábrica; si no, ambos proyectos publican paquetes para las distribuciones Linux habituales) y una captura pública para analizar.
-
Consigue un pcap con tráfico de aplicación real. La opción más simple y sin ningún riesgo son las capturas de referencia de la wiki de Wireshark, por ejemplo
http.cap(una petición y respuesta HTTP) ydns.cap(varias resoluciones DNS), ninguna maliciosa. Para tráfico más parecido al que se analiza en un SOC real, con más protocolos mezclados, malware-traffic-analysis.net es la referencia habitual del sector: cada entrada separa el pcap de tráfico (*-traffic.pcap.zip) del zip con muestras de malware (*-files.zip); descarga únicamente el primero, y hazlo en una máquina virtual aislada si prefieres no arriesgar nada, aunque el pcap en sí no ejecuta código, solo se analiza. -
Pásalo por Zeek:
zeek -r captura.pcap. Según la propia guía rápida, este comando ya escribe en el directorio de trabajo los logs correspondientes a lo que reconoció (como mínimoconn.log, yhttp.logodns.logsegún tu captura;weird.logsi algo no encaja con el protocolo esperado). Ábrelos conzeek-cuto un editor de texto: son tabulados. -
Pásalo por Suricata, de momento sin ninguna regla propia, solo para ver la capa de metadatos de protocolo:
suricata -c suricata.yaml -r captura.pcap -k none -l /tmp/salida(confirma antes queeve-logestá activado en tusuricata.yaml). Abre/tmp/salida/eve.jsony compara: verifica en tu entorno quéevent_typeaparecen para tu captura concreta y si coinciden, campo a campo, con lo que anotó Zeek para la misma conexión. Lo normal es que Zeek traiga algún campo que Suricata no expone igual de fácil, y que Suricata, si tienes cargado un ruleset como ET Open, dispare alguna alerta que Zeek nunca habría generado por sí solo, porque Zeek no juzga, solo registra. -
Escribe tu propia regla. Abre la captura en Wireshark, localiza un dato estable y específico de esa captura (un dominio consultado, una ruta de URI, un User-Agent, un SNI) y escribe una regla que lo busque. Esta plantilla usa
dns.querycomo ejemplo; sustituye el marcador por lo que encuentres de verdad en tu captura (verifica en tu entorno, el contenido exacto depende del pcap que hayas elegido):alert dns any any -> any any (msg:"SOC-LAB: dominio de interes visto en la captura"; dns.query; content:"<dominio-que-encuentres>"; nocase; classtype:bad-unknown; sid:1000001; rev:1;) -
Compruébala:
suricata -c suricata.yaml -r captura.pcap -S local.rules -k none -l /tmp/salida2, y confirma en/tmp/salida2/fast.logque tu sid 1000001 disparó. Si no lo hace, vuelve a la depuración: revisa con--engine-analysissi la regla cargó como esperabas, y confirma con Wireshark que elcontentcoincide carácter a carácter (mayúsculas incluidas, si no pusistenocase) con lo que de verdad aparece en el tráfico.
Preguntas frecuentes
¿Compensa desplegar Zeek si ya tengo Suricata con un buen ruleset comercial?
Sí, porque resuelven problemas distintos. Un ruleset comercial da cobertura rápida contra amenazas conocidas, pero no deja nada que consultar el día que hay que reconstruir qué pasó con una conexión que ninguna firma marcó como maliciosa en su momento. Zeek es el historial que hace posible esa reconstrucción; sin él, la única fuente son las alertas que sí dispararon, que por definición no incluyen lo que aún no tenía firma.
¿Qué diferencia práctica hay entre una alerta de Suricata y una entrada en notice.log de Zeek?
Las dos son juicios, no solo registros, pero nacen de formas distintas de razonar. Una alerta de Suricata nace de comparar tráfico contra un patrón fijo. Una entrada de notice.log nace de un script de Zeek, escrito en su propio lenguaje, que puede razonar sobre estado acumulado a lo largo de una conexión entera (o de varias) antes de decidir que algo merece atención, no solo sobre un patrón puntual.
¿JA3 identifica con certeza el software que generó la conexión?
No. JA3 resume cómo una librería TLS concreta, con una configuración concreta, arma su ClientHello. Distintas aplicaciones que comparten librería y configuración (frecuente en lenguajes con pocas librerías TLS dominantes) comparten el mismo JA3, así que un JA3 coincidente es un indicio a correlar con otra telemetría, nunca una identificación por sí sola.
¿Por qué mi regla de Suricata con content sobre http.uri no dispara si veo la petición en Wireshark?
Repasa la normalización antes que la sintaxis. http.uri trabaja sobre la URI ya normalizada (con las dobles barras colapsadas, por ejemplo), así que si copiaste la URI cruda tal como la ves en Wireshark y esa URI tenía alguna particularidad que la normalización cambia, el texto que buscas y el del buffer ya no coinciden carácter a carácter. Verifica también que la sesión se clasificó como HTTP y no quedó en tcp genérico, con --engine-analysis o mirando el app_proto del evento flow en EVE.
¿Puedo usar reglas de Emerging Threats Open en un entorno comercial sin pedir permiso a nadie?
Para el grueso del ruleset (licencia BSD), sí, siempre que conserves el aviso de copyright que exige esa licencia. El ruleset incluye también un bloque menor bajo GPLv2, con sus propias condiciones, y por separado existe ET Pro, un producto de pago con licencia distinta que no forma parte de lo que descarga suricata-update por defecto. Antes de redistribuir nada, lee el fichero de licencia completo: este módulo resume sus términos, no los sustituye.
