From 19d007dfffcb1b2eaf3a45ebb41b1c965e58882d Mon Sep 17 00:00:00 2001 From: Oxbian Date: Thu, 14 Nov 2024 12:08:34 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20idps=20+=20d=C3=A9tection=20scan=20TCPC?= =?UTF-8?q?onnect,=20SynScan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Demo/Dockerfiles/Dockerfile.idps | 13 ++- Demo/docker-compose.yml | 8 +- README.md | 2 +- idps/main.py | 108 +++++++++++++++++++++++ idps/rules/TCP/Scan/synscan.py | 8 ++ idps/rules/TCP/Scan/tcpconnectscan.py | 8 ++ idps/tcp.py | 118 ++++++++++++++++++++++++++ 7 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 idps/main.py create mode 100644 idps/rules/TCP/Scan/synscan.py create mode 100644 idps/rules/TCP/Scan/tcpconnectscan.py create mode 100644 idps/tcp.py diff --git a/Demo/Dockerfiles/Dockerfile.idps b/Demo/Dockerfiles/Dockerfile.idps index 0f84f02..84a1179 100644 --- a/Demo/Dockerfiles/Dockerfile.idps +++ b/Demo/Dockerfiles/Dockerfile.idps @@ -6,7 +6,14 @@ RUN apk -U upgrade && \ RUN pip install scapy # Copier le script de l'idps -#COPY idps.py /idps.py +WORKDIR /app -# Lancer le script de détection -#CMD ["python", "/idps.py"] +# Copier le contenu du répertoire 'idps' du contexte de build vers '/app/idps' dans le conteneur +COPY idps /app/idps + +# Autres commandes nécessaires pour ton projet +# Par exemple, pour installer des dépendances : +# RUN pip install -r /app/idps/requirements.txt (si applicable) + +# Commande par défaut +CMD ["python", "/app/idps/main.py"] diff --git a/Demo/docker-compose.yml b/Demo/docker-compose.yml index ccc268a..af62119 100644 --- a/Demo/docker-compose.yml +++ b/Demo/docker-compose.yml @@ -3,8 +3,8 @@ services: # Attaquant 1 atk1: build: - context: Dockerfiles/. - dockerfile: Dockerfile.attaquant + context: .. + dockerfile: Demo/Dockerfiles/Dockerfile.attaquant container_name: attaquant1 command: sleep infinity networks: @@ -15,8 +15,8 @@ services: # IDPS idps: build: - context: Dockerfiles/. - dockerfile: Dockerfile.idps + context: .. + dockerfile: Demo/Dockerfiles/Dockerfile.idps container_name: idps command: sleep infinity cap_add: diff --git a/README.md b/README.md index 90dc920..0706ab2 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,4 @@ docker compose up -d - Noyau d'analyse de l'IDS - Interface web pour visualiser les alertes / rechercher dedans -- Moteur de corrélation des alertes (récupération + renvoi dans MySQL). +- Moteur de corrélation des alertes (récupération + renvoi dans MySQL). diff --git a/idps/main.py b/idps/main.py new file mode 100644 index 0000000..d5d1849 --- /dev/null +++ b/idps/main.py @@ -0,0 +1,108 @@ +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 + + +def load_rules(rules_dirpath = "/app/idps/rules"): + """Charger les fonctions de règles du répertoire de règles et des sous répertoires""" + + if not os.path.exists(rules_dirpath): + raise ValueError(f"Le chemin spécifié n'existe pas: {rules_dirpath}") + + if not os.path.isdir(rules_dirpath): + raise ValueError(f"Le chemin spécifié n'est pas un répertoire: {rules_dirpath}") + + rules_functions = {} + # Liste de répertoires à explorer + dirs_to_explore = [rules_dirpath] + + # Explorer chaque répertoire / sous répertoire à la rechercher de fichier de règles + while dirs_to_explore: + current_dir = dirs_to_explore.pop() + + try: + for entry in os.scandir(current_dir): + # Ignorer les liens symboliques + if entry.is_symlink(): + continue + + # Ajouter les répertoires dans la liste à explorer + if entry.is_dir(): + dirs_to_explore.append(entry.path) + elif entry.is_file() and entry.name.endswith(".py"): + # Suppression de l'extension .py + module_name = entry.name[:-3] + + # Déterminer le protocole à partir du répertoire parent + if "TCP" in entry.path: + parent_dir = "TCP" + else: + parent_dir = "WTF" + + if parent_dir not in rules_functions: + rules_functions[parent_dir] = [] + + # Chargement du module + spec = importlib.util.spec_from_file_location(module_name, entry.path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Vérification que le module possède bien la fonction rule + if hasattr(module, "rule"): + rules_functions[parent_dir].append(module.rule) + + except PermissionError: + print(f"Permission refusée pour accéder au répertoire: {current_dir}") + except OSError as e: + print(f"Erreur lors de l'accès au répertoire {current_dir}: {e}") + + return rules_functions + + +def check_frame_w_rules(packet, rules_functions, packets): + """Appliquer chaque règle des fonctions au paquet capturé.""" + + for rule_func in rules_functions: + try: + rule_func(packet, packets) + 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): + #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) + tcp_packets.clean_old_packets() + + +def start_idps(IDS_IFACES = ["eth0","eth1"]): + """Charge les règles et démarre l'IDPS""" + print(f"Chargement des règles...") + rules_functions = load_rules() + print(f"Les règles sont chargé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) + + tcp_packets = tcp.TCP(300) + + # Lancer scapy & envoyer le paquet à chaque règle de l'IDPS + sniff(iface=IDS_IFACES, prn=lambda packet: packet_callback(packet, rules_functions, tcp_packets), store=0) + #wrpcap("idps.pcap", capture) + + +def main(): + print(f"Démarrage de l'IDPS") + start_idps() + print(f"IDPS opérationel") + + +if __name__ == "__main__": + main() diff --git a/idps/rules/TCP/Scan/synscan.py b/idps/rules/TCP/Scan/synscan.py new file mode 100644 index 0000000..7f3b45f --- /dev/null +++ b/idps/rules/TCP/Scan/synscan.py @@ -0,0 +1,8 @@ +# Seuils +TIME_WINDOW = 180 +NB_SEUIL = 5 + + +def rule(packet, tcp_packets): + if (tcp_packets.count_packet_of_type("RA", TIME_WINDOW) + tcp_packets.count_packet_of_type("SA", TIME_WINDOW)) + tcp_packets.count_packet_of_type("R", TIME_WINDOW) >= NB_SEUIL: + print(f"Alerte, seuil dépassés, risque de SynScan") diff --git a/idps/rules/TCP/Scan/tcpconnectscan.py b/idps/rules/TCP/Scan/tcpconnectscan.py new file mode 100644 index 0000000..ddcba16 --- /dev/null +++ b/idps/rules/TCP/Scan/tcpconnectscan.py @@ -0,0 +1,8 @@ +# Seuils +TIME_WINDOW = 180 # 180 secondes pour avoir X paquets +NB_SEUIL = 5 + + +def rule(packet, tcp_packets): + if (tcp_packets.count_packet_of_type("A", TIME_WINDOW) + tcp_packets.count_packet_of_type("RA", TIME_WINDOW)) >= NB_SEUIL: + print(f"Alerte, seuils dépassés, risque de TCPConnectScan") diff --git a/idps/tcp.py b/idps/tcp.py new file mode 100644 index 0000000..a2ea321 --- /dev/null +++ b/idps/tcp.py @@ -0,0 +1,118 @@ +import time + + +class TCP: + def __init__(self, clean_time=300): + """Constructeur de la classe TCP + @param clean_time: temps avant qu'un paquet soit nettoyé""" + + self.packets = {} + self.clean_time = clean_time + + def add_packet(self, ip_src, port_src, ip_dst, port_dst, flags, timestamp): + """Ajoute le suivi d'un paquet dans le dictionnaire""" + + # Initialisation de la liste de paquets pour l'IP source + if ip_src not in self.packets: + self.packets[ip_src] = [] + + if flags == "S": + self.packets[ip_src].append([port_src, ip_dst, port_dst, flags, timestamp]) + return + + elif flags == "SA": + i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S", True) + + if i is not None: + self.packets[ip_dst][i][3] = "SA" + self.packets[ip_dst][i][4] = timestamp + return + else: + self.packets[ip_src].append([port_src, ip_dst, port_dst, flags, timestamp]) + return + + elif flags == "A": + i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "SA") + if i is None: + i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "R", True) + + if i is not None: + self.packets[ip_src][i][3] = "A" + self.packets[ip_src][i][4] = timestamp + return + else: + self.packets[ip_src].append([port_src, ip_dst, port_dst, flags, timestamp]) + return + + elif flags == "RA": + i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A") + + if i is None: + i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S") + + if i is not None: + self.packets[ip_src][i][3] = "RA" + self.packets[ip_src][i][4] = timestamp + return + else: + self.packets[ip_src].append([port_src, ip_dst, port_dst, flags, timestamp]) + return + + elif flags == "R": + i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A") + + if i is None: + i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S") + + if i is not None: + self.packets[ip_src][i][3] = "R" + self.packets[ip_src][i][4] = timestamp + return + else: + self.packets[ip_src].append([port_src, ip_dst, port_dst, flags, timestamp]) + return + + def find_packet_to_replace(self, ip_src, port_src, ip_dst, port_dst, flags, reverse=False): + """Cherche l'indice du paquet dont le flag doit être remplacé""" + if reverse is True: + ip_src, ip_dst = ip_dst, ip_src + port_src, port_dst = port_dst, port_src + + for i, [p_s, ip_d, p_d, f, stamp] in enumerate(self.packets[ip_src]): + if p_s == port_src and ip_d == ip_dst and p_d == port_dst and f == flags: + return i + return None + + def clean_old_packets(self): + """Supprime les paquets qui date de plus longtemps que le temps de clean""" + current_timestamp = time.time() + + # Parcours chaque ip_source de la liste + for ip_src in list(self.packets.keys()): + + # Vérification si le paquet doit être supprimé ou non + i = 0 + while i < len(self.packets[ip_src]): + packet = self.packets[ip_src][i] + if packet[4] <= current_timestamp - self.clean_time: + del self.packets[ip_src][i] + else: + i += 1 + + # Suppression de la case de l'ip source si elle n'existe plus + if not self.packets[ip_src]: + del self.packets[ip_src] + + def count_packet_of_type(self, flag, time_treshold): + """Compte les paquets qui ont le flag choisi et qui sont dans la fenêtre de temps""" + count = 0 + + current_timestamp = time.time() + for ip in list(self.packets.keys()): + for packet in self.packets[ip]: + if packet[3] == flag and packet[4] >= current_timestamp - time_treshold: + count += 1 + return count + + def __getitem__(self, src_ip): + return self.packets.get(src_ip, None)