Skip to content

XS6 - CTR mode is just XOR


Description

Write Up: Tyron
Créateur: profninja
Difficulté: easy
Points: 448
Format du flag: UDCTF{flag}

Enoncé

  • Français: Cette série de problèmes s'appelle l'ÉCOLE DU XOR. Pour une raison que j'ignore, j'adore les problèmes de xor et, au fil des ans, nombreux sont ceux qui ont charmé mon âme. Cette série est un hommage aux nombreuses façons dont le xor apparaît dans les CTF. J'espère que vous verrez un peu de la beauté que je vois à travers eux.
  • English: This series of problems is called the XOR SCHOOL. For whatever reason I just love xor problems and over the years there are many that have charmed my soul. This sequence is an homage to the many many ways that xor shows up in CTFs. I hope you can see some of the beauty that I see through them. -ProfNinja

Pièce(s) jointe(s):


Solution détaillée

lambda.py
#LIVE AT https://i8fgyps3o2.execute-api.us-east-1.amazonaws.com/default/ctrmode?pt=00

import json
import os
import sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad


def lambda_handler(event, context):
    pt=bytes.fromhex(event["queryStringParameters"]["pt"])
    padded = pad(pt, 16)

    probiv = os.environ["probiv"]
    flag = os.environ["flag"]
    padflag = pad(flag.encode(), 16)
    flagcipher = AES.new(os.environ["secretkey"].encode(), AES.MODE_CTR, nonce=probiv.encode())
    pct = flagcipher.encrypt(padflag)

    yourcipher = AES.new(os.environ["secretkey"].encode(), AES.MODE_ECB)
    try:
        encrypted = yourcipher.encrypt(padded)
    except ValueError as e:
        return {'statusCode': 500, "error": str(e)}

    return {
        'statusCode': 200,
        'body': json.dumps({"ciphertext": encrypted.hex(), "probiv": probiv.encode().hex(), "flagenc": pct.hex()})
    }

On peut intéragir avec le serveur en soumettant une chaine en héxadécimal qui va chiffrer mon message, retourner le texte chiffré, l'iv et le flag.

L'url est la suivante : https://i8fgyps3o2.execute-api.us-east-1.amazonaws.com/default/ctrmode?pt=

en requetant deux fois le serveur on recoit les memes résultats :

{
  "ciphertext": "780b443497b0686c7d48def8bf737543", 
  "probiv": "475045713653717a7936644c6d654d", 
  "flagenc": "2cbcef061c2c4401d5bcc6c5569dab80c31daf822c0d424b2aadb5775e7c55047dd600fad942d7a32ce019da5c2edb91911cc166748fd5c4888bd030ae598968"
}

Il y a key reuse et nonce reuse.

En lisant le code source :

flagcipher = AES.new(os.environ["secretkey"].encode(), AES.MODE_CTR, nonce=probiv.encode())
pct = flagcipher.encrypt(padflag)

yourcipher = AES.new(os.environ["secretkey"].encode(), AES.MODE_ECB)
try:
    encrypted = yourcipher.encrypt(padded)
except ValueError as e:
    return {'statusCode': 500, "error": str(e)}

Le flag est chiffré avec AES CTR et mon texte en clair et chiffré avec AES ECB cependant les deux sont chiffrés avec la meme clé.

Pour rappel, AES CTR est simplement le fait de chiffré un compteur précédé d'un nombre (le nonce) avec AES ECB puis de xorer le résultat avec le texte clair.
Tandis que, en AES ECB on chiffre le texte clair directement.

AES CTR mode

Donc le chiffrement du flag c'est :

  • chiffrement de la clé avec compteur de 0
  • opération de xor pour le premier bloc du texte clair

Puis on répète l'opération pour chaque bloc en incrémentant le compteur.

Et pour le déchiffrement,

  • chiffrement de la clé avec compteur de 0
  • opération de xor pour le premier bloc du texte chiffré

La clé chiffrée est appelée le keystream. Si j'envoie le compteur précédé du nonce et que je chiffre tout ca, je récupère le keystream.
Et incrémentant le compteur je récupère assez de keystream pour déchiffrer le flag.

Au final j'obtiens ce script :

solve_xs6.py
import json
import os
import sys
import requests

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from pwn import xor


url = "https://i8fgyps3o2.execute-api.us-east-1.amazonaws.com/default/ctrmode?pt="
req = requests.get(url).content
j = json.loads(req)

iv = bytes.fromhex(j["probiv"])
flag_enc = bytes.fromhex(j["flagenc"])

payload = ""

for i in range(len(flag_enc)//16):
    payload += (iv+i.to_bytes()).hex()

assert len(payload) == len(flag_enc)*2

req = requests.get(url+payload).content
j = json.loads(req)

keystream = bytes.fromhex(j["ciphertext"])

flag = xor(keystream, flag_enc)
print(f"{flag = }")

FLAG : UDCTF{th3r3_15_n0_sp00n_y0uv3_alr34dy_d3c1d3d_NE0}


Retex

Challenge AES nonce reuse classique.


Lien(s) utile(s)