2024 Business CTF - Vault of Hope: Exciting Outpost Recon
Challenge Information
| Attribute | Details |
|---|---|
| Event | 2024 Business CTF - Vault of Hope |
| Category | Crypto |
| Challenge | eXciting Outpost Recon |
| Author | Aliodor |
| Difficulty | Very 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 encryptedKey observations:
- The plaintext is padded to 32-byte boundaries with null bytes
- Each 32-byte chunk is XORed with the current key
- The key is updated after each chunk using SHA-256 hash
- 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 datawith open('output.txt', 'r') as f: encrypted_hex = f.read().strip()
encrypted_data = bytes.fromhex(encrypted_hex)
# Known plaintextknown_plaintext = b'Great and Noble Leader of the Tariaki'known_plaintext_padded = known_plaintext + b'\x00' * (LENGTH - len(known_plaintext))
# Extract first chunkfirst_chunk_encrypted = encrypted_data[:LENGTH]
# Derive initial key by XORinginitial_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 keydecrypted_data = decrypt_data(encrypted_data, initial_key)
# Print resultprint(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 datawith open('output.txt', 'r') as f: encrypted_hex = f.read().strip()
encrypted_data = bytes.fromhex(encrypted_hex)
# Known plaintext startknown_plaintext = b'Great and Noble Leader of the Tariaki'known_plaintext_padded = known_plaintext + b'\x00' * (LENGTH - len(known_plaintext))
# Get first encrypted chunkfirst_chunk_encrypted = encrypted_data[:LENGTH]
# Derive initial keyinitial_key = bytes(a ^ b for a, b in zip(first_chunk_encrypted, known_plaintext_padded))
# Decrypt all datadecrypted_data = decrypt_data(encrypted_data, initial_key)
print(decrypted_data)Run the solver:
python3 solver.pyKey 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