Skip to content

Destruction


Description

Write Up: Tyron
Créateur: tedi_v
Difficulté: moyen
Points: 491
Format du flag: AMSI{flag}

Enoncé

  • Français: Encrypt -> Decrypt -> Encrypt
  • English: Encrypt -> Decrypt -> Encrypt

Pièce(s) jointe(s):


Solution détaillée

On peut communiquer avec un serveur qui nous enverra le flag chiffrée.
Après quoi, il nous proposera de chiffrer une chaine de caractère, mais avant de la chiffre un nombre entre 16 et 192 sera demandé et il servira pour effectuer une operation de XOR sur la clé qu'on a soumis.

3DES.py
#!/bin/python
# pip install pycryptodome
from Crypto.Cipher import DES3
import random
import os

with open('flag', 'r') as file:
    flag = file.read().strip()

def decrypt(ciphertext, key, iv):
    if len(ciphertext) % 8 != 0:
        return b''
    cipher = DES3.new(key, mode=DES3.MODE_CBC, iv=iv)
    return cipher.decrypt(ciphertext)

def encrypt(plaintext, key, iv):
    cipher = DES3.new(key, mode=DES3.MODE_CBC, iv=iv)
    return cipher.encrypt(plaintext)

def flip(key, n):
    bit_string = ['1'] * n + ['0'] * (24 - n)
    random.shuffle(bit_string)
    mask = int(''.join(bit_string), 2).to_bytes(24, byteorder='big')
    return bytes(a ^ b for a, b in zip(key, mask))

key = os.urandom(24)
iv = os.urandom(8)
cipher = encrypt(flag.encode(),key,iv)

def main():

    print(f"Here is my encrypted secret: {cipher.hex()}")
    print("You can decrypt any message you like with my key.")
    challenge = input("\nEnter the ciphertext in hex:\n>>> ")
    try:
        ciphertext = bytes.fromhex(challenge)
        n = int(input("\nNot so fast ;) pick a number between 16 and 192:\n>>> "))
        assert(16 <= n <= 192)
        new_key = flip(key, n)
        plaintext = decrypt(ciphertext, new_key, iv)
        print(f"\nHere you go: {plaintext.hex()}\n")
    except :
        print("\nWrong format.")
        exit(1)
    exit(0)

if __name__ == "__main__":
    main()

D'abord 3DES est basé DES, un algorithme de chiffrement symmétrique. Pour chiffrer une chaine de caractère en 3DES (ou triple DES) le principe est simple :

  • on prend une clé de 168 bits qu'on divise en trois clés de 56 bits : \(K1, K2, K3\)
  • on chiffre le texte en clair avec \(K1\), on le déchiffre avec \(K2\) et on le rechiffre avec \(K3\)
  • on fait l'inverse pour déchiffrer

Note, en réalité on soumet une clé de 192 bits donc 24 octets mais 3 octets ne sont pas utilisés pour le chiffrement/déchiffrement

Sauf que dans le challenge, lorsqu'on va fournir une chaine à déchiffrer au serveur, on va aussi devoir lui fournir un nombre n.

Et ce nombre n va etre utilisé pour flip la clé dans flip(key, n).

En gros une chaine avec un nomnbre n de 1 va etre généré pus complétée de 0 jusqu'à faire une chaine binaire de longueur 192 et ensuite mélanger aléatoirement tout ca.

Puis la clé sera mis en binaire et xorée avec cette chaine.

def flip(key, n):
        bit_string = ['1'] * n + ['0'] * (24 - n)
        random.shuffle(bit_string)
        mask = int(''.join(bit_string), 2).to_bytes(24, byteorder='big')
        return bytes(a ^ b for a, b in zip(key, mask))
Ainsi si on met n=16, il y aura seize "1" et 176 "0".

Mais si on met, n=192, toute la chaine vaudra 1, et coincidence, la clé fait 24 octets donc 192 bits également.

Ainsi j'ai découvert qu'un flippant le texte chiffré avec n=192avant de l'envoyer au serveur, et qu'ensuite on soumet n=192, alors le texte déchiffré que le serveur envoie contient une partie du flag, les 16 derniers octets du texte clair sont lisible plus précisément :

n = 192

given_ct = bytes.fromhex(" given by the server in hex")
print(flip(given_ct[16:], n).hex())

Donc si le serveur envoie 8777b4162af29bb854f92fcfcf0947df92d7b297336fe4e0b2b4f81e85a6ec22856628e43bd4ff7c7a8c7846741d56a9c8ac3a8df8802b641990871d62856744,
Je renvoie : 6d284d68cc901b1f4d4b07e17a5913dd7a99d71bc42b0083

Puis on envoie 192 juste après

Et on recoit : b538060e0cef300c35643638363762353463316432363738

Puis on déchiffre le résultat :

print(bytes.fromhex("b538060e0cef300c35643638363762353463316432363738"))

Donnant : b'\xbe\xb2\xac\xb6\x84\xc0\xae\x8538cf64ea80afb4ff' Avec notamment les 16 derniers octets en clair : 38cf64ea80afb4ff

Problème, en flippant une chaine de plus de 24 caractères avec flip(), le résultat fait 24 caractères et non pas la taille de la chaine au départ (pout info le texte chiffré fait 64 caractères).

Donc il faudra découper une le texte chiffré en plusieurs parties à chaque fois.

En faisant des tests en local je remarque qu'en décalant le début du texte chiffré de 16 à chaue fois, j'obtiens les 16 caractères en clair suivants.

Pour résumer :

flip(ciphertext[0:], 192) -> flag[8:24]
flip(ciphertext[16:], 192) -> flag[24:40]
flip(ciphertext[32:], 192) -> flag[40:56]
flip(ciphertext[48:], 192) -> flag[56:64]

Ainsi, en récupérant chaque morceaux du flag un par un je reconstitue flag[8:64].

Il manque le début, et en fait il suffit de flip le tout premier morceau du flag, soit celui obtenu en envoyant flip(ciphertext[0:], 192) :

decrypted = bytes.fromhex("beb2acb684c0ae8533386366363465613830616662346666")
print(flip(decrypted, 192)[:8] + decrypted[8:])
Qui donne : AMSI{?Qz38cf64ea80afb4ff

FLAG : AMSI{?Qz38cf64ea80afb4ff5d6867b54c1d2678849cbc4c...}


Retex

Le challenge m'a permis de découvrir une nouvelle vulnérabilité bien que je ne saurais pas parfaitementexpliquer quelle en est sa cause (au niveau mathématique).


Lien(s) utile(s)