BUP CTF Powered by Knight Squad - Final Round

Lets Play – Misc Challenge Writeup | BUP CTF Powered by Knight Squad - Final Round

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()

0 people love this