Nous avons intercepté un fichier contenant des informations top secrètes. Cependant, ce fichier semble totalement vide. Nous pensons que ses auteurs ont utilisé un algorithme dont nous avons réussi à obtenir une partie de la documentation.
Votre mission est simple : rendre ces informations lisibles.
Solution
Le fichier chiffré ne contient que 2 caractères différents et en contient 1190 :
Il faut partir sur l'hypothèse que tous les nombres dans la matrice finale montrée dans le PDF sont écrit en binaire où l'on a remplacé 0 et 1 par 2 caractères random.
De là, il faut forcément que les paramètres bits et depth soient des diviseurs de 1190, puisqu'ils représentent la largeur et la hauteur de la matrice.
On sait qu'on a chiffré de l'ASCII (la valeur max est 127), on en déduit que bits = 7
Concernant depth, 5 est la seule valeur plausible, car pour toutes les valeurs ASCII possibles, le maximum de nombres de facteurs premiers est 6 (et il est n'est pas diviseur de 1190).
from sympy import factorint
print(max([len(factorint(n, multiple=True)) for n in range(128)]))
# 6
Maintenant, on va réassembler tous les bits ensemble pour former des nombres. Le caractère qui correspond à 0 ou 1 n'a pas d'importance.
On voit que 41, 42 et 43 reviennent énormément. C'est normal, ils correspondent aux facteurs 1, 2 et 3 comme on peut le déduire du tableau montré dans l'exemple :
Maintenant, il faut déterminer la clé du XOR. Pour ça on va faire l'hypothèse que le nombre le plus présent est le facteur 1, de là, on pourra déterminé avec quoi il a été XOR et donc la clé :
stats = sorted([(n, numbers.count(n)) for n in set(numbers)], key=lambda o: o[1], reverse=True)
print(f'\nStats:\n{stats}')
# On prend le nombre le plus présent
xored_one = stats[0][0]
key = 1 ^ xored_one
print(f'\nKey: {key}')
# Stats:
# [(41, 82), (42, 30), (43, 17), (45, 7), (47, 6), (35, 5), (59, 5), (37, 4), (57, 4), (53, 3), (1, 1), (69, 1), (107, 1), (77, 1), (79, 1), (113, 1), (19, 1)]
#
# Key: 40
On peut désormais XOR tous les nombres avec cette clé :
Il n'y a que des nombres premiers, notre hypothèse se confirme et nous sommes sur la bonne voie.
Dans le PDF, on sait que tous les caractères ASCII sont décomposés en depth facteurs premiers, donc ici en 5 facteurs chacun. La question est de savoir comment ils sont disposés dans notre tableau :
On peut tester les deux, mais c'est bien la seconde façon de faire qui est la bonne :
with open('flag.enc', 'r') as f:
encrypted = f.read()
bits = 7
depth = 5
encrypted = ''.join(['1' if c == ' ' else '0' for c in encrypted])
numbers = [int(encrypted[i:i+bits], 2) for i in range(0, len(encrypted), bits)]
stats = sorted([(n, numbers.count(n)) for n in set(numbers)], key=lambda o: o[1], reverse=True)
xored_one = stats[0][0]
key = 1 ^ xored_one
numbers = [n ^ key for n in numbers]
def mul(values: list[int]) -> int:
acc = 1
for n in values:
acc *= n
return acc
# Fonctionne
width = len(numbers) // depth
groups = [[numbers[width * x + w] for x in range(depth)] for w in range(width)]
caracters = [mul(group) for group in groups]
flag = bytes([mul(group) for group in groups]).decode()
print(flag)
# CYBN{N0t_v3ry_3ff1c1ent_4lg0r1thm}