Énoncé
Cryptography is too costly… We propose a lighter implementation of ASCON. Recover the secret key. To obtain the flag, call getFlag on the key.
Fichiers fournis :
ascon_light.py: une implémentation d’Ascon-AEAD128 dont la permutation a été remplacée ;parameters.txt: un texte clair connu, avec le chiffré, le nonce et le tag correspondants (en base64) ; les données associées (AD) sont vides.
getFlag(key) renvoie BA26{ + blake3(str(key))[:16] + }.
Analyse
Le mode AEAD suit à peu près la norme NIST SP 800-232. Seule la permutation a changé :
def ascon_permutation(S):
H = words(blake3(bytes(S[:3])).digest()[:24], 3) # seule partie non linéaire
for i in range(3): H[i] ^= S[i+2]
S = S[-1:] + S[:-1] # rotation
for i in range(3): S[i] ^= H[i]
return S
On note h = blake3(S0‖S1‖S2). La sortie vaut alors :
| mot | valeur |
|---|---|
| S’0 | S4 ⊕ h0 ⊕ S2 |
| S’1 | S0 ⊕ h1 ⊕ S3 |
| S’2 | S1 ⊕ h2 ⊕ S4 |
| S’3 | S2 |
| S’4 | S3 |
L’initialisation a aussi été modifiée (« Changed for simplicity ») : S = [IV, N0, N1, K0, K1], suivie d’une seule application de la permutation, sans XOR final de la clé.
Vulnérabilité
Dans cette permutation, blake3 ne voit que S0..S2. Or à l’initialisation ces trois mots valent IV, N0, N1, tous publics. On peut donc calculer h soi-même, sans connaître la clé.
Après l’initialisation (AD vide, puis XOR de séparation de domaine sur S4), on obtient :
S0 = K1 ⊕ h0 ⊕ N1
S1 = IV ⊕ h1 ⊕ K0
Le premier bloc chiffré vaut C0 = P0 ⊕ (S0‖S1). Comme le texte clair est connu, on lit S0 et S1 directement, ce qui donne :
K1 = (C0 ⊕ P0)[0:8] ⊕ h0 ⊕ N1
K0 = (C0 ⊕ P0)[8:16] ⊕ h1 ⊕ IV
Un seul bloc de texte clair connu suffit pour récupérer toute la clé. Il n’y a besoin d’aucune cryptanalyse.
Exploitation
- Décoder le nonce, puis découper
N0, N1en mots de 64 bits little-endian. - Calculer
h = blake3(IV‖N0‖N1)[:24]et le découper en trois motsh0, h1, h2. - Calculer
C0 ⊕ P0sur les 16 premiers octets, ce qui donneS0, S1. - En déduire
K1etK0avec les formules ci-dessus, puis formerkey = K0‖K1. - Vérifier en rechiffrant le texte clair : on doit retrouver exactement le chiffré et le tag fournis.
- Calculer
getFlag(key).
Script
solve.py :
cd . && .venv/bin/python -I solve.py
# key = 962d43e30e8b7b5df37e74f47e000cee
# BA26{1ecc173213eb25a7}
Le venv .venv contient les dépendances blake3 et pycryptodome.
Code
"""Too Much Crypto (BlackAlps 2026) - key recovery from the first ciphertext block."""
import sys
from base64 import b64decode
from blake3 import blake3
sys.path.insert(0, "files")
import ascon_light as A # only reused for encrypt (verification) + getFlag
pt = b'Ascon-AEAD128, with less crypto than in NIST SP 800-232.'
ct = b64decode(b'HcRLfNb6GrW9VIyxcMtcXt6coXZOKKu33xZrEOtzb7FBYJUnOfFy6hYE2Gl80ILp8OqWd6BUIoo=')
nonce = b64decode(b'rNo1NnaqG3EQhTlnVcPaTA==')
tag = b64decode(b'a0PWORRlAlq872NDeLLMtg==')
w = lambda b, n: [int.from_bytes(b[8*i:8*i+8], "little") for i in range(n)]
IV = A.IV_AEAD128
N0, N1 = w(nonce, 2)
# blake3 only sees S0..S2 = IV, N0, N1 -> fully public
h = w(blake3(A._words_to_bytes([IV, N0, N1])).digest()[:24], 3)
# After init: S0 = K1^h0^N1, S1 = IV^h1^K0 ; C_0 = P_0 ^ (S0||S1)
S0, S1 = w(bytes(a ^ b for a, b in zip(ct[:16], pt[:16])), 2)
K1 = S0 ^ h[0] ^ N1
K0 = S1 ^ h[1] ^ IV
key = A._words_to_bytes([K0, K1])
c2, t2 = A.ascon_aead128_encrypt(key, nonce, b"", pt)
assert (c2, t2) == (ct, tag), "verification failed"
print("key =", key.hex())
print(A.getFlag(key))