Overview
As the name implies, this challenge presents us with a cipher that uses XOR, but things are a little…shifty.
Solution
We’re given two files: chal.py
and output.txt
. The latter contains hexadecimal characters, which probably decrypt to the flag. We’ll set that aside for now and take a look at the Python script.
Analyzing the script
def process_input(input_text):
shifted_text = input_text[-1:] + input_text[:-1] # Shift right by 1
print(input_text, shifted_text)
# XOR the shifted text against itself
xor_result = ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(input_text, shifted_text))
# Convert the result to hex
hex_output = xor_result.encode('utf-8').hex()
# Print to terminal
print(hex_output)
Looking at the code, we see that the input text is first shifted right by one character. For example, the string "helloworld"
, when shifted right by one character, becomes "dhelloworl"
. All characters move one place to the right except for the last character, which wraps back around to the first position.
After the shift, the original message is XOR’d with the shifted message and converted to hexadecimal which results a ciphertext similar to that contained in output.txt
.
Given that we do not know the original message that the key is based off of, is there a way we can decrypt the contents of output.txt
? Maybe!
Brute forcing victory
We might not know the original message (and therefore, not the key), but we can take a guess, or rather, many guesses.
Assuming that the plaintext only consists of readable characters, we can limit our guesses to those within the range of printable ASCII characters (0 to 127). Based on the encryption logic used in the code, we really only have to guess the first byte. This is because the key for the next character in the ciphertext would be the result of the XOR operation directly preceding the current one. This is called a cascade XOR. We can also check to see whether the output is entirely printable characters and remove the key values that don’t fit within that restriction.
Here is an example of a script that would do what we outlined above:
# Convert hex back to bytes
xor_result = bytes.fromhex(hex_input)
# Try each printable ASCII value for the first byte
for first_byte in range(32, 127): # Printable ASCII range
# Create a candidate for the original input
candidate = bytearray(len(xor_result))
candidate[0] = xor_result[0] ^ first_byte # XOR the first byte
# Cascade the XOR for the rest of the bytes
for i in range(1, len(xor_result)):
candidate[i] = xor_result[i] ^ candidate[i - 1]
candidate = candidate.decode('utf-8')
# Check if the original text is all printable ASCII (excluding the delete character)
if all(32 <= ord(c) <= 126 for c in candidate):
print(f"Valid candidate: {candidate}")
Valid candidate: I present to you, a many to one, one way function! I sure hope that no one figures out a way to break it, because that would be kind of awkward. The flag, in case someone can, is MetaCTF{tw0_w4y_funct10n_m0r3_l1k3_i7}. I guess if you’re reading this, you already broke it… Welp, I’ll just keep believing you haven’t!