Skip to content

Blinding_Light


Description

Write Up: offpath
Créateur: randomdude999
Difficulté: Medium - Hard
Points: 120
Format du flag: crypto{flag}

Enoncé

Voici mon serveur de signature et de vérification de jetons. Je ne suis pas sûr qu'il signe correctement, mais j'ai mis en place certaines garanties pour m'assurer qu'il ne distribuera pas de jetons d'administrateur à n'importe qui.

Here's my token signing and verification server. I'm not sure it's doing signing properly, but I've implemented some safeguards to ensure it won't hand out admin tokens to just anyone.


Solution détaillée

On a un code server et un socket de connexion.

#!/usr/bin/env python3

from Crypto.Util.number import bytes_to_long, long_to_bytes
from utils import listener

FLAG = "crypto{?????????????????????????????}"
ADMIN_TOKEN = b"admin=True"


class Challenge():
    def __init__(self):
        self.before_input = "Watch out for the Blinding Light\n"

    def challenge(self, your_input):
        if 'option' not in your_input:
            return {"error": "You must send an option to this server"}

        elif your_input['option'] == 'get_pubkey':
            return {"N": hex(N), "e": hex(E) }

        elif your_input['option'] == 'sign':
            msg_b = bytes.fromhex(your_input['msg'])
            if ADMIN_TOKEN in msg_b:
                return {"error": "You cannot sign an admin token"}

            msg_i = bytes_to_long(msg_b)
            return {"msg": your_input['msg'], "signature": hex(pow(msg_i, D, N)) }

        elif your_input['option'] == 'verify':
            msg_b = bytes.fromhex(your_input['msg'])
            msg_i = bytes_to_long(msg_b)
            signature = int(your_input['signature'], 16)

            if msg_i < 0 or msg_i > N:
                # prevent attack where user submits admin token plus or minus N
                return {"error": "Invalid msg"}

            verified = pow(signature, E, N)
            if msg_i == verified:
                if long_to_bytes(msg_i) == ADMIN_TOKEN:
                    return {"response": FLAG}
                else:
                    return {"response": "Valid signature"}
            else:
                return {"response": "Invalid signature"}

        else:
            return {"error": "Invalid option"}


listener.start_server(port=13376)

Ce challenge nous présente un coffre-fort sécurisé qui nécessite l'insertion de deux clés pour être déverrouillé. Les clés doivent satisfaire certaines conditions, et une fois insérées correctement, elles permettent d'obtenir le drapeau.

Aperçu du Code Serveur

Le code serveur utilise une méthode XOR complexe pour vérifier si les clés insérées sont correctes. Voici un aperçu des principales fonctionnalités :

  1. Insertion de Clés (insert_key option) :
    • Le serveur accepte une clé sous forme de chaîne hexadécimale.
    • La clé est ajoutée à un dictionnaire self.keys si elle n'est pas déjà présente et s'il y a moins de deux clés insérées.
    • Si la clé commence par le préfixe KEY_START, une valeur spéciale lui est assignée dans le dictionnaire.
  2. Déverrouillage du Coffre-Fort (unlock option) :
    • Le serveur vérifie si deux clés ont été insérées.
    • Il vérifie que la somme des valeurs assignées aux clés est égale à 1.
    • Les clés sont ensuite hachées en utilisant MD5, et une série de transformations XOR est appliquée pour garantir que les hachages sont différents.
    • Si les hachages finaux sont identiques, le coffre-fort est déverrouillé et le drapeau est révélé.

Solution

La solution repose sur la création de deux valeurs de clé différentes qui produisent des hachages MD5 identiques. Voici comment nous pouvons exploiter cela :

  1. Étape 1 : Générer des Clés avec une Collision MD5
    • Nous devons trouver deux valeurs qui, une fois hachées avec MD5, produisent le même résultat. Cela est possible grâce à une attaque de collision sur MD5.
  2. Étape 2 : Insérer les Clés dans le Coffre-Fort
    • Nous insérons les deux clés générées dans le coffre-fort.
  3. Étape 3 : Déverrouiller le Coffre-Fort
    • Une fois les clés insérées, nous envoyons une requête de déverrouillage pour obtenir le drapeau.

Voici le code de la solution :

from pwn import *
from Crypto.Util.number import bytes_to_long, long_to_bytes, inverse
import random

ADMIN_TOKEN = b"admin=True"

def get_pubkey(conn):
    payload = b'{"option": "get_pubkey"}'
    conn.sendline(payload)
    response = conn.recvline().decode()
    N = int(response.split('"N": "')[1].split('", "e"')[0], 16)
    E = int(response.split('"e": "')[1].split('"}')[0], 16)

    return N, E

def get_sign(conn, msg):
    payload = b'{"option": "sign", "msg": "' + msg.hex().encode() + b'"}'
    conn.sendline(payload)
    response = conn.recvline().decode()
    print("Server response:", response)
    sign = int(response.split('"signature": "')[1].split('"}')[0], 16)
    return sign

def verify(conn, msg, sign):
    payload = b'{"option": "verify", "msg": "' + msg.hex().encode() + b'", "signature": "' + hex(sign)[2:].encode() + b'"}'
    conn.sendline(payload)
    response = conn.recvline().decode()
    return response

def solve():

    # m' = adm_token * random_coprim ^e mod N
    # s' = m'^d mod N 
    # so
    # s' = (adm_token * random_coprim ^e)^d mod N
    # so 
    # s' = adm_token^d * random_coprim^e*d mod N

    # in rsa we know that e*d = 1 mod phi(N)

    # so
    # s' = adm_token^d * random_coprim mod N
    # so 
    # s' = adm_token^d * random_coprim * random_coprim^-1 mod N = sign_adm_token

    conn = remote('socket.cryptohack.org', 13376)
    conn.recvuntil(b'\n')

    N, E = get_pubkey(conn)

    # Step 1: Choose a random r coprime with N
    while True:
        r = random.randint(2, N - 1)
        if pow(r, E, N) != 0:
            break

    # Step 2: Compute the blinded message
    m_blinded = (bytes_to_long(ADMIN_TOKEN) * pow(r, E, N)) % N

    # Step 3: Get the signature of the blinded message
    m_blinded_bytes = long_to_bytes(m_blinded)
    sign_blinded = get_sign(conn, m_blinded_bytes)

    # Step 4: Unblind the signature
    r_inv = inverse(r, N)
    admin_token_sign = (sign_blinded * r_inv) % N

    res = verify(conn, ADMIN_TOKEN, admin_token_sign)
    print(res)

    conn.close()


if __name__ == '__main__':
    solve()