DEV Community

CTFDojo
CTFDojo

Posted on Originally published at ctfdojo.com

PicoCTF Easy1 Writeup — Recover an XOR Key with Crib Dragging

The flag is XOR-encrypted with a short repeating key. Since we know every picoCTF flag starts with picoCTF{, we XOR that known text ("crib") with the beginning of the ciphertext to recover the first bytes of the key, then complete the key by finding the repeating pattern.

  • Platform: picoGym
  • Category: Cryptography
  • Points: 200 pts
  • Difficulty: Intermediate
  • Technique: XOR, crib dragging

Challenge description

The challenge provides a hex-encoded ciphertext file, along with the Python script used to encrypt it:

KEY = ???  # unknown, short key

def encrypt(data, key):
    return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))

with open("flag.txt", "rb") as f:
    flag = f.read()

ciphertext = encrypt(flag, KEY)
print(ciphertext.hex())
Enter fullscreen mode Exit fullscreen mode
$ cat ciphertext.txt
3d262c3c061909340c1a1210100c1a1210100c1a1210100c1a1210102e
Enter fullscreen mode Exit fullscreen mode

The KEY is unknown, but the encryption scheme is crystal clear: repeated byte-by-byte XOR, cycling through the key (i % len(key)). It's a classic construction — and classically vulnerable as soon as you know a fragment of the plaintext.

Step 1 — Understand XOR's self-inverse property

The XOR operator has a mathematical property that's very handy for the attacker: it's its own inverse. If ciphertext = plaintext XOR key, then:

plaintext XOR ciphertext = plaintext XOR (plaintext XOR key) = (plaintext XOR plaintext) XOR key = 0 XOR key = key
Enter fullscreen mode Exit fullscreen mode

In other words: as soon as you know both a chunk of plaintext and its corresponding ciphertext, you can recover the key directly, without having to guess it.

Step 2 — Crib dragging with the known prefix

Every picoCTF flag follows the same format: it starts with picoCTF{. That's our "crib" (known plaintext). We XOR this crib with the first bytes of the ciphertext to deduce the first bytes of the key:

ciphertext_hex = "3d262c3c061909340c1a1210100c1a1210100c1a1210100c1a1210102e"
ciphertext = bytes.fromhex(ciphertext_hex)

crib = b"picoCTF{"

key_fragment = bytes(c ^ k for c, k in zip(ciphertext, crib))
print(key_fragment)
Enter fullscreen mode Exit fullscreen mode
b'MOOSEMOO'
Enter fullscreen mode Exit fullscreen mode

We get a key fragment. A pattern jumps out right away: the first 5 bytes MOOSE repeat at the start of the next 3 bytes (MOO) — a clear sign that the real key is 5 bytes long and we simply went past one period with an 8-character crib.

Step 3 — Determine the key length

Looking at key_fragment = MOOSEMOO, the most natural hypothesis is a 5-byte key: MOOSE, which indeed starts over at the 6th position (the following M, since the 8-character crib overruns one period of the 5-byte key).

key = b"MOOSE"
print(len(key))  # 5
Enter fullscreen mode Exit fullscreen mode

This is consistent: a short 5-byte key cycling over the whole message, exactly as expected from the encryption script given in the challenge.

Step 4 — Decrypt the full message

With the complete key in hand, we decrypt the whole file:

from itertools import cycle

ciphertext = bytes.fromhex(ciphertext_hex)
key = b"MOOSE"

plaintext = bytes(c ^ k for c, k in zip(ciphertext, cycle(key)))
print(plaintext.decode())
Enter fullscreen mode Exit fullscreen mode
picoCTF{____________________}
Enter fullscreen mode Exit fullscreen mode

The picoCTF{ prefix confirms the recovered key is correct, and the rest of the message decrypts cleanly through to the closing brace.

🚩 picoCTF{ flag intentionally hidden }

The flag is deliberately hidden — follow the method, you've earned it. 💪

Key takeaways

This challenge illustrates a known-plaintext attack on short repeating-key XOR: knowing just a fragment of the plaintext is enough to recover the entire key, and therefore the entire message.

  • XOR with a short repeating key is vulnerable as soon as part of the plaintext is known — the fixed format of CTF flags (picoCTF{) is often that known fragment
  • Never reuse a short XOR key across multiple blocks or messages: it's the same structural weakness as the Vigenère cipher
  • For real confidentiality needs, prefer a modern authenticated cipher like AES-GCM or ChaCha20-Poly1305, never repeated XOR

Originally published on CTFdojo — join the CTFdojo Discord to discuss writeups and get notified about new ones.

Top comments (0)