Stream Ciphers
Understand and attack stream ciphers - RC4, ChaCha20, and keystream reuse in CTF crypto challenges.
Stream Ciphers
A stream cipher is a symmetric cryptosystem (that is, a means of encryption) where plaintext is combined with a keystream. The keystream is generated by a keystream generator, a deterministic pseudorandom function. The keystream and the plaintext are the inputs to the XOR function, the output of which is the ciphertext.
Given that the method for encrypting data is simply XOR, when we talk about specific stream ciphers we're really talking about a particular keystream generator.
To decrypt the ciphertext, the receiver will need the same key and keystream generator. Once they have this they can make use of the property of XOR: running it twice over the same input returns the original input
How Stream Ciphers Work
Let . Then
Common Stream Ciphers
| Cipher | Notes |
|---|---|
| RC4 | Old, broken - many attacks |
| ChaCha20 | Modern, used in TLS 1.3 |
| Salsa20 | ChaCha's predecessor |
| AES-CTR | Block cipher used as stream cipher |
| AES-GCM | Authenticated stream cipher |
The Fatal Flaw: Nonce/Key Reuse
If the same key and nonce are used twice,
Knowing P1 reveals P2 directly:
p2 = bytes([c1 ^ c2 ^ p1_byte for c1, c2, p1_byte in zip(C1, C2, P1)])Crib Dragging (Known Plaintext Bits)
If you only know a few bytes of one plaintext, use crib dragging to recover both:
def crib_drag(ct1, ct2, crib):
"""Slide a known plaintext snippet over the XORed ciphertexts."""
xored = bytes([a ^ b for a, b in zip(ct1, ct2)]) # = P1 XOR P2
results = []
for i in range(len(xored) - len(crib)):
candidate = bytes([xored[i+j] ^ crib[j] for j in range(len(crib))])
results.append((i, candidate))
return results
# Try known English words/phrases
for offset, candidate in crib_drag(ct1, ct2, b"the "):
if candidate.isascii() and candidate.isprintable():
print(f"Offset {offset}: {candidate}")RC4 Attacks
RC4 has known weaknesses:
FMS Attack (Key Byte Recovery)
In WEP, RC4 is used with a weak IV scheme. The FMS attack recovers key bytes by collecting many packets with related IVs. Tool: aircrack-ng.
RC4 Biases
The first few bytes of RC4 keystream are biased:
from Crypto.Cipher import ARC4
rc4 = ARC4.new(key)
keystream = rc4.encrypt(b'\x00' * 256)
# Byte 0 is biased toward 0x00
# Byte 1 is biased toward small valuesIf the key is short (< 16 bytes), brute-force is feasible.
ChaCha20 / Salsa20
These are secure if used correctly (no nonce reuse):
from Crypto.Cipher import ChaCha20
key = bytes(32) # 32 bytes
nonce = bytes(8) # 8 bytes
cipher = ChaCha20.new(key=key, nonce=nonce)
ct = cipher.encrypt(plaintext)
# Nonce reuse → XOR ciphertexts to get P1 XOR P2AES-CTR (Counter Mode)
AES-CTR uses AES as a stream cipher:
from Crypto.Cipher import AES
key = bytes(16)
nonce = bytes(8) # 8-byte nonce + 8-byte counter = 16-byte block
cipher = AES.new(key, AES.MODE_CTR, nonce=nonce)
ct = cipher.encrypt(plaintext)
# AES-CTR with same nonce → exact same keystream → nonce reuse attack appliesIdentifying Keystream Reuse
Signs of keystream reuse:
- Two ciphertexts same length
ct1 XOR ct2reveals patterns of natural language (space XOR letter = readable)- Challenge description mentions "same key" or "nonce counter reset"
Last updated on