Skip to content

Instantly share code, notes, and snippets.

@ky28059
Last active September 1, 2024 22:28
Show Gist options
  • Save ky28059/c7e3e83bb501755bda31683f0d359578 to your computer and use it in GitHub Desktop.
Save ky28059/c7e3e83bb501755bda31683f0d359578 to your computer and use it in GitHub Desktop.

CyberSpace CTF 2024 — Game with Rin

Nanakura Rin, a very skilled gamer, took one of the flags. You need to defeat her 200 times to get the flag back.

nc game-with-rin.challs.csc.tf 1337

We're given a Python server that looks like this:

from basement_of_rin import NanakuraRin, flag, generate_graph

import time
from random import Random

def Server(m):
	print("Server> " + m)

def Rin(m):
	print("Rin> " + m)

def check_subset(_subset, set):
	subset = sorted(_subset)
	assert len(subset) != 0
	for i in range(len(subset) - 1):
		subset[i] < subset[i + 1]
	for x in subset:
		assert x in set

class disjoint_set:
	def __init__(self, n):
		self.n = n
		self.p = [-1] * self.n
	def root(self, u):
		if self.p[u] < 0:
			return u
		self.p[u] = self.root(self.p[u])
		return self.p[u]
	def share(self, u, v):
		return self.root(u) == self.root(v)
	def merge(self, u, v):
		u, v = self.root(u), self.root(v)
		if u == v:
			return False
		if self.p[u] > self.p[v]:
			u, v = v, u
		self.p[u] += self.p[v]
		self.p[v] = u
		return True
	def clear(self):
		self.p = [-1] * self.n

def check_tree(subset, V, edges):
	assert len(subset) == V - 1
	ds = disjoint_set(V)
	for i in subset:
		assert isinstance(i, int) and 0 <= i < len(edges)
		u, v, w = edges[i]
		assert ds.merge(u, v)

def determine_the_winner(piles):
	# https://en.wikipedia.org/wiki/Nim
	# Rin-chan is perfect, so she'll always make the optimal move.
	# You're perfect too, so you'll always make the perfect move as well.
	# Let's fast-forward the game to the end :)
	nim = 0
	for x in piles:
		nim ^= x
	return "first" if nim != 0 else "second"

class You:
	def __init__(self, V, edges):
		Server(f"{V = }")
		Server(f"{edges = }")
	def first_move(self):
		Server("Please choose S")
		return sorted(list(map(int, input("You> S = ").strip().split(" "))))
	def read_first_move(self, S):
		Rin(f"I choose S = {' '.join([str(i) for i in S])}")
	def second_move(self):
		Server("Please choose T")
		return sorted(list(map(int, input("You> T = ").strip().split(" "))))

Rin("Do you want this flag?")
Rin("I'll consider giving it to you if you defeat me 200 times :)")

time.sleep(0.5)

Server("-----------------------------------------------------------------------------------------")
Server("[Round Manual]")
Server("1. The server generates a set of edges with integer weight on vertices 0 ~ V-1.")
Server("2. You can either choose to go first or second.")
Server("3. First player chooses a subset S of edges of size V-1 which forms a tree.")
Server("( For the definition of tree, see https://en.wikipedia.org/wiki/Tree_(graph_theory) )")
Server("4. Second player chooses a non-empty subset T of S.")
Server("5. Add a pile consisting of w stones for each edge (u, v, w) in T.")
Server("6. Winner of the round is the winner of the nim game on those set of piles.")
Server("-----------------------------------------------------------------------------------------")

time.sleep(0.5)

Rin("GLHF!")

time.sleep(0.5)

