XOR encryption is simple but extremely common in CTFs. It's based on the XOR operation's special property: A ⊕ B ⊕ B = A. This means decryption is the same as encryption!
XOR is the foundation of many encryption schemes. Understanding its properties and weaknesses is essential for CTF crypto challenges.
XOR Basics
1606070;"># XOR Truth Table:2606070;"># 0 ⊕ 0 = 03606070;"># 0 ⊕ 1 = 14606070;"># 1 ⊕ 0 = 15606070;"># 1 ⊕ 1 = 06 7606070;"># Key Properties:8606070;"># A ⊕ A = 0 (anything XOR itself = 0)9606070;"># A ⊕ 0 = A (anything XOR 0 = itself)10606070;"># A ⊕ B = B ⊕ A (commutative)11606070;"># A ⊕ B ⊕ B = A (self-inverse: XOR twice = original)12 13606070;"># This means:14606070;"># plaintext ⊕ key = ciphertext15606070;"># ciphertext ⊕ key = plaintext16 17606070;"># Example:18606070;"># "A" = 0x41 = 0100000119606070;"># key = 0x20 = 0010000020606070;"># XOR: 01100001 = 0x61 = "a"21 22606070;"># XOR "A" with 0x20, get "a"23606070;"># XOR "a" with 0x20, get "A"Single Byte Key Attack
python
1606070;"># If ciphertext is XORed with a single byte, brute force all 256 values2 3def single_byte_xor(ciphertext, key):4 return bytes([b ^ key for b in ciphertext])5 6def brute_force_single_byte(ciphertext):7 for key in range(256):8 plaintext = single_byte_xor(ciphertext, key)9 try:10 decoded = plaintext.decode(606070;">#a5d6ff;">'ascii')11 606070;"># Check if result looks like text12 if decoded.isprintable():13 print(f606070;">#a5d6ff;">"Key {key} (0x{key:02x}): {decoded}")14 except:15 pass16 17606070;"># Example usage:18cipher = bytes.fromhex(606070;">#a5d6ff;">"1b37373331363f78151b7f2b783431333d")19brute_force_single_byte(cipher)20 21606070;"># CyberChef: "XOR Brute Force" operationScoring Results
Add letter frequency scoring to automatically identify the most likely English plaintext. The most readable result is usually correct!
Known Plaintext Attack
python
1606070;"># If you know part of the plaintext, recover the key!2606070;"># plaintext ⊕ key = ciphertext3606070;"># Therefore: plaintext ⊕ ciphertext = key4 5def xor_bytes(a, b):6 return bytes([x ^ y for x, y in zip(a, b)])7 8ciphertext = bytes.fromhex(606070;">#a5d6ff;">"...")9known_plaintext = b606070;">#a5d6ff;">"flag{" # We know flags start with this!10 11606070;"># Recover key bytes for known portion12key_fragment = xor_bytes(ciphertext[:5], known_plaintext)13print(f606070;">#a5d6ff;">"Key fragment: {key_fragment}")14 15606070;"># If key repeats, use this fragment to find key length16606070;"># and decrypt more of the messageRepeating Key XOR
python
1606070;"># Encryption with repeating key2def repeating_key_xor(plaintext, key):3 result = []4 for i, byte in enumerate(plaintext):5 result.append(byte ^ key[i % len(key)])6 return bytes(result)7 8606070;"># Breaking repeating key XOR:9606070;"># 1. Find key length10606070;"># 2. Split cipher into groups by key position11606070;"># 3. Each group is single-byte XOR - brute force each12 13606070;"># Finding key length with Hamming distance:14def hamming_distance(a, b):15 return sum(bin(x ^ y).count(606070;">#a5d6ff;">'1') for x, y in zip(a, b))16 17def find_key_length(cipher, max_len=40):18 scores = []19 for keylen in range(2, max_len):20 chunks = [cipher[i:i+keylen] for i in range(0, len(cipher), keylen)]21 if len(chunks) < 4:22 continue23 606070;"># Compare first chunks and average Hamming distance24 distances = []25 for i in range(min(4, len(chunks)-1)):26 d = hamming_distance(chunks[i], chunks[i+1])27 distances.append(d / keylen) 606070;"># Normalize28 scores.append((keylen, sum(distances)/len(distances)))29 30 606070;"># Lower score = more likely key length31 scores.sort(key=lambda x: x[1])32 return scores[:5] 606070;"># Return top 5 candidatesComplete Repeating Key Attack
python
1606070;">#!/usr/bin/env python32606070;"># Complete attack on repeating-key XOR3 4from collections import Counter5 6def score_english(text):7 606070;">#a5d6ff;">"""Score text based on English letter frequency"""8 freq = 606070;">#a5d6ff;">'etaoinshrdlcumwfgypbvkjxqz'9 score = 010 for c in text.lower():11 if c in freq:12 score += len(freq) - freq.index(c)13 return score14 15def single_byte_xor_decrypt(cipher):16 606070;">#a5d6ff;">"""Try all single-byte keys, return best result"""17 best_score = 018 best_result = None19 best_key = None20 21 for key in range(256):22 plaintext = bytes([b ^ key for b in cipher])23 try:24 decoded = plaintext.decode(606070;">#a5d6ff;">'ascii')25 score = score_english(decoded)26 if score > best_score:27 best_score = score28 best_result = decoded29 best_key = key30 except:31 pass32 33 return best_key, best_result34 35def break_repeating_xor(cipher, key_length):36 606070;">#a5d6ff;">"""Break repeating XOR with known key length"""37 606070;"># Split into columns38 columns = [[] for _ in range(key_length)]39 for i, byte in enumerate(cipher):40 columns[i % key_length].append(byte)41 42 606070;"># Solve each column as single-byte XOR43 key = []44 for col in columns:45 k, _ = single_byte_xor_decrypt(bytes(col))46 key.append(k)47 48 606070;"># Decrypt with recovered key49 key = bytes(key)50 plaintext = bytes([c ^ key[i % len(key)] for i, c in enumerate(cipher)])51 52 return key, plaintext53 54606070;"># Usage:55cipher = bytes.fromhex(606070;">#a5d6ff;">"...")56key_lengths = find_key_length(cipher)57for kl, _ in key_lengths:58 key, plain = break_repeating_xor(cipher, kl)59 print(f606070;">#a5d6ff;">"Key length {kl}: key={key}, plain={plain[:50]}...")CTF XOR Tricks
python
1606070;"># Trick 1: XOR with itself reveals nothing2606070;"># But XOR two ciphertexts encrypted with same key:3606070;"># c1 ⊕ c2 = (p1 ⊕ key) ⊕ (p2 ⊕ key) = p1 ⊕ p24606070;"># Key cancels out! Now you have XOR of plaintexts.5 6606070;"># Trick 2: Flag format known7606070;"># If flag is flag{...}, XOR cipher with "flag{" to get key start8 9606070;"># Trick 3: Null bytes in key10606070;"># If plaintext has patterns (like spaces), key reveals itself11606070;"># plaintext: "HELLO WORLD"12606070;"># If encrypted and has obvious pattern, XOR with common char13 14606070;"># Trick 4: CyberChef shortcuts15606070;"># - XOR: Specify key in hex, UTF8, or decimal16606070;"># - XOR Brute Force: Try all single bytes17606070;"># - Guess the key from context cluespython
1606070;"># Common attack: Two plaintexts, same key (many-time pad)2606070;"># c1 = p1 ⊕ key3606070;"># c2 = p2 ⊕ key4606070;"># c1 ⊕ c2 = p1 ⊕ p25 6606070;"># If we guess p1[i] = space (0x20):7606070;"># c1[i] ⊕ 0x20 = key[i]8606070;"># Then: c2[i] ⊕ key[i] = p2[i]9 10606070;"># Tool: mtp (many-time pad attack)11606070;"># https://github.com/CameronLonsworthy/mtpXOR Tools
bash
1606070;"># CyberChef Operations:2606070;"># - XOR (with known key)3606070;"># - XOR Brute Force (single byte)4606070;"># - Guess the key5 6606070;"># xortool - Automated analysis7pip install xortool8 9xortool cipher.bin10606070;"># Suggests key lengths and attempts decryption11 12xortool -l 5 cipher.bin 606070;"># Force key length 513xortool -c 20 cipher.bin 606070;"># Assume most common char is space (0x20)14 15606070;"># Python one-liner XOR16python3 -c 606070;">#a5d6ff;">"print(bytes([a^b for a,b in zip(open('cipher','rb').read(), b'KEY'*1000)])[:100])"17 18606070;"># xxd for hex manipulation19xxd -p cipher.bin | tr -d 606070;">#a5d6ff;">'\n' # Hex dumpKnowledge Check
Quick Quiz
Question 1 of 2Why is XOR used for encryption?
Key Takeaways
- XOR is self-inverse: encrypt and decrypt are the same operation
- Single-byte XOR: brute force all 256 keys
- Known plaintext: XOR with cipher to recover key
- Repeating key: find length, then solve each position separately
- Two-time pad: XOR ciphertexts to eliminate key
- xortool automates most XOR analysis