Laboratorio 1 - Detectar DOS con Machine Learning

Publicado el 09/04/2023 12:04:00 en Hacking Web.
Author: [x][x] 84kur10 | Total de votos: 4   Vote



Hola a todos, llevo tiempo sin escribir un artículo y quiero aprovechar la temporada de estos temas de IA para escribir un poco relacionado a esto.

Contexto



La pregunta que queremos resolver en este laboratorio es sobre cómo podríamos entrenar un modelo de machine learning que sea capaz de aprender a detectar un ataque de denegación de servicio a un servidor web nginx. Me he inclinado en que el laboratorio sea sobre nginx debido al uso que tiene en la actualidad, Ya que ha superado a Apache como el servidor web más popular además de que con los diferentes módulos se usa para multiples propositos como crear un balanceador de carga, proxy inverso, waf, incluso en kubernetes se usa como ingress para controlar los puntos de acceso al cluster.

Un poco de datos sobre el uso de los servidores web en la actualidad:



Herramientas a usar



Vamos a usar scikit-learn con el cual podemos entrenar modelos de machine learning para diferentes casos de uso. Como el resultado final que buscamos tener es simplemente una bandera que nos indique si estamos ante la presencia de un DOS o no, entonces me he inclidado por usar la regresión logística para esto, ya que esta usa internamente la función sigmoide que se mueve entre los valores de menos infinito a infinito en el eje x y en el eje y entre 0 y 1, esto hace que sea idea para calcular eventos que tienen que ver con la probabilidad de ocurrencia.



Adicionalmente, este laboratorio será puramente de análisis descriptivo, es decir que no servirá para preveer cuando va a ocurrir un DOS si no para saber si estamos ante la presencia de uno. Y puntualmente si este está ocurriendo por cantidad de solicitudes, en otras palabras, no está pensado en detectar un DOS que sea ocasionado debido a una vulnerabilidad.

Vamos a usar como entrada de datos el access log (Que igualmente existe en apache), en este se encuentran los datos de cada solicitud que se ha realizado, a qué hora se hizo y otras particularidades.

Generando datos para entrenamiento



Para esto, he simulado 2 situaciones, la primera en la cual hay un comportamiento natural de navegación en el servidor web en donde la cantidad de peticiones es natural, y otra en la que se realizan multiples solicitudes, para esto he creado el siguiente script:

import sys
import requests
import time

headers = { }

if len(sys.argv) < 4:
    print("Faltan argumentos. Uso: python script.py cantidad URL intervalo")
    sys.exit()

cantidad = int(sys.argv[1])
url = sys.argv[2]
intervalo = int(sys.argv[3])

if intervalo ==0:
    headers = {
        'User-Agent': 'DOSLAB'
    }
else:
    headers = {
        'User-Agent': 'NODOSLAB'
    }

for i in range(cantidad):
    response = requests.get(url, headers=headers)
    print(f"Peticion {i+1}: {response.status_code}")
    time.sleep(intervalo/1000)


Y podemos llamar el script así:
python request_process.py 100 http://localhost 2000


El primer argumento es la cantidad de peticiones que se desea realizar, el segundo es la URL y el tercero es el intervalo en milisegundos que deseamos tener entre cada petición. Internamente el script define que si el intervalo es cero seteará el user agent en un valor u otro, esto debido a que deseamos primero entrenar nuestro modelo desde el access log con aprendizaje supervisado y esto nos obliga a presentar a nuestro modelo datos etiquetados, en este caso me he aprovechado de esta cabecera para definir si es o no un DOS desde el script, en caso de que el intervalo sea 0, seteamos la cebecera DOSLAB para indicar que si, en caso contrario NODOSLAB para indicar que no.

Para esto he levantado un contenedor con nginx en el puerto 80 así:

docker run -p 80:80 -v "D:/LAB/DETECT_DOS":/var/log/nginx --name nginx_container -d nginx


He ejecutado el código con intervalo de 0 y de 2000 en diferentres ocasiones, un ejemplo de la ejecución:

python request_process.py 100 http://localhost 2000
Peticion 1: 200
Peticion 2: 200
Peticion 3: 200
.
.
.


Ahora que ya tenemos los datos en nuestro access log, he creado el siguiente script que lo genera nuestro dataset. A modo de resumen, esto genera un archivo csv dataset_access_dos.csv que tiene las siguientes columnas:

Tiempo: la hora incluyendo los segundos
Peticiones Cantidad de peticiones en ese segundo
Flag: 1 para indicar que si hay un DOS 0 para indicar lo contrario

import csv
import datetime

filename = 'D:LABSSECDETECT_DOSaccess.log'


# Abre el archivo access.log
with open(filename) as archivo:
    lector = csv.reader(archivo, delimiter=' ')

    peticiones_por_segundo = {}
    flag_degenacion_encontrado = {}

    for linea in lector:
        fecha_str = linea[3].lstrip('[').replace(" ","")
        fecha = datetime.datetime.strptime(fecha_str, '%d/%b/%Y:%H:%M:%S')

        if linea[5].startswith('GET'):
            #timestamp = fecha.replace(second=0, microsecond=0)
            timestamp_str = fecha.strftime('%Y-%m-%d %H:%M:%S')

            if timestamp_str in peticiones_por_segundo:
                peticiones_por_segundo[timestamp_str] += 1
            else:
                peticiones_por_segundo[timestamp_str] = 1

            if linea[9]=="DOSLAB":
                flag_degenacion_encontrado[timestamp_str ] = 1
            else:
                flag_degenacion_encontrado[timestamp_str ] = 0

