seeds
Category
Cryptography
Points
259
Tags
So we got a file called main.py
main.py
:
import time, sys, select
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
class RandomGenerator:
def __init__(self, seed = None, modulus = 2 ** 32, multiplier = 157, increment = 1):
if seed is None:
seed = time.asctime()
if type(seed) is int:
self.seed = seed
if type(seed) is str:
self.seed = int.from_bytes(seed.encode(), "big")
if type(seed) is bytes:
self.seed = int.from_bytes(seed, "big")
self.m = modulus
self.a = multiplier
self.c = increment
def randint(self, bits: int):
self.seed = (self.a * self.seed + self.c) % self.m
result = self.seed.to_bytes(4, "big")
while len(result) < bits // 8:
self.seed = (self.a * self.seed + self.c) % self.m
result += self.seed.to_bytes(4, "big")
return int.from_bytes(result, "big") % (2 ** bits)
def randbytes(self, len: int):
return self.randint(len * 8).to_bytes(len, "big")
def input_with_timeout(prompt, timeout=10):
sys.stdout.write(prompt)
sys.stdout.flush()
ready, _, _ = select.select([sys.stdin], [], [], timeout)
if ready:
return sys.stdin.buffer.readline().rstrip(b'\n')
raise Exception
def main():
print("Welcome to the AES Oracle")
randgen = RandomGenerator()
cipher = AES.new(randgen.randbytes(32), AES.MODE_ECB)
flag = open("flag.txt", "rb").read()
ciphertext = cipher.encrypt(pad(flag, AES.block_size))
print(f'{ciphertext = }')
while True:
plaintext = input_with_timeout("What would you like to encrypt? (enter 'quit' to exit) ")
if plaintext == b"quit": break
cipher = AES.new(randgen.randbytes(32), AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
print(f"{ciphertext = }")
if __name__ == "__main__":
main()
main.py
has vuln, first is weak PRNG, the script uses a custom RandomGenerator
class, which is a simple Linear Congruential Generator (LCG), also predicatble seed, and for the key gen process, first it generates 32-byte key to encrypt the flag, second it, generates a new 32-byte key to encrypt and data the user provides, since the entire key sequence is determined by the initial seed time, finding the seed allows the attacker to regenerate both keys.
To solve it, we can brute force the seed time, as same as RandomGenerator
using the guessed time as the seed
solver.py
:
#!/usr/bin/env python3
import time
import sys
import select
import datetime
import pytz
import re
import ast
from pwn import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
class RandomGenerator:
def __init__(self, seed = None, modulus = 2 ** 32, multiplier = 157, increment = 1):
if seed is None:
seed = time.asctime()
if type(seed) is int:
self.seed = seed
if type(seed) is str:
self.seed = int.from_bytes(seed.encode(), "big")
if type(seed) is bytes:
self.seed = int.from_bytes(seed, "big")
self.m = modulus
self.a = multiplier
self.c = increment
def randint(self, bits: int):
self.seed = (self.a * self.seed + self.c) % self.m
result = self.seed.to_bytes(4, "big")
while len(result) < bits // 8:
self.seed = (self.a * self.seed + self.c) % self.m
result += self.seed.to_bytes(4, "big")
return int.from_bytes(result, "big") % (2 ** bits)
def randbytes(self, length: int):
return self.randint(length * 8).to_bytes(length, "big")
def format_asctime_like(dt_obj):
day_str = str(dt_obj.day)
return dt_obj.strftime(f'%a %b {day_str:>2} %H:%M:%S %Y')
def parse_ciphertext(line: bytes) -> bytes:
repr_string = line.strip().decode()
return ast.literal_eval(repr_string)
def solve():
HOST = "tjc.tf"
PORT = 31493
io = remote(HOST, PORT)
server_tz = pytz.timezone('America/New_York')
connect_time = datetime.datetime.now(server_tz)
log.info(f"Waktu koneksi (ET): {connect_time.strftime('%Y-%m-%d %H:%M:%S')}")
io.recvuntil(b"ciphertext = ")
flag_ciphertext_line = io.recvline()
flag_ciphertext = parse_ciphertext(flag_ciphertext_line)
log.info(f"Ciphertext Bendera (hex): {flag_ciphertext.hex()}")
probe_plaintext = b"a" * 16
io.sendlineafter(b"What would you like to encrypt? (enter 'quit' to exit) ", probe_plaintext)
io.recvuntil(b"ciphertext = ")
probe_ciphertext_line = io.recvline()
probe_ciphertext = parse_ciphertext(probe_ciphertext_line)
io.close()
for i in range(-10, 5):
guess_time = connect_time + datetime.timedelta(seconds=i)
seed_str = format_asctime_like(guess_time)
randgen_guess = RandomGenerator(seed=seed_str)
_ = randgen_guess.randbytes(32)
probe_key_guess = randgen_guess.randbytes(32)
cipher_guess = AES.new(probe_key_guess, AES.MODE_ECB)
encrypted_probe_guess = cipher_guess.encrypt(pad(probe_plaintext, AES.block_size))
if encrypted_probe_guess == probe_ciphertext:
log.success(f"Seed waktu yang benar ditemukan: '{seed_str}'")
randgen_correct = RandomGenerator(seed=seed_str)
flag_key = randgen_correct.randbytes(32)
log.info(f"Kunci bendera yang direkonstruksi: {flag_key.hex()}")
decipher = AES.new(flag_key, AES.MODE_ECB)
decrypted_flag_padded = decipher.decrypt(flag_ciphertext)
flag = unpad(decrypted_flag_padded, AES.block_size)
log.success(f"FLAG: {flag.decode()}")
return
log.failure("Gagal menemukan seed yang benar. Coba jalankan lagi atau perlebar rentang waktu.")
if __name__ == "__main__":
solve()
seeds Flag:
tjctf{the_greatest_victory_is_that_which_require_no_battle}