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.

binary

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