LIT CTF 2023: Polypoint

Author Max Niederman
Published 2023-08-08
Tags
Description

Writeup of the crypto/polypoint challenge from the LIT CTF 2023.

This encryption scheme is mathematically unbreakable if you don’t have all the keys! You learnt so in grade school, right?

We are given two files: encode.py and encoded.txt.

encode.py:

from secrets import SystemRandom

gen = SystemRandom()

with open("flag.txt", "rb") as f:
    flag = f.read().strip()

assert len(flag) == 76
c = int.from_bytes(flag)

poly = [c]
for _ in range(10):
    poly.append(gen.randrange(pow(10, 11), pow(10, 12)))

for _ in range(10):
    x = gen.randrange(pow(10, 11), pow(10, 12))
    v = 1
    y = 0
    for i in range(11):
        y += poly[i] * v
        v *= x
    print(f"({x},{y})")

Let’s analyze this in pieces.

from secrets import SystemRandom

gen = SystemRandom()

This is a cryptographic PRNG, so it should probably be treated as truly random for the purposes of the solution.

with open("flag.txt", "rb") as f:
    flag = f.read().strip()

assert len(flag) == 76
c = int.from_bytes(flag)

This is pretty standard for crypto challenges: read the flag and encode it as an integer. We are also given the extra piece of information that the flag is 76 bytes long, which turns out to be useful later.

poly = [c]
for _ in range(10):
    poly.append(gen.randrange(pow(10, 11), pow(10, 12)))

So it creates a list of eleven numbers, the first being the integer representation of the flag, and the rest being random integers between and . I’m going to refer to each of these numbers as , , and so on up to , and the whole list as the eleven-dimensional vector .

for _ in range(10):
    x = gen.randrange(pow(10, 11), pow(10, 12))
    v = 1
    y = 0
    for i in range(11):
        y += poly[i] * v
        v *= x
    print(f"({x},{y})")

One important thing to notice is that nothing is mutated inbetween iterations of the outer loop. This means that we essentially have ten independent trials relating x to y.

Now, let’s look at what each iteration actually does. First, it initializes the variable x to a random integer between and (like ); the variable v to one; and the variable y to zero.

Then, it runs a nested loop for eleven iterations. Each iteration first adds times the current value of v to y, then multiplies v by x.

The first thing to notice is that we know the value of v at every iteration of the inner loop. I’ll denote the value of v in the -th outer loop iteration (trial) and -th inner loop iteration as , and the value of x as . Then we have:

y starts at zero, so its final value will just be the sum of poly[i] * v over the whole inner loop. Written in math notation (with being the final value of y for the -th trial):

The key realization is that this is also the dot product between and :

Now we can write this out for all ten trials:

This is just a linear system of equations! Let’s rewrite it as a matrix equation:

We can’t just plug this in to a calculator and solve it though. The system is underconstrained, so we need some way to limit the search to find the unique true solution, .

First of all, we know that is made up of all integers (), so we should only look for integer solutions. We also have some lower and upper bounds for each of the entries of :

At this point, I reformatted the data as a CSV file and loaded it into Mathematica to do the hard work of actually solving the system for me: