Skip to content
All tools
Modern cryptoRuns locallyNo account

XOR crib dragging for many-time pads

Recover both plaintexts when a one-time pad key is reused, by dragging a guessed word along the XOR of two ciphertexts.

Open in ctfpal

A one-time pad is unbreakable exactly once. Reuse the key on a second message and the key cancels: c1 XOR c2 = m1 XOR m2. The key is gone from the equation entirely, leaving two plaintexts XORed together - and English XORed with English is very far from random.

Dragging the crib

Guess a word likely to appear in one message - the , flag, picoCTF{. XOR it against m1 XOR m2 at every offset. At offsets where the guess is wrong you get noise. At the offset where it is right, the output is the corresponding stretch of the other plaintext, and it reads as English. That readable fragment is a new crib, which you drag again. The two messages unzip each other.

xored = bytes(a ^ b for a, b in zip(c1, c2))

for offset in range(len(xored) - len(crib)):
    window = xored[offset:offset + len(crib)]
    guess = bytes(x ^ y for x, y in zip(window, crib))
    if all(32 <= ch < 127 for ch in guess):
        print(offset, guess)      # readable output means the crib fits here

With more than two ciphertexts under the same key the problem gets easier fast - the space trick becomes a majority vote across all pairs and often recovers the key outright.

Part of a module

4. XOR and the cost of reusing a key

Break single-byte and repeating-key XOR, then recover both plaintexts from a reused one-time pad by crib dragging.

Practise on real challenges

Go deeper

  • XOR, crib dragging, and the two-time padSingle-byte XOR, repeating-key XOR, and keystream reuse are three faces of the same weakness. How to recover a key length from Hamming distance, drag a crib across a XOR of two plaintexts, and know when a stream cipher has handed you the answer.

Related tools