The annual UC San Diego Raccoon Run is happening right now!! Apparently there's an underground gambling ring going on there. Maybe you can make it big?
We're given a Python server that looks like this:
import json
from time import time
import tornado
import tornado.websocket
import tornado.ioloop
import random
import asyncio
import os
from datetime import timedelta
import tornado.web
import tornado.gen
PORT = 8000
NUM_RACCOONS = 8
FINISH_LINE = 1000
TARGET_BET = 1000
STEP_TIME = 0.25 # in seconds
BETTING_TIME = 15 # in seconds
FLAG = os.environ["GZCTF_FLAG"]
active_betters = {}
connections = {}
connection_count = 0
game = None
class RaccoonRun:
def __init__(self):
self.raccoons = [0] * 8
self.finishers = []
self.can_bet = True
self.bet_end = 0
def step(self):
self.can_bet = False
random_int = random.getrandbits(32)
for i in range(NUM_RACCOONS):
self.raccoons[i] += (random_int >> (i * 4)) % 16
for (i, x) in enumerate(self.raccoons):
if x >= FINISH_LINE and i not in self.finishers:
self.finishers.append(i)
return (self.raccoons, self.finishers)
def game_over(self):
return len(self.finishers) >= NUM_RACCOONS
class Gambler:
def __init__(self, account=10):
self.account = account
self.guess = None
self.bet_amount = 0
def bet(self, guess, bet_amount):
if self.validate_bet(guess, bet_amount):
self.guess = guess
self.bet_amount = bet_amount
return True
else:
return False
def validate_bet(self, guess, bet_amount):
if (type(guess) is not list):
return False
if not all(type(x) is int for x in guess):
return False
if len(guess) != NUM_RACCOONS:
return False
if (type(bet_amount) is not int):
return False
if (bet_amount < 0 or bet_amount > self.account):
return False
return True
# updates amount of money in account after game is over and bet comes through
# and then return an boolean indicating whether you won/lost
def check_bet(self, game_instance):
if game_instance.finishers == self.guess:
self.account += self.bet_amount
return True
else:
self.account -= self.bet_amount
return False
def reset_bet(self):
self.guess = None
self.bet_amount = 0
def get_race_information(id):
return json.dumps({"type": "race_information", "can_bet": "true" if game.can_bet else "false", "raccoons": game.raccoons, "finishers": game.finishers, "account": active_betters[id].account})
class RRWebSocketHandler(tornado.websocket.WebSocketHandler):
def open(self):
global game
global active_betters
global connections
global connection_count
self.better_id = connection_count
active_betters[self.better_id] = Gambler()
connections[self.better_id] = self
connection_count += 1
self.write_message(get_race_information(self.better_id))
if game.can_bet:
self.write_message(json.dumps({"type":"betting-starts","until":game.bet_end}))
def on_message(self, message):
try:
data = json.loads(message)
if "type" not in data:
self.write_message(json.dumps({"type": "response", "value": "invalid WebSockets message"}))
elif (data["type"] == "bet"):
if (game.can_bet):
if active_betters[self.better_id].bet(data["order"], data["amount"]):
self.write_message(json.dumps({"type": "response", "value": "bet successfully placed!"}))
else:
self.write_message(json.dumps({"type": "response", "value": "bet is invalid, failed to be placed"}))
else:
self.write_message(json.dumps({"type": "response", "value": "bet cannot be placed after the race starts, failed to be placed"}))
elif (data["type"] == "buy_flag"):
if (active_betters[self.better_id].account > TARGET_BET):
self.write_message(json.dumps({"type": "flag", "value": FLAG}))
elif (data["type"] == "state"):
self.write_message(json.dumps({"type": "response", "value": "bet" if game.can_bet else "race"}))
elif (data["type"] == "account"):
self.write_message(json.dumps({"type": "response", "value": active_betters[self.better_id].account}))
else:
self.write_message(json.dumps({"type": "response", "value": "invalid WebSockets message"}))
except json.JSONDecodeError:
self.write_message(json.dumps({"type": "response", "value": "invalid WebSockets message"}))
def on_close(self):
del active_betters[self.better_id]
del connections[self.better_id]
def game_loop():
global game
print("Raccoons", game.raccoons)
print("Finishers", game.finishers)
for (id, connection) in connections.items():
connection.write_message(get_race_information(id))
game.step()
if game.game_over():
print("Raccoons", game.raccoons)
print("Finishers", game.finishers)
for (id, connection) in connections.items():
connection.write_message(get_race_information(id))
connection.write_message(json.dumps({"type":"result", "value": game.finishers}))
for (id, x) in active_betters.items():
if x.guess != None:
win = x.check_bet(game)
connections[id].write_message(json.dumps({"type": "bet_status", "value": f"you {'won' if win else 'lost'} the bet, your account now has ${x.account}"}))
x.reset_bet()
else:
connections[id].write_message(json.dumps({"type": "bet_status", "value": f"you didn't place a bet, your account now has ${x.account}"}))
print("Every raccoon has finished the race.")
print(f"Starting new game! Leaving {BETTING_TIME} seconds for bets...")
game = RaccoonRun()
game.bet_end = time() + BETTING_TIME
for (id, connection) in connections.items():
connection.write_message(get_race_information(id))
connection.write_message(json.dumps({"type":"betting-starts","until":game.bet_end}))
tornado.ioloop.IOLoop.current().add_timeout(timedelta(seconds=BETTING_TIME), game_loop)
else:
tornado.ioloop.IOLoop.current().add_timeout(timedelta(seconds=STEP_TIME), game_loop)
if __name__ == "__main__":
tornado.ioloop.IOLoop.configure("tornado.platform.asyncio.AsyncIOLoop")
io_loop = tornado.ioloop.IOLoop.current()
asyncio.set_event_loop(io_loop.asyncio_loop)
game = RaccoonRun()
print(f"Starting new game! Leaving {BETTING_TIME} seconds for bets...")
game.bet_end = time() + BETTING_TIME
tornado.ioloop.IOLoop.current().add_timeout(timedelta(seconds=BETTING_TIME), game_loop)
application = tornado.web.Application([
(r"/ws", RRWebSocketHandler),
(r"/(.*)", tornado.web.StaticFileHandler, {"path": "./static", "default_filename": "index.html"})
])
application.listen(PORT)
io_loop.start()
At each step of the race, 4-bit chunks of a random 32-bit int are added to each raccoons position, continuing until all 8 raccoons cross the finish line (> 1000). While we can bet on the order of the raccoons to finish the race, we only win if our guess is exactly correct.
The main idea with this challenge is that because we're given the positions of the raccoons at each step, we can reconstruct the exact 32-bit integer used to reach that step. Feeding that integer into an RNG cracker, we can reverse engineer the RNG being used by the server to run the races, then simulate each race ahead of time to bet flawlessly and buy the flag.
import numpy as np
import websocket
import json
from randcrack import RandCrack
WS_URL = "ws://127.0.0.1:64045/ws"
TARGET_BET = 1000
rc = RandCrack()
raccoons = np.zeros(8, dtype=int)
balance = 10
start_processing = False
stop_processing = False
def on_message(ws, message):
global start_processing, stop_processing, balance, raccoons
print('Received message:', message, flush=True)
data = json.loads(message)
match data['type']:
# Reset race data
case 'bet_status':
raccoons = np.zeros(8, dtype=int)
case 'betting-starts':
start_processing = True
if not rc.state:
return
# If `rc.state` is populated, stop sampling random output
stop_processing = True
# If we have enough to buy the flag, do so
if balance > TARGET_BET:
ws.send(json.dumps({
"type": "buy_flag"
}))
return
# Otherwise, predict the winners and place a bet
r_state = [0] * 8
finishers = []
while len(finishers) != 8:
random_int = rc.predict_getrandbits(32)
for i in range(8):
r_state[i] += (random_int >> (i * 4)) % 16
if r_state[i] >= 1000 and i not in finishers:
finishers.append(i)
print(f"The bet is: {finishers}")
ws.send(json.dumps({
"type": "bet",
"order": finishers,
"amount": balance
}))
balance *= 2
case 'race_information':
# Ignore data if we've started the script in the middle of a race, or after we've reached a betting
# phase with populated `rc.state`.
if not start_processing or stop_processing:
return
if data['can_bet'] == "true":
return
# If we've populated `rc.state` before the end of a race, keep advancing the RNG.
if rc.state:
rc.predict_getrandbits(32)
new_states = np.array(data['raccoons'])
diffs = new_states - raccoons
raccoons = new_states
print(diffs, flush=True)
# print(''.join(reversed(list(map(lambda d: bin(d)[2:].zfill(4), diffs)))))
# Reverse random integer:
# self.raccoons[i] += (random_int >> (i * 4)) % 16
reconstructed = 0
for i, num in enumerate(diffs):
reconstructed += num << (i * 4)
# Mask signed integer back to unsigned for randcrack
reconstructed &= 0xffffffff
rc.submit(reconstructed)
print(f'{rc.counter}: {reconstructed}', flush=True)
if __name__ == "__main__":
ws = websocket.WebSocketApp(WS_URL, on_message=on_message)
ws.run_forever()
After about ~5 games of sampling the RNG and 7 games of betting, we can purchase the flag:
PS C:\Users\kevin\Downloads> python3 .\solve.py
Received message: {"type": "race_information", "can_bet": "true", "raccoons": [0, 0, 0, 0, 0, 0, 0, 0], "finishers": [], "account": 10}
Received message: {"type": "betting-starts", "until": 1715572315.2874234}
Received message: {"type": "race_information", "can_bet": "true", "raccoons": [0, 0, 0, 0, 0, 0, 0, 0], "finishers": [], "account": 10}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [9, 0, 5, 15, 12, 2, 11, 7], "finishers": [], "account": 10}
[ 9 0 5 15 12 2 11 7]
1: 2066543881
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [21, 14, 8, 18, 25, 14, 25, 10], "finishers": [], "account": 10}
[12 14 3 3 13 12 14 3]
2: 1053635564
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [21, 15, 23, 31, 35, 24, 32, 25], "finishers": [], "account": 10}
[ 0 1 15 13 10 10 7 15]
3: 4155170576
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [22, 22, 23, 46, 38, 30, 47, 25], "finishers": [], "account": 10}
[ 1 7 0 15 3 6 15 0]
4: 258207857
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [37, 23, 23, 56, 42, 39, 50, 33], "finishers": [], "account": 10}
[15 1 0 10 4 9 3 8]
5: 2207555615
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [50, 32, 30, 57, 53, 54, 53, 38], "finishers": [], "account": 10}
[13 9 7 1 11 15 3 5]
6: 1408964509
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [51, 33, 31, 71, 60, 58, 67, 38], "finishers": [], "account": 10}
[ 1 1 1 14 7 4 14 0]
7: 239591697
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [57, 35, 44, 84, 63, 72, 81, 45], "finishers": [], "account": 10}
[ 6 2 13 13 3 14 14 7]
8: 2128862502
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [71, 37, 48, 85, 77, 72, 81, 55], "finishers": [], "account": 10}
[14 2 4 1 14 0 0 10]
9: 2685277230
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [75, 47, 63, 94, 87, 82, 87, 63], "finishers": [], "account": 10}
[ 4 10 15 9 10 10 6 8]
10: 2259328932
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [84, 57, 64, 108, 102, 94, 93, 66], "finishers": [], "account": 10}
[ 9 10 1 14 15 12 6 3]
11: 919593385
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [91, 64, 67, 116, 106, 96, 106, 69], "finishers": [], "account": 10}
[ 7 7 3 8 4 2 13 3]
12: 1025803127
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [102, 71, 68, 129, 115, 101, 119, 76], "finishers": [], "account": 10}
[11 7 1 13 9 5 13 7]
13: 2103038331
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [111, 73, 80, 135, 123, 114, 130, 91], "finishers": [], "account": 10}
[ 9 2 12 6 8 13 11 15]
14: 4225264681
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [119, 84, 95, 143, 131, 119, 137, 104], "finishers": [], "account": 10}
[ 8 11 15 8 8 5 7 13]
15: 3612905400
...
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1036, 1052, 1078, 1060, 1042, 975, 931, 1044], "finishers": [3, 2, 0, 1, 4, 7], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1048, 1057, 1082, 1070, 1043, 981, 944, 1045], "finishers": [3, 2, 0, 1, 4, 7], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1055, 1066, 1095, 1080, 1056, 984, 954, 1057], "finishers": [3, 2, 0, 1, 4, 7], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1057, 1071, 1103, 1080, 1059, 984, 955, 1060], "finishers": [3, 2, 0, 1, 4, 7], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1063, 1073, 1111, 1094, 1061, 999, 955, 1061], "finishers": [3, 2, 0, 1, 4, 7], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1075, 1078, 1120, 1095, 1070, 1003, 967, 1066], "finishers": [3, 2, 0, 1, 4, 7, 5], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1087, 1087, 1121, 1098, 1083, 1003, 967, 1077], "finishers": [3, 2, 0, 1, 4, 7, 5], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1098, 1092, 1136, 1111, 1091, 1006, 973, 1091], "finishers": [3, 2, 0, 1, 4, 7, 5], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1111, 1101, 1140, 1122, 1104, 1014, 983, 1102], "finishers": [3, 2, 0, 1, 4, 7, 5], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1115, 1116, 1152, 1136, 1117, 1026, 993, 1107], "finishers": [3, 2, 0, 1, 4, 7, 5], "account": 640}
Received message: {"type": "race_information", "can_bet": "false", "raccoons": [1129, 1117, 1163, 1143, 1117, 1029, 1005, 1108], "finishers": [3, 2, 0, 1, 4, 7, 5, 6], "account": 640}
Received message: {"type": "result", "value": [3, 2, 0, 1, 4, 7, 5, 6]}
Received message: {"type": "bet_status", "value": "you won the bet, your account now has $1280"}
Received message: {"type": "race_information", "can_bet": "true", "raccoons": [0, 0, 0, 0, 0, 0, 0, 0], "finishers": [], "account": 1280}
Received message: {"type": "betting-starts", "until": 1715572927.6933398}
Received message: {"type": "flag", "value": "SDCTF{m3rs3nn3_tw15t3r_15_5cuff3d_b300818768b0}"}