Writeup - Pix2Num
- CTF :
404CTF - Catégorie :
Cryptanalyse - Fichiers fournis :
encrypt.py,number.txt - Format du flag :
404CTF{...} - Auteur du WriteUp :
DonAsako
Contexte
On nous fournit un script Python encrypt.py qui :
- Convertit une image en niveaux de gris (
flag.png) en une longue chaîne binaire (1 pour pixel blanc, 0 pour noir), - La transforme en entier,
- Puis chiffre cet entier bloc par bloc (64 bits) avec un XOR contre une clé aléatoire de 64 bits,
- Le résultat est enregistré dans
number.txt.
Notre objectif est de retrouver l'image originale et d'en extraire le flag.
Observations Clés
- Le chiffrement est un XOR bloc par bloc de 64 bits.
- Chaque bloc :
python bloc = (number & 0xFFFFFFFFFFFFFFFF) ^ key - Le XOR est réversible, donc si on peut déduire un bloc clair, on peut retrouver la clé !
Or, on suppose que les 64 premiers pixels de l'image sont blancs, donc 1 en binaire.
Donc :
known_plain = 0xFFFFFFFFFFFFFFFF
Exploitation
Récupération de la clé
On sait que :
$$chiffre = clair \oplus key \Rightarrow key = chiffre \oplus clair$$
On applique donc :
key = encrypted_bloc ^ known_plain
Puis, on déchiffre chaque bloc en inversant le XOR.
Reconstruction de l'image
- On reconvertit l'entier déchiffré en une chaîne binaire,
- On reconstruit les pixels (0 ou 255),
- On sauvegarde l'image finale.
Code de résolution
from PIL import Image
import sys
sys.set_int_max_str_digits(100000)
WIDTH = 400
HEIGHT = 200
# Déchiffrement bloc par bloc
def decrypt_number(encrypted_number, key):
original_number = 0
shift = 0
while encrypted_number:
bloc = (encrypted_number & 0xFFFFFFFFFFFFFFFF)
decrypted_bloc = bloc ^ key
original_number |= (decrypted_bloc << shift)
encrypted_number >>= 64
shift += 64
return original_number
# Conversion du nombre en image
def number_to_image(number, width, height, output_path):
binary_str = bin(number)[2:].zfill(width * height)
pixels = [(255 if bit == '1' else 0) for bit in binary_str]
image = Image.new('L', (width, height))
image.putdata(pixels)
image.save(output_path)
# Récupération de la clé avec hypothèse de pixels blancs
def recover_key(encrypted_number, known_plain, bit_offset=0):
encrypted_shifted = encrypted_number >> bit_offset
encrypted_bloc = encrypted_shifted & 0xFFFFFFFFFFFFFFFF
key = encrypted_bloc ^ known_plain
return key
# Lecture du fichier chiffré
with open('number.txt', 'r') as f:
encrypted_number = int(f.read())
known_plain = 0xFFFFFFFFFFFFFFFF # Hypothèse: 64 pixels blancs
key = recover_key(encrypted_number, known_plain)
print(f"[+] Clé retrouvée : {hex(key)}")
original_number = decrypt_number(encrypted_number, key)
number_to_image(original_number, WIDTH, HEIGHT, 'decrypted_flag.png')
print("[+] Image enregistrée sous decrypted_flag.png")
Résultat
Une fois l'image reconstruite, on lit le flag directement sur decrypted_flag.png.

Flag
404CTF{4n_A11eN_hA9_b33n_70UnD}
Conclusion
Ce challenge montre à quel point le XOR est prévisible si une partie du clair est connue ou devinable. Le chiffrement n'est pas sûr s'il repose uniquement sur XOR sans aléa fort ou sans clé renouvelée.
🔐 Il ne faut jamais supposer que des données d'entrée (comme une image) sont totalement inconnues.