Points: 450
Description
Great job finding out what the APT did with the LLM! GA was able to check their network logs and figure out which developer copy and pasted the malicious code; that developer works on a core library used in firmware for the U.S. Joint Cyber Tactical Vehicle (JCTV)! This is worse than we thought!
You ask GA if they can share the firmware, but they must work with their legal teams to release copies of it (even to the NSA). While you wait, you look back at the data recovered from the raid. You discover an additional drive that you haven’t yet examined, so you decide to go back and look to see if you can find anything interesting on it. Sure enough, you find an encrypted file system on it, maybe it contains something that will help!
Unfortunately, you need to find a way to decrypt it. You remember that Emiko joined the Cryptanalysis Development Program (CADP) and might have some experience with this type of thing. When you reach out, he’s immediately interested! He tells you that while the cryptography is usually solid, the implementation can often have flaws. Together you start hunting for something that will give you access to the filesystem.
What is the password to decrypt the filesystem?
Downloads
- disk image of the USB drive which contains the encrypted filesystem (disk.dd.tar.gz)
- Interesting files from the user’s directory (files.zip)
- Interesting files from the bin/ directory (bins.zip)
Prompt:
- Enter the password (hope it works!)
Solution
We can start by unzipping all of the provided files using zip and tar. This leaves us with the following files.
cobra@arch:~/codebreaker/task5$ ls -la *
-r--r--r-- 1 cobra cobra 137438953472 Sep 16 11:00 disk.dd
bins:
total 23628
drwxr-xr-x 2 cobra cobra 4096 Jan 16 21:40 .
drwxr-xr-x 4 cobra cobra 4096 Jan 16 21:41 ..
-rwxr-xr-x 1 cobra cobra 12618792 May 21 2023 pidgin_rsa_encryption
-rwxr-xr-x 1 cobra cobra 11564784 May 21 2023 pm
files:
total 20
drwxr-xr-x 5 cobra cobra 4096 Jan 16 21:41 .
drwxr-xr-x 4 cobra cobra 4096 Jan 16 21:41 ..
drwxr-xr-x 2 cobra cobra 4096 Aug 7 19:59 .keys
drwxr-xr-x 3 cobra cobra 4096 Aug 15 10:53 .passwords
drwxr-xr-x 3 cobra cobra 4096 Aug 12 18:22 .purple
Looking at the files directory first, we can see some public/private keys, passwords, and logs.
cobra@arch:~/codebreaker/task5/files$ ls .*
.keys:
4C1D_public_key.pem 570RM_private_key.pem 570RM_public_key.pem B055M4N_public_key.pem PL46U3_public_key.pem V3RM1N_public_key.pem
.passwords:
6bed776cb619ea72d942e0b49ea1de0d
.purple:
logs
The only private key that is present is 570RM_private_key.pem, so it must be the user who these files belong to: 570RM. Additionally, the private key is encrypted, so we can’t use it without the passphrase.
cobra@arch:~/codebreaker/task5/files/.keys$ cat 570RM_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,291A6AC7A18AA0D7FC078EE18FDF93EF
pzL+zlfDR6yS1HLm/0gHJ1f8oeSRgL2vI2Ta8gsHzCEVbY8V2gvkanLkgpCFKbWC
ILluAJoR9pX6FF13qO8DWPgDKUppNIl/pSdlSKtckxeq5+1nZFCnzfZjT/wsJrqu
...
Looking at the passwords directory, we can see a lot of encrypted passwords, including the password for the drive that we’re after.
cobra@arch:~/codebreaker/task5/files/.passwords/6bed776cb619ea72d942e0b49ea1de0d$ ls -la
total 120
drwxr-xr-x 2 cobra cobra 4096 Aug 15 10:53 .
drwxr-xr-x 3 cobra cobra 4096 Aug 15 10:53 ..
-rw-r--r-- 1 cobra cobra 34 Aug 11 23:01 AmazonWebServices
-rw-r--r-- 1 cobra cobra 34 Aug 14 03:12 Apple
-rw-r--r-- 1 cobra cobra 34 Aug 14 23:27 Discord
...
-rw-r--r-- 1 cobra cobra 34 Aug 11 23:01 USB-128
-rw-r--r-- 1 cobra cobra 34 Aug 12 18:26 WhatsApp
-rw-r--r-- 1 cobra cobra 34 Aug 9 08:24 YouTube
-rw-r--r-- 1 cobra cobra 34 Aug 11 23:51 Zoom
Looking into the logs directory, we can see messages sent between the user we have access to and others. Each of these is an HTML file, so we can open them in a web browser to view them more easily.





