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.
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)
- https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)
- https://pycryptodome.readthedocs.io/en/latest/src/cipher/classic.html#ctr-mode