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 :

  1. Convertit une image en niveaux de gris (flag.png) en une longue chaîne binaire (1 pour pixel blanc, 0 pour noir),
  2. La transforme en entier,
  3. Puis chiffre cet entier bloc par bloc (64 bits) avec un XOR contre une clé aléatoire de 64 bits,
  4. 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.