I try to play it manually. It asks some question like:
Let's Play
Author: NomanProdhan
Keep playing with me. If you win, I will give the flag.
Creating a question for you...
Legend for this one: +→+, -→*, *→/, /→-
Expression: ((6 - (4 + 20)) - 5)
Answer:
I need to answer that. When I answered then it asks me another question and so on. After playing for a while I found some pattern and some question is repeatedly asking. BUt sometime it asked different question like this (they are some always though):
Creating a question for you...
ROT13 this: synt
Your answer:
Creating a question for you...
How many bits in a byte?
Your answer:
But to do that manually It takes lots of time. So I decided to do that with python script. So with the help of chatgpt i made a script that gives me flag.
#!/usr/bin/env python3
import socket
import re
import base64
import codecs
import ast
HOST = "160.187.130.84"
PORT = 9000
ARROW = "→" # unicode arrow used in legends
# --- Safe integer-expression evaluator (supports + - * / and parentheses) ---
class IntEval(ast.NodeVisitor):
def visit_Expression(self, node):
return self.visit(node.body)
def visit_UnaryOp(self, node):
v = self.visit(node.operand)
if isinstance(node.op, ast.UAdd):
return +v
if isinstance(node.op, ast.USub):
return -v
raise ValueError("Unsupported unary operator")
def visit_Num(self, node): # Py<3.8
return int(node.n)
def visit_Constant(self, node): # Py>=3.8
if isinstance(node.value, (int, float)):
return int(node.value)
raise ValueError("Unsupported constant")
def visit_BinOp(self, node):
a = self.visit(node.left)
b = self.visit(node.right)
if isinstance(node.op, ast.Add):
return a + b
if isinstance(node.op, ast.Sub):
return a - b
if isinstance(node.op, ast.Mult):
return a * b
if isinstance(node.op, (ast.Div, ast.FloorDiv)):
if b == 0:
raise ZeroDivisionError("division by zero")
# Truncate toward zero like CTF servers usually expect
return int(a / b)
raise ValueError("Unsupported operator")
def generic_visit(self, node):
raise ValueError(f"Unsupported syntax: {type(node).__name__}")
def eval_int_expr(expr: str) -> int:
tree = ast.parse(expr, mode="eval")
return IntEval().visit(tree)
# --- Legend parsing & expression remap ---
def parse_legend(line: str):
"""
Example line:
'Legend for this one: +→/, -→*, *→-, /→+'
Returns dict like {'+': '/', '-': '*', '*': '-', '/': '+'}
"""
legend = {}
try:
part = line.split(":", 1)[1].strip()
except Exception:
return legend
for token in part.split(","):
token = token.strip()
if not token:
continue
# Accept either '->' or '→'
if ARROW in token:
left, right = token.split(ARROW)
else:
left, right = token.split("->")
legend[left.strip()] = right.strip()
return legend
def remap_expr(expr: str, legend: dict) -> str:
# First replace operators with placeholders
placeholders = {'+': 'A', '-': 'B', '*': 'C', '/': 'D'}
rev_placeholders = {v: k for k, v in placeholders.items()}
tmp = []
for ch in expr:
if ch in placeholders:
tmp.append(placeholders[ch])
else:
tmp.append(ch)
tmp = "".join(tmp)
# Replace placeholders with mapped ops (default = original)
final = []
for ch in tmp:
if ch in rev_placeholders:
original = rev_placeholders[ch]
target = legend.get(original, original)
final.append(target)
else:
final.append(ch)
return "".join(final)
# --- Question handlers ---
def solve_linebuffer(buf: str):
"""
Inspect the current buffer and decide whether we have a complete
question to answer. If so, return (answer_string, consume_until).
Otherwise return (None, None).
"""
# Legend + Expression
m_leg = re.search(r"Legend for this one:.*", buf)
m_exp = re.search(r"Expression:\s*(.*)", buf)
m_ans = re.search(r"\n(?:Answer|Your answer)(?:\s*\(integer\))?:", buf)
if m_leg and m_exp and m_ans:
legend = parse_legend(m_leg.group(0))
expr = m_exp.group(1).strip()
mapped = remap_expr(expr, legend)
val = eval_int_expr(mapped)
return str(val), len(buf)
# Just math.
m_just = re.search(r"Just math\.", buf)
m_exp2 = re.search(r"Expression:\s*(.*)", buf)
m_ans2 = re.search(r"\n(?:Answer|Your answer)\s*\(integer\):", buf)
if m_just and m_exp2 and m_ans2:
expr = m_exp2.group(1).strip()
val = eval_int_expr(expr)
return str(val), len(buf)
# ROT13
m_rot = re.search(r"ROT13 this:\s*(.*)", buf)
if m_rot and re.search(r"\nYour answer:", buf):
s = m_rot.group(1).strip()
return codecs.decode(s, "rot_13"), len(buf)
# Base64
m_b64 = re.search(r"Decode base64:\s*([A-Za-z0-9+/=]+)", buf)
if m_b64 and re.search(r"\nYour answer:", buf):
b = base64.b64decode(m_b64.group(1)).decode("utf-8", "ignore")
return b, len(buf)
# Hex to decimal
m_hex = re.search(r"Hex to decimal:\s*(0x[0-9a-fA-F]+)", buf)
if m_hex and re.search(r"\nYour answer:", buf):
return str(int(m_hex.group(1), 16)), len(buf)
# Multiplication style (7 x 8)
m_mul = re.search(r"What is\s+(-?\d+)\s*[xX*×]\s*(-?\d+)\s*\?", buf)
if m_mul and re.search(r"\nYour answer:", buf):
a, b = int(m_mul.group(1)), int(m_mul.group(2))
return str(a * b), len(buf)
# Bits in a byte
if re.search(r"How many bits in a byte\?", buf) and re.search(r"\nYour answer:", buf):
return "8", len(buf)
# 2-letter country code for Japan
if re.search(r"2-?letter country code for Japan\??", buf, re.IGNORECASE) and re.search(r"\nYour answer:", buf):
return "JP", len(buf)
# Plain Expression + Answer
if m_exp and m_ans:
expr = m_exp.group(1).strip()
val = eval_int_expr(expr)
return str(val), len(buf)
return None, None
def main():
with socket.create_connection((HOST, PORT), timeout=10) as s:
s.settimeout(2.0)
buf = ""
print("[*] Connected.")
while True:
try:
chunk = s.recv(4096)
if not chunk:
break
text = chunk.decode("utf-8", "ignore")
print(text, end="")
buf += text
# Check for flag
mflag = re.search(r"(BUPCTF\{[^}]+\})", buf)
if mflag:
print("\n[!] FLAG:", mflag.group(1))
return
# Compute answer
ans, upto = solve_linebuffer(buf)
if ans is not None:
s.sendall((ans.strip() + "\n").encode())
print(f"[>] {ans.strip()}") # local echo
buf = "" # reset buffer
except socket.timeout:
continue
if __name__ == "__main__":
main()