Hace como un año comencé a desplegar unas alarmas en el trabajo utilizando FSM para procesar los logs y obtener alertas tempranas ante ciertos incidentes.
Luego me dí cuenta que pude haberlo hecho utilizando consultas SQL, lo cual me llevó a reflexionar mucho sobre el tema, hacer algunas prueba y preparar una charla que dí en el trabajo de modo virtual pero no podré dar afuera por que todas las conferencias medio que están caidas.
Esto viene a ser un resumen de lo que pudiste haber visto en FLiSOL, H4CK3D, OWASP Latam Tour, LACNIC y no digo Ekoparty por que nunca me aceptan nada.
Voy a asumir que tenés una idea de los temas, que sólo te estoy aportando las relaciones entre las ideas. Además yo no soy ninguna autoridad de conocimiento, sólo estoy expresando opiniones más o menos bien sustentadas.
Voy a usar el siguiente vocabulario, los dos primeros corresponden al uso común y el último lo uso para manejarnos ahora.
Evento: cada cambio sin connotación positiva o negativa
Incidente: un cambio con connotación negativa
Caso: conjunto de eventos en análisis que conducen a un incidente.
Un caso simple
Partamos de un sencillo requerimiento atemporal, no hay tiempo ni información variable involucradas.
Si hay una conexión a la IP_perimetral en el puerto 8081 desde una IP_externa que no está en la red 211.10.10.0/24 y tiene cierto "DATO", quiero una alarma, evitarlo o incluso agredir.
Esta es un típica regla stateless, se puede resolver con lógica combinacional:
[grafico logico]
IP destino en IP_perimetral
AND
puerto es 8081
AND
IP origen NOT en red 211.10.10.0/24
AND
mensaje contiene "DATO"
Tambien con una consulta SQL:
SELECT * FROM conexiones
WHERE
ip_destino IN (SELECT ip FROM red_perimetral)
AND
port_destino = 8081
AND
ip_origen NOT IN ( SELECT ip FROM red_externa)
AND
datos like “%DATO%”;
O con una FSM de un solo estado, que contendría la lógica combinacional previamente citada, en pseudo-C:
if (
pertence( IP_destino , IP_perimetral) )
& puerto == 8081
& ! pertenece(IP_origen, 211.10.10.0.24)
& strpos("DATO", mensaje) {
generar_alerta();
}
Algunos atributos apropiados para nuestros ejemplos:
- Origen
- Destino
- Tipo de mensaje
- Contenido del mensaje
- Longitud del mensaje
- Horario
Un caso complejo
Si hay un scan desde una IP_externa y desde esa misma IP hay luego una conexión con un DATO y luego se ve ese DATO en una copia entre nodos internos de una IP_interna_origen a una IP_interna_destino y finalmente hay una conexión saliente de esa ip interna de destino con ese DATO, quiero una alarma, evitarlo o incluso agredir.
Que es difícil de leer, mejor no lo leas, leé esto:
Si se da la siguiente secuencia de eventos
- Scan desde una IP_externa
- Conexión desde esa IP_externa con un DATO
- Copia de datos entre dos nodos con ese DATO desde una IP_interna_origen a una IP_interna_destino
- Conexión saliente de la IP_interna_destino con ese DATO
Fijate que las partes coloreadas no están predefinidas, son variables que deben
concidir entre las distintas partes de la reglas
A los atributos previamente mencionados se suma "Relación entre los mensajes".
Lógica combinacional
Este es el terreno de Boole, es prácticamente instántanea, corre a la velocidad máxima de propagación entre compuertas si está implementada en hardware, que es factible usando FPGA. Si corre en software, son pocas y simples instrucciones. Cuando se le agrega memoria, se llama secuencial, siendo un caso particular es la FSM.
FSM
Dice wikipedia que:
Un autómata finito (AF) o máquina de estado finito es un modelo computacional que realiza cómputos en forma automática sobre una entrada para producir una salida.
Que es lo mismo que hace cualquier proceso o programa, lo específico es que es un cierto modelo computacional, mirá los componentes de una FSM:
- estados
- entradas
- salidas
- función estado(estado,entrada)
- función salida(estado,entrada)
- estado inicial
Un proceso genérico no tiene necesariamente la función de estado, ni siquiera estados.
Las funciones de entrada y salida son combinacionales, quizás con efectos colaterales cuando hay más memoria que estado en sí.
Un ejemplo muy natural por decir de algún modo es un operating system scheduler que es su expresión más baja se reduce a esto:
Lo que está diciendo es que cuando se crea un nuevo proceso, va al estado "Ready To Run". Cuando el OSS decide que corra, Running, del cual puede salir cuando termina, cuando se le termina el tiempo asignado de ejecución y el OSS lo regresa a R2R o cuando pide una operación que implica largos tiempos de espera como acceso a un periférico. El sistema operativo detecta eso y elije mandarlo a dormir para liberar la CPU para otro proceso. Cuando la operación de entrada salida concluye, el proceso pasa nuevamente a R2R y así.
Como se implementa, puede ser como en mi demo con el estado atributo en la instancia, pero tambien puede estar el objeto (o estructura) en una tabla y una de sus columnas ser el estado. Tambien y me gusta más intuitivamente pues nunca implementé un OSS, en tablas o listas separadas, una para cada estado.
Consulta SQL
Asumo que sabés al menos que es un join sencillo.
En el contexto de lo que estamos analizando, estas son las principales características para comparar entre FSM y consulta SQL:
En la FSM el tiempo está implícito, hace falta que los eventos lleguen ordenados, falla si no. No hace falta guardar estos eventos, sí algún que otro dato para parametrizar las reglas y para el reporte final. Esos datos y el overhead de las instancias de cada caso genera un consumo creciente de memoria. Dado que da una respuesta de muy baja latencia, puede ser usada en situación de bloqueo, esto es que no sólo alerte sino impida la acción final del ataque. Es más difícil de programar pues es... programar, justo.
Las Consultas SQL por su lado necesitan el tiempo pero este está explícito en el timestamp del mensaje y necesita conservar los mensajes, de lo cual se encarga el DB Engine con todo sus ventajas de escalamiento. No lo veo mucho en situación de bloqueo, quizás si para un sistema transaccional pero no para algo donde la latencia sea crítica. Es más fácil de programar pues no es programar, es escribir SQL, lo cuál al comienzo es más difícil cuando es complejo, pero luego es más de lo mismo.
Una FSM puede alimentarse de un batch ordenado. O sea que tambien se puede usar para detectar en archivos históricos. Me imagino que no debe ser más eficiente que DB pues las DB estan diseñadas para ser eficientes en ese escenario.
Como machete, FSM vs Query SQL:
FSM
|
Query SQL
| |
Tiempo
|
Implícito
|
Explícito
|
Orden de eventos
|
Indispensable
|
Indiferente
|
IO
|
Stream
|
Stream/batch
|
Almacenamiento
|
Descartable
|
Indispensable
|
Memoria
|
Creciente si no se purga
|
DB Engine (*)
|
Real Time
|
Hard (sniffer)
|
Soft
|
Modo
|
Detección y Bloqueo
|
Detección
|
Escalamiento
|
Provisto por S.O.
|
Provisto por DB
|
Conocimientos
|
Programación
|
SQL
|
Cambios lógica
|
Difícil
|
Fácil
|
Embebible
|
Si
|
Mmmh
|
Con embebible me refiero a que se podría tomar un microntrolador y/o FPGA y hacer un modulito que se conecte a la red y cumpla alguna función digna de mención, no tanto con una DB, para la cual hace falta un sistema operativo detras, mmmh, por eso, mmmh, no.
Características compartidas
- Hay que tener en cuenta un "tiempo de vida", un "timeout", tras el cual un caso debe ser desestimado en el caso de FSM o una antigüedad de log ignorada en el de SQL. Esto reduce el tamaño de la DB/espacio en memoria.
- Pueden haber alertas intermedias, en el caso de FSM simplemente emitir mensajes en estados previos al final, en el de SQL consultas que no incluyan las últimas condiciones, una especie de copiar->pegar->podar de la consulta.
Para SQL hay un optimización híbrida, en lugar de ejecutar siempre toda la consulta, buscar la condición del último evento, en nuestro ejemplo una conexión hacia el exterior, si existe, hacer la consulta completa. Quizás las optimizaciones que hacen los DB Engines hacen eso o se les puede sugerir, sé muy poco de DB.
Dicho de otra manera, cuando llega el último evento, me fijo si han ocurrido las condiciones para llegar hasta ahí.
Hice una demo que está en github, te cuento:
Modificá export.php y generás la base de datos para las Consultas SQL, que están en el archivo... consultas.sql.
SimpleFSM.java es una POC para casos no solapados, es para concentrarse en un ejemplo de FSM y en la lógica y cuestiones secundarias como leer de STDIN.
FSM.java es la implementación más correcta, donde una una colección de casos, de paso, las clases deberían llamarse CasoSimple y Caso, no el nombre de la implementación, pero cuando lo hice estaba mas concentrado en la FSM que en las recomendaciones de Domain Driven Design.
Fijate que al pasar de SimpleFSM a FSM el evento que lleva al primer estado es extraido de la lógica común. Para entender bien mirá la evolución por el paso intermedio, FSM2, que debería llamarse FSM_fail, donde estoy haciendo dos cosas a la vez: la FSM y la gestión de la colección.
Lo correcto es hacer FSM y de ahí fijarse como optimizar para que los loops terminen antes si pueden y esas cositas.