Looking at these messages, the most signficant thing is that the APT group is sharing encrypted credentials and saving them in their password managers.
Let’s start looking into bins to see how their encyption scheme works.
We are provided with two binaries: pidgin_rsa_encryption and pm.
cobra@arch:~/codebreaker/task5/bins$ ls -la
total 23628
drwxr-xr-x 2 cobra cobra 4096 Jan 16 21:40 .
drwxr-xr-x 4 cobra cobra 4096 Jan 16 21:41 ..
-rwxr-xr-x 1 cobra cobra 12618792 May 21 2023 pidgin_rsa_encryption
-rwxr-xr-x 1 cobra cobra 11564784 May 21 2023 pm
Each of these binaries is a stripped ELF executable.
cobra@arch:~/codebreaker/task5/bins$ file pm
pm: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0230dd29d2f5ba42b3274ff7981105c752577832, for GNU/Linux 2.6.32, stripped
We could dump these into Binary Ninja or Ghidra to explore further. However, simply running either of the binaries reveals that they are compiled Python code.
root@8420b2b1cef8:/challenge/bins# ./pm
Usage: pm.py <command>
Commands:
init - Create a new master password
add - Add a new password
gen - Generate a new password
read - Retrieve a password
help - Print this help file
root@8420b2b1cef8:/challenge/bins# ./pidgin_rsa_encryption
Usage: python pidgin_rsa_encryption.py <mode> [<recipient> <message> <public_key> | <encrypted_message> <password>]
Modes:
send <recipient> <message> <public_key> - Send an encrypted message
receive <encrypted_message> <password> <private_key> - Decrypt the given encrypted message
This will make our reversing job much easier. Python compiles to bytecode, so we just need to use a tool to extract the pyc files and another tool to decompile these files into easily readible Python scripts.
We can try to use pyinstxtractor to extract these pyc files. This should work if the binary was created with PyInstaller.
root@8420b2b1cef8:/challenge# python3 pyinstxtractor.py bins/pidgin_rsa_encryption
[+] Processing bins/pidgin_rsa_encryption
[+] Pyinstaller version: 2.1+
[+] Python version: 3.11
[+] Length of package: 12566823 bytes
[+] Found 116 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: pidgin_rsa_encryption.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.11 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: bins/pidgin_rsa_encryption
You can now use a python decompiler on the pyc files within the extracted directory
Although the script works succesfully, it does note that its probably better to run it with a matching Python version (Python 3.11) to prevent unmarshalling errors. So, we can install Python 3.11 and run it again for both binaries.
root@8420b2b1cef8:/challenge# python3.11 pyinstxtractor.py bins/pidgin_rsa_encryption
[+] Processing bins/pidgin_rsa_encryption
[+] Pyinstaller version: 2.1+
[+] Python version: 3.11
[+] Length of package: 12566823 bytes
[+] Found 116 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: pidgin_rsa_encryption.pyc
[+] Found 588 files in PYZ archive
[+] Successfully extracted pyinstaller archive: bins/pidgin_rsa_encryption
You can now use a python decompiler on the pyc files within the extracted directory
root@8420b2b1cef8:/challenge# python3.11 pyinstxtractor.py bins/pm
[+] Processing bins/pm
[+] Pyinstaller version: 2.1+
[+] Python version: 3.11
[+] Length of package: 11512814 bytes
[+] Found 68 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pm.pyc
[+] Found 160 files in PYZ archive
[+] Successfully extracted pyinstaller archive: bins/pm
You can now use a python decompiler on the pyc files within the extracted directory
We can see the pyc files we’re after now in pidgin_rsa_encryption_extracted/pidgin_rsa_encryption.pyc and pm_extracted/pm.pyc.
Now we just need to decompile these files to get readable Python code. There’s two popular tools we can look into to accomplish this:
The only problem is that both is that neither of these support Python 3.11 decompilation. Notably however, pycdc does have bytecode disassembly support with their pycdas tool. See this issue.
root@8420b2b1cef8:/challenge# pycdc/pycdas pidgin_rsa_encryption_extracted/pidgin_rsa_encryption.pyc
pidgin_rsa_encryption.pyc (Python 3.11)
[Code]
File Name: pidgin_rsa_encryption.py
Object Name: <module>
Qualified Name: <module>
...
[Disassembly]
0 RESUME 0
2 NOP
4 LOAD_GLOBAL 1: NULL + open
16 LOAD_FAST 0: pub_key
18 LOAD_CONST 1: 'rb'
20 PRECALL 2
24 CALL 2
34 BEFORE_WITH
36 STORE_FAST 1: f
38 LOAD_GLOBAL 3: NULL + RSA
50 LOAD_ATTR 2: import_key
60 LOAD_FAST 1: f
62 LOAD_METHOD 3: read
...
root@57ac61c40769:/challenge# pycdc/pycdas pm_extracted/pm.pyc
pm.pyc (Python 3.11)
[Code]
File Name: pm.py
Object Name: <module>
Qualified Name: <module>
...
[Disassembly]
0 RESUME 0
2 LOAD_GLOBAL 1: NULL + PBKDF2HMAC
14 LOAD_GLOBAL 3: NULL + hashes
26 LOAD_ATTR 2: SHA256
36 PRECALL 0
40 CALL 0
50 LOAD_CONST 1: 32
52 LOAD_GLOBAL 6: SALT
64 LOAD_CONST 2: 100000
66 LOAD_GLOBAL 9: NULL + default_backend
78 PRECALL 0
...
Now we can see all the instructions in semi-human-readable format. This can be supplemented with running pycdc which will produce some of the actual Python code, but with several chunks malformed or missing due to the unsupported opcodes.
Luckily, this challenge introduced me to the latest and greatest Python bytecode decompiler. ChatGPT. By simply prompting GPT with the question “Can you decompile this python bytecode?” followed by some output from pycdas, we can quickly see a decompiled result. The best way to do this is feed one function at a time to GPT and aggregate them all into a single Python script for each binary. You can either do this for all functions or just the functions that are incomplete or malformed from the pycdc output. I found GPT to be more reliable though.
Following that methodology, we can now see the Python scripts for each executable.
# pm.py
import os
import sys
import hashlib
import time
import string
import random
from getpass import getpass
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
SALT = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
def derive_key(password):
kdf = PBKDF2HMAC(algorithm = hashes.SHA256(), length = 32, salt = SALT, iterations = 100000, backend = default_backend())
return kdf.derive(password.encode())
def generate_password(length):
character_list = string.ascii_letters + string.digits + string.punctuation
password = []
for i in range(length):
randomchar = random.choice(character_list)
password.append(randomchar)
print("Your password is " + ''.join(password))
return ''.join(password)
def encrypt_password(spassword, password):
key = derive_key(password)
ts = str(int(time.time() / 60)).encode('utf-8')
iv = hashlib.md5(ts).digest()
cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend = default_backend())
encryptor = cipher.encryptor()
encrypted_password = encryptor.update(spassword.encode()) + encryptor.finalize()
return iv + encrypted_password
def decrypt_password(encrypted_data, password):
key = derive_key(password)
iv = encrypted_data[:16]
encrypted_password = encrypted_data[16:]
cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend = default_backend())
decryptor = cipher.decryptor()
decrypted_password = decryptor.update(encrypted_password) + decryptor.finalize()
return decrypted_password.decode()
def save_password(filename, password, spassword):
encrypted_password = encrypt_password(spassword, password)
with open(filename, 'wb') as file:
file.write(encrypted_password)
print(f"Successfully saved password to {filename}")
def load_password(filename, password):
with open(filename, 'rb') as file:
encrypted_data = file.read()
return decrypt_password(encrypted_data, password)
def usage():
print("Usage: python pm.py <command>")
print("Commands:")
print(" init - Initialize password manager")
print(" add - Add a new password")
print(" read - Read a stored password")
print(" gen - Generate a password")
print(" help - Display this help message")
def main():
if len(sys.argv) != 2:
usage()
sys.exit(1)
command = sys.argv[1]
if command == 'init':
homedir = os.path.expanduser('~')
passdir = homedir + '/.passwords'
if not os.path.isdir(passdir):
os.mkdir(passdir)
password = getpass.getpass(prompt="Enter your master password: ")
passhash = hashlib.md5(password.encode('utf-8')).hexdigest()
dirname = passdir + '/' + passhash
if not os.path.isdir(dirname):
os.mkdir(dirname)
else:
print('directory already exists for that master password')
elif command == 'add':
password = getpass.getpass(prompt="Enter your master password: ")
passhash = hashlib.md5(password.encode('utf-8')).hexdigest()
dirname = os.path.expanduser('~') + '/.passwords/' + passhash
if not os.path.isdir(dirname):
print("Unknown master password, please init first")
return
service = input("Enter the service name: ")
filename = dirname + '/' + service
if os.path.isfile(filename):
print("A password was already stored for that service.")
return
spassword = input(f"Enter the password to store for {service}: ")
save_password(filename, password, spassword)
elif command == 'read':
password = getpass.getpass(prompt="Enter your master password: ")
passhash = hashlib.md5(password.encode('utf-8')).hexdigest()
dirname = os.path.expanduser('~') + '/.passwords/' + passhash
if not os.path.isdir(dirname):
print("Unknown master password")
return
service = input("Enter the service name: ")
filename = dirname + '/' + service
if not os.path.isfile(filename):
print("No password stored for that service using that master password")
return
spassword = load_password(filename, password)
print(f"Password for {service}: {spassword}")
elif command == 'gen':
password = getpass.getpass(prompt="Enter your master password: ")
passhash = hashlib.md5(password.encode('utf-8')).hexdigest()
dirname = os.path.expanduser('~') + '/.passwords/' + passhash
if not os.path.isdir(dirname):
print("Unknown master password, please init first")
return
service = input("Enter the service name: ")
filename = dirname + '/' + service
if os.path.isfile(filename):
print("A password was already stored for that service.")
return
pass_len = input("Enter the password length (default 18): ") or "18"
spassword = generate_password(int(pass_len))
save_password(filename, password, spassword)
elif command == 'help':
usage()
else:
print("Unknown command")
if __name__ == '__main__':
main()
# pidgin_rsa_encryption.py
import sys
import math
import base64
import random
import dbus
from Crypto.PublicKey import RSA
from rsa import core
def load_public_key(pub_key):
try:
with open(pub_key, 'rb') as f:
public_key = RSA.import_key(f.read())
return public_key
except Exception:
print("public key not found")
sys.exit(1)
def load_private_key(password, priv_key):
try:
with open(priv_key, 'rb') as f:
private_key = RSA.import_key(f.read(), password)
return private_key
except ValueError:
print("Incorrect password")
sys.exit(1)
except Exception:
print("private key not found or password incorrect")
sys.exit(1)
def encrypt_chunk(chunk, public_key):
k = math.ceil(public_key.n.bit_length() / 8)
pad_len = k - len(chunk)
random.seed(None)
padding = bytes([random.randrange(1, 255) for _ in range(pad_len - 3)])
padding = b'\x00\x02' + padding + b'\x00'
padded_chunk = padding + chunk.encode()
input_nr = int.from_bytes(padded_chunk, byteorder='big')
crypted_nr = core.encrypt_int(input_nr, public_key.e, public_key.n)
encrypted_chunk = crypted_nr.to_bytes(k, byteorder='big')
return base64.b64encode(encrypted_chunk).decode()
def decrypt_chunk(encrypted_chunk, private_key):
try:
decoded_chunk = base64.b64decode(encrypted_chunk)
except Exception:
print("Invalid message")
sys.exit(1)
input_nr = int.from_bytes(decoded_chunk, byteorder='big')
decrypted_nr = core.decrypt_int(input_nr, private_key.d, private_key.n)
decrypted_chunk = decrypted_nr.to_bytes(256, byteorder='big')
unpadded_chunk = decrypted_chunk[2:]
end_of_pad = unpadded_chunk.find(b'\x00')
unpadded_chunk = unpadded_chunk[end_of_pad + 1:]
return unpadded_chunk.decode()
def encrypt_message(message, public_key):
chunk_size = 245
encrypted_chunks = []
for i in range(0, len(message), chunk_size):
chunk = message[i:i + chunk_size]
encrypted_chunk = encrypt_chunk(chunk, public_key)
encrypted_chunks.append(encrypted_chunk)
return ' '.join(encrypted_chunks)
def decrypt_message(encrypted_message, private_key):
encrypted_chunks = encrypted_message.split(' ')
decrypted_message = ''.join(decrypt_chunk(chunk, private_key) for chunk in encrypted_chunks)
return decrypted_message
def send_message_to_pidgin(message, recipient):
try:
bus = dbus.SessionBus()
purple = bus.get_object('im.pidgin.purple.PurpleService', '/im/pidgin/purple/PurpleObject')
except Exception:
print("Could not send message to pidgin - not connected")
sys.exit(1)
iface = dbus.Interface(purple, 'im.pidgin.purple.PurpleInterface')
accounts = iface.PurpleAccountsGetAllActive()
if not accounts:
print("No active Pidgin accounts found.")
return
account = accounts[0]
conv = iface.PurpleConversationNew(1, account, recipient)
im = iface.PurpleConvIm(conv)
iface.PurpleConvImSend(im, message)
def main():
if len(sys.argv) < 2:
print('Usage: python pidgin_rsa_encryption.py <mode> [<recipient> <message> <public_key> | <encrypted_message> <password>]')
print('Modes:')
print(' send <recipient> <message> <public_key> - Send an encrypted message')
print(' receive <encrypted_message> <password> <private_key> - Decrypt the given encrypted message')
sys.exit(1)
mode = sys.argv[1]
if mode == 'send':
if len(sys.argv) != 5:
print('Usage: python pidgin_rsa_encryption.py send <recipient> <message> <public_key>')
sys.exit(1)
recipient = sys.argv[2]
message = sys.argv[3]
pub_key = sys.argv[4]
public_key = load_public_key(pub_key)
encrypted_message = encrypt_message(message, public_key)
send_message_to_pidgin(encrypted_message, recipient)
print('Encrypted message sent to Pidgin.')
elif mode == 'receive':
if len(sys.argv) != 5:
print('Usage: python pidgin_rsa_encryption.py receive <encrypted_message> <password> <private_key>')
sys.exit(1)
encrypted_message = sys.argv[2]
password = sys.argv[3]
priv_key = sys.argv[4]
private_key = load_private_key(password, priv_key)
decrypted_message = decrypt_message(encrypted_message, private_key)
print('Decrypted message:', decrypted_message)
else:
print("Invalid mode. Use 'send' or 'receive'.")
if __name__ == '__main__':
main()
pm.py is the password manager they’re using and pidgin_rsa_encryption.py is used for encrypting messages sent between themselves.
Now, we just need to find some flaws in their cryptrography.
Looking back at the message logs we have access to, 570RM sent the same AWS password to three people. Each of these passwords is encrypted with RSA.
Additionally, if we look at 570RM’s public key, we can see that e = 3.
root@57ac61c40769:/challenge/files/.keys# openssl rsa -in 570RM_public_key.pem -pubin -text -noout
Public-Key: (2048 bit)
Modulus:
00:be:de:4f:ff:db:74:c9:70:cb:dc:fd:e5:99:74:
50:59:65:63:33:a0:f9:c8:55:dc:16:9c:c5:a9:bb:
41:0c:7e:48:a9:e4:89:e8:4a:ee:80:28:b9:42:82:
a5:30:2b:0e:4b:5d:ed:c9:b3:0c:20:cc:1d:33:5b:
70:b7:f5:5a:91:23:14:d4:49:ef:b4:18:71:84:86:
8b:98:2a:2b:cf:ae:d2:c6:66:56:e0:34:20:d0:f9:
e0:ed:da:a7:df:40:bb:14:6c:f6:32:fe:11:f7:85:
4c:23:fd:c1:0e:fc:ea:85:93:d5:f2:6c:31:53:9b:
9a:86:df:c7:76:8e:66:50:76:94:ed:8d:96:07:f5:
19:3b:f5:b2:dd:8d:ff:c5:54:bb:09:b8:d5:2c:1a:
42:05:46:46:75:41:2f:67:08:59:63:35:4f:5c:86:
f9:91:34:9b:2a:cc:c4:31:00:37:6d:9e:39:f4:42:
21:af:18:44:92:71:cc:f2:60:9a:2b:83:fc:18:05:
69:8a:16:31:f9:48:66:7f:23:5a:02:87:e6:fa:71:
ab:00:3b:a3:a8:91:6d:ad:c7:4c:b4:cd:a4:26:63:
db:ae:e0:1e:71:b1:b3:ee:ab:20:59:28:63:5d:27:
c4:62:c7:e0:8b:a4:e6:69:83:d3:42:af:55:6d:49:
80:99
Exponent: 3 (0x3)
Typically when using RSA, it is best to use an exponent of 65537. Small exponents tend to make RSA vulnerable to various attacks. In this case, we can use Hastad’s Broadcast Attack.
As outlined in the linked article, we have a system of three equations which is solveable with the Chinese Remainder Theorem. Specifically, we have three copies of an identical plaintext message, encrypted with three separate public keys. If the exponent were a larger value like 65537, we would need 65537 public keys.
Once we find a solution using the theorem, we can take its cube root to calculate the original message.
We can combine this all into a single Python script to recover the AWS password.
import sys
import base64
from sympy import mod_inverse, integer_nthroot
from Crypto.PublicKey import RSA
def load_public_key_n(pub_key):
try:
with open(pub_key, 'rb') as f:
public_key = RSA.import_key(f.read())
return public_key.n
except Exception:
print('Public key not found')
sys.exit(1)
def recover_message(ciphertexts, moduli):
cipher_ints = [int.from_bytes(base64.b64decode(ct), byteorder='big') for ct in ciphertexts]
N = moduli[0] * moduli[1] * moduli[2]
result = 0
for i in range(3):
Ni = N // moduli[i]
mi = mod_inverse(Ni, moduli[i])
result += cipher_ints[i] * mi * Ni
result %= N
message_int, exact = integer_nthroot(result, 3)
if not exact:
raise ValueError('Failed to recover exact cube root')
message = message_int.to_bytes((message_int.bit_length() + 7) // 8, byteorder='big')
end_of_pad = message.find(b'\x00', 2)
return message[end_of_pad + 1:].decode()
c1 = 'nchhy9QgQR0o6jbwWDFy6IQVYB6rUGF3SnDTlOpCbOx6uFUPK2jdzaTAKgOzzhakNJmomeRVlz75zeJHHaP2+L7LXnOZOSWEVm/XlqO/z2TrBQavdjCfILaJyZOIDUjGVV5lfbptV6RygGbB/mELrmxRvx9hWO4zqGvU7XySVhNOMXryhvNsAVontgeSCimihJOv+ojxZNmP/KVYbat5OogPte4aa7Vu6X5cafwNtoxxV9Nz5ZMyJl9vrdn0c6jv9253GOUpj95MvOSd+cCyUcrsrY3BXYdsI/lPZhwuRmXUUE1L/tMG2bbGT9t6gdaEXeEn9m5zT1vhIsxq5ng35Q=='
n1 = load_public_key_n('files/.keys/4C1D_public_key.pem')
c2 = 'kciYY2nILyBBRETQ1m24tbfVS92NbraBI/4zLHG1mMIIeUxhPCPN1ThgX2Y2+y1Cqpz98+OFSc6gKlBW4ByscDWyax7PjLq/pG5OJJKOi8+mdd4DQRwtLRt24WBtvYRo+NSUVHuDl/XyvVZQzwT+M9GhxP2TdGxweOxq9MX5kf+ZqWJ52LROBv3JonvRhq1DP42OkjpIOLbdf+0QEpTUEHcikXr59UJ+sOYnd7uAkj6t7XF+4QnC3b2Dd32Ws6FFmo7jgW+WbnMikHj0IBd4QpYMgKhaUftjQJFROqeRnNXGW889xEuaa7xunw6ElV9SUsWCvFEvQu85iQb+i9cHwg=='
n2 = load_public_key_n('files/.keys/PL46U3_public_key.pem')
c3 = 'RHO3OUwTPonT128LNkl/EkoegYecl+0lFu89h9CfxYF2kJ8K+Fqs9cR++CIlLSkowHm+iI4aWv/dfKb50WNpYU8GKgk0dJS5fkDGDLLUmZWfhTnsl0WF1lg2x2qWGSJgbThJJ08Pz3YpSVbVSAc2RSx32R+unpY2YZjDOfmOlzcv+GQHlpzSjRj0G9b4+7maLgUQ7f1gyDQ/SPEeATZB82QoTUQ3DgdABW0eGyDrSyN6UCkMl6NO8f0OHnWlv+7Wej436csoMKrEdttbGw/85ZFiIWdHnIGvL8o/tjBGReLnvAlsnkvXS1WvJLDsVpVhWRAMgJFwfhr8cP8OTct2CQ=='
n3 = load_public_key_n('files/.keys/V3RM1N_public_key.pem')
ciphertexts = [c1, c2, c3]
moduli = [n1, n2, n3]
message = recover_message(ciphertexts, moduli)
print('Recovered message:', message)
Running the script succesfully decrypts the message and displays the password.
root@57ac61c40769:/challenge# python3 decrypt_aws_password.py
Recovered message: Hey! I needed to update the AWS password since it expired. The new password is ))za!]-#,#rh0||c-Z. Please add it to your password managers. Thanks!
So, now we have the AWS password, but we still need to find another flaw somewhere in order to get the USB password.
Notably, the AWS password is also stored in 570RM’s password manager under the AmazonWebServices file.
Looking at pm.py, we can see that all passwords are encrypted with AES using Cipher Feedback Mode (CFB).
def encrypt_password(spassword, password):
key = derive_key(password)
ts = str(int(time.time() / 60)).encode('utf-8')
iv = hashlib.md5(ts).digest()
cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend = default_backend())
encryptor = cipher.encryptor()
encrypted_password = encryptor.update(spassword.encode()) + encryptor.finalize()
return iv + encrypted_password
This diagram I found online was helpful in getting a better understanding of how it works.

