RSS Amplifier

Nathan Ellison · Jun 14, 2024

Hack The Box - EXciting Outpost Recon

0
Sign in to vote or save

Nathan Ellison

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'))

References

Read the original on nathan-ellison.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.