EXciting Outpost Recon was one of the “Very Easy” difficulty cryptography challenges for the Hack The Box Business CTF 2024. It uses XOR encryption to encrypt the flag, while simultaneously giving away the beginning of the message, leading to the recovery of the entire message.
Challenge Description
Hijacking the outpost responsible for housing the messengers of the core gangs, we have managed to intercept communications between a newly-elected leader and the Tariaki, a well-established and powerful gang. In an attempt to sow conflict and prevent the creation of a singular all-powerful coalition to oppress the common people, we want YOU to use this message to our advantage. Can you use their obsequiousness to your advantage?
Challenge Script
1from hashlib import sha256
2
3import os
4LENGTH = 32
5
6def encrypt_data(data, k):
7 data += b'\x00' * (-len(data) % LENGTH)
8 encrypted = b''
9
10 for i in range(0, len(data), LENGTH):
11 chunk = data[i:i+LENGTH]
12
13 for a, b in zip(chunk, k):
14 encrypted += bytes([a ^ b])
15 k = sha256(k).digest()
16 return encrypted
17
18
19key = os.urandom(32)
20
21with open('plaintext.txt', 'rb') as f:
22 plaintext = f.read()
23
24assert plaintext.startswith(b'Great and Noble Leader of the Tariaki') # have to make sure we are aptly sycophantic
25
26with open('ciphertext.txt', 'wb') as f:
27 enc = encrypt_data(plaintext, key)
28 f.write(enc)
Enumeration
There wasn’t a lot of code to go through for this challenge.
The script begins with determining a random key to use for the enryption operation using the os.urandom() function to generate a random 32 byte string:
19key = os.urandom(32)
This function generates a random string of bytes from an OS-specific source of randomness. Operating systems collect random data such as mouse movements and disk activity to later use for generating random numbers. Because urandom() is being used, it isn’t possible to guess what the key might be.
Information Leak
The script then opens a file named plaintext.txt in rb mode, meaning “read binary”, and reads out the binary data into a variable plaintext. The script then presents a major clue for solving the challenge:
24assert plaintext.startswith(b'Great and Noble Leader of the Tariaki')
The script validates the beginning of the string that will be encrypted, and by doing so, gives away the beginning of the encrypted message.
encrypt_data
The script defines its own function for encrypting strings of bytes that are passed to it in the data variable. It is also passed a variable k, the key:
27enc = encrypt_data(plaintext, key)
The encryption function breaks the byte string up into chunks that are the same size as the key (32 bytes) using string splicing:
11chunk = data[i:i+LENGTH]
The script then performs a bitwise XOR on the data and key bits:
13for a, b in zip(chunk, k):
14 encrypted += bytes([a ^ b])
XOR
XOR (exclusive OR) is a logic operation that returns true only if one of its inputs is true. The symbol used to represent this operation is $\oplus$. Consider the following truth table:
| Input A | Input B | Output |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 1 | 0 |
The output is only 1 if either A or B is 1 (but not both). Another interesting fact about XOR is that you can rearrange the equation to work out the original inputs (the plaintext and key in our challenge). Consider the following equations:
$$A \oplus B = C$$ $$A \oplus C = B$$ $$B \oplus C = A$$
If A = 1, and B = 0, then the equations turn into the following:
$$1 \oplus 0 = 1$$ $$1 \oplus 1 = 0$$ $$0 \oplus 1 = 1$$
These all adhere to the truth table that was defined earlier. What this means is that if we know some portion of the plaintext, it is possible to recover the key used to encrypt it. With the key in hand, it would then be possible to decrypt the entire message. To illustrate this point, I’ll relabel the equation variables:
$$\text{plaintext} \oplus \text{key} = \text{ciphertext}$$ $$\text{plaintext} \oplus \text{ciphertext} = \text{key}$$ $$\text{ciphertext} \oplus \text{key} = \text{plaintext}$$
You might wonder, if we know the plaintext (which we use to work out the key), why do we need to bother working out the key at all? The reason is that we don’t need to know the entire plaintext to work out the key. We only need to know a length of plaintext that is equal to the length of the key.
Solution
This is the script that I used to decrypt the flag:
1# borrow the encryption function from the challenge script
2def encrypt_data(data, k):
3 data += b'\x00' * (-len(data) % LENGTH)
4 encrypted = b''
5
6 for i in range(0, len(data), LENGTH):
7 chunk = data[i:i+LENGTH]
8
9 for a, b in zip(chunk, k):
10 encrypted += bytes([a ^ b])
11 k = sha256(k).digest()
12 return encrypted
13
14# define the leaked plaintext
15plaintext_start = bytearray(b'Great and Noble Leader of the Tariaki')
16# get the first 32 bytes of the plaintext
17plaintext_first_32 = plaintext_start[:32]
18
19# open the encrypted flag file and read in the data
20with open('output.txt', 'r') as f:
21 # convert flag from hex to byte array
22 dehex_flag = bytearray.fromhex(f.read())
23
24# calculate the key with first 32 bytes of plaintext and encrypted flag
25key = encrypt_data(plaintext_first_32, dehex_flag[:32])
26print(f'Key: {key}')
27
28# decrypt the flag
29print(encrypt_data(dehex_flag, key).decode('UTF-8'))

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.