with open('dataset_access_dos.csv', 'w', newline='') as file:
    writer = csv.writer(file)
    writer.writerow(["tiempo", "peticiones", "flag"])
    for timestamp_str,peticiones in peticiones_por_segundo.items():
        flag = flag_degenacion_encontrado[timestamp_str ]
        writer.writerow([timestamp_str,peticiones , flag])



Ahora tenemos nuestro dataset:

tiempo,peticiones,flag
2023-04-10 00:15:51,173,1
2023-04-10 00:15:52,230,1
2023-04-10 00:15:53,210,1
2023-04-10 00:15:54,230,1
2023-04-10 00:15:55,232,1
2023-04-10 00:15:56,246,1
2023-04-10 00:15:57,211,1
2023-04-10 00:15:58,268,1
2023-04-10 00:15:59,251,1
2023-04-10 00:16:00,234,1
2023-04-10 00:16:01,247,1
2023-04-10 00:16:02,244,1
2023-04-10 00:16:03,259,1




Ahora con esto listo, he creado el siguiente script que permite cargar los datos en nuestro modelo de sklearn para entrenarlo:

import pickle
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LogisticRegression
import pandas as pd
import os
directorio="model_trained"

if not os.path.exists(directorio):
    os.makedirs(directorio)

dosds = pd.read_csv('dataset_access_dos.csv',sep=',')
print(dosds.head(5))

caracteristicas = ["peticiones"]
clasificacion = 'flag'
x = dosds[caracteristicas]
y = dosds[clasificacion]


X_train, X_test, y_train, y_test = train_test_split(x, y, test_size = 20, random_state = 0)

logreg = LogisticRegression(max_iter=10000)
logreg.fit(X_train, y_train)
#logreg.fit(x, y)

print(logreg.score(X_test, y_test))

# Almacennado el modelo
filename = directorio+'/model_dos.sav'
pickle.dump(logreg, open(filename, 'wb'))


Con esto dividimos el dataset para entrenar con el 80% y el 20% lo dejamos para probar.

X_train, X_test, y_train, y_test = train_test_split(x, y, test_size = 20, random_state = 0)


Entenamos el modelo:
logreg.fit(X_train, y_train)


Medimos el score, si se acerca a 1 de forma positiva quiere decir que estamos en buen camino, además usamos datos de test que no son conocidos por el modelo para medir esto.

print(logreg.score(X_test, y_test))


Como verán he usado la librería pickle para dejar en un archivo el modelo entrenado y listo.



El performance del modelo es 1, esto es en apareciencia bueno (Entre más cercano a 1 es mejor), sin embargo este modelo es bastante sencillo de una sola característica y todos los datos con los que se entrenó los simulamos.

He creado el siguiente script para terminar de probarlo, verán que le envío 200,1,10,20 que son cantidades de peticiones en segundos:

import numpy as np
import csv
import pickle
from sklearn.linear_model import LogisticRegression
import pandas as pd

model_path="model_trained/model_dos.sav"
loaded_model = pickle.load(open(model_path, 'rb'))
label=["NORMAL","DOS"]

peticiones = np.array([200,1,10,20]).reshape(-1, 1)

predict = loaded_model.predict(peticiones)
print(str(peticiones[0]) +" - "+label[predict[0]])
print(str(peticiones[1]) +" - "+label[predict[1]])
print(str(peticiones[2]) +" - "+label[predict[2]])
print(str(peticiones[3]) +" - "+label[predict[3]])


Salida:

[200] - DOS
[1] - NORMAL
[10] - NORMAL
[20] - NORMAL


Al parecer para 1 petición en 1 segundo, 20 peticiones en 1 segundo y 20 en un segundo se considera normal, sin embargo 200 peticiones en 1 segundo lo toma como un DOS.

Acá termina el laboratorio 1, imagine poder llevar esto a monitorear cada minuto nuestro access log y automatizar este ciclo de vida con Airflow para saber cuando tenemos un posible DOS, depronto podríamos agregar más características como por ejemplo el uso de CPU del servidor y quizás hacer que aplique algunas políticas automáticamente etc.

Saludos.





Comments:


[x]
[x][x] MuldeR (5 m) : 55652 Muy interesante la idea, te invito a que desarrolles más! Sería chido que además de la cantidad de conexiones chequeara el consumo de recursos y los recursos libres y que respondiera bloqueando los ataques... Pienso que hacerlo funcional no está muy lejos de los códigos de ejemplo que hiciste.
TMB sería interesante monitorear apache/ngix y que en caso de producirse una denegación que rompa el proceso le haga un restart o start luego de banear y detener el atk.

Otro paper muy interesante para terminar bien la semana! Agradezco muchísimo la colaboración y ya quiero ver qué más tienes para aportar bro. +1 sin duda;)


[x]
[x][x] n4ch0m4n (5 m) : 55655 +1 muy bueno para leer y releerlo


[x]
[x][x] Maese (5 m) : 55667 La idea se ve interesante pero no entiendo bien el desarrollo, me voy a tener que poner a aprender a usar scikit, el artículo se ve muy bien y me desperto curiosidad asi que te dejo mi voto.


[x]
[x][x] MuldeR (5 m) : 55684 Veo que le agregaste las imágenes, excelente 8aku, gracias por compartirlo ;)