Après avoir compris le fonctionnement du ransomware grâce à la rétro-ingénierie, la phase de déchiffrement a pu commencer avec quelques prérequis:
# Clé de chiffrement
KEY = b"iment super !!!!"
# Vecteur d'initialisation, utilisé pour déchiffrer le premier block de 16 bytes de la chaine.
# Il est essentiel puisque le premier block ne peut pas bénéficier de l'opération xor (base du chainage AES CBC)
# avec le précédent block. Cette première opération est donc effectuée avec l'IV.
IV = b"la srs c'est vra"
# Les fichier qu'on va déchiffrer sont ceux dont l'extension est .srs
TARGET_EXTENSION = ".srs"
Dans un soucis de praticité, et d’adaptabilité, le programme interprète différents flags lors de son lancement:
####################################################################################
#
# Récupération des paramètres du programme
#
####################################################################################
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--path', type=str, help='Path from which the decryption will start')
parser.add_argument('-r', '--recursive', action='store_true', help='Recurse over directories')
parser.add_argument('-v', '--verbose', action='store_true', help='Display execution flow')
args = parser.parse_args()
Que l’on peut consulter via la commande:
python decrypt.py -h

Ces différents flags sont accédés à différents endroit du code pour moduler le comportement du programme.
Afin de concentrer toutes les opérations sur les chemins telles que les ouvertures de fichiers, les fermetures, la normalisation du chemin sous forme de string en fonction de l’os sur lequel le programme est exécuté, bien qu’en pratique le ransomware n’opère que sur les os Windows.
# Classe pour la manipilation des path
class StandardPath:
# En AES128, les blocks de bytes à déchiffrer doivent être d'une taille multiple
# de 16, on ajoute donc du padding lorsque ce n'est pas le cas
def padding_to_mod_16bytes(string):
nb_byte_padding = 16 - (len(string) % 16)
return string + b" " * (nb_byte_padding % 16)
# Créé un path qui est syntaxiquement correcte vis à vis de l'os sur lequel le script s'exécute
def standard_path_compose(*argv) -> str:
return str(os.path.join(*argv))
# Modifie un path afin qu'il correspondent aux exigences de l'os sur lequel le script s'exécute
def standardize_path(string_path : str) -> str:
return str(os.path.normpath(string_path))
# Retourne l'extension d'un chemin de fichier
def get_extension(string_path : str) -> str:
return str(Path(string_path).suffix)
# Retourne True si le chemin de fichier contient l'extension souhaitée
def has_extension(string_path : str, extension : str) -> bool:
return (Path(string_path).suffix == extension)
# Enlève l'extension d'un fichier
def remove_extension(string_path : str) -> str:
return str(Path(string_path).with_suffix(""))
# Vérifie que le chemin mène à un fichier
def is_file(string_path : str) -> bool:
return os.path.isfile(string_path)
# Vérifie que le chemin mène à un dossier
def is_dir(string_path : str) -> bool:
return os.path.isdir(string_path)
# Verifie l'existance d'un chemin
def exists(string_path : str) -> bool:
return os.path.exists(string_path)
# Vérifie que le chemin mène au fichier existe et peut être accédé en lecture
def is_file_readable(string_path : str) -> bool:
return StandardPath.exists(string_path) and os.access(string_path, os.R_OK)
# Vérifie que le chemin mène au fichier existe et peut être accédé en écriture
def is_file_writable(string_path : str) -> bool:
return StandardPath.exists(string_path) and os.access(string_path, os.W_OK)
# Ouvre un fichier
def open(string_path : str, options : FILE_ACCESS_OPTION):
try:
return open(string_path, options.value)
except IOError:
return None
# Lis l'intégralité du contenu d'un fichier
def read(f) -> str:
try:
return f.read()
except IOError:
return None
# Lis l'intégralité du contenu d'un fichier et ajoute le padding nécessaire
def read_padding(f, padding=16):
try:
content = f.read()
return StandardPath.padding_to_mod_16bytes(content)
except IOError:
return None
# Ecrit dans un fichier
def write(f, content : str):
try:
return f.write(content)
except IOError:
return
# Ferme un fichier
def close(f):
try:
f.close()
except IOError:
return
A été ajouté à la classe StandardPath une Enum permettant de restreindre l’accès aux fichiers en lecture d’octets et en écritures d’octets afin de permettre le déchiffrement sur des chaines de bytes.
# Enum pour définir et limiter les possibles options d'accès aux fichiers
class FILE_ACCESS_OPTION(Enum):
READ_B = "rb" # read_bytes
WRITE_B = "wb" # write_bytes
# Classe pour la manipilation des path
class StandardPath:
# [...]
# Ouvre un fichier
def open(string_path : str, options : FILE_ACCESS_OPTION):
try:
return open(string_path, options.value)
except IOError:
return None
La manipulation de fichier se fait donc à-présent de manière transparente vis à vis de l’os sur lequel le programme est lancé.
Le parcours de fichiers se déroule de manière récursive à partir d’un chemin renseigné par l’utilisateur via les flags. Le path pouvant être une expression régulière, la bibliothèque glob peut être utilisée.
La fonction consiste simplement à récupérer tous les fichiers / dossiers du niveau de la branche sur laquelle le chemin se trouve. Si l’object trouvé est un fichier, on lance la phase de déchiffrement, si c’est un dossier, la fonction s’appelle récursivement (si le flags -r est actif).
Ainsi, toute l’arboressance dont la racine est string_path est parcourue:
####################################################################################
#
# Parcours récursif de l'arborescence de fichiers
#
####################################################################################
# Fonction dont le but est de parcourir l'arborescence de fichiers récursivement
# à partir du premier chemin, renseigné dans PATH
def file_handler(string_path : str):
# On parcours tous les fichiers / dossiers à partir d'une regex
for string_path in glob.iglob(string_path):
if StandardPath.is_file(string_path):
# Déchiffrement si l'objet manipulé est un fichier
restore_file_encrypted(string_path)
elif StandardPath.is_dir(string_path):
# Récursion si l'objet manipulé est un dossier
if vars(args).get("recursive"):
file_handler(StandardPath.standard_path_compose(string_path, "*"))
Le coeur de fonctionnement du script est la fonction decrypt_block(). Celle-ci permet de déchiffrer une chaine d’octets à-partir de l’IV (Vecteur d’Initialisation) et de la clé. Cette fonction se contente donc juste de déchiffrer et ne se soucie en aucun cas du type de conteneur à déchiffrer:
# Déchiffre une chaine de bytes
def decrypt_block(key : bytes, iv : bytes, encrypted_data : bytes) -> bytes:
# Créé un objet de chiffrement / Déchiffrement AES
aes = AES.new(key, AES.MODE_CBC, iv)
# Déchiffre une string
decrypted_data = aes.decrypt(encrypted_data)
return decrypted_data
Une autre fonction qui va limiter les accès à decrypt_block() est nécessaire. restore_file_encrypted() va servir de liant entre la fonction de parcours des fichiers file_handler() et decrypt_block(). Celle-ci va s’assurer que les fichiers convoités pour le déchiffrement portent bien l’extension .srs, et qu’il sont accessible en lecture.
Si tel est le cas, un fichier de sortie est créé, portant le même nom que le fichier chiffré, mais dépourvu de son extension .srs:
####################################################################################
#
# Récupère les fichier chiffrés
#
####################################################################################
def restore_file_encrypted(string_path):
# Vérifie que le fichier que l'on cherche à restaurer ait bien un extension en .srs, et qu'il soit accessible en lecture
if not StandardPath.has_extension(string_path, TARGET_EXTENSION) or not StandardPath.is_file_readable(string_path):
return
# Le chemin du fichier dépourvu de l'extension .srs représente le chemin du fichier restauré
string_output_path = StandardPath.remove_extension(string_path)
# Ouverture des fichiers d'entrée et de sortie
in_file = StandardPath.open(string_path, FILE_ACCESS_OPTION.READ_B)
out_file = StandardPath.open(string_output_path, FILE_ACCESS_OPTION.WRITE_B)
if in_file == None or out_file == None:
return
# Récupération du contenu d'entrée, auquel on ajoute le padding nécessaire pour que le nombre
# de caractère soit un multiple de 16
crypted_content = StandardPath.read_padding(in_file)
# Récupération du contenu déchiffré et écriture de celui-ci dans le fichier de destination
decrypted_content = decrypt_block(KEY, IV, crypted_content)
StandardPath.write(out_file, decrypted_content)
# Fermeture des fichiers
StandardPath.close(out_file)
StandardPath.close(in_file)
if vars(args).get('verbose'):
print(Fore.RED + string_path + Style.RESET_ALL + " restored as " + Fore.GREEN + string_output_path + Fore.RESET)
Afin de pouvoir lancer le script sur un os Windows, quelques prérequis sur l’environnement sont à prévoir:
Entrer dans la barre de recherche Windows “microsoft store” (1)
Sélectionner l’application “Microsoft Store” (2):

