Athena Wiki

Stream Ciphers

cryptointermediate

Understand and attack stream ciphers - RC4, ChaCha20, and keystream reuse in CTF crypto challenges.

cryptostream-cipherrc4chacha20keystreamnonce-reusexor

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 KS=PRNG(K,nonce)\mathrm{KS} = \mathrm{PRNG}(K, \mathrm{nonce}). Then

C=PKS,P=CKS.C = P \oplus \mathrm{KS}, \qquad P = C \oplus \mathrm{KS}.

Common Stream Ciphers

CipherNotes
RC4Old, broken - many attacks
ChaCha20Modern, used in TLS 1.3
Salsa20ChaCha's predecessor
AES-CTRBlock cipher used as stream cipher
AES-GCMAuthenticated stream cipher

The Fatal Flaw: Nonce/Key Reuse

If the same key and nonce are used twice,

C1=P1KS,C2=P2KS    C1C2=P1P2.C_1 = P_1 \oplus \mathrm{KS},\quad C_2 = P_2 \oplus \mathrm{KS} \;\Rightarrow\; C_1 \oplus C_2 = P_1 \oplus P_2.

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 values

If 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 P2

AES-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 applies

Identifying Keystream Reuse

Signs of keystream reuse:

  • Two ciphertexts same length
  • ct1 XOR ct2 reveals patterns of natural language (space XOR letter = readable)
  • Challenge description mentions "same key" or "nonce counter reset"

Last updated on

On this page