#!/usr/bin/env python3 """ LCG Treasure - Flask app (Crypto CTF) Generates an LCG with modulus m = 2**32 and picks parameters a,c and initial state x0. Exposes a challenge page with several consecutive outputs (as unsigned 32-bit integers). Players must recover a and c (mod 2^32) and predict the next output. Flag is returned when /flag?token= matches the real next value. This is intentionally weak for CTF learning purposes. """ import os import random from flask import Flask, render_template, request, abort, jsonify import struct app = Flask(__name__) # modulus M = 2**32 # configuration NUM_TOTAL = 6 # total LCG outputs generated (we keep extras) NUM_OBSERVED = 3 # number of consecutive outputs shown to the player FLAG_FILE = "flag.txt" # helper: generate LCG ensuring invertible difference for solver def generate_lcg_instance(): while True: # pick odd multiplier (common in LCGs), 32-bit unsigned a = random.randrange(1, M) if a % 2 == 0: a |= 1 # make odd c = random.randrange(0, M) x0 = random.randrange(0, M) seq = [x0] for i in range(1, NUM_TOTAL): seq.append((a * seq[-1] + c) % M) # We need the difference (x1 - x0) to be invertible mod 2^32. # Inverse exists iff (x1 - x0) is odd (because modulus is power of two). diff = (seq[1] - seq[0]) % M if diff & 1 == 1: # good: solver will be able to compute modular inverse modulo 2^32 return a, c, seq # initialize once A, C, SEQ = generate_lcg_instance() OBSERVED = SEQ[:NUM_OBSERVED] TARGET = SEQ[NUM_OBSERVED] # token to predict @app.route("/") def challenge(): # Present observed tokens as decimal and hex for convenience obs_pairs = [{"dec": x, "hex": f"0x{x:08x}"} for x in OBSERVED] return render_template("challenge.html", observed=obs_pairs, note=f"Predict the next 32-bit output (unsigned) after the {NUM_OBSERVED} shown values.") @app.route("/api/observed") def api_observed(): obs = [{"dec": x, "hex": f"0x{x:08x}"} for x in OBSERVED] return jsonify({ "observed": [x for x in OBSERVED], "observed_hex": [f"0x{x:08x}" for x in OBSERVED], "note": f"Predict the next 32-bit output (unsigned) after the {NUM_OBSERVED} shown values." }) @app.route("/flag") def flag(): token = request.args.get("token", "").strip() if token == "": abort(400, "Missing token parameter. Provide unsigned decimal token or 0xhex.") # accept decimal or hex prefixed by 0x try: if token.startswith("0x") or token.startswith("0X"): val = int(token, 16) & 0xffffffff else: val = int(token) & 0xffffffff except: abort(400, "Invalid token format.") if val == TARGET: if os.path.exists(FLAG_FILE): with open(FLAG_FILE, "r") as f: return f.read().strip() + "\n" else: return "FLAG_MISSING\n" else: abort(403, "Incorrect token.")