Le grand écart

404CTF{you_spot_w3ll_the_differences}

Catégorie: Stegano Difficulté: medium Flag: -

Challenge

Description


En sortant de votre cours de gymnastique, vous tombez sur un étrange bouquin dans les vestiaires... À vous d'en trouver tous les secrets.

Découverte du challenge

Récupération du texte original

La première chose à remarquer est qu'il s'agit d'un texte, certes, mais certains caractères semblent bizarres. Plus précisément, certains octets sont différents de ce qu'ils devraient être.

Pour avoir une base de comparaison et commencer à chercher, il faut donc trouver le fichier original.

Rien de plus simple grâce à internet, on peut simplement faire une recherche Google sur une partie intacte du texte et on découvre alors qu'il s'agit de Moby-Dick or the whale. On peut maintenant faire une recherche grâce aux Google Dork sur le type txt pour obtenir le fichier qu'il nous faut.

Parmi les résultats, j'ai trouvé et utilisé celui-ci


Différences entre les fichiers

Maintenant que l'on a les deux versions, on peut s'amuser à regarder les différences

Bon, il y en a pas mal sur les premières lignes et sachant que le texte en fait 15 000, on va automatiser tout ça grâce à Python.

Tout d'abord, on peut voir que nos deux fichiers font la même taille, ça nous indique déjà qu'on a récupéré le bon texte.

import os

challenge = 'challenge.txt'
original = 'mobydick.txt'

print(os.stat(challenge).st_size)
print(os.stat(original).st_size)

# 643210
# 643210

À présent, on va itérer simultanément sur les deux fichiers caractère par caractère et les comparer. Dès qu'ils sont différents, on va afficher la position dans le fichier ainsi que le XOR entre les deux.

Pourquoi faire un XOR ? Car c'est une opération très courante dans le chiffrement et la stéganographie puisqu'elle est réversible. C'est donc un réflexe à avoir lorsque l'on a plusieurs valeurs à comparer.

import os

challenge = 'challenge.txt'
original = 'mobydick.txt'

size = os.stat(challenge).st_size
assert size == os.stat(original).st_size

# rb pour lire en mode octet
f_challenge = open(challenge, 'rb')
f_original = open(original, 'rb')

for i in range(size):
    # Récupère le prochain octet
    c_challenge = f_challenge.read(1)
    c_original = f_original.read(1)
    
    # Si ils sont différents
    if c_challenge != c_original:
        # XOR entre les deux octets récupérés
        xored = bytes([c_challenge[0] ^ c_original[0]])
        
        # Affiche où dans le fichier, le caractère du challenge
        # celui de l'original et le résultat du xor entre les deux
        print(f"{i:0>6} {c_challenge}\t{c_original}\t{xored}")
    
f_challenge.close()
f_original.close()

# 000150 b'\xa9'  b' '    b'\x89'
# 000180 b'\x04'  b'T'    b'P'
# 000210 b'n'     b' '    b'N'
# 000240 b'\x02'  b'E'    b'G'
# 000270 b'C'     b'N'    b'\r'
# 000300 b'^'     b'T'    b'\n'
# 000330 b':'     b' '    b'\x1a'
# 000360 b'\x00'  b'\n'   b'\n'
# 000480 b'-'     b' '    b'\r'
# 000510 b'i'     b' '    b'I'
# 000540 b'\x1f'  b'W'    b'H'
# 000570 b'\x05'  b'A'    b'D'
# 000600 b'\x17'  b'E'    b'R'

Bingo, on voit dans nos print plusieurs choses intéressantes :

  • A partir de l'octet 150, on a une variation tous les 30 octets.

  • Le XOR entre cette variation et le texte original affiche PNG ... IHDR, on a donc un .png face à nous

  • On voit des "sauts" (300, 330, 360, 480), ils sont surement dus au fait que le fichier contient des octets nuls. Sauf que 0 XOR x = x, il ne faut donc pas se faire avoir et bien prendre tous les 30 octets même s'ils sont identiques.

Ce qu'on va faire, c'est tout simplement rediriger le résultat du XOR dans un fichier tierce, que l'on nommera étonnement flag.png.

import os

challenge = 'challenge.txt'
original = 'mobydick.txt'
flag = 'flag.png'

size = os.stat(challenge).st_size
assert size == os.stat(original).st_size

f_challenge = open(challenge, 'rb')
f_original = open(original, 'rb')
f_flag = open(flag, 'wb')

for i in range(150, size, 30):
    f_challenge.seek(i)                 # Se déplace dans le fichier
    c_challenge = f_challenge.read(1)   # 1 octet lu
    
    f_original.seek(i)                  # Se déplace dans le fichier
    c_original = f_original.read(1)     # 1 octet lu
    
    xored = bytes([c_challenge[0] ^ c_original[0]])
    f_flag.write(xored)                 # Ecrit le résulat du xor dans le fichier

f_challenge.close()
f_original.close()
f_flag.close()

Suite du challenge

Maintenant que nous avons récupéré l'image, il nous faut trouver le flag. En bas à gauche, on voit des pixels de couleurs différentes, ce qui fait penser tout de suite à une technique de LSB un peu foireuse.

Façon simple

On se rend sur AperiSolve et l'on importe l'image. Le site lance plusieurs outils sur le fichier et parmi eux, Zsteg détecte bien un LSB avec le flag


Façon puriste

On commence par reverse le LSB que l'on a vu plus tôt en bas de l'image. Je dis LSB, mais il faut prendre tous les bits de l'octet de chaque canal de couleur (sauf l'alpha).

from PIL import Image

img = Image.open(flag)
data = ''
for x in range(15):
    r, g, b, a = img.getpixel((x, img.height - 1))
    data += chr(r) + chr(g) + chr(b)
print(data.strip('\x00'))

# https://www.youtube.com/watch?v=dQw4w9WgXcQ

Bien joué, vous vous êtes fait RickRoll...

En réalité, c'était un indice pour vérifier les autres pixels. Si l'on regarde les pixels au début de l'image, on peut voir que les canaux toujours varient entre deux valeurs alors que c'est un fond uni.

from PIL import Image

img = Image.open(flag)
data = b''
for x in range(15):
    r, g, b, a = img.getpixel((x, 0))
    print(r, g, b, a)

# 12 8 77 254
# 12 9 77 254
# 12 8 76 254
# 12 8 76 254
# 12 8 76 254
# 12 8 76 254
# 12 8 77 255
# 12 9 76 254

On en déduit qu'il faut regarder le dernier bit de chaque canal pour reconstruire le message

from PIL import Image

img = Image.open(flag)
data = ''
for x in range(128):
    # Ajoute 0 ou 1 en fonction du dernier bit de chaque canal
    data += ''.join([f'{n & 1}' for n in img.getpixel((x, 0))])

# Regroupe par 8 bits et interprête en caractère
print(''.join(chr(int(data[i:i+8], 2)) for i in range(0, len(data), 8)))

# &404CTF{you_spot_w3ll_the_differences}

Dernière mise à jour

Cet article vous a-t-il été utile ?