Skip to main content

(Difficile) SOS Raid 2/2

Description

Lors de l'étape une du challenge nous avons pu récupérer deux fichier. 

Un fichier .txt qui contenaint le flag de la partie une.

Une image .png qui semble être corrompue.

Le but de cette partie est de réparer cette image afin de récupérer notre flag.

Analyse

Pour réparrer un PNG il faut encore commprendre comment fonctionne ce format.

Heureusement pour nous la page Wikipédia est relativement bien faite et nous apporte l'ensemble des informations qu'il nous faut.

Un PNG ce structure de la manière suivante:

  • Une entête (signature) 8 octets : 89 50 4E 47 0D 0A 1A 0A
  • Une liste de Chunk (blocs d'informations diverses)
    • Un Chunk IHDR (25 octets)
    • Un Chunk IDAT (longeur variable)
    • Un Chunk IEND (12 octets)

Chaque Chunk ce compose de la manière suivante :

  • Une taille = longeur de la section data (4 octets)
  • Le Type de Chunk (4 octets)
  • La data (n octets)
  • La somme de contrôle avec l'algorithme CRC (4 octets) elle revient à calculer CRC(Data, CRC(Type))

Solution

Pour ce challenge, il va falloir avancer petit à petit afin de trouver pour chaque chunk si il y a une erreur et si oui comment la corriger.

Pour cela on va commencer par créer un script qui permet de récupérer chaque Chunk de notre PNG :

data = open("flag_c0rr_pt3d.png", "rb").read()

# Signature
sig = data[:8]

# IHDR
IHDR = data[8:33]

# Autre Chunks
data = data[33:]
data_size = len(data)
i = 0
chunks = []
while i < data_size:
    chunk_size = data[i : i+4]
    n = int.from_bytes(chunk_size, byteorder='big')
    chunk_type = data[i+4 : i+8]
    chunk_data = data[i+8 : i+8+n]
    chunk_crc = data[i+8+n : i+8+n+4]
    chunks.append([chunk_size, chunk_type, chunk_data, chunk_crc])
    i+=8+n+4

Signature

La signature est facile à réparer puisqu'elle ne change jamais. Ici nous pouvons voir qu'elle n'est pas la bonne :

image-1655627176066.png

On va donc commencer à créer notre réparation :

# Signature
sig = data[:8]
rep = bytes([137,80,78,71,13,10,26,10])

IHDR

Le chunk IHDR est aussi à part car il a une taille fixe, pour voir si il est coorect on va regarder si le champ taille est correcte et si la somme de contrôle est aussi correcte :

# IHDR
IHDR = data[8:33]
size = IHDR[:4]
print(int.from_bytes(size, byteorder='big'))
check = zlib.crc32(IHDR[8:21], zlib.crc32(IHDR[4:8]))
print(check)
print(int.from_bytes(IHDR[21:], byteorder='big'))

Output :

image-1655628091367.png

On remarque ici que la taille n'est pas la bonne (taille normal = 13)

Et les deux sommes de contrôle ne sont pas les bonnes non plus.

La taille est facile à réparer il suffit de changer son nombre. Pour la somme de contrôle il faut savoir si c'est le champ CRC qui est mauvais ou si ce sont nos données qui sont mauvaises. Pour cela on va investiguer sur la champ data. 
Pour un chunk IHDR, le champ data est divisé de la manière suivante :

  • Largeur de l'image (4 octets)
  • Hauteur de l'image (4 octets)
  • Profondeur de bits (1 octet)
  • Type de couleur (1 octet)
  • Méthode de compression (1 octet)
  • Méthode de filtrage (1 octet)
  • Méthode d'entrelacement (1 octet)

On va alors afficher chaque valeur pour voir à quoi elle correspond :

# IHDR
IHDR = data[8:33]
size = IHDR[:4]
print(int.from_bytes(IHDR[8:12], byteorder='big'))
print(int.from_bytes(IHDR[12:16], byteorder='big'))
print(IHDR[17])
print(IHDR[18])
print(IHDR[19])
print(IHDR[20])
print(IHDR[21])

Output : 

image-1655628661464.png

On remarque ici un problème sur la hauteur le nombre semble beaucoup trop grand :

image-1655628721220.png

Rappelons nous que nous sommes dans un CTF et que les modifications sont souvent simples à trouver, ici on va modifier le f en un 0 ce qui nous donne 648 de hauteur. Cela semble cohérent puisqu'il s'agit d'un ratio 16:9 de plus notre somme de contrôle est maintenant cohérente avec nos données. On peut a donc réparé notre chunk :

# IHDR
IHDR = data[8:33]
new_IHDR = b"\x00\x00\x00\x0D" + IHDR[4:12] + b"\x00\x00\x02\x88" + IHDR[16:]
rep += new_IHDR

Autres chunks

Pour les autres chunks on va parcourir chacun des chunks et on va vérifier si la somme de contrôle est bone, si ce n'est pas le cas on va s'arrêter afin d'investiguer à la main :

# Autre Chunks
data = data[33:]
data_size = len(data)
i = 0
chunks = []
while i < data_size:
    chunk_size = data[i : i+4]
    n = int.from_bytes(chunk_size, byteorder='big')
    chunk_type = data[i+4 : i+8]
    chunk_data = data[i+8 : i+8+n]
    chunk_crc = data[i+8+n : i+8+n+4]
    chunks.append([chunk_size, chunk_type, chunk_data, chunk_crc])
    if int.from_bytes(chunk_crc, byteorder='big') != zlib.crc32(chunk_data, zlib.crc32(chunk_type)):
        print(i)
        print(chunk_size)
        print(chunk_type)
        print(int.from_bytes(chunk_crc, byteorder='big'))
        print(zlib.crc32(chunk_data, zlib.crc32(chunk_type)))
        break
    i+=8+n+4

Output : 

image-1655629501781.png

Tout les chunks semblent bon sauf le dernier Chunk, le chunk IEND.

Ce dernier marque la fin de l'image. Il est réglementé tout comme le chunk IHDR.

Ici la taille n'est pas la bonne il doit avoir une taille de 0 car il ne comporte aucune donnée.

On peut donc réparer notre chunk IEND :

# Autre Chunks
data = data[33:]
new_chunks = data[:61818] + b"\x00\x00\x00\x00" + data[61822:]
rep += new_chunks

Ne reste plus qu'a stocker le tout dans un nouveau fichier png :

import zlib

data = open("flag_c0rr_pt3d.png", "rb").read()

# Signature
sig = data[:8]
rep = bytes([137,80,78,71,13,10,26,10])

# IHDR
IHDR = data[8:33]
new_IHDR = b"\x00\x00\x00\x0D" + IHDR[4:12] + b"\x00\x00\x02\x88" + IHDR[16:]
rep += new_IHDR

# Autre Chunks
data = data[33:]
new_chunks = data[:61818] + b"\x00\x00\x00\x00" + data[61822:]
rep += new_chunks

file = open("new.png", "wb")
file.write(rep)

On peut donc ouvrir notre image :

image-1655630160926.png

flag : 404CTF{L4_C0rr_pt10N_s3_r_p4r_}