Intro 1
CTF Write-up: Intro Crypto 1 - Keystream Reuse Attack
Challenge: Intro Crypto 1
Description: What is this non(c/s)ence everyonce is taking about? Given 255 ciphers and a hex encoded flag
Attachments: (Presumably ciphers.txt containing 255 hex-encoded lines and the hex-encoded flag ciphertext mentioned in the code)
Analysis
The Hint: The description "non(c/s)ence" and "everyonce" strongly hints at problems related to nonce reuse or the reuse of a one-time pad/keystream. Stream ciphers (or the One-Time Pad, OTP) rely on XORing plaintext with a keystream (
Ciphertext = Plaintext ^ Keystream). The security relies crucially on the keystream never being reused for different plaintexts with the same key.The Input: We are given multiple ciphertexts (255 of them) and the specific ciphertext corresponding to the flag. This setup, combined with the hint, points towards a scenario where the same keystream was likely used to encrypt multiple different plaintexts, including the flag.
The Vulnerability (Keystream Reuse): If the same keystream
Kis used to encrypt two different plaintextsP1andP2:C1 = P1 ^ KC2 = P2 ^ KXORing the two ciphertexts cancels out the keystream:C1 ^ C2 = (P1 ^ K) ^ (P2 ^ K) = P1 ^ P2 ^ K ^ K = P1 ^ P2This means XORing any two ciphertexts produced with the same keystream reveals the XOR sum of their corresponding plaintexts.
Attack Plan - The "Crib" or Known Plaintext Assumption
While C1 ^ C2 = P1 ^ P2 is useful, it doesn't directly give us either plaintext. However, there's a special case often found in CTFs:
What if one of the plaintexts was all zeros (or another known value)? Let's say
P1consisted entirely of null bytes (P1 = b'\x00\x00\x00...'). Then:C1 = P1 ^ K = 0s ^ K = KThis means the ciphertextC1is the keystream itself!
Recovering the Flag: If we find this specific ciphertext (
C1 = K), we can XOR it with the flag's ciphertext (C_flag = P_flag ^ K) to recover the flag's plaintext (P_flag):C1 ^ C_flag = K ^ (P_flag ^ K) = K ^ P_flag ^ K = P_flag
The attack plan is therefore:
Assume one of the 255 provided ciphertexts (
ciphertexts[i]) corresponds to the encryption of an all-zero plaintext, meaningciphertexts[i]is the keystream.Iterate through each
ciphertexts[i].XOR
ciphertexts[i]with theflag_ciphertext.If the result starts with the known flag format prefix (e.g.,
CSCG{), we have likely found the correctciphertexts[i](the keystream) and successfully recovered the flag.
Implementation (Solver Script)
The provided Python script executes this attack plan:
from binascii import unhexlify, hexlify
# read the ciphertexts from the file (assuming it exists)
ciphertexts = []
try:
with open("ciphers.txt", "r") as f:
for line in f:
# Skip empty lines if any
line = line.strip()
if line:
ciphertexts.append(unhexlify(line))
except FileNotFoundError:
print("Error: ciphers.txt not found. Please ensure it's in the same directory.")
exit(1)
except Exception as e:
print(f"Error reading ciphers.txt: {e}")
exit(1)
# The hex-encoded ciphertext of the flag
flag_ciphertext_hex = "2188df14ac8cfc479f2c8ec3656a0f4c337f19c9eacee03a9d79bf75fcea51fde507838dbd9413feb164bd966558eb6f667eefc537542b7377ea43579e40be74cebfede6a0107921efbd"
try:
flag_ciphertext = unhexlify(flag_ciphertext_hex)
except Exception as e:
print(f"Error decoding flag ciphertext hex: {e}")
exit(1)
print(f"Loaded {len(ciphertexts)} ciphertexts.")
print(f"Flag ciphertext length: {len(flag_ciphertext)} bytes.")
found = False
for i, ct in enumerate(ciphertexts):
# Ensure we only XOR up to the shortest length to avoid errors
min_len = min(len(ct), len(flag_ciphertext))
if min_len == 0:
continue # Skip empty ciphertexts
# Perform the XOR operation
possible_flag_bytes = bytes(a ^ b for a, b in zip(ct[:min_len], flag_ciphertext[:min_len]))
# Check if the result starts with the expected flag prefix
# Use bytes for comparison
if possible_flag_bytes.startswith(b"CSCG{"):
print(f"\nPotential match found with Ciphertext index {i}:")
try:
# Try decoding the full XOR result
decoded_flag = possible_flag_bytes.decode('ascii')
print(f"Recovered Flag: {decoded_flag}")
found = True
# Assuming only one correct answer, we can break
break
except UnicodeDecodeError:
print(f"Result starts with prefix but contains non-ASCII bytes. Raw bytes: {possible_flag_bytes}")
if not found:
print("\nNo flag starting with 'CSCG{' found using this method.")
Execution and Result
Running the script performs the XOR operations. As shown in the provided success output, the script found a match:
Counter 23: CSCG{turns_out_that_once_in_nonce_is_actually_important_who'd've_thought?}This indicates that ciphertexts[23] was indeed the keystream (likely the result of encrypting all zeros). XORing it with flag_ciphertext revealed the flag.
Flag: CSCG{turns_out_that_once_in_nonce_is_actually_important_who'd've_thought?}
Conclusion
This challenge perfectly illustrates the danger of keystream reuse in stream ciphers and OTPs, often caused by reusing a nonce or cryptographic key. By XORing ciphertexts created with the same keystream, we can eliminate the key and potentially recover plaintexts, especially if one of the plaintexts is known (like a string of zeros). The flag itself cleverly comments on the importance of nonce uniqueness.
Last updated
Was this helpful?