Rev 413 pts 6 solves

Hearts2Hearts

3G0 Write-up by3G0
Given h2h.pyc and flag.jpg.enc. Recover the AES key/IV from the compiled Python and decrypt the image.

Hearts2Hearts challenge card

Overview

This challenge provides two files: h2h.pyc and flag.jpg.enc. To decrypt flag.jpg.enc we first have to understand h2h.pyc. We decompiled it with the online tool pylingual.io, which recovered the following source:

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

def lcg(seed, a, c, m, size):
    key = []
    for _ in range(size):
        seed = (a * seed + c) % m
        key.append(seed % 256)
    return bytes(key)

def generate_iv(seed, rounds=4):
    def round_function(data, key):
        return (data << 1 ^ key) % 256
    left = seed[:8]
    right = seed[8:]
    for _ in range(rounds):
        new_left = right
        new_right = bytes([left[i] ^ round_function(right[i], i) for i in range(8)])
        left, right = (new_left, new_right)
    return left + right

def generate_key_and_iv():
    seed = b'nyomanayucarmenita'
    lcg_seed = sum(seed)
    key = lcg(lcg_seed, a=1664525, c=1013904223, m=4294967296, size=16)
    iv = generate_iv(seed)
    print(key, iv)
    return (key, iv)

def encrypt_flag():
    key, iv = generate_key_and_iv()
    cipher = AES.new(key, AES.MODE_CBC, iv)
    try:
        with open('flag.jpg', 'rb') as f:
            flag = f.read()
        flag = pad(flag, 16)
        encrypted_flag = cipher.encrypt(flag)
        with open('flag.jpg.enc', 'wb') as f:
            f.write(encrypted_flag)
        print('Flag encrypted successfully')
    except FileNotFoundError:
        print('flag.jpg not found!')

def main():
    encrypt_flag()

if __name__ == '__main__':
    main()

Analysis

Everything needed to reproduce the key and IV is hard-coded:

  • The AES key is generated by a Linear Congruential Generator (LCG) seeded with sum(b'nyomanayucarmenita'), using the well-known Numerical Recipes constants a = 1664525, c = 1013904223, m = 2³², producing 16 bytes.
  • The IV is derived from a small 4-round Feistel-like function over the constant seed b'nyomanayucarmenita'.

Since both the key and IV are fully deterministic from constants baked into the binary, we can simply re-run the same generation functions and then decrypt in AES-CBC mode.

Solution

We reuse the original generate_key_and_iv() (call it recover_key_and_iv()) and add a decrypt_flag():

def decrypt_flag():
    """Decrypt the encrypted flag file."""
    key, iv = recover_key_and_iv()
    try:
        with open('flag.jpg.enc', 'rb') as f:
            encrypted_flag = f.read()

        cipher = AES.new(key, AES.MODE_CBC, iv)
        decrypted_flag = unpad(cipher.decrypt(encrypted_flag), 16)

        with open('recovered_flag.jpg', 'wb') as f:
            f.write(decrypted_flag)

        print('Flag decrypted successfully and saved as recovered_flag.jpg')
    except FileNotFoundError:
        print('flag.jpg.enc not found!')
    except ValueError as e:
        ...

Running the decryptor produces recovered_flag.jpg, which contains the flag:

Recovered flag image

Flag

CTFITB{4D333kKk_c4RM3n_k0K_lUCu_b4n93777}