Description du challenge
Le challenge expose un service réseau qui permet de :
- Récupérer un message chiffré (le flag)
- Chiffrer un message de notre choix
Exemple d’interaction avec le service :
Exemple
┌──(crazycat256㉿kali)-[~]
└─$ nc sixctf.neoreo.fr 12345
Tiens, le vla ton kdo de merde:
6a428b9d4ca54cbcb5aca4ef6ea55fb3a6ba3c72bc4ff65fba6666db109f9e4fc195fd80a8a8331c1e37afcd4ecf66dc
Tu veux crypter un autre truc (AAAAAH IL a dit crypter le con) ? (o/n): o
Tu veux chiffrer quoi bg ? : HelloWorld!
KDO : 1464b3b758a26f94868ab6b528940885
La source nous est donnée :
app.py
from os import environ, urandom
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from binascii import hexlify
BANNER="""
_______ .__ .__ __ .___
\ _ \ __ _____ _________|__|______ | | ____ | | _______ __| _/____
/ /_\ \| | \ \/ /\_ __ \ \_ __ \ | | _/ __ \ | |/ /\__ \ / __ |/ _ \
\ \_/ \ | /\ / | | \/ || | \/ | |_\ ___/ | < / __ \_/ /_/ ( <_> )
\_____ /____/ \_/ |__| |__||__| |____/\___ > |__|_ \(____ /\____ |\____/
\/ \/ \/ \/ \/ """
FLAG = environ.get('FLAG', '6CTF{TH4T_1S_F4_FL4G_F0R_T3STING}')
class Encryptor:
"""
A custom class to encrypt stuff
"""
def __init__(self):
self.key = urandom(16)
self.iv = urandom(12)
def wrap(self, data):
"""
Wrap the data with strong AES encryption
"""
cipher = AES.new(self.key, 6, nonce=self.iv)
data = data.encode()
return hexlify(cipher.encrypt(pad(data, 16))).decode()
def say(message):
print(f"[Lutin malicieux]: {message}")
if __name__ == '__main__':
print("-"*50)
print(BANNER)
print("-"*50)
encryptor = Encryptor()
say(f"Tiens, le vla ton kdo de merde:\n {encryptor.wrap(FLAG)}")
say(" \nTu veux crypter un autre truc (AAAAAH IL a dit crypter le con) ? (o/n): ")
print("-"*50)
ans = input().lower()
if ans == 'o':
say("Tu veux chiffrer quoi bg ? : ")
message = input()
say(f"KDO : {encryptor.wrap(message)}")
say("Tchuss")
exit(0)
Note: dans AES.new(self.key, 6, nonce=self.iv)
, le paramètre 6
correspond à l’algorithme AES-CTR
Analyse
En examinant le code source, on se rend compte que les 2 messages sont chiffrés avec la même clé et le même nonce
Partie vulnérable
def __init__(self):
self.key = urandom(16)
self.iv = urandom(12) # Le même nonce est utilisé pour tous les chiffrements
La réutilisation du nonce en AES-CTR est une vulnérabilité critique qui peut compromettre la confidentialité des messages. Quand un nonce est réutilisé, le même keystream est généré pour les deux messages et cela permet de comparer les chiffrés et d’en déduire des informations sur le clair
D’ailleurs, “nonce” signifie littéralement “number used once” (nombre utilisé une seule fois), le principe est donc justement de ne jamais le réutiliser
Exploitation
La réutilisation du nonce fait que lorsque notre message test contient le bon caractère à la bonne position, le chiffrement de cette partie sera identique au chiffrement du flag. En testant chaque caractère puis mesurant le nombre de différences entre les chiffrés, on peut déterminer quel caractère est correct à chaque position.
Voici le script d’exploitation :
Solution
from pwn import *
context.log_level = "critical"
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"
flag = "6CTF{"
diffmap = {}
while True:
for char in alphabet:
# Connexion au service
r = remote("sixctf.neoreo.fr", 12345)
# Récupération du flag chiffré
q = r.recvuntil(b"(o/n): ")
kdo1 = q.split(b"de merde:\n")[1].split(b"\n")[0]
# Envoi de notre message test
r.sendline(b"o")
q = r.recvuntil(b"bg ? : ")
r.sendline(flag + char)
out = r.recvuntil(b"!")
r.close()
# Extraction du chiffré de notre message
kdo2 = out.split(b"KDO : ")[1].split(b"\n")[0]
# Calcul du nombre de différences entre les 2 chiffrés
diff = sum(1 for i in range(len(kdo2)) if kdo1[i] != kdo2[i])
diffmap[char] = diff
# Le caractère donnant le moins de différences est probablement correct
min_char, min_diff = min(diffmap.items(), key=lambda x: x[1])
print(f"Min char: {min_char} - Min diff: {min_diff}")
flag += min_char
print(flag)
if flag[-1] == "}":
break
Output
Min char: P - Min diff: 19
6CTF{P
Min char: L - Min diff: 18
6CTF{PL
Min char: Z - Min diff: 16
6CTF{PLZ
Min char: _ - Min diff: 14
6CTF{PLZ_
Min char: B - Min diff: 12
6CTF{PLZ_B
Min char: 3 - Min diff: 10
6CTF{PLZ_B3
Min char: _ - Min diff: 7
6CTF{PLZ_B3_
Min char: C - Min diff: 5
6CTF{PLZ_B3_C
Min char: 4 - Min diff: 3
6CTF{PLZ_B3_C4
Min char: R - Min diff: 2
6CTF{PLZ_B3_C4R
Min char: 3 - Min diff: 32
6CTF{PLZ_B3_C4R3
Min char: F - Min diff: 27
6CTF{PLZ_B3_C4R3F
Min char: U - Min diff: 26
6CTF{PLZ_B3_C4R3FU
Min char: L - Min diff: 26
6CTF{PLZ_B3_C4R3FUL
Min char: L - Min diff: 24
6CTF{PLZ_B3_C4R3FULL
Min char: _ - Min diff: 22
6CTF{PLZ_B3_C4R3FULL_
Min char: W - Min diff: 20
6CTF{PLZ_B3_C4R3FULL_W
Min char: H - Min diff: 18
6CTF{PLZ_B3_C4R3FULL_WH
Min char: 3 - Min diff: 16
6CTF{PLZ_B3_C4R3FULL_WH3
Min char: N - Min diff: 13
6CTF{PLZ_B3_C4R3FULL_WH3N
Min char: _ - Min diff: 12
6CTF{PLZ_B3_C4R3FULL_WH3N_
Min char: U - Min diff: 10
6CTF{PLZ_B3_C4R3FULL_WH3N_U
Min char: S - Min diff: 8
6CTF{PLZ_B3_C4R3FULL_WH3N_US
Min char: 1 - Min diff: 6
6CTF{PLZ_B3_C4R3FULL_WH3N_US1
Min char: N - Min diff: 4
6CTF{PLZ_B3_C4R3FULL_WH3N_US1N
Min char: G - Min diff: 2
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG
Min char: _ - Min diff: 32
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_
Min char: A - Min diff: 21
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_A
Min char: E - Min diff: 20
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_AE
Min char: S - Min diff: 16
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_AES
Min char: _ - Min diff: 16
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_AES_
Min char: G - Min diff: 14
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_AES_G
Min char: C - Min diff: 12
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_AES_GC
Min char: M - Min diff: 10
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_AES_GCM
Min char: } - Min diff: 0
6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_AES_GCM}
Le flag est donc 6CTF{PLZ_B3_C4R3FULL_WH3N_US1NG_AES_GCM}
Note: contrairement à ce qu’indique le flag, le mode utilisé est en réalité AES-CTR