From c4df869596e6ab7b1066bc6095f0034e6714d821 Mon Sep 17 00:00:00 2001 From: Oxbian Date: Sat, 23 Nov 2024 12:15:10 -0500 Subject: [PATCH] feat: protection system for the idps --- Demo/config/config-idps.json | 12 +++- Demo/config/config-ids.json | 12 +++- config.json | 11 ++- idps/database.py | 15 ++--- idps/main.py | 96 ++++++++++++++------------- idps/protection.py | 49 ++++++++++++++ idps/rules/TCP/DOS/syndos.py | 17 +++-- idps/rules/TCP/DOS/synflood.py | 17 +++-- idps/rules/TCP/DOS/tcpconnectflood.py | 20 +++--- idps/rules/TCP/Scan/ackscan.py | 19 +++--- idps/rules/TCP/Scan/finscan.py | 20 +++--- idps/rules/TCP/Scan/nullscan.py | 21 +++--- idps/rules/TCP/Scan/synscan.py | 19 +++--- idps/rules/TCP/Scan/tcpconnectscan.py | 19 +++--- idps/rules/TCP/Scan/xmasscan.py | 19 +++--- idps/tcp.py | 38 +++++++---- 16 files changed, 259 insertions(+), 145 deletions(-) create mode 100644 idps/protection.py diff --git a/Demo/config/config-idps.json b/Demo/config/config-idps.json index 02f0cb8..7e734aa 100644 --- a/Demo/config/config-idps.json +++ b/Demo/config/config-idps.json @@ -6,26 +6,36 @@ "db_user": "sidps", "db_password": "SUPERPASSWORD", "db_port": "3306", + "protection": 1, "cef_version": 1, "device_product": "SIDPS", "device_vendor": "ArKa", "device_version": "vAlpha", "synscan_time": 180, "synscan_count": 5, + "synscan_bantime": 300, "tcpconnectscan_time": 180, "tcpconnectscan_count": 5, + "tcpconnectscan_bantime": 300, "ackscan_time": 180, "ackscan_count": 5, + "ackscan_bantime": 300, "finscan_time": 180, "finscan_count": 5, + "finscan_bantime": 300, "nullscan_time": 180, "nullscan_count": 5, + "nullscan_bantime": 300, "xmasscan_time": 180, "xmasscan_count": 5, + "xmasscan_bantime": 300, "synflood_time": 60, "synflood_count": 100, + "synflood_bantime": 300, "tcpconnectflood_time": 60, "tcpconnectflood_count": 100, + "tcpconnectflood_bantime": 300, "syndos_time": 60, - "syndos_count": 100 + "syndos_count": 100, + "syndos_bantime": 300 } diff --git a/Demo/config/config-ids.json b/Demo/config/config-ids.json index 7b203bb..4e091b7 100644 --- a/Demo/config/config-ids.json +++ b/Demo/config/config-ids.json @@ -6,26 +6,36 @@ "db_user": "sidps", "db_password": "SUPERPASSWORD", "db_port": "3306", + "protection": 0, "cef_version": 1, "device_product": "Sonde IDS", "device_vendor": "ArKa", "device_version": "vAlpha", "synscan_time": 180, "synscan_count": 5, + "synscan_bantime": 300, "tcpconnectscan_time": 180, "tcpconnectscan_count": 5, + "tcpconnectscan_bantime": 300, "ackscan_time": 180, "ackscan_count": 5, + "ackscan_bantime": 300, "finscan_time": 180, "finscan_count": 5, + "finscan_bantime": 300, "nullscan_time": 180, "nullscan_count": 5, + "nullscan_bantime": 300, "xmasscan_time": 180, "xmasscan_count": 5, + "xmasscan_bantime": 300, "synflood_time": 60, "synflood_count": 100, + "synflood_bantime": 300, "tcpconnectflood_time": 60, "tcpconnectflood_count": 100, + "tcpconnectflood_bantime": 300, "syndos_time": 60, - "syndos_count": 100 + "syndos_count": 100, + "syndos_bantime": 300 } diff --git a/config.json b/config.json index 02f0cb8..d827136 100644 --- a/config.json +++ b/config.json @@ -12,20 +12,29 @@ "device_version": "vAlpha", "synscan_time": 180, "synscan_count": 5, + "synscan_bantime": 300, "tcpconnectscan_time": 180, "tcpconnectscan_count": 5, + "tcpconnectscan_bantime": 300, "ackscan_time": 180, "ackscan_count": 5, + "ackscan_bantime": 300, "finscan_time": 180, "finscan_count": 5, + "finscan_bantime": 300, "nullscan_time": 180, "nullscan_count": 5, + "nullscan_bantime": 300, "xmasscan_time": 180, "xmasscan_count": 5, + "xmasscan_bantime": 300, "synflood_time": 60, "synflood_count": 100, + "synflood_bantime": 300, "tcpconnectflood_time": 60, "tcpconnectflood_count": 100, + "tcpconnectflood_bantime": 300, "syndos_time": 60, - "syndos_count": 100 + "syndos_count": 100, + "syndos_bantime": 300 } diff --git a/idps/database.py b/idps/database.py index bba8130..efdb6f0 100644 --- a/idps/database.py +++ b/idps/database.py @@ -46,13 +46,13 @@ class Database: # Paramètres pour la requête SQL params = { - "cef_version": self.get_key("cef_version", 1), + "cef_version": self.config.get("cef_version", 1), "date_alerte": date_alert, "agent_severity": agent_severity, "device_event_class_id": device_event_class_id, - "device_product": self.get_key("device_product", "SIDPS"), - "device_vendor": self.get_key("device_vendor", "ArKa"), - "device_version": self.get_key("device_version", "vAlpha"), + "device_product": self.config.get("device_product", "SIDPS"), + "device_vendor": self.config.get("device_vendor", "ArKa"), + "device_version": self.config.get("device_version", "vAlpha"), "name": name, "src": src, "dst": dst, @@ -72,10 +72,3 @@ class Database: cursor.close() except mysql.connector.Error as err: print("Erreur lors de l'envoi de l'alerte: {}".format(err)) - - def get_key(self, key, default_val): - """Donne le contenue d'un paramètre spécifique de la config - @param key: clé du paramètre souhaité - @param default_val: valeur par défaut si la clé n'existe pas""" - - return self.config.get(key, default_val) diff --git a/idps/main.py b/idps/main.py index 08934a1..379e8fb 100644 --- a/idps/main.py +++ b/idps/main.py @@ -1,13 +1,48 @@ -from scapy.all import sniff, TCP, IP -from scapy.config import conf -conf.debug_dissector = 2 - import importlib.util import os import time import tcp import database import json +import protection + +from scapy.all import sniff, TCP, IP +from scapy.config import conf +conf.debug_dissector = 2 + + +def check_frame_w_rules(packet, rules_functions, objets): + """Appliquer chaque règle des fonctions au paquet capturé. + @param packet: Paquet actuel à analyser + @param rules_functions: liste de fonctions de règles + @param objets: Dictionnaire contenant le dictionnaire de config, la liste des paquets tcp précédents, + la db, et le gestionnaire de règles Iptables""" + + for rule_func in rules_functions: + try: + rule_func(packet, objets) + except Exception as e: + print(f"Erreur lors de l'exécution de la règle : {e}") + + +def packet_callback(packet, rules_functions, objets): + """Callback réception d'un paquet + @param packet: Paquet actuel à classer + @param rules_functions: liste des fonctions de règles + @param objets: Dictionnaire contenant le dictionnaire de config, la liste des paquets tcp précédents, + la db, et le gestionnaire de règles Iptables""" + + # Nettoyage des règles et paquets TCP dépassé + objets["iptables_manager"].del_rules() + objets["tcp_packets"].clean_old_packets() + + if IP in packet and TCP in packet: + packet_origin = objets["tcp_packets"].add_packet(packet[IP].src, packet[TCP].sport, packet[IP].dst, packet[TCP].dport, packet[TCP].flags, time.time()) + + # Stockage du paquet originel lié à ce paquet pour identifier la provenance de l'attaque + objets['pkt_origin'] = packet_origin + + check_frame_w_rules(packet, rules_functions['TCP'], objets) def load_rules(rules_dirpath = "/app/idps/rules"): @@ -68,37 +103,6 @@ def load_rules(rules_dirpath = "/app/idps/rules"): return rules_functions -def check_frame_w_rules(packet, rules_functions, packets, db): - """Appliquer chaque règle des fonctions au paquet capturé. - @param packet: Paquet actuel à analyser - @param rules_functions: liste de fonctions de règles - @param packets: liste des paquets précédents (utile pour TCP) - @param db: Objet database pour envoyer les alertes à la BDD - """ - - for rule_func in rules_functions: - try: - rule_func(packet, packets, db) - except Exception as e: - print(f"Erreur lors de l'exécution de la règle : {e}") - - -def packet_callback(packet, rules_functions, tcp_packets, db): - """Callback réception d'un paquet - @param packet: Paquet actuel à classer - @param rules_functions: liste des fonctions de règles - @param tcp_packets: Objet contenant une liste des paquets tcp précédents - @param db: Objet database pour envoyer des alertes à la BDD - """ - - #print(packet) - if IP in packet and TCP in packet: - tcp_packets.add_packet(packet[IP].src, packet[TCP].sport, packet[IP].dst, packet[TCP].dport, packet[TCP].flags, time.time()) - #print(tcp_packets[packet[IP].src]) - check_frame_w_rules(packet, rules_functions['TCP'], tcp_packets, db) - tcp_packets.clean_old_packets() - - def read_config(config_filepath='config.json'): """Charge les configurations depuis le fichier de config""" @@ -115,29 +119,29 @@ def read_config(config_filepath='config.json'): def start_idps(): """Charge les règles et démarre l'IDPS""" - print(f"Récupération des configurations") + print("Récupération des configurations") config = read_config() - print(f"Configurations chargées") + print("Configurations chargées") - print(f"Chargement des règles...") + print("Chargement des règles...") rules_functions = load_rules(config["rules_dirpath"]) - print(f"Les règles sont chargées") + print("Les règles sont chargées") - print(f"Connexion à la base de données") + print("Connexion à la base de données") db = database.Database(config) - print(f"Connexion réussite à la base de données") - - # Opti possible: charger les règles par protocole, permettant des filtrages et donc optimiser - # le nombre de fonctions vérifiant le paquet (snort s'arrête à la première corrélation par exemple) + print("Connexion réussite à la base de données") tcp_packets = tcp.TCP(300) + protection_system = protection.Protection(config["protection"]) + + objets = {"config": config, "database": db, "tcp_packets": tcp_packets, "iptables_manager": protection_system} # Lancer scapy & envoyer le paquet à chaque règle de l'IDPS - sniff(iface=config["ifaces"], prn=lambda packet: packet_callback(packet, rules_functions, tcp_packets, db), store=0) + sniff(iface=config["ifaces"], prn=lambda packet: packet_callback(packet, rules_functions, objets), store=0) def main(): - print(f"Démarrage de l'IDPS") + print("Démarrage de l'IDPS") start_idps() diff --git a/idps/protection.py b/idps/protection.py new file mode 100644 index 0000000..aef2c50 --- /dev/null +++ b/idps/protection.py @@ -0,0 +1,49 @@ +import subprocess +import time + + +class Protection: + """Classe pour activer la protection du système avec iptables""" + + def __init__(self, activate = 0): + """Initialisation de la protection avec une liste pour stockées les règles créer""" + self.rules = [] + self.activate = int(activate) + + def add_rule(self, rule, duration): + """Ajouter une règle dans iptables + @param rule: Règle à ajouter + @param duration: Durée d'execution de la règle""" + + print(f"Rule: {rule}, {duration}, {self.activate}") + if self.activate == 0: + return + + print("Rule run") + try: + subprocess.run(rule.split(' '), check=True) + print(f"[iptables] Règle ajouter {rule}") + self.rules.append([rule, time.time(), duration]) + except subprocess.CalledProcessError as e: + print(f"[iptables] Erreur suppression de la règle {rule}: {e}") + + def del_rules(self): + """Supprimer les règles obsolètes de l'iptables""" + + if self.activate is False: + return + + curr = time.time() + for i, elt in enumerate(self.rules, 0): + if elt[1] + elt[2] <= curr: + self.del_rule(self, elt[0].replace("-I", "-D"), i) + + def del_rule(self, rule, i): + """Supprimer une règle dans iptables""" + + try: + subprocess.run(rule.split(' '), check=True) + print("[iptables] Règle supprimer {rule}") + self.rules.pop(i) + except subprocess.CalledProcessError as e: + print(f"[iptables] Erreur suppression de la règle {rule}: {e}") diff --git a/idps/rules/TCP/DOS/syndos.py b/idps/rules/TCP/DOS/syndos.py index 54480e9..22ad701 100644 --- a/idps/rules/TCP/DOS/syndos.py +++ b/idps/rules/TCP/DOS/syndos.py @@ -2,7 +2,7 @@ from datetime import datetime import time -def rule(packet, tcp_packets, db): +def rule(packet, objets): """ Règle SYN Dos: Un SYN DOS va envoyer des requêtes TCP avec le flag SYN en très grand nombre afin de surcharger le serveur ou la cible avec des grosses quantités de données @@ -15,17 +15,19 @@ def rule(packet, tcp_packets, db): return # Initialisation des paramètres à partir de la configuration si nécessaire - if (rule.seuil == 0 and rule.time_window == 0): - rule.time_window = db.get_key("synsdos_time", 60) - rule.seuil = db.get_key("syndos_count", 100) + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("synsdos_time", 60) + rule.seuil = objets["config"].get("syndos_count", 100) + rule.ban_time = objets["config"].get("syndos_bantime", 300) # Comptage des paquets TCP correspondant aux motifs spécifiques - syndeny_count = tcp_packets.count_packet_of_type(["S", "RA"], rule.time_window, True) - synaccept_count = tcp_packets.count_packet_of_type(["S", "SA", "R"], rule.time_window, True) + syndeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + synaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "R"], rule.time_window, True) # Détection si le seuil est dépassé et que la charge utile des paquets est non nulle if (syndeny_count + synaccept_count >= rule.seuil and len(packet['TCP'].payload) > 0): - db.send_alert(datetime.now(), 5, None, "Syn DOS", packet['IP'].src, packet['IP'].dst, proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->Reset et Syn->Reset ACK", act="Alerte") + objets["database"].send_alert(datetime.now(), 5, None, "Syn DOS", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->Reset et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) print("Alerte, seuil dépassé, risque de Syn DOS détecté.") rule.cooldown = time.time() @@ -34,3 +36,4 @@ def rule(packet, tcp_packets, db): rule.cooldown = 0 rule.time_window = 0 rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/DOS/synflood.py b/idps/rules/TCP/DOS/synflood.py index 520f9f5..8cc895b 100644 --- a/idps/rules/TCP/DOS/synflood.py +++ b/idps/rules/TCP/DOS/synflood.py @@ -2,7 +2,7 @@ from datetime import datetime import time -def rule(packet, tcp_packets, db): +def rule(packet, objets): """ Règle SYN Flood: Un SYN Flood va envoyer des requêtes TCP avec le flag SYN en très grand nombre afin de surcharger le serveur ou la cible @@ -15,17 +15,19 @@ def rule(packet, tcp_packets, db): return # Initialisation des paramètres à partir de la configuration si nécessaire - if (rule.seuil == 0 and rule.time_window == 0): - rule.time_window = db.get_key("synsflood_time", 60) - rule.seuil = db.get_key("synflood_count", 100) + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("synsflood_time", 60) + rule.seuil = objets["config"].get("synflood_count", 100) + rule.ban_time = objets["config"].get("synflood_bantime", 300) # Comptage des paquets TCP correspondant aux motifs spécifiques - syndeny_count = tcp_packets.count_packet_of_type(["S", "RA"], rule.time_window, True) - synaccept_count = tcp_packets.count_packet_of_type(["S", "SA", "R"], rule.time_window, True) + syndeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + synaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "R"], rule.time_window, True) # Détection si le seuil est dépassé if (syndeny_count + synaccept_count >= rule.seuil): - db.send_alert(datetime.now(), 5, None, "Syn flood", packet['IP'].src, packet['IP'].dst, proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->Reset et Syn->Reset ACK", act="Alerte") + objets["database"].send_alert(datetime.now(), 5, None, "Syn flood", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->Reset et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) print("Alerte, seuil dépassé, risque de Syn Flood détecté.") rule.cooldown = time.time() @@ -34,3 +36,4 @@ def rule(packet, tcp_packets, db): rule.cooldown = 0 rule.time_window = 0 rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/DOS/tcpconnectflood.py b/idps/rules/TCP/DOS/tcpconnectflood.py index ddb34e2..d1536da 100644 --- a/idps/rules/TCP/DOS/tcpconnectflood.py +++ b/idps/rules/TCP/DOS/tcpconnectflood.py @@ -2,7 +2,7 @@ from datetime import datetime import time -def rule(packet, tcp_packets, db): +def rule(packet, objets): """Règle TCPConnect Flood: Un flood TCP connect va effectuer une connexion TCP en très grand nombre Si le port est ouvert le serveur acceptera la connexion SYN -> SYN ACK -> ACK -> Reset ACK @@ -13,17 +13,19 @@ def rule(packet, tcp_packets, db): return # Vérification si nécessaire de récupérer les variables depuis la config - if (rule.seuil == 0 and rule.time_window == 0): - rule.time_window = db.get_key("tcpconnectflood_time", 60) - rule.seuil = db.get_key("tcpconnectflood_count", 100) + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("tcpconnectflood_time", 60) + rule.seuil = objets["config"].get("tcpconnectflood_count", 100) + rule.ban_time = objets["config"].get("tcpconnectflood_bantime", 300) # Comptage du nombre de scan tcp connect acceptés et refusés - tcpconnectdeny_count = tcp_packets.count_packet_of_type(["S", "RA"], rule.time_window, True) - tcpconnectaccept_count = tcp_packets.count_packet_of_type(["S", "SA", "A", "RA"], rule.time_window, True) + tcpconnectdeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + tcpconnectaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "A", "RA"], rule.time_window, True) if (tcpconnectaccept_count + tcpconnectdeny_count >= rule.seuil): - db.send_alert(datetime.now(), 5, None, "TCPConnect Flood", packet['IP'].src, packet['IP'].dst, proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->ACK->Reset ACK et Syn->Reset ACK", act="Alerte") - print(f"Alerte, seuils dépassés, risque de TCPconnect Flood") + objets["database"].send_alert(datetime.now(), 5, None, "TCPConnect Flood", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->ACK->Reset ACK et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuils dépassés, risque de TCPconnect Flood") rule.cooldown = time.time() @@ -31,4 +33,4 @@ def rule(packet, tcp_packets, db): rule.cooldown = 0 rule.time_window = 0 rule.seuil = 0 - +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/ackscan.py b/idps/rules/TCP/Scan/ackscan.py index 3b321f5..916d2ff 100644 --- a/idps/rules/TCP/Scan/ackscan.py +++ b/idps/rules/TCP/Scan/ackscan.py @@ -2,7 +2,7 @@ from datetime import datetime import time -def rule(packet, tcp_packets, db): +def rule(packet, objets): """Règle ACK Scan: Un ACK Scan va envoyer des requêtes TCP avec le flag ACK Si le firewall ne bloque pas, alors il répond avec le flag Reset @@ -12,17 +12,19 @@ def rule(packet, tcp_packets, db): return # Vérification si nécessaire de récupérer les variables depuis la config - if (rule.seuil == 0 and rule.time_window == 0): - rule.time_window = db.get_key("ackscan_time", 180) - rule.seuil = db.get_key("ackscan_count", 5) + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("ackscan_time", 180) + rule.seuil = objets["config"].get("ackscan_count", 5) + rule.ban_time = objets["config"].get("ackscan_bantime", 300) # Comptage nombre de scan ack acceptés et refusés - ackdeny_count = tcp_packets.count_packet_of_type(["A", "R"], rule.time_window, True) - ackaccept_count = tcp_packets.count_packet_of_type(["A"], rule.time_window, True) + ackdeny_count = objets["tcp_packets"].count_packet_of_type(["A", "R"], rule.time_window, True) + ackaccept_count = objets["tcp_packets"].count_packet_of_type(["A"], rule.time_window, True) if (ackaccept_count + ackdeny_count >= rule.seuil): - db.send_alert(datetime.now(), 5, None, "ACK scan", packet['IP'].src, packet['IP'].dst, proto="TCP", reason="Détection de nombreux patterns de Ack->Reset et Ack pas de réponse", act="Alerte") - print(f"Alerte, seuil dépassés, risque d'Ack scan") + objets["database"].send_alert(datetime.now(), 5, None, "ACK scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Ack->Reset et Ack pas de réponse", act="Alerte") + print("Alerte, seuil dépassés, risque d'Ack scan") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) rule.cooldown = time.time() @@ -30,3 +32,4 @@ def rule(packet, tcp_packets, db): rule.cooldown = 0 rule.time_window = 0 rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/finscan.py b/idps/rules/TCP/Scan/finscan.py index d6c4ae9..b3a4801 100644 --- a/idps/rules/TCP/Scan/finscan.py +++ b/idps/rules/TCP/Scan/finscan.py @@ -2,27 +2,30 @@ from datetime import datetime import time -def rule(packet, tcp_packets, db): +def rule(packet, objets): """Règle Fin Scan: Un Fin Scan va envoyer des requêtes TCP avec le flag Fin Si le port est ouvert alors le serveur répondra pas Sinon le port est fermé et le serveur répondra: Reset ACK """ + if (rule.cooldown + rule.time_window > time.time()): return # Vérification si nécessaire de récupérer les variables depuis la config - if (rule.seuil == 0 and rule.time_window == 0): - rule.time_window = db.get_key("finscan_time", 180) - rule.seuil = db.get_key("finscan_count", 5) + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("finscan_time", 180) + rule.seuil = objets["config"].get("finscan_count", 5) + rule.ban_time = objets["config"].get("finscan_bantime", 300) # Comptage du nombre de scan fin acceptés et refusés - findeny_count = tcp_packets.count_packet_of_type(["F", "RA"], rule.time_window, True) - finaccept_count = tcp_packets.count_packet_of_type(["F"], rule.time_window, True) + findeny_count = objets["tcp_packets"].count_packet_of_type(["F", "RA"], rule.time_window, True) + finaccept_count = objets["tcp_packets"].count_packet_of_type(["F"], rule.time_window, True) if (findeny_count + finaccept_count >= rule.seuil): - db.send_alert(datetime.now(), 5, None, "Fin scan", packet['IP'].src, packet['IP'].dst, proto="TCP", reason="Détection de nombreux patterns de Fin->Reset Ack et Fin->rien", act="Alerte") - print(f"Alerte, seuil dépassés, risque de Fin Scan") + objets["database"].send_alert(datetime.now(), 5, None, "Fin scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Fin->Reset Ack et Fin->rien", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassés, risque de Fin Scan") rule.cooldown = time.time() @@ -30,3 +33,4 @@ def rule(packet, tcp_packets, db): rule.cooldown = 0 rule.time_window = 0 rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/nullscan.py b/idps/rules/TCP/Scan/nullscan.py index 6c686cb..beb9427 100644 --- a/idps/rules/TCP/Scan/nullscan.py +++ b/idps/rules/TCP/Scan/nullscan.py @@ -2,7 +2,7 @@ from datetime import datetime import time -def rule(packet, tcp_packets, db): +def rule(packet, objets): """Règle Null Scan: Un Null Scan va envoyer des requêtes TCP avec aucun flag d'actif Si le port est ouvert alors le serveur ne répondra pas @@ -12,17 +12,19 @@ def rule(packet, tcp_packets, db): return # Vérification si nécessaire de récupérer les variables depuis la config - if (rule.seuil == 0 and rule.time_window == 0): - rule.time_window = db.get_key("nullscan_time", 180) - rule.seuil = db.get_key("nullscan_count", 5) + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("nullscan_time", 180) + rule.seuil = objets["config"].get("nullscan_count", 5) + rule.ban_time = objets["config"].get("nullscan_bantime", 300) # Comptage du nombre de scan null acceptés et refusés - nulldeny_count = tcp_packets.count_packet_of_type(["", "RA"], rule.time_window, True) - nullaccept_count = tcp_packets.count_packet_of_type([""], rule.time_window, True) + nulldeny_count = objets["tcp_packets"].count_packet_of_type(["", "RA"], rule.time_window, True) + nullaccept_count = objets["tcp_packets"].count_packet_of_type([""], rule.time_window, True) - if (nulldeny_count + nulldeny_count >= rule.seuil): - db.send_alert(datetime.now(), 5, None, "Null scan", packet['IP'].src, packet['IP'].dst, proto="TCP", reason="Détection de nombreux patterns de None->Reset Ack et None -> rien", act="Alerte") - print(f"Alerte, seuil dépassés, risque de Null Scan") + if (nullaccept_count + nulldeny_count >= rule.seuil): + objets["database"].send_alert(datetime.now(), 5, None, "Null scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de None->Reset Ack et None -> rien", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassés, risque de Null Scan") rule.cooldown = time.time() @@ -30,3 +32,4 @@ def rule(packet, tcp_packets, db): rule.cooldown = 0 rule.time_window = 0 rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/synscan.py b/idps/rules/TCP/Scan/synscan.py index 9a5e5dc..2c522bc 100644 --- a/idps/rules/TCP/Scan/synscan.py +++ b/idps/rules/TCP/Scan/synscan.py @@ -2,7 +2,7 @@ from datetime import datetime import time -def rule(packet, tcp_packets, db): +def rule(packet, objets): """Règle SYN Scan: Un SYNScan va envoyer des requêtes TCP avec le flag SYN Si le port est ouvert alors le serveur répondra: Syn ACK, puis le client Reset la connexion @@ -12,17 +12,19 @@ def rule(packet, tcp_packets, db): return # Vérification si nécessaire de récupérer les variables depuis la config - if (rule.seuil == 0 and rule.time_window == 0): - rule.time_window = db.get_key("synscan_time", 180) - rule.seuil = db.get_key("synscan_count", 5) + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("synscan_time", 180) + rule.seuil = objets["config"].get("synscan_count", 5) + rule.ban_time = objets["config"].get("synscan_bantime", 300) # Comptage du nombre de scan syn acceptés et refusés - syndeny_count = tcp_packets.count_packet_of_type(["S", "RA"], rule.time_window, True) - synaccept_count = tcp_packets.count_packet_of_type(["S", "SA", "R"], rule.time_window, True) + syndeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + synaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "R"], rule.time_window, True) if (synaccept_count + syndeny_count >= rule.seuil): - db.send_alert(datetime.now(), 5, None, "Syn scan", packet['IP'].src, packet['IP'].dst, proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->Reset et Syn->Reset ACK", act="Alerte") - print(f"Alerte, seuil dépassés, risque de SynScan") + objets["database"].send_alert(datetime.now(), 5, None, "Syn scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->Reset et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("/sbin/iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassés, risque de SynScan") rule.cooldown = time.time() @@ -30,3 +32,4 @@ def rule(packet, tcp_packets, db): rule.cooldown = 0 rule.time_window = 0 rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/tcpconnectscan.py b/idps/rules/TCP/Scan/tcpconnectscan.py index 69253bd..24bf1fa 100644 --- a/idps/rules/TCP/Scan/tcpconnectscan.py +++ b/idps/rules/TCP/Scan/tcpconnectscan.py @@ -2,7 +2,7 @@ from datetime import datetime import time -def rule(packet, tcp_packets, db): +def rule(packet, objets): """Règle TCPConnect Scan: Un scan TCP connect va effectuer une connexion TCP en entier sur chaque port scanné. Si le port est ouvert le serveur acceptera la connexion SYN -> SYN ACK -> ACK -> Reset ACK @@ -13,17 +13,19 @@ def rule(packet, tcp_packets, db): return # Vérification si nécessaire de récupérer les variables depuis la config - if (rule.seuil == 0 and rule.time_window == 0): - rule.time_window = db.get_key("tcpconnectscan_time", 180) - rule.seuil = db.get_key("tcpconnectscan_count", 5) + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("tcpconnectscan_time", 180) + rule.seuil = objets["config"].get("tcpconnectscan_count", 5) + rule.ban_time = objets["config"].get("tcpconnectscan_bantime", 300) # Comptage du nombre de scan tcp connect acceptés et refusés - tcpconnectdeny_count = tcp_packets.count_packet_of_type(["S", "RA"], rule.time_window, True) - tcpconnectaccept_count = tcp_packets.count_packet_of_type(["S", "SA", "A", "RA"], rule.time_window, True) + tcpconnectdeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + tcpconnectaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "A", "RA"], rule.time_window, True) if (tcpconnectaccept_count + tcpconnectdeny_count >= rule.seuil): - db.send_alert(datetime.now(), 5, None, "TCPConnect Scan", packet['IP'].src, packet['IP'].dst, proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->ACK->Reset ACK et Syn->Reset ACK", act="Alerte") - print(f"Alerte, seuils dépassés, risque de TCPConnectScan") + objets["database"].send_alert(datetime.now(), 5, None, "TCPConnect Scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->ACK->Reset ACK et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuils dépassés, risque de TCPConnectScan") rule.cooldown = time.time() @@ -31,3 +33,4 @@ def rule(packet, tcp_packets, db): rule.cooldown = 0 rule.time_window = 0 rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/xmasscan.py b/idps/rules/TCP/Scan/xmasscan.py index 17cd359..4709e8f 100644 --- a/idps/rules/TCP/Scan/xmasscan.py +++ b/idps/rules/TCP/Scan/xmasscan.py @@ -2,7 +2,7 @@ from datetime import datetime import time -def rule(packet, tcp_packets, db): +def rule(packet, objets): """Règle XMAS Scan: Un XMAS Scan va envoyer des requêtes TCP avec le flag Fin, Push, Urg Si le port est ouvert alors le serveur répondra pas @@ -12,17 +12,19 @@ def rule(packet, tcp_packets, db): return # Vérification si nécessaire de récupérer les variables depuis la config - if (rule.seuil == 0 and rule.time_window == 0): - rule.time_window = db.get_key("xmasscan_time", 180) - rule.seuil = db.get_key("xmasscan_count", 5) + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("xmasscan_time", 180) + rule.seuil = objets["config"].get("xmasscan_count", 5) + rule.ban_time = objets["config"].get("xmasscan_bantime", 300) # Comptage du nombre de scan XMAS acceptés et refusés - xmasdeny_count = tcp_packets.count_packet_of_type(["FPU", "RA"], rule.time_window, True) - xmasaccept_count = tcp_packets.count_packet_of_type(["FPU"], rule.time_window, True) + xmasdeny_count = objets["tcp_packets"].count_packet_of_type(["FPU", "RA"], rule.time_window, True) + xmasaccept_count = objets["tcp_packets"].count_packet_of_type(["FPU"], rule.time_window, True) if (xmasaccept_count + xmasdeny_count >= rule.seuil): - db.send_alert(datetime.now(), 5, None, "XMAS scan", packet['IP'].src, packet['IP'].dst, proto="TCP", reason="Détection de nombreux patterns de Fin Push Urg -> rien et Fin Push Urg->Reset ACK", act="Alerte") - print(f"Alerte, seuil dépassés, risque de XMAS Scan") + objets["database"].send_alert(datetime.now(), 5, None, "XMAS scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Fin Push Urg -> rien et Fin Push Urg->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassés, risque de XMAS Scan") rule.cooldown = time.time() @@ -30,3 +32,4 @@ def rule(packet, tcp_packets, db): rule.cooldown = 0 rule.time_window = 0 rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/tcp.py b/idps/tcp.py index edbdf03..ca6d7ae 100644 --- a/idps/tcp.py +++ b/idps/tcp.py @@ -20,15 +20,15 @@ class TCP: if flags == "S": self.packets[ip_src].append([port_src, ip_dst, port_dst, ["S"], timestamp]) - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) elif flags is None: self.packets[ip_src].append([port_src, ip_dst, port_dst, [""], timestamp]) - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) elif flags == "FPU": self.packets[ip_src].append([port_src, ip_dst, port_dst, ["FPU"], timestamp]) - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) elif flags == "SA": i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S") @@ -36,10 +36,10 @@ class TCP: if i is not None: self.packets[ip][i][3].append("SA") self.packets[ip][i][4] = timestamp - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) else: self.packets[ip_src].append([port_src, ip_dst, port_dst, ["SA"], timestamp]) - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) elif flags == "A": i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "SA") @@ -49,10 +49,10 @@ class TCP: if i is not None: self.packets[ip][i][3].append("A") self.packets[ip][i][4] = timestamp - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) else: self.packets[ip_src].append([port_src, ip_dst, port_dst, ["A"], timestamp]) - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) elif flags == "RA": i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A") @@ -63,10 +63,10 @@ class TCP: if i is not None: self.packets[ip][i][3].append("RA") self.packets[ip][i][4] = timestamp - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) else: self.packets[ip_src].append([port_src, ip_dst, port_dst, ["RA"], timestamp]) - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) elif flags == "R": i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A") @@ -77,10 +77,10 @@ class TCP: if i is not None: self.packets[ip][i][3].append("R") self.packets[ip][i][4] = timestamp - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) else: self.packets[ip_src].append([port_src, ip_dst, port_dst, ["R"], timestamp]) - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) elif flags == "F": i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A") @@ -88,10 +88,10 @@ class TCP: if i is not None: self.packets[ip][i][3].append("F") self.packets[ip][i][4] = timestamp - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) else: self.packets[ip_src].append([port_src, ip_dst, port_dst, ["F"], timestamp]) - return + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) def find_packet_to_replace(self, ip_src, port_src, ip_dst, port_dst, flags): """Cherche l'indice et le port de source du paquet dont le flag doit être remplacé""" @@ -147,3 +147,15 @@ class TCP: """Retourne la liste des paquets liés à une adresse IP, pour du déboggage""" return self.packets.get(src_ip, None) + + def return_origin_packet(self, ip_src, port_src, ip_dst, port_dst, ip = None): + """Retourne le paquet d'origine par rapport à l'ip de référence""" + + if ip is None: + return [ip_src, port_src, ip_dst, port_dst] + else: + if ip == ip_src: + return [ip_src, port_src, ip_dst, port_dst] + elif ip == ip_dst: + return [ip_dst, port_dst, ip_src, port_src] +