Skip to content

Commit 898a175

Browse files
committed
Merge branch 'main' of https://github.com/mouwaficbdr/CryptoForensic-Python into feature/chacha20_analyzer
2 parents 64611ee + dba536a commit 898a175

12 files changed

Lines changed: 470 additions & 159 deletions

main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from src.detecteur_crypto import DetecteurCryptoOrchestrateur
2+
print(DetecteurCryptoOrchestrateur().analyser_fichier_specifique('data/mission1.enc'))

src/__init__.py

Whitespace-only changes.

src/analyzers/__init__.py

Whitespace-only changes.

src/analyzers/aes_cbc_analyzer.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,14 @@ def identifier_algo(self, chemin_fichier_chiffre: str) -> float:
5050
donnees_chiffres = contenu_fichier[16:]
5151

5252
if len(donnees_chiffres) % 16 == 0: #Heuristique taille multipe de 16 bytes (Vérifie si les donnéese chiffrés sont en bloc de 16 octets, caractéristique de l'aes cbc)
53-
probabilite = 0.6
53+
probabilite = 0.5
5454
else:
55-
return 0.0
55+
probabilite = 0.0
5656

5757
entropie = calculer_entropie(donnees_chiffres)
5858

5959
if entropie > 7.5: #Heuristique entropie élevée (L'entropie doit être supérieur à 7.5 pour confirmer le chiffrement robuste caractéristique des algos de chiffrement)
60-
probabilite += 0.4
61-
else:
62-
return 0.0
60+
probabilite += 0.5
6361

6462
except FileNotFoundError:
6563
return 0.0

src/analyzers/aes_gcm_analyzer.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from ..crypto_analyzer import CryptoAnalyzer
2+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
3+
from cryptography.hazmat.primitives import hashes
4+
import re
5+
6+
class Aes_Gcm_Analyzer(CryptoAnalyzer):
7+
'''Détermine si l'algo aes_gcm est utilisé, génère des clés et tente de de déchffrer un fichier chiffré en utilisant les clés générées.
8+
9+
Cette classe a trois méthodes principales:
10+
- identifier_algo: Détermine si l'algo de chiffrement utilsé sur le fichier chiffré qui lui est passé en paramètre est l'aes_gcm.
11+
- generer_cles_candidates: Génère une liste de clés candidates pour le déchiffrement du fichier chiffré
12+
- dechiffrer: fait le déchiffrement proprement dit sur la base de la liste des clés générées
13+
14+
Attributes:
15+
_PBKDF2_SALT: le salt utilisé pour le chiffrement
16+
_PBKDF2_ITERATIONS: le nombre d'itérations faites au chiffrement
17+
_PBKDF2_LONGUEUR_CLE: la longueur en octets de la clé à utiliser
18+
19+
'''
20+
21+
_PBKDF2_SALT = b"AES_GCM_SALT_2024" #Fourni
22+
_PBKDF2_ITERATIONS = 10000 #Fourni
23+
_PBKDF2_LONGUEUR_CLE = 32 #Longueur de la clé
24+
25+
def __filtrer_dictionnaire_par_indice(self, chemin_dictionnaire: str) -> list[str]:
26+
"""
27+
Filtre le dictionnaire en se basant sur les indices de la mission 4.
28+
L'indice pointe vers le format de clé "Acronyme en majuscules + 4 chiffres".
29+
30+
Args:
31+
chemin_dictionnaire(str): Le chemin vers le fichier de dictionnaire.
32+
33+
Returns:
34+
list[str]: Une liste de mots de passe filtrés.
35+
"""
36+
mots_filtres: list[str] = []
37+
38+
# L'année courante
39+
annee_courante = "2024" #Normalement 2025 mais on considère 2024 pour se conformer à la wordlist
40+
41+
# Définition du motif d'acronyme de 4 lettres en majuscules
42+
# On utilise une expression régulière pour plus de robustesse
43+
motif_acronyme = re.compile(r"^[A-Z]{4}$")
44+
45+
try:
46+
with open(chemin_dictionnaire, "r", encoding="utf-8") as f:
47+
for ligne in f:
48+
mot = ligne.strip()
49+
50+
# Vérifie si le mot de passe correspond au format de l'indice
51+
# ex: NATO2024, UN2024, etc.
52+
if mot.endswith(annee_courante):
53+
acronyme = mot[:-4] # Extrait la partie acronyme
54+
if motif_acronyme.match(acronyme):
55+
mots_filtres.append(mot)
56+
57+
except FileNotFoundError:
58+
print(f"Erreur : Le fichier de dictionnaire '{chemin_dictionnaire}' est introuvable.")
59+
return []
60+
61+
return mots_filtres
62+
63+
def generer_cles_candidates(self, chemin_dictionnaire: str) -> list[bytes]:
64+
'''
65+
Génère les clées candidates pour déchiffrer le fichier à partir de la liste retournée par filtrer_dictionnaire_par_indices.
66+
67+
Args:
68+
chemin_dictionnaire(str): le chemin du dictionnaire de mots de passes pour l'attaque par dictionnaire.
69+
70+
Returns:
71+
list[bytes]: liste des clés candidates.
72+
'''
73+
74+
mots_de_passe_cible = self.__filtrer_dictionnaire_par_indice(chemin_dictionnaire)
75+
76+
clees_candidates: list[bytes] = []
77+
kdf = PBKDF2HMAC(
78+
algorithm=hashes.SHA256(),
79+
length=self._PBKDF2_LONGUEUR_CLE,
80+
iterations=self._PBKDF2_ITERATIONS,
81+
salt=self._PBKDF2_SALT
82+
)
83+
for mot_de_passe in mots_de_passe_cible:
84+
mot_de_passe_en_octets: bytes = mot_de_passe.encode('utf-8')
85+
cle_derivee: bytes = kdf.derive(mot_de_passe_en_octets)
86+
clees_candidates.append(cle_derivee)
87+
88+
return clees_candidates

