2024 Business CTF - Vault of Hope: Exciting Outpost Recon

Challenge Information

AttributeDetails
Event2024 Business CTF - Vault of Hope
CategoryCrypto
ChallengeeXciting Outpost Recon
AuthorAliodor
DifficultyVery Easy

Summary

This cryptography challenge provides an encryption script and encrypted output. The encryption uses a combination of XOR and SHA-256 hashing with a 32-byte random key. The plaintext is known to start with “Great and Noble Leader of the Tariaki”, which enables a known plaintext attack. Participants must use this known plaintext to derive the initial key and decrypt the entire message.


Analysis

Encryption Algorithm (from source.py):

def encrypt_data(data, k):
data += b'\x00' * (-len(data) % LENGTH) # Pad to 32-byte chunks
encrypted = b''
for i in range(0, len(data), LENGTH):
chunk = data[i:i+LENGTH]
# XOR each byte with corresponding key byte
for a, b in zip(chunk, k):
encrypted += bytes([a ^ b])
# Derive next key using SHA-256
k = sha256(k).digest()
return encrypted

Key observations:

  1. The plaintext is padded to 32-byte boundaries with null bytes
  2. Each 32-byte chunk is XORed with the current key
  3. The key is updated after each chunk using SHA-256 hash
  4. The encrypted output is provided in hexadecimal format

The known plaintext attack is possible because:

  • The first chunk always starts with “Great and Noble Leader of the Tariaki”
  • This 37-byte string is padded to 32 bytes for the first chunk calculation
  • XORing the encrypted first chunk with the known plaintext recovers the initial key

Solution

Step 1: Derive the Initial Key

from hashlib import sha256
LENGTH = 32
# Read the encrypted data
with open('output.txt', 'r') as f:
encrypted_hex = f.read().strip()
encrypted_data = bytes.fromhex(encrypted_hex)
# Known plaintext
known_plaintext = b'Great and Noble Leader of the Tariaki'
known_plaintext_padded = known_plaintext + b'\x00' * (LENGTH - len(known_plaintext))
# Extract first chunk
first_chunk_encrypted = encrypted_data[:LENGTH]
# Derive initial key by XORing
initial_key = bytes(a ^ b for a, b in zip(first_chunk_encrypted, known_plaintext_padded))

Step 2: Decrypt the Full Message

def decrypt_data(data, k):
decrypted = b''
for i in range(0, len(data), LENGTH):
chunk = data[i:i+LENGTH]
# XOR with current key
for a, b in zip(chunk, k):
decrypted += bytes([a ^ b])
# Update key using SHA-256
k = sha256(k).digest()
return decrypted
# Decrypt with derived key
decrypted_data = decrypt_data(encrypted_data, initial_key)
# Print result
print(decrypted_data)

Complete solver.py:

from hashlib import sha256
LENGTH = 32
def decrypt_data(data, k):
decrypted = b''
for i in range(0, len(data), LENGTH):
chunk = data[i:i+LENGTH]
for a, b in zip(chunk, k):
decrypted += bytes([a ^ b])
k = sha256(k).digest()
return decrypted
# Read the encrypted data
with open('output.txt', 'r') as f:
encrypted_hex = f.read().strip()
encrypted_data = bytes.fromhex(encrypted_hex)
# Known plaintext start
known_plaintext = b'Great and Noble Leader of the Tariaki'
known_plaintext_padded = known_plaintext + b'\x00' * (LENGTH - len(known_plaintext))
# Get first encrypted chunk
first_chunk_encrypted = encrypted_data[:LENGTH]
# Derive initial key
initial_key = bytes(a ^ b for a, b in zip(first_chunk_encrypted, known_plaintext_padded))
# Decrypt all data
decrypted_data = decrypt_data(encrypted_data, initial_key)
print(decrypted_data)

Run the solver:

Terminal window
python3 solver.py

Key Takeaways

  • Known plaintext attacks are powerful against stream ciphers when plaintext structure is known
  • XOR cipher strength depends entirely on the randomness and uniqueness of the key
  • Repeating key derivation through hashing doesn’t provide security if the first key is compromised
  • Padding schemes can leak information about the original plaintext length
  • Block cipher modes without authentication are vulnerable to manipulation
  • SHA-256 used for key derivation is deterministic and predictable given an initial key