216 lines
7.3 KiB
Python
216 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
import socketserver
|
|
import random
|
|
import io
|
|
|
|
FLAG_PRICE = 1_000_000
|
|
INITIAL_COINS = 50 # tiny starting balance
|
|
MAX_TURNS = 10
|
|
MAX_LEGAL_LOAN = 1000 # max positive amount per loan
|
|
|
|
|
|
def load_flag():
|
|
# Flag is supplied via environment variable
|
|
return os.getenv("FLAG", "XMAS{dummy_flag_for_testing}")
|
|
|
|
|
|
def to_int(s: str) -> int:
|
|
return int(s.strip())
|
|
|
|
|
|
def add_coins(balance: int, delta: int) -> int:
|
|
"""
|
|
Update the balance using 32-bit unsigned wraparound.
|
|
|
|
This is the core bug: using a 32-bit "safe" ledger but letting the
|
|
user control delta directly (including large negatives).
|
|
"""
|
|
return (balance + delta) & 0xFFFFFFFF
|
|
|
|
|
|
def play_game(reader, writer):
|
|
"""
|
|
Core game logic. Works with any text reader/writer pair.
|
|
- CLI mode: stdin/stdout
|
|
- TCP mode: wrapped socket rfile/wfile
|
|
"""
|
|
flag = load_flag()
|
|
coins = INITIAL_COINS
|
|
price = FLAG_PRICE
|
|
turns = 0
|
|
|
|
def send(line: str = ""):
|
|
writer.write(line + "\n")
|
|
writer.flush()
|
|
|
|
send("=== Grinch's Lottery ===")
|
|
send("Welcome to Whoville's shadiest Christmas lottery!")
|
|
send("You start with a few coal-coins. Win enough to buy the special prize.")
|
|
send(f"The shiny present costs {price} coins.")
|
|
send("")
|
|
while turns < MAX_TURNS:
|
|
turns += 1
|
|
send(f"\n[Turn {turns}/{MAX_TURNS}] Your balance: {coins} coins")
|
|
send("Choose an action:")
|
|
send(" 1) Work a little (earn some coins)")
|
|
send(" 2) Buy lottery tickets (10 coins each)")
|
|
send(" 3) Take a shady loan from the Grinch")
|
|
send(" 4) Buy the shiny present")
|
|
send(" 5) Exit")
|
|
send("> ")
|
|
choice = reader.readline()
|
|
if not choice:
|
|
break
|
|
choice = choice.strip()
|
|
|
|
if choice == "1":
|
|
# Small honest income, no way to reach the flag fairly
|
|
earn = random.randint(1, 20)
|
|
coins = add_coins(coins, earn)
|
|
send(f"You did some odd jobs for the Whos and earned {earn} coins.")
|
|
|
|
elif choice == "2":
|
|
# Lottery tickets that cost coins and rarely pay out
|
|
send("How many tickets do you want to buy? (10 coins each)")
|
|
send("> ")
|
|
line = reader.readline()
|
|
if not line:
|
|
break
|
|
try:
|
|
n = to_int(line)
|
|
except ValueError:
|
|
send("That's not even a number. The Grinch laughs you out of town.")
|
|
break
|
|
|
|
# Make lottery 'honest' so it's not the exploit:
|
|
if n <= 0:
|
|
send("You must buy at least 1 ticket, no refunds.")
|
|
continue
|
|
|
|
cost = 10 * n
|
|
|
|
# If you can't afford it, don't touch the balance at all
|
|
if cost > coins:
|
|
send("You don't have enough coins for that many tickets.")
|
|
send("The Grinch refuses to give you credit for lottery tickets.")
|
|
continue
|
|
|
|
# Now this subtraction is safe (no wrap) because cost <= coins
|
|
coins = add_coins(coins, -cost)
|
|
send(f"You bought {n} tickets for {cost} coins.")
|
|
|
|
wins = 0
|
|
# Cap iteration count so people can't hang the server
|
|
for _ in range(min(n, 1000)):
|
|
if random.random() < 0.05:
|
|
prize = random.randint(5, 50)
|
|
coins = add_coins(coins, prize)
|
|
wins += 1
|
|
if wins:
|
|
send(f"You won on {wins} ticket(s)! Your luck slightly improves.")
|
|
else:
|
|
send("No luck this time. The Grinch grins.")
|
|
|
|
elif choice == "3":
|
|
# Shady loan service with a "safety limit"... that only checks positive amounts.
|
|
send("How much do you want from the Grinch's loan shark service?")
|
|
send("> ")
|
|
line = reader.readline()
|
|
if not line:
|
|
break
|
|
try:
|
|
delta = to_int(line)
|
|
except ValueError:
|
|
send("You mumble nonsense. The Grinch gets bored and leaves.")
|
|
break
|
|
|
|
# Safety check that *only* constrains positive loans.
|
|
if delta > MAX_LEGAL_LOAN:
|
|
send("Too risky! The Grinch's auditor caps loans at "
|
|
f"{MAX_LEGAL_LOAN} coins per request.")
|
|
send("Smaller loans only, please.")
|
|
continue
|
|
|
|
# Negative values are not checked at all. This is the intended bug:
|
|
# a large negative 'repayment' will underflow and wrap to a huge balance.
|
|
before = coins
|
|
coins = add_coins(coins, delta)
|
|
send(f"Ledger update: {before} -> {coins} (delta: {delta})")
|
|
|
|
elif choice == "4":
|
|
# Check if the player can afford the flag
|
|
if coins >= price:
|
|
send("You jingle enough coins to buy the shiny present!")
|
|
send(f"Inside you find a note: {flag}")
|
|
send("Merry XMAS and beware of greedy Grinches.")
|
|
return
|
|
else:
|
|
price += FLAG_PRICE # price goes up if you try to buy without enough coins
|
|
send(f"Not enough coins. The Grinch cackles and raises the price to {price} coins.")
|
|
|
|
elif choice == "5":
|
|
send("You leave the lottery, slightly colder but maybe wiser.")
|
|
return
|
|
|
|
else:
|
|
send("The Grinch doesn't recognize that option and kicks you out.")
|
|
return
|
|
|
|
send("The Grinch closes the stand for the night. Come back with a better plan.")
|
|
|
|
|
|
# === TCP server glue (thread-per-connection, like gunicorn workers) ===
|
|
|
|
class GrinchHandler(socketserver.StreamRequestHandler):
|
|
def handle(self):
|
|
# Simple timeout so connections don't hang forever
|
|
self.request.settimeout(60)
|
|
|
|
# Wrap the raw socket file descriptors into text-mode file objects
|
|
reader = io.TextIOWrapper(self.rfile, encoding="utf-8", newline="\n")
|
|
writer = io.TextIOWrapper(
|
|
self.wfile,
|
|
encoding="utf-8",
|
|
newline="\n",
|
|
write_through=True,
|
|
)
|
|
|
|
try:
|
|
play_game(reader, writer)
|
|
except Exception:
|
|
# Hide internals from players
|
|
try:
|
|
writer.write("An error occurred. The Grinch eats the stack trace.\n")
|
|
writer.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
# Basic "load balancing": one thread per client connection
|
|
allow_reuse_address = True
|
|
daemon_threads = True
|
|
|
|
|
|
def run_server(host: str = "0.0.0.0", port: int = 9000):
|
|
with ThreadedTCPServer((host, port), GrinchHandler) as server:
|
|
server.serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Usage:
|
|
# ./grinch_lottery.py -> CLI mode on stdin/stdout
|
|
# ./grinch_lottery.py server -> TCP server 0.0.0.0:9000
|
|
# ./grinch_lottery.py server HOST PORT
|
|
if len(sys.argv) >= 2 and sys.argv[1] == "server":
|
|
host = sys.argv[2] if len(sys.argv) >= 3 else "0.0.0.0"
|
|
port = int(sys.argv[3]) if len(sys.argv) >= 4 else 9000
|
|
run_server(host, port)
|
|
else:
|
|
# CLI mode: interact via stdin/stdout (no network)
|
|
reader = sys.stdin
|
|
writer = sys.stdout
|
|
play_game(reader, writer)
|