GCM Nonce Reuse
Introduction
Section titled “Introduction”Nonce (Number used ONCE) is a random or pseudo-random number that are intended for singular use in cryptographic operations.
On AES-GCM, AES is used to generate a keystream(the pseudo-random bit sequence used to encrypt the plaintext), and then XORs that against the plaintext, same as any stream cipher. This is the “Counter mode” part of GCM (Galois/Counter Mode).
The mechanism:
- Take a nonce and a counter starting at 0.
- Encrypt nonce || counter with AES. Bump the counter. Encrypt again. Repeat.
- String those outputs together — that’s the keystream.
- XOR that keystream against the plaintext to get ciphertext.
keystream = AES(key, nonce||0), AES(key, nonce||1), AES(key, nonce||2), … ciphertext = plaintext XOR keystream
So, the keystream depends on exactly two things: the key and the nonce.
Same key and same nonce → same keystream, every time.
That’s why the nonce exists — it’s literally the only thing making each message’s keystream different when reusing the same key over and over.
Vulnerability
Section titled “Vulnerability”Now, this exercise has the following text on visiting the site:
Welcome to our chat system:
admin: The key for this challenge is : N/K/gMqTARuuqaYzhO2URo0K9iez1Nvi5QO/wT/cvhIMA3p/
# For transparency, you can find our military grade encryption code below:cipher = OpenSSL::Cipher.new('aes-256-gcm')cipher.encryptcipher.iv = '000000000000'cipher.key = $keyBase64.encode64(cipher.update(str) + cipher.final)The code hardcodes the IV (nonce) to '000000000000', which is used for every encryption:
cipher.iv = '000000000000'This creates catastrophic cryptographic failure, as in AES-GCM, reusing the same key + nonce pair, leads to recovering the keystream and decrypting any ciphertext encrypted with that pair.
Attack Explanation
Section titled “Attack Explanation”AES-GCM is essentially a stream cipher: ciphertext = plaintext XOR keystream. If the nonce never changes, the keystream is always the same.
Given two messages encrypted with the same key+nonce:
C1 = P1 XOR KSC2 = P2 XOR KSXORing them cancels the keystream: C1 XOR C2 = P1 XOR P2
More usefully: if we can recover the keystream, we can decrypt anything:
KS = C1 XOR P1P2 = C2 XOR KSExploitation
Section titled “Exploitation”- Register a user and send a message with known plaintext (e.g., “aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa”).
- Capture the ciphertext of that message.
- Extract the keystream by XORing the known plaintext with its ciphertext.
- Use the keystream to decrypt the admin’s ciphertext.
import base64
# Known/Sent plaintextknown_plaintext = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
# Our message's ciphertext (base64)ct_b64 = "b6a90c3BVh7irf5h0KHBQYlauiew09iutFHvxD3fu0VYUX56wVFVjIYlAIoKgfY/vjUDRrqrLck7ZKcgoRqKI8uDTaDaF+YJfA=="
# Admin's ciphertext (To decrypt)admin_ct_b64 = "N/K/gMqTARuuqaYzhO2URo0K9iez1Nvi5QO/wT/cvhIMA3p/"
def xor_bytes(a, b): return bytes(x ^ y for x, y in zip(a, b))
# base64 Decode both ciphertextsct = base64.b64decode(ct_b64)admin_ct = base64.b64decode(admin_ct_b64)
# Step 1: Recover keystream# KS = CT XOR PTkeystream = xor_bytes(ct, known_plaintext)
# Step 2: Decrypt admin ciphertextdecrypted = xor_bytes(admin_ct, keystream)
print(f"[+] Decrypted: {decrypted.decode('utf-8', errors='replace')}")Make the nonce unique, every single time.
cipher.iv = SecureRandom.random_bytes(12) # fresh nonce per message# send the nonce along with the ciphertext — it doesn't need to be secret, just uniqueA strictly incrementing counter works too, as long as it never resets and never overlaps across restarts or multiple servers. The one thing that can never happen is a nonce that’s fixed, predictable, or reused across messages.