HackTheBox SPG Challenge Writeup | Cryptography CTF Challenges
Introduction
The HackTheBox SPG challenge write-up details a cryptographic CTF puzzle where users decrypt an encrypted flag using a password generated from a master key. By analyzing the password generation process — where characters are chosen based on bitwise operations on the master key — participants can reverse-engineer the key. The guide explains using AES-ECB with SHA-256 hashing and provides Python snippets to retrieve the password, convert it to binary, and decrypt the flag systematically.
HackTheBox SPG Description
After successfully joining the academy, there is a process where you have to log in to eclass in order to access notes in each class and get the current updates for the ongoing prank labs. When you attempt to log in, though, your browser crashes, and all your files get encrypted. This is yet another prank for the newcomers. The only thing provided is the password generator script. Can you crack it, unlock your files, and log in to the spooky platform?
Methodology
This challenge involves creating a simple password generator, with the goal of recovering the master key. Since the alphabet used to generate the password is fixed and known, we can retrieve the master key incrementally by examining each password character and determining which half it belongs to. With the master key identified, we can then decrypt the flag using AES-ECB.
Walkthrough
We are given two files:
- source.py: This script serves as the main program that encrypts the flag.
- output.txt: This file contains the encrypted flag along with the generated password.
The challenge features a basic secure password generator (SPG) that sets a secret master key as the seed for generating passwords. Multiple passwords can then be derived from this key.
The main function operates in a simple sequence: a password is generated by the password generator and provided to us. The master key is then utilized as the encryption key to encrypt the flag using AES-ECB.
def main():
password = generate_password()
encryption_key = sha256(MASTER_KEY).digest()
cipher = AES.new(encryption_key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(FLAG, 16))
with open('output.txt', 'w') as f:
f.write(f'Your Password : {password}\nEncrypted Flag : {b64encode(ciphertext).decode()}')
Let’s examine the generate_password
function more closely. This function creates a password derived from the master key, likely by taking the master key as input or using it as a seed to generate a sequence of characters. The function would typically involve the following steps:
- Initialization with the Master Key: The master key serves as the basis for generating the password, either by directly influencing the sequence of characters or by setting the initial conditions (e.g., as a seed in a pseudo-random generator).
- Character Selection: Using the known alphabet, characters are selected based on conditions influenced by the master key. For example, the function might divide the alphabet into sections, then use the master key to decide which section each character of the password will come from.
- Password Output: After iterating through the master key (or the seed-derived sequence), a password is generated and returned, ready to be used or analyzed further.
This function is pivotal because understanding its logic helps reveal the relationship between the master key and the output password, which is key to reversing the process to recover the master key and ultimately decrypt the flag.
ALPHABET = string.ascii_letters + string.digits + '~!@#$%^&*'
def generate_password():
master_key = int.from_bytes(MASTER_KEY, 'little')
password = '' while master_key:
bit = master_key & 1
if bit:
password += random.choice(ALPHABET[:len(ALPHABET)//2])
else:
password += random.choice(ALPHABET[len(ALPHABET)//2:])
master_key >>= 1 return password
First, let’s analyze the setup of the generate_password
function and its underlying mechanism:
- Alphabet Structure: The alphabet includes all ASCII letters (both lowercase and uppercase), digits from
0
to9
, and special symbols~!@#$%^&*
. The order of these characters is crucial because it directly affects how characters are selected during password generation. - Password Generation Process:
- The master key is converted into an integer using little-endian representation.
- The function then iterates through each bit of the master key from right to left.
- Depending on each bit’s value:
- If the bit is
1
, a random character from the first half of the alphabet (abcdefghijklmnopqrstuvwxyzABCDEFGHI
) is added to the password. - If the bit is
0
, a random character from the second half (JKLMNOPQRSTUVWXYZ0123456789~!@#$%^&*
) is appended.
The main goal is to uncover a potential vulnerability in this password generation process to retrieve the master key, which is essential for decrypting the flag, given AES’s strength in encryption.
To proceed systematically, let’s first write a function to load data from the output file (output.txt
). This function will help us access the encrypted flag and password, allowing further analysis. Here’s a sample function outline:
def load_output_data(file_path):
with open(file_path, 'r') as file:
data = file.readlines()
# Assume data contains two lines: the password and encrypted flag
password = data[0].strip()
encrypted_flag = data[1].strip()
return password, encrypted_flag
This function will read the output.txt
file, extracting the password and encrypted flag, which we can then analyze to identify patterns or weaknesses in the password generation process.
You can customize the above code to fit the challenge as shown below:
from base64 import b64decode
def load_output_dat():
with open('output.txt') as f:
password = f.readline().split(' : ')[1]
flag = b64decode(f.readline().split(' : ')[1])
return password, flag
To generate a password locally using the generate_password
function with the master key this_is_a_test_password
, we need to simulate the function’s logic based on the details we’ve discussed. This includes converting the master key to an integer and using it to select characters from the two fixed parts of the alphabet.
43r2ipt1QJUw6dz#lYScVuGOHn@Wkox1bABkuYH9y#Jj8ex&gj8Pcpg1ADHbbYq&n1Y1PoA1khmIFQl#X2d$Elo~l9n2OypXiE*OdfyU2%q0wgD0inszDPzZ#3!PDFvQrM@^XvB9ExK@Dnf&mcV%rmCOdnj5IjCXDhfkNdj9ZqUWkHqTYVHW#aa
To recover the master key bit-by-bit from the password, we can leverage the fixed halves of the alphabet and identify each password character’s “half” rather than its exact position. Here’s how we can approach it:
- Map Character to Bit: For each character in the password:
- If the character belongs to the first half of the alphabet (
abcdefghijklmnopqrstuvwxyzABCDEFGHI
), the corresponding bit in the master key is1
. - If the character belongs to the second half (
JKLMNOPQRSTUVWXYZ0123456789~!@#$%^&*
), the bit is0
.
- Construct Master Key Bits: By iterating through each character of the password, we can construct the master key in binary by appending
0
or1
based on the character’s half. - Convert Binary to Integer: Once we have a binary representation of the master key (from right to left), we can convert this binary sequence into an integer. This integer can then be interpreted as the master key using little-endian byte order.
We’ll implement a Python script that goes through each character in the password, identifies which half of the alphabet it belongs to, and assigns a corresponding bit (1
or 0
). Since the password generation used the reversed master key, we’ll reverse the bit sequence we construct and convert it to a big-endian integer.
A general purpose Python script can be found below:
def recover_master_key(password):
# Define the two halves of the alphabet
first_half = set("abcdefghijklmnopqrstuvwxyzABCDEFGHI")
second_half = set("JKLMNOPQRSTUVWXYZ0123456789~!@#$%^&*")
# Initialize an empty list to store bits
bits = []
# Iterate over each character in the password
for char in password:
if char in first_half:
bits.append('1') # 1-bit for first half characters
elif char in second_half:
bits.append('0') # 0-bit for second half characters
else:
raise ValueError(f"Unexpected character '{char}' in password.") # Reverse bits to align with original master key order and join them
master_key_binary = ''.join(bits[::-1]) # Convert the binary string to an integer (big-endian)
master_key_int = int(master_key_binary, 2)
# Convert integer to bytes
byte_length = (len(bits) + 7) // 8 # Calculate byte length
master_key_bytes = master_key_int.to_bytes(byte_length, 'big')
return master_key_bytes# Example usage
password = "43r..." # Substitute with the actual password from output.txt
master_key = recover_master_key(password)
print("Recovered master key:", master_key)
Explanation of the Code:
- Identify Character Half: We iterate through each password character:
- If it belongs to
first_half
, we append1
tobits
. - If it belongs to
second_half
, we append0
.
- Reverse Bit Order: Since the master key was reversed for password generation, we reverse
bits
back to its original order. - Convert to Integer: The reversed binary sequence is interpreted in big-endian order to create the integer form of the master key.
- Convert to Bytes: Finally, we convert the integer into bytes to get the master key in its original format.
This recovered master key can then be used for decryption or other analysis steps, depending on the challenge requirements.
Remember that we want the above script to be fine-tuned to fit the challenge, therefore we can create a shorter one:
import string
# Define allowed characters
valid_chars = string.ascii_letters + string.digits + '~!@#$%^&*'def generate_master_key(password):
# Initialize binary key
binary_key = ''
# Create binary representation based on character's position
for char in password:
if char in valid_chars[:len(valid_chars) // 2]:
binary_key += '1'
else:
binary_key += '0'
# Reverse the binary key before returning
return binary_key[::-1]
And given the master key, the flag can be then decrypted:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from hashlib import sha256
from Crypto.Util.number import long_to_bytes
def decrypt_data(master_key, encrypted_data):
# Convert the binary key to bytes and reverse it
key_bytes = long_to_bytes(int(master_key, 2))[::-1]
# Hash the key to create a 256-bit encryption key
aes_key = sha256(key_bytes).digest()
# Set up AES decryption in ECB mode
cipher = AES.new(aes_key, AES.MODE_ECB)
# Decrypt the data
decrypted_data = cipher.decrypt(encrypted_data)
return decrypted_data
Done
You can also watch: