2026-07-12 20:22:15 +01:00

88 lines
3.0 KiB
Python

#!/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=<next_value> 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.")