This is a writeup for the Fetch The Flag 2023 Quick Maths
challenge.
Challenge notes
To try and get better grades in math, I made a program that gave me timed quizzes. Funny thing is that as I got better at the questions in this test, I got worse grades on my math tests.
NOTE: Float answers are rounded to 1 decimal points.
NOTE: And here’s another twist… the answers to division questions depend on integer division or double division. I.e., 3/5 = 0 3/5.0 = .6
Press the Start button in the top-right to begin this challenge. Connect with:
nc challenge.ctf.games 32237
Connecting to the challenge server
Using the command supplied in the challenge notes, I use nc
to connect to
the challenge server. The server prompts you to solve math challenges and times
you out when you don’t answer fast enough.
Welcome! To be honest, I am a Computer Science major but I was never any good at math in school. I seemed to always get Cs.
Note to self: Round all answers to 1 decimal point, where applicable.
Do you want to give it a chance? (Y/n): y
Awesome, good luck!
What is 97.2 / 33.7?
Too Slow!!!
Good bye :(
I decided to use this as an opportunity to test out the nclib
library in Python. You can use nclib
to automate
interacting with telnet-like interfaces over the network.
The protocol
The challenge server writes 6 lines of text and asks the user to enter y
.
After that, the server prompts you to solve a long chain of arithmetic
questions.
Here is one more transcript from the server:
Welcome! To be honest, I am a Computer Science major but I was never any good at math in school. I seemed to always get Cs.
Note to self: Round all answers to 1 decimal point, where applicable.
Do you want to give it a chance? (Y/n): y
Awesome, good luck!
What is 3 / 5?
What is 3 / 5.0?
Too Slow!!!
Good bye :(
As noted in the challenge notes, the server cares about integer and float division.
The solution script
I first define a few test cases in pytest
to make sure that I get the
integer versus float division right:
from calculator import calculate
def test_all() -> None:
assert calculate("What is 3 / 5?") == 0
assert calculate("What is 3 / 5.0?") == 0.6
assert calculate("What is 76.0 * 36.1?") == 2743.6
assert calculate("Correct!\nWhat is 76.0 * 36.1?") == 2743.6
This is the calculate
function in Python:
import re, operator
FORMULA_RE = re.compile(
r"(?:.+\n)?What is ([0-9.]+) ([*/+-]) ([0-9.]+)\?",
re.MULTILINE,
)
OPERATORS = {
"*": operator.mul,
"/": operator.truediv,
"+": operator.add,
"-": operator.sub,
}
def calculate(formula: str) -> Union[int, float]:
"""Evaluate a formula."""
match = FORMULA_RE.match(formula)
assert match is not None, f"Couldn't match {formula}"
left, op_chr, right = match.group(1, 2, 3)
floats = '.' in left or '.' in right
op = OPERATORS[op_chr]
if floats:
return round(op(float(left), float(right)), 1)
return int(op(int(left), int(right)))
The calculate
function performs the following tasks:
- Use a regular expression to parse the math formula from the server’s question.
- Pick the right operator from a dictionary called
OPERATORS
- Determine if the answer is expected to be a floating point number or integer.
- If a floating point number is required, run the calculation and return the result rounded to 1 decimal point
- If an integer is required, round the result and return it.
Here’s the full script that solved the challenge:
#!/usr/bin/env python3
import operator
import re
from typing import Union
import nclib
HOST = "challenge.ctf.games", 32237
FORMULA_RE = re.compile(
r"(?:.+\n)?What is ([0-9.]+) ([*/+-]) ([0-9.]+)\?",
re.MULTILINE,
)
OPERATORS = {
"*": operator.mul,
"/": operator.truediv,
"+": operator.add,
"-": operator.sub,
}
def calculate(formula: str) -> Union[int, float]:
"""Evaluate a formula."""
match = FORMULA_RE.match(formula)
assert match is not None, f"Couldn't match {formula}"
left, op_chr, right = match.group(1, 2, 3)
floats = '.' in left or '.' in right
op = OPERATORS[op_chr]
if floats:
return round(op(float(left), float(right)), 1)
return int(op(int(left), int(right)))
def main():
nc = nclib.Netcat(HOST)
print(nc.recv_until("(Y/n): ").decode())
nc.send_line("y")
print(nc.recv_until("Awesome, good luck!\n").decode())
while True:
line = nc.recv_until("? ").decode()
print("Next question:", line)
line = line.strip()
result = calculate(line)
print("Calculated the result:", result, "-- sending now")
nc.send_line(str(result))
if __name__ == "__main__":
main()