Entrer dans la barre de recherche Microsoft Store “python” (1)
Sélectionner Python en version 3.9 (2):

Cliquer sur “Obtenir” (1):

Cliquer sur “Non merci” (1):

Vérifier que Python est bien installé en tapant dans un invite de commandes la commande:
python --version

Python est alors installé en version 3.9.
Ouvrir maintenant son terminal favori. Ici ce sera Git bash**.
Installer le paquet d’environnement virtuel Python virtualenv:
pip install virtualenv
Si tout à bien fonctionné, un dossier venv a été créé:

Créer l’environnement virtuel avec la commande:
python -m venv venv
ls

Rejoindre alors l’environnement virtuel:
source venv/Scripts/activate
Une annotation “(venv)” devrait alors apparaitre sur votre prompt:

Installer les dépendances du script:
pip install -r requirements.txt

La commande:
python decrypt.py -h
permet de voir quels flags peuvent être interprétés, et leur fonction:

Voici l’architecture dans laquelle le programme va s’exécuter:
tree . /F
Commande exécutée depuis l’Invite de commandes Windows:

Lancer le programme:
python decrypt.py --recursive --verbose --path "./*"
La sortie de l’exécution nous montre quels fichiers ont été considérés comme chiffrés (rouge), et où est-ce qu’ils ont été déchiffrés (vert).

Voici l’architecture après l’exécution du programme:
tree . /F
Commande exécutée depuis l’Invite de commandes Windows:

Pour quitter l’environnement virtuel:
deactivate
