SPG
3 minutes to read
We are given the Python source code used to encrypt the flag:
from hashlib import sha256
import string, random
from secret import MASTER_KEY, FLAG
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from base64 import b64encode
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
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()}')
if __name__ == '__main__':
main()
And we also have the output of the script:
Your Password : t*!zGnf#LKO~drVQc@n%oFFZyvhvGZq8zbfXKvE1#*R%uh*$M6c$zrxWedrAENFJB7xz0ps4zh94EwZOnVT9&h
Encrypted Flag : GKLlVVw9uz/QzqKiBPAvdLA+QyRqyctsPJ/tx8Ac2hIUl8/kJaEvHthHUuwFDRCs
Source code analysis
Basically, the script takes a secret value MASTER_KEY and generates a password based on it. Then, the value of MASTER_KEY is used to derive an AES key using SHA256 to encrypt the flag. That’s what we have in main:
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()}')
Therefore, the objective is to find out the value of MASTER_KEY, so that we can derive the same AES key and decrypt the ciphertext.
To achieve this, we must use the generated password, which comes from generate_password:
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
As can be seen, it takes the bits of MASTER_KEY (interpreted as a little-endian integer) to choose a random character from ALPHABET:
ALPHABET = string.ascii_letters + string.digits + '~!@#$%^&*'
The key point is that if the bit is 1, the character is taken randomly from the first half of ALPHABET, whereas if the bit is 0, the character is taken from the second half.
Solution
The idea is to take the generated password one character at a time and determine if it belongs to the first or the second half of ALPHABET. As a result, we will know if the coresponding bit of MASTER_KEY is set to 1 or 0.
We can implement a simple algorithm to crack the password and find MASTER_KEY:
def crack_password(password):
master_key = 0
for i, p in enumerate((password)):
if p in ALPHABET[:len(ALPHABET) // 2]:
master_key |= 1 << i
return master_key.to_bytes((7 + len(password)) // 8, 'little')
Now we can use this main function to find MASTER_KEY and decrypt the ciphertext:
def main():
password = 't*!zGnf#LKO~drVQc@n%oFFZyvhvGZq8zbfXKvE1#*R%uh*$M6c$zrxWedrAENFJB7xz0ps4zh94EwZOnVT9&h'
ciphertext = 'GKLlVVw9uz/QzqKiBPAvdLA+QyRqyctsPJ/tx8Ac2hIUl8/kJaEvHthHUuwFDRCs'
MASTER_KEY = crack_password(password)
encryption_key = sha256(MASTER_KEY).digest()
cipher = AES.new(encryption_key, AES.MODE_ECB)
print(unpad(cipher.decrypt(b64decode(ciphertext)), AES.block_size).decode())
Flag
If we run the script, we will get the flag:
$ python3 solve.py
HTB{m4ll34bl3_p4ssw0rd_g3n3r4t0r!}
The full script can be found in here: solve.py.