88 lines
3.0 KiB
Python
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.")
|