src/analyzers/blowfish_analyzer.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
from ..detecteur_crypto import CryptoAnalyzer
2+
from ..utils import calculer_entropie
3+
import hashlib
4+
from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes
5+
from cryptography.hazmat.primitives.padding import PKCS7
6+
class Blowfish_Analyzer(CryptoAnalyzer):
7+
'''Détermine si l'algo blowfish est utilisé, génère des clés et tente de de déchffrer un fichier chiffré en utilisant les clés générées.
8+
9+
Cette classe a trois méthodes:
10+
- identifier_algo: Détermine si l'algo de chiffrement utilsé sur le fichier chiffré qui lui est passé en paramètre est blowfish.
11+
- generer_cles_candidates: Génère une liste de clés candidates pour le déchiffrement du fichier chiffré
12+
- dechiffrer: fait le déchiffrement proprement dit sur la base de la liste des clés générées
13+
14+
Attributes:
15+
__BLOWFISH_TAILLE_BLOC : taille à considérer pour les blocs de données chiffrés que le PKCS7 doit prendre en compte (8 bits)
16+
__BLOWFISH_TAILLE_IV : taille du vecteur d'initialisation en début de fichier (8 bits)
17+
18+
'''
19+
20+
__BLOWFISH_TAILLE_BLOC = 8
21+
__BLOWFISH_TAILLE_IV = 8
22+
23+
def identifier_algo(self, chemin_fichier_chiffre: str) -> float:
24+
'''
25+
Détermine la probabilité que l'algo de chiffrement utilisé soit blowfish en:
26+
27+
- vérifiant la présence d'un IV à l'en-tête (taille fichier > 8 octets) et que la taille du fichier est un multiple de 8 (blocs de 8 octets pour l'algo blowfish)
28+
- calculant l'entropie des données chiffrées
29+
- calculant l'entropie des sous blocs
30+
31+
Args:
32+
chemin_fichier_chiffre(str): Le chemin du fichier chiffré à traiter (mission1.enc).
33+
34+
Returns:
35+
float: probabilité calculée.
36+
'''
37+
38+
score = 0.0
39+
try:
40+
with open(chemin_fichier_chiffre, "rb") as f:
41+
contenu_fichier: bytes = f.read()
42+
taille_totale = len(contenu_fichier)
43+
TAILLE_IV = 8
44+
45+
# Heuristique 1 : Vérification de la taille (le critère le plus important)
46+
if taille_totale > TAILLE_IV and taille_totale % 8 == 0:
47+
score += 0.4
48+
49+
donnees_chiffrees = contenu_fichier[TAILLE_IV:]
50+
51+
# Heuristique 2 : Vérification de l'entropie globale
52+
entropie_globale = calculer_entropie(donnees_chiffrees)
53+
if entropie_globale > 7.5:
54+
score += 0.3
55+
56+
# Heuristique 3 : Vérification du "pattern Blowfish" (entropie par sous-blocs)
57+
taille_donnees = len(donnees_chiffrees)
58+
moitie = taille_donnees // 2
59+
60+
entropie_moitie1 = calculer_entropie(donnees_chiffrees[:moitie])
61+
entropie_moitie2 = calculer_entropie(donnees_chiffrees[moitie:])
62+
63+
if entropie_moitie1 > 7.5 and entropie_moitie2 > 7.5:
64+
score += 0.3
65+
66+
except FileNotFoundError:
67+
return 0.0
68+
69+
return score
70+
71+
72+
def __filtrer_dictionnaire_par_indice(self, chemin_dictionnaire: str) -> list[str]:
73+
"""
74+
Filtre le dictionnaire en se basant sur les indices de la mission 3.
75+
L'indice pointe vers un format de clé "sha + nombre + chiffres simples".
76+
77+
Args:
78+
chemin_dictionnaire(str): Le chemin vers le fichier de dictionnaire.
79+
80+
Returns:
81+
list[str]: Une liste de mots de passe filtrés.
82+
"""
83+
mots_filtres: list[str] = []
84+
85+
# Indices pour le préfixe et le suffixe
86+
prefixes = ("sha256", "sha384", "sha512", "sha1")
87+
suffixes = ("123", "456", "789")
88+
89+
try:
90+
with open(chemin_dictionnaire, "r", encoding="utf-8") as f:
91+
for ligne in f:
92+
mot = ligne.strip()
93+
94+
# Vérifie si le mot commence par un préfixe et se termine par un suffixe
95+
if mot.startswith(prefixes) and mot.endswith(suffixes):
96+
mots_filtres.append(mot)
97+
98+
except FileNotFoundError:
99+
print(f"Erreur : Le fichier de dictionnaire '{chemin_dictionnaire}' est introuvable.")
100+
return []
101+
102+
return mots_filtres
103+
104+
def generer_cles_candidates(self, chemin_dictionnaire: str) -> list[bytes]:
105+
"""
106+
Génère une liste de clés candidates pour le déchiffrement.
107+
Les candidats incluent les mots de passe directs, leur hash MD5 et leur hash SHA1.
108+
109+
Args:
110+
chemin_dictionnaire(str): Le chemin vers le fichier de dictionnaire.
111+
112+
Returns:
113+
list[bytes]: Une liste des clés candidates sous forme d'octets.
114+
"""
115+
cles_candidates: list[bytes] = []
116+
# Utilisation de la méthode privée pour filtrer les mots
117+
mots_de_passe_cible = self.__filtrer_dictionnaire_par_indice(chemin_dictionnaire)
118+
119+
for mot in mots_de_passe_cible:
120+
mot_en_bytes = mot.encode("utf-8")
121+
122+
# 1. Ajouter le mot de passe direct comme clé candidate
123+
cles_candidates.append(mot_en_bytes)
124+
125+
# 2. Hachage MD5 et ajout à la liste (en bytes)
126+
hash_md5 = hashlib.md5(mot_en_bytes).digest()
127+
cles_candidates.append(hash_md5)
128+
129+
# 3. Hachage SHA1 et ajout à la liste (en bytes)
130+
hash_sha1 = hashlib.sha1(mot_en_bytes).digest()
131+
cles_candidates.append(hash_sha1)
132+
133+
return cles_candidates
134+
135+
def dechiffrer(self, chemin_fichier_chiffre: str, cle_donnee: bytes) -> bytes:
136+
"""
137+
Déchiffre le fichier supposé crypté par l'algorithme blowfish avec la clé donnée en respectant les critères de
138+
- récupération de l'IV
139+
- suppression de padding
140+
141+
Args:
142+
chemin_fichier_chiffre (str): le chemin vers le fichier chiffré
143+
clee_donnee (bytes): La clé à utiliser pour le déchiffrement
144+
Returns:
145+
bytes: les données originales
146+
"""
147+
148+
#La taille de clé est dans l'intervalle 32-448bits et est multiple de 8
149+
if len(cle_donnee) not in range(32, 448, 8):
150+
raise ValueError('Taille de clé invalide.')
151+
152+
try:
153+
154+
algorithm_blowfish = algorithms.Blowfish(cle_donnee)
155+
texte_chiffre = ''
156+
157+
#Récupération de l'IV et des texte chiffré das le fichier
158+
with open(chemin_fichier_chiffre, 'rb') as f:
159+
initialization_vector = f.read(self.__BLOWFISH_TAILLE_IV)
160+
texte_chiffre = f.read()
161+
f.close()
162+
163+
#Initialisation du cipher
164+
cipher = Cipher(algorithm_blowfish, modes.CBC(initialization_vector))
165+
decrypteur = cipher.decryptor()
166+
167+
#Suppresseur de padding
168+
supresseur_padding = PKCS7(self.__BLOWFISH_TAILLE_BLOC).unpadder()
169+
170+
#Décriptage des données avec le padding(remplissage aléatoire)
171+
donnees_chiffrees_avec_padding = decrypteur.update(texte_chiffre) + decrypteur.finalize()
172+
173+
#Suppression du padding et récupération de la donnée finale
174+
donnees_originales = supresseur_padding.update(donnees_chiffrees_avec_padding) + supresseur_padding.finalize()
175+
return donnees_originales
176+
177+
except FileNotFoundError:
178+
raise
179+
180+

0 commit comments

Comments
 (0)