Forensics / Decrypt Me
00. Metainfo
CTF: | UofTCTF |
CTFtime | CTFtime |
Task: | Decrypt Me |
Category: | PWN |
01. Description
Introduction
This task comes from UofTCTF, under the forensics category. A small RAR archive file is available for download. The task description is as follows:
“I encrypted my encryption script, but I forgot the password. Can you help me decrypt it?”
02. RAR
Find the password for the RAR file:
rar2john flag.rar
flag.rar:$rar5$16$1d7cb8859a6c3c8e30a9db7a501811ac$15$280234db9d29c6ab216b74e6a89ec226$8$d12d4ba211b9c642
./hashcat.exe -O -a0 -m13000 .\hashe\uoftctf.txt .\dict\rockyou.txt
$rar5$16$1d7cb8859a6c3c8e30a9db7a501811ac$15$280234db9d29c6ab216b74e6a89ec226$8$d12d4ba211b9c642:toronto416
Password found: toronto416
After unpacking, we find only one file: flag.py. However, we need the file flag.enc. Below flag.py:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Hash import SHA256
from time import time
import random
random.seed(int(time()))
KEY = SHA256.new(str(random.getrandbits(256)).encode()).digest()
FLAG = "uoftctf{fake_flag}"
def encrypt_flag(flag, key):
cipher = AES.new(key, AES.MODE_EAX)
ciphertext, tag = cipher.encrypt_and_digest(flag.encode())
return cipher.nonce + ciphertext
def main():
encrypted_flag = encrypt_flag(FLAG, KEY)
with open("flag.enc", "wb") as f:
f.write(encrypted_flag)
if __name__ == "__main__":
main()
The file flag.enc must be somewhere; we can see it in the binary data of the RAR file.
03. NTFS stream
So, we check the NTFS data streams, but there’s nothing interesting there:
Get-Item -Path flag.rar -Stream *
PSPath : Microsoft.PowerShell.Core\FileSystem::D:\flag.rar::$DATA
PSParentPath : Microsoft.PowerShell.Core\FileSystem::D:\
PSChildName : flag.rar::$DATA
PSDrive : D
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
FileName : D:\flag.rar
Stream : :$DATA
Length : 672
PSPath : Microsoft.PowerShell.Core\FileSystem::D:\flag.rar:Zone.Identifier
PSParentPath : Microsoft.PowerShell.Core\FileSystem::D:\
PSChildName : flag.rar:Zone.Identifier
PSDrive : D
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
FileName : D:\flag.rar
Stream : Zone.Identifier
Length : 588
However, there is something here:
Get-Item -Path flag.py -Stream *
PS D:\moje_programy\CTF\ctftime\2025-uofctf\forens-decrypt-me\unpack> Get-Item -Path flag.py -Stream *
...
PSPath : Microsoft.PowerShell.Core\FileSystem::D:\flag.py:flag.enc
PSParentPath : Microsoft.PowerShell.Core\FileSystem::D:\
PSChildName : flag.py:flag.enc
PSDrive : D
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
FileName : D:\flag.py
Stream : flag.enc
Length : 57
...
Get-Content ".\flag.py:flag.enc" -Encoding Byte -Raw > extracted_flag.enc.badencode
However, this produced an incorrect output (UTF-16 encoded). The resulting file was 120 bytes instead of the expected 57 bytes. To generate the correct output, use this command:
Get-Content ".\flag.py:flag.enc" -Encoding Byte -Raw > flag.enc.decimal
As a result, we have the numbers in decimal format. However, we can easily convert them into raw bytes. Here’s a quick conversion using my code:
with open("flag.enc.decimal", 'r', encoding='utf-16') as file:
numbers_list = [int(line.strip()) for line in file if line.strip().isdigit()]
with open("flag.enc", 'wb') as file:
file.write(bytes(numbers_list))
04. decode AES
To decode the flag, we need to address a critical part of the code that uses random.seed(int(time())). This means the seed is generated based on the system time when the file was created or modified. To proceed, we must:
- Retrieve the file’s modification date.
- This will give us a time range to brute-force the seed.
- Decode the flag using the extracted seed and the encryption logic.
Python source code
#!/usr/bin/env python3
import os
import random
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
import datetime
ENC_FILE = "flag.enc"
ENCODER_FILE = "flag.py" # zakładamy, że tu jest nasz skrypt szyfrujący
def main():
# 1. Odczytaj datę modyfikacji pliku flag.py (w sekundach od epoki 1970)
mtime = os.path.getmtime(ENCODER_FILE)
candidate_time = int(mtime) # bierzemy wartość całkowitą
print(f"[+] Data modyfikacji {ENCODER_FILE} = {datetime.datetime.utcfromtimestamp(candidate_time)} UTC")
print(f"[+] Używamy tej sekundy jako seed PRNG = {candidate_time}")
# 2. Odczytujemy zawartość zaszyfrowanego pliku
with open(ENC_FILE, "rb") as f:
enc_data = f.read()
# 3. Rozdzielamy: nonce (pierwsze 16 bajtów) i ciphertext (reszta)
nonce = enc_data[:16]
ciphertext = enc_data[16:]
# 4. Generujemy klucz tak samo jak w encryptorze
random.seed(candidate_time)
rbits = random.getrandbits(256)
key = SHA256.new(str(rbits).encode()).digest()
# 5. Deszyfrujemy (AES.MODE_EAX, bez weryfikacji taga – bo go nie zapisujemy)
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
plaintext = cipher.decrypt(ciphertext)
# 6. Sprawdzamy, czy wygląda na poprawną flagę:
if b"ctf{" in plaintext or b"flag" in plaintext or b"uoftctf{" in plaintext:
print("[+] Udało się odszyfrować prawdopodobną flagę:")
try:
print(" ", plaintext.decode("utf-8"))
except UnicodeDecodeError:
print(" (Binarna treść) ", plaintext)
else:
print("[-] Odszyfrowane dane nie wyglądają na flagę.")
print(" Być może data modyfikacji pliku .py nie pokrywa się z momentem uruchomienia encryptora.")
if __name__ == "__main__":
main()
05. Flag and Conlusion
Even though the file was small, the task wasn’t that easy—but it was awesome! And this is a flag.
uoftctf{ads_and_aes_are_one_letter_apart}
Zostaw komentarz