Effectively, the plaintext is encrypted using an IV for the first block then the ciphertext is used as the IV for the following block and so on.
However, the interesting thing here is that if two encrypted messages use the same IV where the plaintext of one message is known, the first block of the unknown message is vulnerable to a known plaintext attack. All we need to do is take the known message’s first block ciphertext and XOR it with the first block plaintext to receive the first block keystream. Since the other unknown message has the same key and IV, we can XOR this keystream with its first block to get that block’s plaintext.
As visible in encrypt_password, the IV for the encrypted passwords is always the current timestamp in minutes hashed with MD5.
ts = str(int(time.time() / 60)).encode('utf-8')
iv = hashlib.md5(ts).digest()
Looking back at the passwords directory again, the AWS and USB passwords were saved within the same minute, so they most have the same IV, meaning we should be able to perform the attack.
root@57ac61c40769:/challenge/files/.passwords/6bed776cb619ea72d942e0b49ea1de0d# ls -la
...
-rw-r--r-- 1 ubuntu ubuntu 34 Aug 11 23:01 AmazonWebServices
...
-rw-r--r-- 1 ubuntu ubuntu 34 Aug 11 23:01 USB-128
...
The IV is also directly appended in front of each encrypted password, so they can be compared to verify.
Each password is the same length of 34 bytes. Subtracting the size of the IV (16 byte MD5 hash), we know that each password is 18 bytes long which is also the default password length for password generation in pm.py.
pass_len = input("Enter the password length (default 18): ") or "18"
spassword = generate_password(int(pass_len))
Additionally, the block size in AES is 16 bytes which means we can decrypt 16 of the 18 bytes in the USB password. Bruteforcing the final two bytes shouldn’t take very long.
We can combine this methodology into a single script to recover the first plaintext block of the password.
def load_ciphertext(filename):
with open(filename, 'rb') as file:
encrypted_data = file.read()
return encrypted_data
aws_password_encrypted = load_ciphertext('files/.passwords/6bed776cb619ea72d942e0b49ea1de0d/AmazonWebServices')
usb_password_encrypted = load_ciphertext('files/.passwords/6bed776cb619ea72d942e0b49ea1de0d/USB-128')
aws_password = b'))za!]-#,#rh0||c-Z'
aws_enc_block1 = aws_password_encrypted[16:32]
aws_block1 = aws_password[:16]
usb_enc_block1 = usb_password_encrypted[16:32]
keystream_block1 = bytearray(16)
for i in range(16):
keystream_block1[i] = aws_enc_block1[i] ^ aws_block1[i]
usb_block1 = bytearray(16)
for i in range(16):
usb_block1[i] = keystream_block1[i] ^ usb_enc_block1[i]
usb_block1 = usb_block1.decode('utf-8')
print(f'USB Password: {usb_block1}??')
Running it works successfully.
root@57ac61c40769:/challenge# python3 plaintext_attack.py
USB Password: TON*7d%]r_#"ONGa??
Looking at disk.dd now, it doesn’t appear to be encrypted.
cobra@arch:~/codebreaker/task5$ file disk.dd
disk.dd: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkfs.fat", sectors/cluster 64, reserved sectors 64, Media descriptor 0xf8, sectors/track 63, heads 255, sectors 268435440 (volumes > 32 MB), FAT (32 bit), sectors/FAT 32768, serial number 0x6d316b65, label: "USB-128 "
We can mount it after creating a loopback device.
cobra@arch:~/codebreaker/task5$ sudo losetup -fP disk.dd
cobra@arch:~/codebreaker/task5$ sudo mount /dev/loop0 /mnt
Now we can see that the encrypted data is managed by two scripts, unlock and lock.
cobra@arch:~/codebreaker/task5$ ls -la /mnt
total 196
drwxr-xr-x 5 root root 32768 Dec 31 1969 .
drwxr-xr-x 17 root root 4096 Dec 19 18:50 ..
drwxr-xr-x 2 root root 32768 Jul 31 20:02 .bin
drwxr-xr-x 2 root root 32768 Jul 31 20:02 .data
drwxr-xr-x 2 root root 32768 Jul 31 20:02 data
-rwxr-xr-x 1 root root 76 Jul 31 20:02 lock
-rwxr-xr-x 1 root root 91 Jul 31 20:02 unlock
All data is encrypted with gocryptfs.
╭─cobra@arch /mnt/.bin
╰─$ cat /mnt/.data/gocryptfs.conf
{
"Creator": "gocryptfs v2.4.0",
"EncryptedKey": "jWWTF0dLjRMuAWbW8TtrSsMef4UsJ/4zuea7iJ/O5kd/WPfFy2J3Kd9MaASSmUFD/ZIZj7iMh5TnfQMNQQWZsg==",
"ScryptObject": {
"Salt": "7L03vNYs7dDbLTWcEMaM4leqDKY9g4ADIOpQ2/nOl7k=",
"N": 65536,
"R": 8,
"P": 1,
"KeyLen": 32
},
"Version": 2,
"FeatureFlags": [
"HKDF",
"GCMIV128",
"DirIV",
"EMENames",
"LongNames",
"Raw64",
"AESSIV"
]
}
The unlock and lock binaries just run gocryptfs with unlocking configured to only leave the directory unlocked for 60 seconds before locking it again.
root@6c32dc10a99d:/mnt# cat unlock
#!/bin/bash
cd "$(dirname "$0")"
sleep 1
exec ./.bin/gocryptfs "$@" -i 60s ./.data ./data
root@6c32dc10a99d:/mnt# cat lock
#!/bin/bash
cd "$(dirname "$0")"
sleep 1
sync
fusermount -u ./data
sleep 1
Instead of using these scripts which restrict the amount of time we will have access to encrypted data, we can just run gocryptfs directly in our bruteforce script to iterate until we find the last two characters of the password.
In order to improve the speed of the script, utilizing multithreading is also helpful.
import string
import pexpect
from concurrent.futures import ThreadPoolExecutor, as_completed
chars = string.ascii_letters + string.digits + string.punctuation
def attempt_password(password):
child = pexpect.spawn('./.bin/gocryptfs', ['./.data', './data'])
child.expect('Password:')
child.sendline(password)
child.expect(pexpect.EOF)
output = child.before.decode('utf-8')
return password,output
with ThreadPoolExecutor(max_workers=20) as executor:
future_to_password = { executor.submit(attempt_password, f'TON*7d%]r_#"ONGa{char1}{char2}'): (char1, char2)
for char1 in chars for char2 in chars }
for future in as_completed(future_to_password):
password, output = future.result()
if 'incorrect' in output:
print(password, end='\r')
else:
print(f'\nFound password: {password}')
break
After a short period of time, the script succesfully finds the correct password.
root@6c32dc10a99d:/mnt# python3 /challenge/brute.py
Found password: TON*7d%]r_#"ONGaNv
Submitting it solves the task.
Result
It worked! OMG that was some bad crypto.