for round_number in range(1, 201):
	Server(f"Round {round_number}")

	V, edges = generate_graph(round_number)
	assert isinstance(V, int)
	assert 30 <= V <= 200
	assert len(edges) <= 300
	for u, v, w in edges:
		assert isinstance(u, int) and isinstance(v, int) and isinstance(w, int)
		assert 0 <= u < V and 0 <= v < V and 0 <= w < 2**(V-1)

	first, second = You(V, edges), NanakuraRin(V, edges)
	
	Rin("Do you want to go [first] or [second]?")
	resp = input("You> ").strip()
	if resp not in ["first", "second"]:
		Rin("That's not an option >:(")
		exit(0)

	if resp == "first":
		Rin("Sure, you go first :D")
	else:
		Rin("Ok, I go first :D")
		first, second = second, first

	S = first.first_move()
	second.read_first_move(S)
	check_subset(S, [i for i in range(len(edges))])
	check_tree(S, V, edges)

	if resp == "first":
		Rin("My turn!")
	else:
		Rin("Your turn!")

	T = second.second_move()
	check_subset(T, S)

	if resp == "first":
		Rin(f"I choose T = {' '.join([str(i) for i in T])}")

	winner = determine_the_winner([edges[i][2] for i in T])

	r = Random()

	if winner != resp:
		Rin(r.choice(["Git gud", "Noob", "Easy"]))
		exit(0)

	Rin(r.choice(["Not like this!", "You won :(", "Ouch!", "You got lucky this time."]))

Rin("GG Well played!")
Rin("I guess I have no choice but to give you the flag.")
Rin(f"{flag}")

On paper, the game works as follows:

  • The server picks a V and generates random weighted edges between those nodes.
  • We choose whether to go first or second.
  • Player 1 chooses a subset of size V - 1 that forms a tree.
  • Player 2 chooses a arbitrary (non-empty) subset of player 1's choice.
  • This final subset is translated into Nim piles, and the player who wins Nim wins the round.

Unfortunately, there doesn't seem to be duplicate protection in the subset choosing: we can always go second, and pick the same edge twice to guarantee that XOR of the weights = 0 and we win Nim.

Here's a script that does just that:

import pwn

conn = pwn.remote('game-with-rin.challs.csc.tf', 1337)

# Solve proof of work
res = conn.recvuntil(b'Solution?')
print(res)
sol = input('Solution: ')
conn.sendline(sol.encode())

for i in range(200):
    conn.recvuntil(b'Rin> Do you want to go [first] or [second]?')
    conn.sendline(b'second')

    conn.recvuntil(b'Rin> I choose S = ')
    first = conn.recvline().decode().split(" ")[0]
    conn.sendline(f'{first} {first}'.encode())

    print(i, conn.recvline().strip().decode())

print(conn.recvall().decode())
[x] Opening connection to game-with-rin.challs.csc.tf on port 1337
[x] Opening connection to game-with-rin.challs.csc.tf on port 1337: Trying 34.47.157.234
[+] Opening connection to game-with-rin.challs.csc.tf on port 1337: Done
b'== proof-of-work: enabled ==\nplease solve a pow first\nYou can run the solver with:\n    python3 <(curl -sSL https://goo.gle/kctf-pow) solve s.AE5X.AAD7HTl9L2wrq2RmljReSe+H\n===================\n\nSolution?'
Solution: s.AAAO+qBXiJTTaK0jlC9QLDOQlE+eLqJ2cuvjB9Xh6+m49FO/xJpKU+hdTKqF+QKTIlF1pDhU7X3+uSJfkUocg6Pv49kIvWSbuFX7GJg5G4wf+XrZTZCKQcRZMNT9savz2AIKAE4vEYZ3Y5qrj/Uk769dgIGqoa1yC/pkhcRCtBMmS75pFUgsITcvUaJ0j0BZVuLgb+X35Q8uLdC0ktJFX2+O
0 Rin> Your turn!
1 Rin> Your turn!
2 Rin> Your turn!
...
196 Rin> Your turn!
197 Rin> Your turn!
198 Rin> Your turn!
199 Rin> Your turn!
[x] Receiving all data
[x] Receiving all data: 33B
[x] Receiving all data: 195B
[+] Receiving all data: Done (195B)
[*] Closed connection to game-with-rin.challs.csc.tf port 1337
Server> Please choose T
You> T = Rin> You got lucky this time.
Rin> GG Well played!
Rin> I guess I have no choice but to give you the flag.
Rin> CSCTF{I_just_wanted_to_spend_time_with_you_baka!}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment